@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.
@@ -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 makeWsSync = (options: WsSyncOptions): Effect.Effect<SyncBackend<SyncMetadata>, never, Scope.Scope> =>
31
- Effect.gen(function* () {
32
- // TODO also allow for auth scenarios
33
- const wsUrl = `${options.url}/websocket?storeId=${options.storeId}`
34
-
35
- const { isConnected, incomingMessages, send } = yield* connect(wsUrl)
36
-
37
- /**
38
- * We need to account for the scenario where push-caused PullRes message arrive before the pull-caused PullRes message.
39
- * i.e. a scenario where the WS connection is created but before the server processed the initial pull, a push from
40
- * another client triggers a PullRes message sent to this client which we need to stash until our pull-caused
41
- * PullRes message arrives at which point we can combine the stashed events with the pull-caused events and continue.
42
- */
43
- const stashedPullBatch: WSMessage.PullRes['batch'][number][] = []
44
-
45
- // We currently only support one pull stream for a sync backend.
46
- let pullStarted = false
47
-
48
- const api = {
49
- isConnected,
50
- pull: (args) =>
51
- Effect.gen(function* () {
52
- if (pullStarted) {
53
- return shouldNeverHappen(`Pull already started for this sync backend.`)
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
- pullStarted = true
57
-
58
- let pullResponseReceived = false
59
-
60
- const requestId = nanoid()
61
- const cursor = Option.getOrUndefined(args)?.cursor.global
62
-
63
- yield* send(WSMessage.PullReq.make({ cursor, requestId }))
64
-
65
- return Stream.fromPubSub(incomingMessages).pipe(
66
- Stream.tap((_) =>
67
- _._tag === 'WSMessage.Error' && _.requestId === requestId
68
- ? new InvalidPullError({ message: _.message })
69
- : Effect.void,
70
- ),
71
- Stream.filterMap((msg) => {
72
- if (msg._tag === 'WSMessage.PullRes') {
73
- if (msg.requestId.context === 'pull') {
74
- if (msg.requestId.requestId === requestId) {
75
- pullResponseReceived = true
76
-
77
- if (stashedPullBatch.length > 0 && msg.remaining === 0) {
78
- const pullResHead = msg.batch.at(-1)?.mutationEventEncoded.id ?? EventId.ROOT.global
79
- // Index where stashed events are greater than pullResHead
80
- const newPartialBatchIndex = stashedPullBatch.findIndex(
81
- (batchItem) => batchItem.mutationEventEncoded.id > pullResHead,
82
- )
83
- const batchWithNewStashedEvents =
84
- newPartialBatchIndex === -1 ? [] : stashedPullBatch.slice(newPartialBatchIndex)
85
- const combinedBatch = [...msg.batch, ...batchWithNewStashedEvents]
86
- return Option.some({ ...msg, batch: combinedBatch, remaining: 0 })
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
- return Option.some(msg)
97
+ // Ignore
98
+ return Option.none()
89
99
  }
90
100
  } else {
91
- // Ignore
92
- return Option.none()
93
- }
94
- } else {
95
- if (pullResponseReceived) {
96
- return Option.some(msg)
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
- yield* send(WSMessage.PushReq.make({ batch, requestId }))
131
-
132
- yield* ready
133
-
134
- const createdAt = new Date().toISOString()
135
-
136
- return { metadata: Array.from({ length: batch.length }, () => Option.some({ createdAt })) }
137
- }),
138
- } satisfies SyncBackend<SyncMetadata>
139
-
140
- return api
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* () {