Skip to content

Commit e753f80

Browse files
committed
Merge branch 'main' into fix-join
2 parents 743943a + 262b9e5 commit e753f80

File tree

13 files changed

+9242
-11858
lines changed

13 files changed

+9242
-11858
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,6 @@ node_modules
2525
package-lock.json
2626
pnpm-works
2727
runner-results
28+
vite.config.ts.timestamp*
2829
yalc.lock
2930
yarn.lock

demos/automerge-repo-todos/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,6 @@
4747
"vite": "^5.0.10",
4848
"vite-plugin-top-level-await": "^1.4.1",
4949
"vite-plugin-wasm": "^3.3.0",
50-
"vitest": "^1.0.4"
50+
"vitest": "^1.6.0"
5151
}
5252
}

demos/automerge-repo-todos/vite.config.ts.timestamp-1715244817566-08b30f5a8271.mjs

-28
This file was deleted.

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,12 @@
5656
"lerna": "^7.4.2",
5757
"npm-run-all": "^4.1.5",
5858
"prettier": "^3.1.0",
59+
"shx": "^0.3.4",
5960
"tsup": "^8.0.1",
6061
"typescript": "^5.3.2",
6162
"vite": "^5.0.3",
6263
"vite-tsconfig-paths": "^4.2.1",
63-
"vitest": "^1.2.1",
64+
"vitest": "^1.6.0",
6465
"xo": "^0.56.0"
6566
}
6667
}

packages/auth-provider-automerge-repo/src/AuthProvider.ts

+13-12
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { hash } from '@localfirst/crypto'
1111
import { debug, memoize, pause } from '@localfirst/shared'
1212
import { type AbstractConnection } from 'AbstractConnection.js'
1313
import { AnonymousConnection } from 'AnonymousConnection.js'
14+
import { buildServerUrl } from 'buildServerUrl.js'
1415
import { getShareId } from 'getShareId.js'
1516
import { pack, unpack } from 'msgpackr'
1617
import { isJoinMessage, type JoinMessage } from 'types.js'
@@ -228,19 +229,18 @@ export class AuthProvider extends EventEmitter<AuthProviderEvents> {
228229
public async registerTeam(team: Auth.Team) {
229230
await this.addTeam(team)
230231

231-
const registrations = this.#server.map(async url => {
232-
// url could be "localhost:3000" or "syncserver.example.com"
233-
const host = url.split(':')[0] // omit port
232+
const registrations = this.#server.map(async server => {
233+
const { origin, hostname } = buildServerUrl(server)
234234

235235
// get the server's public keys
236-
const response = await fetch(`http://${url}/keys`)
236+
const response = await fetch(`${origin}/keys`)
237237
const keys = await response.json()
238238

239239
// add the server's public keys to the team
240-
team.addServer({ host, keys })
240+
team.addServer({ host: hostname, keys })
241241

242242
// register the team with the server
243-
await fetch(`http://${url}/teams`, {
243+
await fetch(`${origin}/teams`, {
244244
method: 'POST',
245245
headers: { 'Content-Type': 'application/json' },
246246
body: JSON.stringify({
@@ -336,8 +336,10 @@ export class AuthProvider extends EventEmitter<AuthProviderEvents> {
336336
* Registers a share with all of our sync servers.
337337
*/
338338
public async registerPublicShare(shareId: ShareId) {
339-
const registrations = this.#server.map(async url => {
340-
await fetch(`http://${url}/public-shares`, {
339+
const registrations = this.#server.map(async server => {
340+
const { origin } = buildServerUrl(server)
341+
342+
await fetch(`${origin}/public-shares`, {
341343
method: 'POST',
342344
headers: { 'Content-Type': 'application/json' },
343345
body: JSON.stringify({ shareId }),
@@ -625,7 +627,6 @@ export class AuthProvider extends EventEmitter<AuthProviderEvents> {
625627
#removeConnection(shareId: ShareId, peerId: PeerId) {
626628
const connection = this.#connections.get([shareId, peerId])
627629
if (connection && connection.state !== 'disconnected') {
628-
connection.stop()
629630
this.#connections.delete([shareId, peerId])
630631
}
631632
}
@@ -796,9 +797,9 @@ type Config = {
796797

797798
/**
798799
* If we're using one or more sync servers, we provide their hostnames. The hostname should
799-
* include the domain, as well as the port (if any). It should not include the protocol (e.g.
800-
* `https://` or `ws://`) or any path (e.g. `/sync`). For example, `localhost:3000` or
801-
* `syncserver.mydomain.com`.
800+
* include the domain, as well as the port (if any). If you don't include a protocol, we'll
801+
* assume you want to use http. Any path (e.g. `/sync`) will be ignored.
802+
* For example, `localhost:3000` or `https://syncserver.mydomain.com`.
802803
*/
803804
server?: string | string[]
804805
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/**
2+
* Prepends the given host with protocol if it's missing, and returns a URL object.
3+
*/
4+
export const buildServerUrl = (host: string) => {
5+
// assume http if no protocol provided (for backwards compatibility)
6+
if (!host.includes('//')) {
7+
host = `http://${host}`
8+
}
9+
return new URL(host)
10+
}

packages/auth-provider-automerge-repo/src/test/AuthProvider.test.ts

+7-3
Original file line numberDiff line numberDiff line change
@@ -160,9 +160,13 @@ describe('auth provider for automerge-repo', () => {
160160
invitationSeed: 'passw0rd',
161161
})
162162

163-
// grrr foiled again
164-
const authWorked = await authenticatedInTime(alice, eve)
165-
expect(authWorked).toBe(false) // ✅
163+
// alice learns that eve should kick rocks
164+
const { message: aliceMessage } = await eventPromise(alice.authProvider, 'localError')
165+
expect(aliceMessage).toBe("The peer's invitation wasn't accepted")
166+
167+
// eve gets told to kick rocks
168+
const { message: eveMessage } = await eventPromise(eve.authProvider, 'remoteError')
169+
expect(eveMessage).toBe("Your invitation wasn't accepted")
166170

167171
teardown()
168172
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { describe, it, expect } from 'vitest'
2+
import { buildServerUrl } from '../buildServerUrl.js'
3+
4+
describe('buildServerUrl', () => {
5+
it('should prepend http:// when no protocol is provided', () => {
6+
const { protocol, hostname } = buildServerUrl('example.com')
7+
expect(protocol).toBe('http:')
8+
expect(hostname).toBe('example.com')
9+
})
10+
11+
it('should not prepend http:// when https protocol is provided', () => {
12+
const { protocol, hostname } = buildServerUrl('https://example.com')
13+
expect(protocol).toBe('https:')
14+
expect(hostname).toBe('example.com')
15+
})
16+
17+
it('should not prepend http:// when http protocol is provided', () => {
18+
const { protocol, hostname } = buildServerUrl('http://example.com')
19+
expect(protocol).toBe('http:')
20+
expect(hostname).toBe('example.com')
21+
})
22+
23+
it('should handle hosts with ports', () => {
24+
const { protocol, hostname, port } = buildServerUrl('example.com:8080')
25+
expect(protocol).toBe('http:')
26+
expect(hostname).toBe('example.com')
27+
expect(port).toBe('8080')
28+
})
29+
30+
it('should handle localhost', () => {
31+
const { protocol, hostname, port } = buildServerUrl('localhost:3000')
32+
expect(protocol).toBe('http:')
33+
expect(hostname).toBe('localhost')
34+
expect(port).toBe('3000')
35+
})
36+
})

packages/auth-syncserver/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
],
1717
"scripts": {
1818
"build": "tsup",
19-
"postbuild": "tsc -p tsconfig.build.json --emitDeclarationOnly && cp src/*.html dist",
19+
"postbuild": "tsc -p tsconfig.build.json --emitDeclarationOnly && shx cp src/*.html dist",
2020
"test": "vitest",
2121
"test:log": "cross-env DEBUG='localfirst*' DEBUG_COLORS=1 vitest --reporter basic"
2222
},

packages/auth/src/connection/Connection.ts

+11-4
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
TIMEOUT,
2525
createErrorMessage,
2626
type ConnectionErrorType,
27+
UNHANDLED,
2728
} from 'connection/errors.js'
2829
import { getDeviceUserFromGraph } from 'connection/getDeviceUserFromGraph.js'
2930
import * as identity from 'connection/identity.js'
@@ -694,10 +695,16 @@ export class Connection extends EventEmitter<ConnectionEvents> {
694695
this.#machine = createActor(machine)
695696

696697
// emit and log all transitions
697-
this.#machine.subscribe(state => {
698-
const summary = stateSummary(state.value as string)
699-
this.emit('change', summary)
700-
this.#log(`⏩ ${summary} `)
698+
this.#machine.subscribe({
699+
next: state => {
700+
const summary = stateSummary(state.value as string)
701+
this.emit('change', summary)
702+
this.#log(`⏩ ${summary} `)
703+
},
704+
error: error => {
705+
console.error('Connection encountered an unhandled error', error)
706+
this.#fail(UNHANDLED)
707+
},
701708
})
702709

703710
// add automatic logging to all events

packages/auth/src/connection/errors.ts

+4
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export const MEMBER_REMOVED = 'MEMBER_REMOVED' as const
88
export const NEITHER_IS_MEMBER = 'NEITHER_IS_MEMBER' as const
99
export const SERVER_REMOVED = 'SERVER_REMOVED' as const
1010
export const TIMEOUT = 'TIMEOUT' as const
11+
export const UNHANDLED = 'UNHANDLED' as const
1112

1213
export const connectionErrors: Record<string, ErrorDefinition> = {
1314
[DEVICE_REMOVED]: {
@@ -48,6 +49,9 @@ export const connectionErrors: Record<string, ErrorDefinition> = {
4849
localMessage: "We didn't hear back from the peer; giving up",
4950
remoteMessage: "The peer didn't hear back from you, so they gave up",
5051
},
52+
[UNHANDLED]: {
53+
localMessage: 'An unhandled error occurred',
54+
},
5155
}
5256

5357
/** Creates an error payload with an appropriate message for the local or remote user */

0 commit comments

Comments
 (0)