@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.
- package/LICENSE +201 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/WebSocket.d.ts +14 -0
- package/dist/WebSocket.d.ts.map +1 -0
- package/dist/WebSocket.js +52 -0
- package/dist/WebSocket.js.map +1 -0
- package/dist/cf-types.d.ts +2 -0
- package/dist/cf-types.d.ts.map +1 -0
- package/dist/cf-types.js +2 -0
- package/dist/cf-types.js.map +1 -0
- package/dist/make-adapter.d.ts +9 -0
- package/dist/make-adapter.d.ts.map +1 -0
- package/dist/make-adapter.js +87 -0
- package/dist/make-adapter.js.map +1 -0
- package/dist/make-client-durable-object.d.ts +34 -0
- package/dist/make-client-durable-object.d.ts.map +1 -0
- package/dist/make-client-durable-object.js +25 -0
- package/dist/make-client-durable-object.js.map +1 -0
- package/dist/make-sqlite-db.d.ts +31 -0
- package/dist/make-sqlite-db.d.ts.map +1 -0
- package/dist/make-sqlite-db.js +194 -0
- package/dist/make-sqlite-db.js.map +1 -0
- package/dist/mod.d.ts +5 -0
- package/dist/mod.d.ts.map +1 -0
- package/dist/mod.js +4 -0
- package/dist/mod.js.map +1 -0
- package/dist/polyfill.d.ts +2 -0
- package/dist/polyfill.d.ts.map +1 -0
- package/dist/polyfill.js +40 -0
- package/dist/polyfill.js.map +1 -0
- package/dist/sync-provider-client.d.ts +12 -0
- package/dist/sync-provider-client.d.ts.map +1 -0
- package/dist/sync-provider-client.js +24 -0
- package/dist/sync-provider-client.js.map +1 -0
- package/dist/sync-provider-rpc-client.d.ts +2 -0
- package/dist/sync-provider-rpc-client.d.ts.map +1 -0
- package/dist/sync-provider-rpc-client.js +139 -0
- package/dist/sync-provider-rpc-client.js.map +1 -0
- package/dist/sync-provider-ws-client.d.ts +2 -0
- package/dist/sync-provider-ws-client.d.ts.map +1 -0
- package/dist/sync-provider-ws-client.js +40 -0
- package/dist/sync-provider-ws-client.js.map +1 -0
- package/package.json +38 -0
- package/src/WebSocket.ts +69 -0
- package/src/cf-types.ts +20 -0
- package/src/make-adapter.ts +144 -0
- package/src/make-client-durable-object.ts +91 -0
- package/src/make-sqlite-db.ts +261 -0
- package/src/mod.ts +12 -0
- 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 @@
|
|
|
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 @@
|
|
|
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
|
+
}
|
package/src/WebSocket.ts
ADDED
|
@@ -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
|
+
})
|
package/src/cf-types.ts
ADDED
|
@@ -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
|
+
)
|