@livestore/sync-cf 0.3.0-dev.22 → 0.3.0-dev.24
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 +11 -2
- package/dist/cf-worker/durable-object.d.ts.map +1 -1
- package/dist/cf-worker/durable-object.js +8 -6
- package/dist/cf-worker/durable-object.js.map +1 -1
- package/dist/cf-worker/worker.d.ts +7 -1
- package/dist/cf-worker/worker.d.ts.map +1 -1
- package/dist/cf-worker/worker.js +37 -8
- package/dist/cf-worker/worker.js.map +1 -1
- package/dist/common/mod.d.ts +5 -0
- package/dist/common/mod.d.ts.map +1 -1
- package/dist/common/mod.js +5 -0
- package/dist/common/mod.js.map +1 -1
- package/dist/common/ws-message-types.d.ts +105 -33
- package/dist/common/ws-message-types.d.ts.map +1 -1
- package/dist/common/ws-message-types.js +13 -13
- package/dist/common/ws-message-types.js.map +1 -1
- package/dist/sync-impl/ws-impl.d.ts +2 -5
- package/dist/sync-impl/ws-impl.d.ts.map +1 -1
- package/dist/sync-impl/ws-impl.js +16 -6
- package/dist/sync-impl/ws-impl.js.map +1 -1
- package/package.json +3 -3
- package/src/cf-worker/durable-object.ts +8 -6
- package/src/cf-worker/worker.ts +74 -28
- package/src/common/mod.ts +7 -0
- package/src/common/ws-message-types.ts +14 -13
- package/src/sync-impl/ws-impl.ts +122 -110
package/src/sync-impl/ws-impl.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/// <reference lib="dom" />
|
|
2
2
|
|
|
3
|
-
import type { SyncBackend } from '@livestore/common'
|
|
4
|
-
import { InvalidPullError, InvalidPushError } from '@livestore/common'
|
|
3
|
+
import type { SyncBackend, SyncBackendConstructor } from '@livestore/common'
|
|
4
|
+
import { InvalidPullError, InvalidPushError, UnexpectedError } from '@livestore/common'
|
|
5
5
|
import { EventId } from '@livestore/common/schema'
|
|
6
6
|
import { LS_DEV, shouldNeverHappen } from '@livestore/utils'
|
|
7
|
-
import type { Scope } from '@livestore/utils/effect'
|
|
8
7
|
import {
|
|
9
8
|
Deferred,
|
|
10
9
|
Effect,
|
|
@@ -15,130 +14,143 @@ import {
|
|
|
15
14
|
Schema,
|
|
16
15
|
Stream,
|
|
17
16
|
SubscriptionRef,
|
|
17
|
+
UrlParams,
|
|
18
18
|
WebSocket,
|
|
19
19
|
} from '@livestore/utils/effect'
|
|
20
20
|
import { nanoid } from '@livestore/utils/nanoid'
|
|
21
21
|
|
|
22
|
-
import { WSMessage } from '../common/mod.js'
|
|
22
|
+
import { SearchParamsSchema, WSMessage } from '../common/mod.js'
|
|
23
23
|
import type { SyncMetadata } from '../common/ws-message-types.js'
|
|
24
24
|
|
|
25
25
|
export interface WsSyncOptions {
|
|
26
26
|
url: string
|
|
27
|
-
storeId: string
|
|
28
27
|
}
|
|
29
28
|
|
|
30
|
-
export const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
29
|
+
export const makeCfSync =
|
|
30
|
+
(options: WsSyncOptions): SyncBackendConstructor<SyncMetadata> =>
|
|
31
|
+
({ storeId, payload }) =>
|
|
32
|
+
Effect.gen(function* () {
|
|
33
|
+
const urlParamsData = yield* Schema.encode(SearchParamsSchema)({
|
|
34
|
+
storeId,
|
|
35
|
+
payload,
|
|
36
|
+
}).pipe(UnexpectedError.mapToUnexpectedError)
|
|
37
|
+
|
|
38
|
+
const urlParams = UrlParams.fromInput(urlParamsData)
|
|
39
|
+
const wsUrl = `${options.url}/websocket?${UrlParams.toString(urlParams)}`
|
|
40
|
+
|
|
41
|
+
const { isConnected, incomingMessages, send } = yield* connect(wsUrl)
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* We need to account for the scenario where push-caused PullRes message arrive before the pull-caused PullRes message.
|
|
45
|
+
* i.e. a scenario where the WS connection is created but before the server processed the initial pull, a push from
|
|
46
|
+
* another client triggers a PullRes message sent to this client which we need to stash until our pull-caused
|
|
47
|
+
* PullRes message arrives at which point we can combine the stashed events with the pull-caused events and continue.
|
|
48
|
+
*/
|
|
49
|
+
const stashedPullBatch: WSMessage.PullRes['batch'][number][] = []
|
|
50
|
+
|
|
51
|
+
// We currently only support one pull stream for a sync backend.
|
|
52
|
+
let pullStarted = false
|
|
53
|
+
|
|
54
|
+
const api = {
|
|
55
|
+
isConnected,
|
|
56
|
+
pull: (args) =>
|
|
57
|
+
Effect.gen(function* () {
|
|
58
|
+
if (pullStarted) {
|
|
59
|
+
return shouldNeverHappen(`Pull already started for this sync backend.`)
|
|
60
|
+
}
|
|
55
61
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
62
|
+
pullStarted = true
|
|
63
|
+
|
|
64
|
+
let pullResponseReceived = false
|
|
65
|
+
|
|
66
|
+
const requestId = nanoid()
|
|
67
|
+
const cursor = Option.getOrUndefined(args)?.cursor.global
|
|
68
|
+
|
|
69
|
+
yield* send(WSMessage.PullReq.make({ cursor, requestId }))
|
|
70
|
+
|
|
71
|
+
return Stream.fromPubSub(incomingMessages).pipe(
|
|
72
|
+
Stream.tap((_) =>
|
|
73
|
+
_._tag === 'WSMessage.Error' && _.requestId === requestId
|
|
74
|
+
? new InvalidPullError({ message: _.message })
|
|
75
|
+
: Effect.void,
|
|
76
|
+
),
|
|
77
|
+
Stream.filterMap((msg) => {
|
|
78
|
+
if (msg._tag === 'WSMessage.PullRes') {
|
|
79
|
+
if (msg.requestId.context === 'pull') {
|
|
80
|
+
if (msg.requestId.requestId === requestId) {
|
|
81
|
+
pullResponseReceived = true
|
|
82
|
+
|
|
83
|
+
if (stashedPullBatch.length > 0 && msg.remaining === 0) {
|
|
84
|
+
const pullResHead = msg.batch.at(-1)?.mutationEventEncoded.id ?? EventId.ROOT.global
|
|
85
|
+
// Index where stashed events are greater than pullResHead
|
|
86
|
+
const newPartialBatchIndex = stashedPullBatch.findIndex(
|
|
87
|
+
(batchItem) => batchItem.mutationEventEncoded.id > pullResHead,
|
|
88
|
+
)
|
|
89
|
+
const batchWithNewStashedEvents =
|
|
90
|
+
newPartialBatchIndex === -1 ? [] : stashedPullBatch.slice(newPartialBatchIndex)
|
|
91
|
+
const combinedBatch = [...msg.batch, ...batchWithNewStashedEvents]
|
|
92
|
+
return Option.some({ ...msg, batch: combinedBatch, remaining: 0 })
|
|
93
|
+
} else {
|
|
94
|
+
return Option.some(msg)
|
|
95
|
+
}
|
|
87
96
|
} else {
|
|
88
|
-
|
|
97
|
+
// Ignore
|
|
98
|
+
return Option.none()
|
|
89
99
|
}
|
|
90
100
|
} else {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
} else {
|
|
98
|
-
stashedPullBatch.push(...msg.batch)
|
|
99
|
-
return Option.none()
|
|
101
|
+
if (pullResponseReceived) {
|
|
102
|
+
return Option.some(msg)
|
|
103
|
+
} else {
|
|
104
|
+
stashedPullBatch.push(...msg.batch)
|
|
105
|
+
return Option.none()
|
|
106
|
+
}
|
|
100
107
|
}
|
|
101
108
|
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return Option.none()
|
|
105
|
-
}),
|
|
106
|
-
// This call is mostly here to for type narrowing
|
|
107
|
-
Stream.filter(Schema.is(WSMessage.PullRes)),
|
|
108
|
-
)
|
|
109
|
-
}).pipe(Stream.unwrap),
|
|
110
|
-
|
|
111
|
-
push: (batch) =>
|
|
112
|
-
Effect.gen(function* () {
|
|
113
|
-
const ready = yield* Deferred.make<void, InvalidPushError>()
|
|
114
|
-
const requestId = nanoid()
|
|
115
|
-
|
|
116
|
-
yield* Stream.fromPubSub(incomingMessages).pipe(
|
|
117
|
-
Stream.tap((_) =>
|
|
118
|
-
_._tag === 'WSMessage.Error' && _.requestId === requestId
|
|
119
|
-
? Deferred.fail(ready, new InvalidPushError({ reason: { _tag: 'Unexpected', message: _.message } }))
|
|
120
|
-
: Effect.void,
|
|
121
|
-
),
|
|
122
|
-
Stream.filter((_) => _._tag === 'WSMessage.PushAck' && _.requestId === requestId),
|
|
123
|
-
Stream.take(1),
|
|
124
|
-
Stream.tap(() => Deferred.succeed(ready, void 0)),
|
|
125
|
-
Stream.runDrain,
|
|
126
|
-
Effect.tapCauseLogPretty,
|
|
127
|
-
Effect.fork,
|
|
128
|
-
)
|
|
129
109
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
110
|
+
return Option.none()
|
|
111
|
+
}),
|
|
112
|
+
// This call is mostly here to for type narrowing
|
|
113
|
+
Stream.filter(Schema.is(WSMessage.PullRes)),
|
|
114
|
+
)
|
|
115
|
+
}).pipe(Stream.unwrap),
|
|
116
|
+
|
|
117
|
+
push: (batch) =>
|
|
118
|
+
Effect.gen(function* () {
|
|
119
|
+
const ready = yield* Deferred.make<void, InvalidPushError>()
|
|
120
|
+
const requestId = nanoid()
|
|
121
|
+
|
|
122
|
+
yield* Stream.fromPubSub(incomingMessages).pipe(
|
|
123
|
+
Stream.tap((_) =>
|
|
124
|
+
_._tag === 'WSMessage.Error' && _.requestId === requestId
|
|
125
|
+
? Deferred.fail(ready, new InvalidPushError({ reason: { _tag: 'Unexpected', message: _.message } }))
|
|
126
|
+
: Effect.void,
|
|
127
|
+
),
|
|
128
|
+
Stream.filter((_) => _._tag === 'WSMessage.PushAck' && _.requestId === requestId),
|
|
129
|
+
Stream.take(1),
|
|
130
|
+
Stream.tap(() => Deferred.succeed(ready, void 0)),
|
|
131
|
+
Stream.runDrain,
|
|
132
|
+
Effect.tapCauseLogPretty,
|
|
133
|
+
Effect.fork,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
yield* send(WSMessage.PushReq.make({ batch, requestId }))
|
|
137
|
+
|
|
138
|
+
yield* ready
|
|
139
|
+
|
|
140
|
+
const createdAt = new Date().toISOString()
|
|
141
|
+
|
|
142
|
+
return { metadata: Array.from({ length: batch.length }, () => Option.some({ createdAt })) }
|
|
143
|
+
}),
|
|
144
|
+
metadata: {
|
|
145
|
+
name: '@livestore/cf-sync',
|
|
146
|
+
description: 'LiveStore sync backend implementation using Cloudflare Workers & Durable Objects',
|
|
147
|
+
protocol: 'ws',
|
|
148
|
+
url: options.url,
|
|
149
|
+
},
|
|
150
|
+
} satisfies SyncBackend<SyncMetadata>
|
|
151
|
+
|
|
152
|
+
return api
|
|
153
|
+
})
|
|
142
154
|
|
|
143
155
|
const connect = (wsUrl: string) =>
|
|
144
156
|
Effect.gen(function* () {
|