@livestore/sync-cf 0.2.0 → 0.3.0-dev.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.tsbuildinfo +1 -1
- package/dist/cf-worker/durable-object.d.ts +40 -34
- package/dist/cf-worker/durable-object.d.ts.map +1 -1
- package/dist/cf-worker/durable-object.js +158 -123
- package/dist/cf-worker/durable-object.js.map +1 -1
- package/dist/cf-worker/index.d.ts +1 -6
- package/dist/cf-worker/index.d.ts.map +1 -1
- package/dist/cf-worker/index.js +30 -64
- package/dist/cf-worker/index.js.map +1 -1
- package/dist/cf-worker/make-worker.d.ts +6 -0
- package/dist/cf-worker/make-worker.d.ts.map +1 -0
- package/dist/cf-worker/make-worker.js +31 -0
- package/dist/cf-worker/make-worker.js.map +1 -0
- package/dist/cf-worker/mod.d.ts +3 -0
- package/dist/cf-worker/mod.d.ts.map +1 -0
- package/dist/cf-worker/mod.js +3 -0
- package/dist/cf-worker/mod.js.map +1 -0
- package/dist/cf-worker/types.d.ts +2 -0
- package/dist/cf-worker/types.d.ts.map +1 -0
- package/dist/cf-worker/types.js +2 -0
- package/dist/cf-worker/types.js.map +1 -0
- package/dist/cf-worker/worker.d.ts +6 -0
- package/dist/cf-worker/worker.d.ts.map +1 -0
- package/dist/cf-worker/worker.js +29 -0
- package/dist/cf-worker/worker.js.map +1 -0
- package/dist/common/mod.d.ts +2 -0
- package/dist/common/mod.d.ts.map +1 -0
- package/dist/common/mod.js +2 -0
- package/dist/common/mod.js.map +1 -0
- package/dist/common/ws-message-types.d.ts +92 -216
- package/dist/common/ws-message-types.d.ts.map +1 -1
- package/dist/common/ws-message-types.js +13 -9
- package/dist/common/ws-message-types.js.map +1 -1
- package/dist/sync-impl/mod.d.ts +2 -0
- package/dist/sync-impl/mod.d.ts.map +1 -0
- package/dist/sync-impl/mod.js +2 -0
- package/dist/sync-impl/mod.js.map +1 -0
- package/dist/sync-impl/ws-impl.d.ts +5 -13
- package/dist/sync-impl/ws-impl.d.ts.map +1 -1
- package/dist/sync-impl/ws-impl.js +69 -69
- package/dist/sync-impl/ws-impl.js.map +1 -1
- package/package.json +17 -8
- package/src/cf-worker/durable-object.ts +233 -153
- package/src/cf-worker/mod.ts +2 -0
- package/src/cf-worker/worker.ts +39 -0
- package/src/common/ws-message-types.ts +18 -9
- package/src/sync-impl/ws-impl.ts +120 -121
- package/.netlify/state.json +0 -3
- package/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/8ee300993f9a2c909aede59646369fa0cc28d25304b9b9af94f54d4543ad60f4.sqlite +0 -0
- package/.wrangler/state/v3/do/websocket-server-WebSocketServer/2c3676d859102e448b271e1b4336b1e40778992b14728e2a2bd642c0f225e9c5.sqlite +0 -0
- package/.wrangler/state/v3/do/websocket-server-WebSocketServer/6eb1111cecdc6ae7e3a38c3d7db0f3ec7de5b318e682cdcf0ff0dc881cbbcee7.sqlite +0 -0
- package/.wrangler/state/v3/do/websocket-server-WebSocketServer/c4aa0e61dcd784604bee52282348e0538ab69ec76e1652322bbccd539d9ff3ee.sqlite +0 -0
- package/.wrangler/state/v3/do/websocket-server-WebSocketServer/d87433156774c933da83e962eba16107cf02ade2f133d7205963558fe47db3ff.sqlite +0 -0
- package/.wrangler/tmp/dev-33iU4b/index.js +0 -42167
- package/.wrangler/tmp/dev-33iU4b/index.js.map +0 -8
- package/.wrangler/tmp/dev-9rcIR8/index.js +0 -18887
- package/.wrangler/tmp/dev-9rcIR8/index.js.map +0 -8
- package/.wrangler/tmp/dev-rI63Kk/index.js +0 -42165
- package/.wrangler/tmp/dev-rI63Kk/index.js.map +0 -8
- package/.wrangler/tmp/dev-txPodK/index.js +0 -42118
- package/.wrangler/tmp/dev-txPodK/index.js.map +0 -8
- package/src/cf-worker/index.ts +0 -84
- package/tsconfig.json +0 -12
- package/wrangler.toml +0 -21
- /package/src/common/{index.ts → mod.ts} +0 -0
- /package/src/sync-impl/{index.ts → mod.ts} +0 -0
package/src/sync-impl/ws-impl.ts
CHANGED
|
@@ -1,82 +1,70 @@
|
|
|
1
1
|
/// <reference lib="dom" />
|
|
2
2
|
|
|
3
|
-
import type { SyncBackend
|
|
3
|
+
import type { SyncBackend } from '@livestore/common'
|
|
4
4
|
import { InvalidPullError, InvalidPushError } from '@livestore/common'
|
|
5
|
+
import { pick } from '@livestore/utils'
|
|
5
6
|
import type { Scope } from '@livestore/utils/effect'
|
|
6
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
Deferred,
|
|
9
|
+
Effect,
|
|
10
|
+
Option,
|
|
11
|
+
PubSub,
|
|
12
|
+
Queue,
|
|
13
|
+
Schedule,
|
|
14
|
+
Schema,
|
|
15
|
+
Stream,
|
|
16
|
+
SubscriptionRef,
|
|
17
|
+
WebSocket,
|
|
18
|
+
} from '@livestore/utils/effect'
|
|
7
19
|
import { nanoid } from '@livestore/utils/nanoid'
|
|
8
20
|
|
|
9
|
-
import { WSMessage } from '../common/
|
|
21
|
+
import { WSMessage } from '../common/mod.js'
|
|
22
|
+
import type { SyncMetadata } from '../common/ws-message-types.js'
|
|
10
23
|
|
|
11
|
-
export interface WsSyncOptions
|
|
12
|
-
type: 'cf'
|
|
24
|
+
export interface WsSyncOptions {
|
|
13
25
|
url: string
|
|
14
|
-
|
|
26
|
+
storeId: string
|
|
15
27
|
}
|
|
16
28
|
|
|
17
|
-
|
|
18
|
-
syncBackend: WsSyncOptions
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
declare global {
|
|
22
|
-
interface LiveStoreGlobal extends LiveStoreGlobalCf {}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export const makeWsSync = (options: WsSyncOptions): Effect.Effect<SyncBackend<null>, never, Scope.Scope> =>
|
|
29
|
+
export const makeWsSync = (options: WsSyncOptions): Effect.Effect<SyncBackend<SyncMetadata>, never, Scope.Scope> =>
|
|
26
30
|
Effect.gen(function* () {
|
|
27
|
-
|
|
31
|
+
// TODO also allow for auth scenarios
|
|
32
|
+
const wsUrl = `${options.url}/websocket?storeId=${options.storeId}`
|
|
28
33
|
|
|
29
34
|
const { isConnected, incomingMessages, send } = yield* connect(wsUrl)
|
|
30
35
|
|
|
31
|
-
const metadata = Option.none()
|
|
32
|
-
|
|
33
36
|
const api = {
|
|
34
37
|
isConnected,
|
|
35
|
-
pull: (args
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
Stream.tap((_) =>
|
|
66
|
-
_._tag === 'WSMessage.Error' ? new InvalidPullError({ message: _.message }) : Effect.void,
|
|
67
|
-
),
|
|
68
|
-
Stream.filter(Schema.is(WSMessage.PullRes)),
|
|
69
|
-
Stream.takeUntil((_) => _.hasMore === false),
|
|
70
|
-
Stream.map((_) => _.events),
|
|
71
|
-
Stream.flattenIterables,
|
|
72
|
-
Stream.map((mutationEventEncoded) => ({
|
|
73
|
-
mutationEventEncoded,
|
|
74
|
-
metadata,
|
|
75
|
-
persisted: true,
|
|
76
|
-
})),
|
|
77
|
-
)
|
|
78
|
-
}).pipe(Stream.unwrap),
|
|
79
|
-
push: (mutationEventEncoded, persisted) =>
|
|
38
|
+
pull: (args) =>
|
|
39
|
+
Effect.gen(function* () {
|
|
40
|
+
const requestId = nanoid()
|
|
41
|
+
const cursor = Option.getOrUndefined(args)?.cursor.global
|
|
42
|
+
|
|
43
|
+
yield* send(WSMessage.PullReq.make({ cursor, requestId }))
|
|
44
|
+
|
|
45
|
+
return Stream.fromPubSub(incomingMessages).pipe(
|
|
46
|
+
Stream.filter((_) => (_._tag === 'WSMessage.PullRes' ? _.requestId === requestId : true)),
|
|
47
|
+
Stream.tap((_) =>
|
|
48
|
+
_._tag === 'WSMessage.Error' && _.requestId === requestId
|
|
49
|
+
? new InvalidPullError({ message: _.message })
|
|
50
|
+
: Effect.void,
|
|
51
|
+
),
|
|
52
|
+
Stream.filter(Schema.is(Schema.Union(WSMessage.PushBroadcast, WSMessage.PullRes))),
|
|
53
|
+
Stream.map((msg) =>
|
|
54
|
+
msg._tag === 'WSMessage.PushBroadcast'
|
|
55
|
+
? { batch: [pick(msg, ['mutationEventEncoded', 'metadata'])], remaining: 0 }
|
|
56
|
+
: {
|
|
57
|
+
batch: msg.events.map(({ mutationEventEncoded, metadata }) => ({
|
|
58
|
+
mutationEventEncoded,
|
|
59
|
+
metadata,
|
|
60
|
+
})),
|
|
61
|
+
remaining: msg.remaining,
|
|
62
|
+
},
|
|
63
|
+
),
|
|
64
|
+
)
|
|
65
|
+
}).pipe(Stream.unwrap),
|
|
66
|
+
|
|
67
|
+
push: (batch) =>
|
|
80
68
|
Effect.gen(function* () {
|
|
81
69
|
const ready = yield* Deferred.make<void, InvalidPushError>()
|
|
82
70
|
const requestId = nanoid()
|
|
@@ -85,11 +73,12 @@ export const makeWsSync = (options: WsSyncOptions): Effect.Effect<SyncBackend<nu
|
|
|
85
73
|
Stream.filter((_) => _._tag !== 'WSMessage.PushBroadcast' && _.requestId === requestId),
|
|
86
74
|
Stream.tap((_) =>
|
|
87
75
|
_._tag === 'WSMessage.Error'
|
|
88
|
-
? Deferred.fail(ready, new InvalidPushError({ message: _.message }))
|
|
76
|
+
? Deferred.fail(ready, new InvalidPushError({ reason: { _tag: 'Unexpected', message: _.message } }))
|
|
89
77
|
: Effect.void,
|
|
90
78
|
),
|
|
91
79
|
Stream.filter(Schema.is(WSMessage.PushAck)),
|
|
92
|
-
|
|
80
|
+
// TODO bring back filterting of "own events"
|
|
81
|
+
// Stream.filter((_) => _.mutationId === mutationEventEncoded.id.global),
|
|
93
82
|
Stream.take(1),
|
|
94
83
|
Stream.tap(() => Deferred.succeed(ready, void 0)),
|
|
95
84
|
Stream.runDrain,
|
|
@@ -97,13 +86,15 @@ export const makeWsSync = (options: WsSyncOptions): Effect.Effect<SyncBackend<nu
|
|
|
97
86
|
Effect.fork,
|
|
98
87
|
)
|
|
99
88
|
|
|
100
|
-
yield* send(WSMessage.PushReq.make({
|
|
89
|
+
yield* send(WSMessage.PushReq.make({ batch, requestId }))
|
|
101
90
|
|
|
102
|
-
yield*
|
|
91
|
+
yield* ready
|
|
103
92
|
|
|
104
|
-
|
|
93
|
+
const createdAt = new Date().toISOString()
|
|
94
|
+
|
|
95
|
+
return { metadata: Array.from({ length: batch.length }, () => Option.some({ createdAt })) }
|
|
105
96
|
}),
|
|
106
|
-
} satisfies SyncBackend<
|
|
97
|
+
} satisfies SyncBackend<SyncMetadata>
|
|
107
98
|
|
|
108
99
|
return api
|
|
109
100
|
})
|
|
@@ -111,9 +102,11 @@ export const makeWsSync = (options: WsSyncOptions): Effect.Effect<SyncBackend<nu
|
|
|
111
102
|
const connect = (wsUrl: string) =>
|
|
112
103
|
Effect.gen(function* () {
|
|
113
104
|
const isConnected = yield* SubscriptionRef.make(false)
|
|
114
|
-
const
|
|
105
|
+
const socketRef: { current: globalThis.WebSocket | undefined } = { current: undefined }
|
|
115
106
|
|
|
116
|
-
const incomingMessages = yield* PubSub.unbounded<Exclude<WSMessage.BackendToClientMessage, WSMessage.Pong>>()
|
|
107
|
+
const incomingMessages = yield* PubSub.unbounded<Exclude<WSMessage.BackendToClientMessage, WSMessage.Pong>>().pipe(
|
|
108
|
+
Effect.acquireRelease(PubSub.shutdown),
|
|
109
|
+
)
|
|
117
110
|
|
|
118
111
|
const waitUntilOnline = isConnected.changes.pipe(Stream.filter(Boolean), Stream.take(1), Stream.runDrain)
|
|
119
112
|
|
|
@@ -122,80 +115,86 @@ const connect = (wsUrl: string) =>
|
|
|
122
115
|
// Wait first until we're online
|
|
123
116
|
yield* waitUntilOnline
|
|
124
117
|
|
|
125
|
-
|
|
118
|
+
yield* Effect.spanEvent(
|
|
119
|
+
`Sending message: ${message._tag}`,
|
|
120
|
+
message._tag === 'WSMessage.PushReq'
|
|
121
|
+
? {
|
|
122
|
+
id: message.batch[0]!.id,
|
|
123
|
+
parentId: message.batch[0]!.parentId,
|
|
124
|
+
batchLength: message.batch.length,
|
|
125
|
+
}
|
|
126
|
+
: message._tag === 'WSMessage.PullReq'
|
|
127
|
+
? { cursor: message.cursor ?? '-' }
|
|
128
|
+
: {},
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
// TODO use MsgPack instead of JSON to speed up the serialization / reduce the size of the messages
|
|
132
|
+
socketRef.current!.send(Schema.encodeSync(Schema.parseJson(WSMessage.Message))(message))
|
|
126
133
|
})
|
|
127
134
|
|
|
128
135
|
const innerConnect = Effect.gen(function* () {
|
|
129
136
|
// If the browser already tells us we're offline, then we'll at least wait until the browser
|
|
130
137
|
// thinks we're online again. (We'll only know for sure once the WS conneciton is established.)
|
|
131
|
-
while (navigator.onLine === false) {
|
|
138
|
+
while (typeof navigator !== 'undefined' && navigator.onLine === false) {
|
|
132
139
|
yield* Effect.sleep(1000)
|
|
133
140
|
}
|
|
134
141
|
// if (navigator.onLine === false) {
|
|
135
142
|
// yield* Effect.async((cb) => self.addEventListener('online', () => cb(Effect.void)))
|
|
136
143
|
// }
|
|
137
144
|
|
|
138
|
-
const
|
|
145
|
+
const socket = yield* WebSocket.makeWebSocket({ url: wsUrl, reconnect: Schedule.exponential(100) })
|
|
146
|
+
|
|
147
|
+
yield* SubscriptionRef.set(isConnected, true)
|
|
148
|
+
socketRef.current = socket
|
|
149
|
+
|
|
139
150
|
const connectionClosed = yield* Deferred.make<void>()
|
|
140
151
|
|
|
141
|
-
const pongMessages = yield* Queue.unbounded<WSMessage.Pong>()
|
|
152
|
+
const pongMessages = yield* Queue.unbounded<WSMessage.Pong>().pipe(Effect.acquireRelease(Queue.shutdown))
|
|
142
153
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
154
|
+
yield* Effect.eventListener(socket, 'message', (event: MessageEvent) =>
|
|
155
|
+
Effect.gen(function* () {
|
|
156
|
+
const decodedEventRes = Schema.decodeUnknownEither(Schema.parseJson(WSMessage.BackendToClientMessage))(
|
|
157
|
+
event.data,
|
|
158
|
+
)
|
|
147
159
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
} else {
|
|
152
|
-
if (decodedEventRes.right._tag === 'WSMessage.Pong') {
|
|
153
|
-
Queue.offer(pongMessages, decodedEventRes.right).pipe(Effect.runSync)
|
|
160
|
+
if (decodedEventRes._tag === 'Left') {
|
|
161
|
+
console.error('Sync: Invalid message received', decodedEventRes.left)
|
|
162
|
+
return
|
|
154
163
|
} else {
|
|
155
|
-
|
|
164
|
+
if (decodedEventRes.right._tag === 'WSMessage.Pong') {
|
|
165
|
+
yield* Queue.offer(pongMessages, decodedEventRes.right)
|
|
166
|
+
} else {
|
|
167
|
+
// yield* Effect.logDebug(`decodedEventRes: ${decodedEventRes.right._tag}`)
|
|
168
|
+
yield* PubSub.publish(incomingMessages, decodedEventRes.right)
|
|
169
|
+
}
|
|
156
170
|
}
|
|
157
|
-
}
|
|
158
|
-
|
|
171
|
+
}),
|
|
172
|
+
)
|
|
159
173
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
174
|
+
yield* Effect.eventListener(socket, 'close', () => Deferred.succeed(connectionClosed, void 0))
|
|
175
|
+
|
|
176
|
+
yield* Effect.eventListener(socket, 'error', () =>
|
|
177
|
+
Effect.gen(function* () {
|
|
178
|
+
socket.close(3000, 'Sync: WebSocket error')
|
|
179
|
+
yield* Deferred.succeed(connectionClosed, void 0)
|
|
180
|
+
}),
|
|
181
|
+
)
|
|
163
182
|
|
|
164
183
|
// NOTE it seems that this callback doesn't work reliably on a worker but only via `window.addEventListener`
|
|
165
184
|
// We might need to proxy the event from the main thread to the worker if we want this to work reliably.
|
|
166
|
-
|
|
185
|
+
// eslint-disable-next-line unicorn/prefer-global-this
|
|
186
|
+
if (typeof self !== 'undefined') {
|
|
187
|
+
// eslint-disable-next-line unicorn/prefer-global-this
|
|
188
|
+
yield* Effect.eventListener(self, 'offline', () => Deferred.succeed(connectionClosed, void 0))
|
|
189
|
+
}
|
|
167
190
|
|
|
168
191
|
yield* Effect.addFinalizer(() =>
|
|
169
192
|
Effect.gen(function* () {
|
|
170
|
-
|
|
171
|
-
self.removeEventListener('offline', offlineHandler)
|
|
172
|
-
wsRef.current?.close()
|
|
173
|
-
wsRef.current = undefined
|
|
193
|
+
socketRef.current = undefined
|
|
174
194
|
yield* SubscriptionRef.set(isConnected, false)
|
|
175
195
|
}),
|
|
176
196
|
)
|
|
177
197
|
|
|
178
|
-
ws.addEventListener('message', messageHandler)
|
|
179
|
-
|
|
180
|
-
if (ws.readyState === WebSocket.OPEN) {
|
|
181
|
-
wsRef.current = ws
|
|
182
|
-
SubscriptionRef.set(isConnected, true).pipe(Effect.runSync)
|
|
183
|
-
} else {
|
|
184
|
-
ws.addEventListener('open', () => {
|
|
185
|
-
wsRef.current = ws
|
|
186
|
-
SubscriptionRef.set(isConnected, true).pipe(Effect.runSync)
|
|
187
|
-
})
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
ws.addEventListener('close', () => {
|
|
191
|
-
Deferred.succeed(connectionClosed, void 0).pipe(Effect.runSync)
|
|
192
|
-
})
|
|
193
|
-
|
|
194
|
-
ws.addEventListener('error', () => {
|
|
195
|
-
ws.close()
|
|
196
|
-
Deferred.succeed(connectionClosed, void 0).pipe(Effect.runSync)
|
|
197
|
-
})
|
|
198
|
-
|
|
199
198
|
const checkPingPong = Effect.gen(function* () {
|
|
200
199
|
// TODO include pong latency infomation in network status
|
|
201
200
|
yield* send({ _tag: 'WSMessage.Ping', requestId: 'ping' })
|
|
@@ -204,7 +203,7 @@ const connect = (wsUrl: string) =>
|
|
|
204
203
|
yield* Queue.take(pongMessages).pipe(Effect.timeout(5000))
|
|
205
204
|
|
|
206
205
|
yield* Effect.sleep(25_000)
|
|
207
|
-
}).pipe(Effect.withSpan('@livestore/sync-cf:connect:checkPingPong'))
|
|
206
|
+
}).pipe(Effect.withSpan('@livestore/sync-cf:connect:checkPingPong'), Effect.ignore)
|
|
208
207
|
|
|
209
208
|
yield* waitUntilOnline.pipe(
|
|
210
209
|
Effect.andThen(checkPingPong.pipe(Effect.forever)),
|
|
@@ -212,10 +211,10 @@ const connect = (wsUrl: string) =>
|
|
|
212
211
|
Effect.forkScoped,
|
|
213
212
|
)
|
|
214
213
|
|
|
215
|
-
yield*
|
|
216
|
-
}).pipe(Effect.scoped)
|
|
214
|
+
yield* connectionClosed
|
|
215
|
+
}).pipe(Effect.scoped, Effect.withSpan('@livestore/sync-cf:connect'))
|
|
217
216
|
|
|
218
|
-
yield* innerConnect.pipe(Effect.forever, Effect.tapCauseLogPretty, Effect.forkScoped)
|
|
217
|
+
yield* innerConnect.pipe(Effect.forever, Effect.interruptible, Effect.tapCauseLogPretty, Effect.forkScoped)
|
|
219
218
|
|
|
220
219
|
return { isConnected, incomingMessages, send }
|
|
221
220
|
})
|
package/.netlify/state.json
DELETED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|