@livestore/adapter-cloudflare 0.0.0-snapshot-8452e32b7fbfc129741b253b9c853f866b52129f

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.
Files changed (50) hide show
  1. package/LICENSE +201 -0
  2. package/dist/.tsbuildinfo +1 -0
  3. package/dist/WebSocket.d.ts +14 -0
  4. package/dist/WebSocket.d.ts.map +1 -0
  5. package/dist/WebSocket.js +52 -0
  6. package/dist/WebSocket.js.map +1 -0
  7. package/dist/cf-types.d.ts +2 -0
  8. package/dist/cf-types.d.ts.map +1 -0
  9. package/dist/cf-types.js +2 -0
  10. package/dist/cf-types.js.map +1 -0
  11. package/dist/make-adapter.d.ts +9 -0
  12. package/dist/make-adapter.d.ts.map +1 -0
  13. package/dist/make-adapter.js +87 -0
  14. package/dist/make-adapter.js.map +1 -0
  15. package/dist/make-client-durable-object.d.ts +34 -0
  16. package/dist/make-client-durable-object.d.ts.map +1 -0
  17. package/dist/make-client-durable-object.js +25 -0
  18. package/dist/make-client-durable-object.js.map +1 -0
  19. package/dist/make-sqlite-db.d.ts +31 -0
  20. package/dist/make-sqlite-db.d.ts.map +1 -0
  21. package/dist/make-sqlite-db.js +194 -0
  22. package/dist/make-sqlite-db.js.map +1 -0
  23. package/dist/mod.d.ts +5 -0
  24. package/dist/mod.d.ts.map +1 -0
  25. package/dist/mod.js +4 -0
  26. package/dist/mod.js.map +1 -0
  27. package/dist/polyfill.d.ts +2 -0
  28. package/dist/polyfill.d.ts.map +1 -0
  29. package/dist/polyfill.js +40 -0
  30. package/dist/polyfill.js.map +1 -0
  31. package/dist/sync-provider-client.d.ts +12 -0
  32. package/dist/sync-provider-client.d.ts.map +1 -0
  33. package/dist/sync-provider-client.js +24 -0
  34. package/dist/sync-provider-client.js.map +1 -0
  35. package/dist/sync-provider-rpc-client.d.ts +2 -0
  36. package/dist/sync-provider-rpc-client.d.ts.map +1 -0
  37. package/dist/sync-provider-rpc-client.js +139 -0
  38. package/dist/sync-provider-rpc-client.js.map +1 -0
  39. package/dist/sync-provider-ws-client.d.ts +2 -0
  40. package/dist/sync-provider-ws-client.d.ts.map +1 -0
  41. package/dist/sync-provider-ws-client.js +40 -0
  42. package/dist/sync-provider-ws-client.js.map +1 -0
  43. package/package.json +38 -0
  44. package/src/WebSocket.ts +69 -0
  45. package/src/cf-types.ts +20 -0
  46. package/src/make-adapter.ts +144 -0
  47. package/src/make-client-durable-object.ts +91 -0
  48. package/src/make-sqlite-db.ts +261 -0
  49. package/src/mod.ts +12 -0
  50. package/src/polyfill.ts +44 -0
@@ -0,0 +1,24 @@
1
+ import { makeCfSync } from '@livestore/sync-cf';
2
+ import { Effect, Schedule } from '@livestore/utils/effect';
3
+ import { makeWebSocket } from "./WebSocket.js";
4
+ /**
5
+ * Specialized sync backend used for Cloudflare Workers compatible only with `@livestore/sync-cf`
6
+ */
7
+ export const makeSyncProviderClient = ({ durableObject }) => (args) => {
8
+ // Create a WebSocket factory that uses Cloudflare Durable Objects
9
+ const webSocketFactory = (wsUrl) => Effect.gen(function* () {
10
+ const url = new URL(wsUrl);
11
+ const socket = yield* makeWebSocket({
12
+ durableObject,
13
+ url,
14
+ reconnect: Schedule.exponential(100),
15
+ });
16
+ return socket;
17
+ });
18
+ // Use the unified ws-impl with the Cloudflare WebSocket factory
19
+ return makeCfSync({
20
+ url: 'https://unused.com', // URL is constructed internally by ws-impl
21
+ webSocketFactory,
22
+ })(args);
23
+ };
24
+ //# sourceMappingURL=sync-provider-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync-provider-client.js","sourceRoot":"","sources":["../src/sync-provider-client.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AAE/C,OAAO,EAAE,MAAM,EAAE,QAAQ,EAA8B,MAAM,yBAAyB,CAAA;AAEtF,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAO9C;;GAEG;AACH,MAAM,CAAC,MAAM,sBAAsB,GACjC,CAAC,EAAE,aAAa,EAAuC,EAAkD,EAAE,CAC3G,CAAC,IAAI,EAAE,EAAE;IACP,kEAAkE;IAClE,MAAM,gBAAgB,GAAG,CACvB,KAAa,EAC+D,EAAE,CAC9E,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;QAClB,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,CAAA;QAC1B,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,aAAa,CAAC;YAClC,aAAa;YACb,GAAG;YACH,SAAS,EAAE,QAAQ,CAAC,WAAW,CAAC,GAAG,CAAC;SACrC,CAAC,CAAA;QACF,OAAO,MAAyC,CAAA;IAClD,CAAC,CAAC,CAAA;IAEJ,gEAAgE;IAChE,OAAO,UAAU,CAAC;QAChB,GAAG,EAAE,oBAAoB,EAAE,2CAA2C;QACtE,gBAAgB;KACjB,CAAC,CAAC,IAAI,CAAC,CAAA;AACV,CAAC,CAAA"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=sync-provider-rpc-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync-provider-rpc-client.d.ts","sourceRoot":"","sources":["../src/sync-provider-rpc-client.ts"],"names":[],"mappings":""}
@@ -0,0 +1,139 @@
1
+ // import { InvalidPullError, InvalidPushError, SyncBackend } from '@livestore/common'
2
+ // import { layerProtocolDurableObject } from '@livestore/common-cf'
3
+ // import * as CfSyncBackend from '@livestore/sync-cf/cf-worker'
4
+ // import {
5
+ // Effect,
6
+ // Layer,
7
+ // Option,
8
+ // Queue,
9
+ // RpcClient,
10
+ // RpcSerialization,
11
+ // Stream,
12
+ // SubscriptionRef,
13
+ // } from '@livestore/utils/effect'
14
+ // import { nanoid } from '@livestore/utils/nanoid'
15
+ // import type * as CfWorker from './cf-types.ts'
16
+ export {};
17
+ // // Extended RPC interface for sync backend stub
18
+ // interface SyncBackendRpcStub extends CfWorker.DurableObjectStub, CfSyncBackend.SyncBackendRpcInterface {}
19
+ // export type MakeRpcSyncBackendOptions = {
20
+ // /** Sync backend Durable Object stub for RPC calls */
21
+ // syncBackendStub: SyncBackendRpcStub
22
+ // /** This client's ID for subscription callbacks */
23
+ // clientId: string
24
+ // /** This client's Durable Object ID for subscription callbacks */
25
+ // durableObjectId: string
26
+ // }
27
+ // /**
28
+ // * RPC-based sync client that uses direct Durable Object RPC calls
29
+ // * instead of WebSocket connections
30
+ // */
31
+ // export const makeRpcSyncProviderClient =
32
+ // ({
33
+ // syncBackendStub,
34
+ // clientId,
35
+ // durableObjectId,
36
+ // }: MakeRpcSyncBackendOptions): SyncBackend.SyncBackendConstructor<{ createdAt: string }> =>
37
+ // ({ storeId, payload }) =>
38
+ // Effect.gen(function* () {
39
+ // const isConnected = yield* SubscriptionRef.make(true)
40
+ // // PubSub for incoming messages from RPC callbacks
41
+ // const ProtocolLive = layerProtocolDurableObject((payload) => syncBackendStub.rpc(payload)).pipe(
42
+ // Layer.provide(RpcSerialization.layerJson),
43
+ // )
44
+ // const rpcClient = yield* RpcClient.make(CfSyncBackend.SyncDoRpc).pipe(Effect.provide(ProtocolLive))
45
+ // // Nothing to do here
46
+ // const connect = Effect.void
47
+ // const pull: SyncBackend.SyncBackend<{ createdAt: string }>['pull'] = (args) =>
48
+ // Effect.gen(function* () {
49
+ // const initialCursor = Option.getOrUndefined(args)?.cursor.global
50
+ // const cursorRef = { current: initialCursor }
51
+ // // const incomingMessages = yield* PubSub.unbounded<SyncBackend.PullResItem>()
52
+ // const messagesQueue =
53
+ // yield* Queue.unbounded<SyncBackend.PullResItem<{ createdAt: string }>>().pipe(
54
+ // // Effect.acquireRelease(Queue.shutdown),
55
+ // )
56
+ // const requestId = nanoid()
57
+ // const runPull = Effect.gen(function* () {
58
+ // yield* rpcClient.SyncDoRpc.Pull({
59
+ // requestId,
60
+ // cursor: cursorRef.current,
61
+ // storeId,
62
+ // }).pipe(
63
+ // Stream.mapError((cause) => new InvalidPullError({ cause })),
64
+ // Stream.map((_) => ({ batch: _.batch, remaining: _.remaining })),
65
+ // Stream.tap((msg) => Effect.log(`RPC pulled ${msg.batch.length} events from sync provider`)),
66
+ // Stream.tap((msg) =>
67
+ // Effect.sync(() => {
68
+ // if (msg.batch.length > 0) {
69
+ // cursorRef.current = msg.batch.at(-1)!.eventEncoded.seqNum
70
+ // }
71
+ // }),
72
+ // ),
73
+ // Stream.withSpan('rpc-sync-client:pull'),
74
+ // Stream.tapErrorCause((cause) => Effect.logError(cause)),
75
+ // Stream.tapChunk((msg) => Queue.offerAll(messagesQueue, msg)),
76
+ // Stream.runDrain,
77
+ // )
78
+ // })
79
+ // yield* rpcClient.SyncDoRpc.Subscribe({ clientId, storeId, requestId, durableObjectId, payload }).pipe(
80
+ // // yield* Stream.succeed('ok').pipe(
81
+ // // Stream.repeat(Schedule.spaced(1000)),
82
+ // Stream.tapLogWithLabel('rpc-sync-client:subscribe'),
83
+ // Stream.tap(() => runPull),
84
+ // Stream.onDone(() => Effect.log('rpc-sync-client:subscribe done')),
85
+ // Stream.onEnd(Effect.log('rpc-sync-client:subscribe end')),
86
+ // Stream.runDrain,
87
+ // Effect.tapCauseLogPretty,
88
+ // Effect.tap(() => Effect.log('rpc-sync-client:subscribe tap')),
89
+ // // Effect.forkScoped,
90
+ // Effect.fork,
91
+ // Effect.tapCauseLogPretty,
92
+ // )
93
+ // // setInterval(() => {
94
+ // // console.log('runPull')
95
+ // // }, 1000)
96
+ // yield* runPull
97
+ // yield* Effect.addFinalizerLog('rpc-sync-client:finalizer')
98
+ // // console.log('client do pull', args)
99
+ // // return Stream.empty
100
+ // return Stream.fromQueue(messagesQueue)
101
+ // }).pipe(Stream.unwrapScoped)
102
+ // const push: SyncBackend.SyncBackend<{ createdAt: string }>['push'] = (batch) =>
103
+ // Effect.gen(function* () {
104
+ // yield* Effect.log(`RPC Sync Client: Pushing ${batch.length} events`)
105
+ // if (batch.length === 0) {
106
+ // return
107
+ // }
108
+ // yield* rpcClient.SyncDoRpc.Push({ requestId: nanoid(), batch, storeId }).pipe(
109
+ // Effect.tapCauseLogPretty,
110
+ // Effect.mapError((cause) => new InvalidPushError({ reason: { _tag: 'Unexpected', cause } })),
111
+ // // Effect.orDie,
112
+ // )
113
+ // yield* Effect.log(`RPC Sync Client: Successfully pushed ${batch.length} events`)
114
+ // // console.log(`RPC Sync Client: Pushing ${batch.length} events`)
115
+ // // const pushReq = WSMessage.PushReq.make({
116
+ // // requestId: `rpc-push-${Date.now()}`,
117
+ // // batch,
118
+ // // })
119
+ // // // Direct RPC call to sync backend
120
+ // // // yield* Effect.tryPromise({
121
+ // // // try: () => syncBackendStub.push(pushReq),
122
+ // // // catch: (error) => new InvalidPushError({ reason: { _tag: 'Unexpected', message: String(error) } }),
123
+ // // // })
124
+ // // console.log(`RPC Sync Client: Successfully pushed ${batch.length} events`)
125
+ // }).pipe(Effect.withSpan('rpc-sync-client:push'))
126
+ // return SyncBackend.of({
127
+ // connect,
128
+ // isConnected,
129
+ // pull,
130
+ // push,
131
+ // metadata: {
132
+ // name: 'rpc-sync-client',
133
+ // description: 'Cloudflare Durable Object RPC Sync Client',
134
+ // protocol: 'rpc',
135
+ // storeId,
136
+ // },
137
+ // })
138
+ // }).pipe(Effect.withSpan('makeRpcSyncProviderClient'))
139
+ //# sourceMappingURL=sync-provider-rpc-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync-provider-rpc-client.js","sourceRoot":"","sources":["../src/sync-provider-rpc-client.ts"],"names":[],"mappings":"AAAA,sFAAsF;AACtF,oEAAoE;AACpE,gEAAgE;AAChE,WAAW;AACX,YAAY;AACZ,WAAW;AACX,YAAY;AACZ,WAAW;AACX,eAAe;AACf,sBAAsB;AACtB,YAAY;AACZ,qBAAqB;AACrB,mCAAmC;AACnC,mDAAmD;AACnD,iDAAiD;;AAEjD,kDAAkD;AAElD,4GAA4G;AAE5G,4CAA4C;AAC5C,0DAA0D;AAC1D,wCAAwC;AACxC,uDAAuD;AACvD,qBAAqB;AACrB,sEAAsE;AACtE,4BAA4B;AAC5B,IAAI;AAEJ,MAAM;AACN,qEAAqE;AACrE,sCAAsC;AACtC,MAAM;AACN,2CAA2C;AAC3C,OAAO;AACP,uBAAuB;AACvB,gBAAgB;AAChB,uBAAuB;AACvB,gGAAgG;AAChG,8BAA8B;AAC9B,gCAAgC;AAChC,8DAA8D;AAE9D,2DAA2D;AAE3D,yGAAyG;AACzG,qDAAqD;AACrD,UAAU;AAEV,4GAA4G;AAE5G,8BAA8B;AAC9B,oCAAoC;AAEpC,uFAAuF;AACvF,oCAAoC;AACpC,6EAA6E;AAE7E,yDAAyD;AAEzD,2FAA2F;AAC3F,kCAAkC;AAClC,6FAA6F;AAC7F,0DAA0D;AAC1D,gBAAgB;AAEhB,uCAAuC;AAEvC,sDAAsD;AACtD,gDAAgD;AAChD,2BAA2B;AAC3B,2CAA2C;AAC3C,yBAAyB;AACzB,uBAAuB;AACvB,6EAA6E;AAC7E,iFAAiF;AACjF,6GAA6G;AAC7G,oCAAoC;AACpC,sCAAsC;AACtC,gDAAgD;AAChD,gFAAgF;AAChF,sBAAsB;AACtB,sBAAsB;AACtB,mBAAmB;AACnB,yDAAyD;AACzD,yEAAyE;AACzE,8EAA8E;AAC9E,iCAAiC;AACjC,gBAAgB;AAChB,eAAe;AAEf,mHAAmH;AACnH,mDAAmD;AACnD,yDAAyD;AACzD,mEAAmE;AACnE,yCAAyC;AACzC,iFAAiF;AACjF,yEAAyE;AACzE,+BAA+B;AAC/B,wCAAwC;AACxC,6EAA6E;AAC7E,oCAAoC;AACpC,2BAA2B;AAC3B,wCAAwC;AACxC,cAAc;AAEd,mCAAmC;AACnC,wCAAwC;AACxC,wBAAwB;AAExB,2BAA2B;AAE3B,uEAAuE;AAEvE,mDAAmD;AACnD,mCAAmC;AAEnC,mDAAmD;AACnD,uCAAuC;AAEvC,wFAAwF;AACxF,oCAAoC;AACpC,iFAAiF;AAEjF,sCAAsC;AACtC,qBAAqB;AACrB,cAAc;AAEd,2FAA2F;AAC3F,wCAAwC;AACxC,2GAA2G;AAC3G,+BAA+B;AAC/B,cAAc;AAEd,6FAA6F;AAE7F,8EAA8E;AAE9E,wDAAwD;AACxD,sDAAsD;AACtD,wBAAwB;AACxB,kBAAkB;AAElB,kDAAkD;AAClD,6CAA6C;AAC7C,8DAA8D;AAC9D,wHAAwH;AACxH,qBAAqB;AAErB,0FAA0F;AAC1F,2DAA2D;AAE3D,gCAAgC;AAChC,mBAAmB;AACnB,uBAAuB;AACvB,gBAAgB;AAChB,gBAAgB;AAChB,sBAAsB;AACtB,qCAAqC;AACrC,sEAAsE;AACtE,6BAA6B;AAC7B,qBAAqB;AACrB,aAAa;AACb,WAAW;AACX,4DAA4D"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=sync-provider-ws-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync-provider-ws-client.d.ts","sourceRoot":"","sources":["../src/sync-provider-ws-client.ts"],"names":[],"mappings":""}
@@ -0,0 +1,40 @@
1
+ // import type { SyncBackend } from '@livestore/common'
2
+ // import { makeCfSync } from '@livestore/sync-cf'
3
+ // import type * as CfSyncBackend from '@livestore/sync-cf/cf-worker'
4
+ // import type { WSMessage } from '@livestore/sync-cf/common'
5
+ // import { Effect, Schedule, type Scope, type WebSocket } from '@livestore/utils/effect'
6
+ // import type * as CfWorker from './cf-types.ts'
7
+ // import { makeWebSocket } from './WebSocket.ts'
8
+ export {};
9
+ // export type MakeDurableObjectSyncBackendOptions = {
10
+ // /** WebSocket URL to connect to the sync backend Durable Object */
11
+ // durableObject: CfWorker.DurableObjectStub<CfSyncBackend.SyncBackendRpcInterface>
12
+ // }
13
+ // /**
14
+ // * Specialized sync backend used for Cloudflare Workers compatible only with `@livestore/sync-cf`
15
+ // */
16
+ // export const makeWsSyncProviderClient =
17
+ // ({
18
+ // durableObject,
19
+ // }: MakeDurableObjectSyncBackendOptions): SyncBackend.SyncBackendConstructor<WSMessage.SyncMetadata> =>
20
+ // (args) => {
21
+ // // Create a WebSocket factory that uses Cloudflare Durable Objects
22
+ // const webSocketFactory = (
23
+ // wsUrl: string,
24
+ // ): Effect.Effect<globalThis.WebSocket, WebSocket.WebSocketError, Scope.Scope> =>
25
+ // Effect.gen(function* () {
26
+ // const url = new URL(wsUrl)
27
+ // const socket = yield* makeWebSocket({
28
+ // durableObject,
29
+ // url,
30
+ // reconnect: Schedule.exponential(100),
31
+ // })
32
+ // return socket as unknown as globalThis.WebSocket
33
+ // })
34
+ // // Use the unified ws-impl with the Cloudflare WebSocket factory
35
+ // return makeCfSync({
36
+ // url: 'https://unused.com', // URL is constructed internally by ws-impl
37
+ // webSocketFactory,
38
+ // })(args)
39
+ // }
40
+ //# sourceMappingURL=sync-provider-ws-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync-provider-ws-client.js","sourceRoot":"","sources":["../src/sync-provider-ws-client.ts"],"names":[],"mappings":"AAAA,uDAAuD;AACvD,kDAAkD;AAClD,qEAAqE;AACrE,6DAA6D;AAC7D,yFAAyF;AACzF,iDAAiD;AACjD,iDAAiD;;AAEjD,sDAAsD;AACtD,uEAAuE;AACvE,qFAAqF;AACrF,IAAI;AAEJ,MAAM;AACN,oGAAoG;AACpG,MAAM;AACN,0CAA0C;AAC1C,OAAO;AACP,qBAAqB;AACrB,2GAA2G;AAC3G,gBAAgB;AAChB,yEAAyE;AACzE,iCAAiC;AACjC,uBAAuB;AACvB,uFAAuF;AACvF,kCAAkC;AAClC,qCAAqC;AACrC,gDAAgD;AAChD,2BAA2B;AAC3B,iBAAiB;AACjB,kDAAkD;AAClD,aAAa;AACb,2DAA2D;AAC3D,WAAW;AAEX,uEAAuE;AACvE,0BAA0B;AAC1B,+EAA+E;AAC/E,0BAA0B;AAC1B,eAAe;AACf,MAAM"}
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@livestore/adapter-cloudflare",
3
+ "version": "0.0.0-snapshot-8452e32b7fbfc129741b253b9c853f866b52129f",
4
+ "type": "module",
5
+ "sideEffects": [
6
+ "./src/polyfill.ts"
7
+ ],
8
+ "exports": {
9
+ ".": "./dist/mod.js"
10
+ },
11
+ "dependencies": {
12
+ "@cloudflare/workers-types": "4.20250807.0",
13
+ "@livestore/common": "0.0.0-snapshot-8452e32b7fbfc129741b253b9c853f866b52129f",
14
+ "@livestore/common-cf": "0.0.0-snapshot-8452e32b7fbfc129741b253b9c853f866b52129f",
15
+ "@livestore/livestore": "0.0.0-snapshot-8452e32b7fbfc129741b253b9c853f866b52129f",
16
+ "@livestore/sqlite-wasm": "0.0.0-snapshot-8452e32b7fbfc129741b253b9c853f866b52129f",
17
+ "@livestore/utils": "0.0.0-snapshot-8452e32b7fbfc129741b253b9c853f866b52129f",
18
+ "@livestore/sync-cf": "0.0.0-snapshot-8452e32b7fbfc129741b253b9c853f866b52129f"
19
+ },
20
+ "devDependencies": {
21
+ "wrangler": "^4.30.0"
22
+ },
23
+ "files": [
24
+ "package.json",
25
+ "src",
26
+ "dist"
27
+ ],
28
+ "license": "Apache-2.0",
29
+ "publishConfig": {
30
+ "access": "public",
31
+ "sideEffects": [
32
+ "./dist/polyfill.js"
33
+ ]
34
+ },
35
+ "scripts": {
36
+ "test": "echo No tests yet"
37
+ }
38
+ }
@@ -0,0 +1,69 @@
1
+ import type { Schedule, Scope } from '@livestore/utils/effect'
2
+ import { Effect, Exit, identity, WebSocket } from '@livestore/utils/effect'
3
+ import type * as CfWorker from './cf-types.ts'
4
+
5
+ // TODO refactor using Effect socket implementation
6
+ // https://github.com/Effect-TS/effect/blob/main/packages%2Fexperimental%2Fsrc%2FDevTools%2FClient.ts#L113
7
+ // "In a Stream pipeline everything above the pipeThrough is the outgoing (send) messages. Everything below is the incoming (message event) messages."
8
+ // https://github.com/Effect-TS/effect/blob/main/packages%2Fplatform%2Fsrc%2FSocket.ts#L451
9
+
10
+ /**
11
+ * Creates a WebSocket connection and waits for the connection to be established.
12
+ * Automatically closes the connection when the scope is closed.
13
+ */
14
+ export const makeWebSocket = ({
15
+ // do,
16
+ reconnect,
17
+ url,
18
+ durableObject,
19
+ }: {
20
+ /** CF Sync Backend DO with `/sync` endpoint */
21
+ durableObject: CfWorker.DurableObjectStub
22
+ url: URL
23
+ reconnect?: Schedule.Schedule<unknown> | false
24
+ }): Effect.Effect<CfWorker.WebSocket, WebSocket.WebSocketError, Scope.Scope> =>
25
+ Effect.gen(function* () {
26
+ // yield* validateUrl(url)
27
+
28
+ const socket = yield* Effect.tryPromise({
29
+ try: () =>
30
+ durableObject.fetch(url, { headers: { Upgrade: 'websocket' } }).then((res: any) => {
31
+ if (!res.webSocket) {
32
+ throw new Error('WebSocket upgrade failed')
33
+ }
34
+ return res.webSocket as CfWorker.WebSocket
35
+ }),
36
+ catch: (cause) => new WebSocket.WebSocketError({ cause }),
37
+ }).pipe(reconnect ? Effect.retry(reconnect) : identity, Effect.withSpan('make-websocket-durable-object'))
38
+
39
+ socket.accept()
40
+
41
+ /**
42
+ * Common WebSocket close codes: https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close
43
+ * 1000: Normal closure
44
+ * 1001: Endpoint is going away, a server is terminating the connection because it has received a request that indicates the client is ending the connection.
45
+ * 1002: Protocol error, a server is terminating the connection because it has received data on the connection that was not consistent with the type of the connection.
46
+ * 1011: Internal server error, a server is terminating the connection because it encountered an unexpected condition that prevented it from fulfilling the request.
47
+ *
48
+ * For reference, here are the valid WebSocket close code ranges:
49
+ * 1000-1999: Reserved for protocol usage
50
+ * 2000-2999: Reserved for WebSocket extensions
51
+ * 3000-3999: Available for libraries and frameworks
52
+ * 4000-4999: Available for applications
53
+ */
54
+ yield* Effect.addFinalizer(
55
+ Effect.fn(function* (exit) {
56
+ try {
57
+ if (Exit.isFailure(exit)) {
58
+ socket.close(3000)
59
+ } else {
60
+ socket.close(1000)
61
+ }
62
+ } catch (error) {
63
+ yield* Effect.die(new WebSocket.WebSocketError({ cause: error }))
64
+ }
65
+ }),
66
+ )
67
+
68
+ return socket
69
+ })
@@ -0,0 +1,20 @@
1
+ export {
2
+ type D1Database,
3
+ type D1Result,
4
+ type DurableObject,
5
+ type DurableObjectNamespace,
6
+ type DurableObjectState,
7
+ type DurableObjectStorage,
8
+ type DurableObjectStub,
9
+ type MessageEvent,
10
+ Request,
11
+ Response,
12
+ Rpc,
13
+ type SqlStorage,
14
+ SqlStorageCursor,
15
+ SqlStorageStatement,
16
+ type SqlStorageValue,
17
+ WebSocket,
18
+ WebSocketPair,
19
+ WebSocketRequestResponsePair,
20
+ } from '@cloudflare/workers-types'
@@ -0,0 +1,144 @@
1
+ import {
2
+ type Adapter,
3
+ ClientSessionLeaderThreadProxy,
4
+ type LockStatus,
5
+ liveStoreStorageFormatVersion,
6
+ makeClientSession,
7
+ type SyncOptions,
8
+ UnexpectedError,
9
+ } from '@livestore/common'
10
+ import { type DevtoolsOptions, Eventlog, LeaderThreadCtx, makeLeaderThreadLayer } from '@livestore/common/leader-thread'
11
+ import { LiveStoreEvent } from '@livestore/livestore'
12
+ import { sqliteDbFactory } from '@livestore/sqlite-wasm/cf'
13
+ import { loadSqlite3Wasm } from '@livestore/sqlite-wasm/load-wasm'
14
+ import { Effect, FetchHttpClient, Layer, SubscriptionRef, WebChannel } from '@livestore/utils/effect'
15
+ import type * as CfWorker from './cf-types.ts'
16
+
17
+ export const makeAdapter =
18
+ ({
19
+ storage,
20
+ clientId,
21
+ syncOptions,
22
+ sessionId,
23
+ }: {
24
+ storage: CfWorker.DurableObjectStorage
25
+ clientId: string
26
+ syncOptions: SyncOptions
27
+ sessionId: string
28
+ }): Adapter =>
29
+ (adapterArgs) =>
30
+ Effect.gen(function* () {
31
+ const { storeId, /* devtoolsEnabled, shutdown, bootStatusQueue, */ syncPayload, schema } = adapterArgs
32
+
33
+ const devtoolsOptions = { enabled: false } as DevtoolsOptions
34
+
35
+ const sqlite3 = yield* Effect.promise(() => loadSqlite3Wasm())
36
+
37
+ const makeSqliteDb = sqliteDbFactory({ sqlite3 })
38
+
39
+ const syncInMemoryDb = yield* makeSqliteDb({ _tag: 'in-memory', storage, configureDb: () => {} }).pipe(
40
+ UnexpectedError.mapToUnexpectedError,
41
+ )
42
+
43
+ const schemaHashSuffix =
44
+ schema.state.sqlite.migrations.strategy === 'manual' ? 'fixed' : schema.state.sqlite.hash.toString()
45
+
46
+ const dbState = yield* makeSqliteDb({
47
+ _tag: 'storage',
48
+ storage,
49
+ fileName: getStateDbFileName(schemaHashSuffix),
50
+ configureDb: () => {},
51
+ }).pipe(UnexpectedError.mapToUnexpectedError)
52
+
53
+ const dbEventlog = yield* makeSqliteDb({
54
+ _tag: 'storage',
55
+ storage,
56
+ fileName: `eventlog@${liveStoreStorageFormatVersion}.db`,
57
+ configureDb: () => {},
58
+ }).pipe(UnexpectedError.mapToUnexpectedError)
59
+
60
+ const shutdownChannel = yield* WebChannel.noopChannel<any, any>()
61
+
62
+ // Use Durable Object sync backend if no backend is specified
63
+
64
+ const layer = yield* Layer.build(
65
+ makeLeaderThreadLayer({
66
+ schema,
67
+ storeId,
68
+ clientId,
69
+ makeSqliteDb,
70
+ syncOptions,
71
+ dbState,
72
+ dbEventlog,
73
+ devtoolsOptions,
74
+ shutdownChannel,
75
+ syncPayload,
76
+ }),
77
+ )
78
+
79
+ const { leaderThread, initialSnapshot } = yield* Effect.gen(function* () {
80
+ const { dbState, dbEventlog, syncProcessor, extraIncomingMessagesQueue, initialState } = yield* LeaderThreadCtx
81
+
82
+ const initialLeaderHead = Eventlog.getClientHeadFromDb(dbEventlog)
83
+ // const initialLeaderHead = EventSequenceNumber.ROOT
84
+
85
+ const leaderThread = ClientSessionLeaderThreadProxy.of(
86
+ {
87
+ events: {
88
+ pull: ({ cursor }) => syncProcessor.pull({ cursor }),
89
+ push: (batch) =>
90
+ syncProcessor.push(
91
+ batch.map((item) => new LiveStoreEvent.EncodedWithMeta(item)),
92
+ { waitForProcessing: true },
93
+ ),
94
+ },
95
+ initialState: { leaderHead: initialLeaderHead, migrationsReport: initialState.migrationsReport },
96
+ export: Effect.sync(() => dbState.export()),
97
+ getEventlogData: Effect.sync(() => dbEventlog.export()),
98
+ getSyncState: syncProcessor.syncState,
99
+ sendDevtoolsMessage: (message) => extraIncomingMessagesQueue.offer(message),
100
+ },
101
+ {
102
+ // overrides: testing?.overrides?.clientSession?.leaderThreadProxy
103
+ },
104
+ )
105
+
106
+ const initialSnapshot = dbState.export()
107
+
108
+ return { leaderThread, initialSnapshot }
109
+ }).pipe(Effect.provide(layer))
110
+
111
+ syncInMemoryDb.import(initialSnapshot)
112
+
113
+ const lockStatus = yield* SubscriptionRef.make<LockStatus>('has-lock')
114
+
115
+ const clientSession = yield* makeClientSession({
116
+ ...adapterArgs,
117
+ sqliteDb: syncInMemoryDb,
118
+ webmeshMode: 'proxy',
119
+ connectWebmeshNode: Effect.fnUntraced(function* ({ webmeshNode }) {
120
+ console.log('connectWebmeshNode', { webmeshNode })
121
+ // if (devtoolsOptions.enabled) {
122
+ // yield* Webmesh.connectViaWebSocket({
123
+ // node: webmeshNode,
124
+ // url: `ws://${devtoolsOptions.host}:${devtoolsOptions.port}`,
125
+ // openTimeout: 500,
126
+ // }).pipe(Effect.tapCauseLogPretty, Effect.forkScoped)
127
+ // }
128
+ }),
129
+ leaderThread,
130
+ lockStatus,
131
+ clientId,
132
+ sessionId,
133
+ isLeader: true,
134
+ // Not really applicable for node as there is no "reload the app" concept
135
+ registerBeforeUnload: (_onBeforeUnload) => () => {},
136
+ })
137
+
138
+ return clientSession
139
+ }).pipe(
140
+ Effect.withSpan('@livestore/adapter-cloudflare:makeAdapter', { attributes: { clientId, sessionId } }),
141
+ Effect.provide(FetchHttpClient.layer),
142
+ )
143
+
144
+ const getStateDbFileName = (suffix: string) => `state${suffix}@${liveStoreStorageFormatVersion}.db`
@@ -0,0 +1,91 @@
1
+ import type { UnexpectedError } from '@livestore/common'
2
+ import { createStore, type LiveStoreSchema, provideOtel, type Store, type Unsubscribe } from '@livestore/livestore'
3
+ import type * as CfSyncBackend from '@livestore/sync-cf/cf-worker'
4
+ import { makeDoRpcSync } from '@livestore/sync-cf/client'
5
+ import { Effect, Logger, LogLevel, Scope } from '@livestore/utils/effect'
6
+ import type * as CfWorker from './cf-types.ts'
7
+ import { makeAdapter } from './make-adapter.ts'
8
+
9
+ declare class Response extends CfWorker.Response {}
10
+
11
+ export type MakeDurableObjectClassOptions<TSchema extends LiveStoreSchema = LiveStoreSchema.Any> = {
12
+ schema: TSchema
13
+ // storeId: string
14
+ clientId: string
15
+ sessionId: string
16
+ onStoreReady?: (store: Store<TSchema>) => Effect.SyncOrPromiseOrEffect<void, UnexpectedError>
17
+ // makeStore?: (adapter: Adapter) => Promise<Store<TSchema>>
18
+ // onLiveStoreEvent?: (event: LiveStoreEvent.ForSchema<TSchema>) => Promise<void>
19
+ registerQueries?: (store: Store<TSchema>) => Effect.SyncOrPromiseOrEffect<ReadonlyArray<Unsubscribe>>
20
+ syncBackendUrl?: string
21
+ // Hook for custom request handling (e.g., testing endpoints)
22
+ handleCustomRequest?: (
23
+ request: CfWorker.Request,
24
+ ensureStore: Effect.Effect<Store<TSchema>, UnexpectedError, never>,
25
+ ) => Effect.SyncOrPromiseOrEffect<CfWorker.Response | undefined, UnexpectedError>
26
+ }
27
+
28
+ export type Env = {
29
+ SYNC_BACKEND_DO: CfWorker.DurableObjectNamespace
30
+ }
31
+
32
+ export type MakeDurableObjectClass = <TSchema extends LiveStoreSchema = LiveStoreSchema.Any>(
33
+ options: MakeDurableObjectClassOptions<TSchema>,
34
+ ) => {
35
+ new (ctx: CfWorker.DurableObjectState, env: Env): CfWorker.DurableObject & CfWorker.Rpc.DurableObjectBranded
36
+ }
37
+
38
+ export type CreateStoreDoOptions<TSchema extends LiveStoreSchema = LiveStoreSchema.Any> = {
39
+ schema: TSchema
40
+ storeId: string
41
+ clientId: string
42
+ sessionId: string
43
+ storage: CfWorker.DurableObjectStorage
44
+ syncBackendDurableObject: CfWorker.DurableObjectStub<CfSyncBackend.SyncBackendRpcInterface>
45
+ durableObjectId: string
46
+ bindingName: string
47
+ livePull?: boolean
48
+ }
49
+
50
+ export const createStoreDo = <TSchema extends LiveStoreSchema = LiveStoreSchema.Any>({
51
+ schema,
52
+ storeId,
53
+ clientId,
54
+ sessionId,
55
+ storage,
56
+ syncBackendDurableObject,
57
+ durableObjectId,
58
+ bindingName,
59
+ livePull = false,
60
+ }: CreateStoreDoOptions<TSchema>) =>
61
+ Effect.gen(function* () {
62
+ const scope = yield* Scope.make()
63
+
64
+ const adapter = makeAdapter({
65
+ clientId,
66
+ sessionId,
67
+ storage,
68
+ syncOptions: {
69
+ backend: makeDoRpcSync({
70
+ syncBackendStub: syncBackendDurableObject,
71
+ durableObjectContext: { bindingName, durableObjectId },
72
+ }),
73
+ livePull, // Uses DO RPC callbacks for reactive pull
74
+ // backend: makeHttpSync({ url: `http://localhost:8787`, livePull: { pollInterval: 500 } }),
75
+ initialSyncOptions: { _tag: 'Blocking', timeout: 500 },
76
+ // backend: makeWsSyncProviderClient({ durableObject: syncBackendDurableObject }),
77
+ },
78
+ })
79
+
80
+ return yield* createStore({ schema, adapter, storeId }).pipe(Scope.extend(scope), provideOtel({}))
81
+ })
82
+
83
+ export const createStoreDoPromise = <TSchema extends LiveStoreSchema = LiveStoreSchema.Any>(
84
+ options: CreateStoreDoOptions<TSchema>,
85
+ ) =>
86
+ createStoreDo(options).pipe(
87
+ Logger.withMinimumLogLevel(LogLevel.Debug),
88
+ Effect.provide(Logger.consoleWithThread('DoClient')),
89
+ Effect.tapCauseLogPretty,
90
+ Effect.runPromise,
91
+ )