@livestore/sync-cf 0.0.0-snapshot-909cdd1ac2fd591945c2be2b0f53e14d87f3c9d4

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.
@@ -0,0 +1,58 @@
1
+ import { mutationEventSchemaEncodedAny } from '@livestore/common/schema';
2
+ import { Schema } from '@livestore/utils/effect';
3
+ export const PullReq = Schema.TaggedStruct('WSMessage.PullReq', {
4
+ requestId: Schema.String,
5
+ /** Omitting the cursor will start from the beginning */
6
+ cursor: Schema.optional(Schema.Number),
7
+ });
8
+ export const PullRes = Schema.TaggedStruct('WSMessage.PullRes', {
9
+ requestId: Schema.String,
10
+ // /** The */
11
+ // cursor: Schema.String,
12
+ events: Schema.Array(mutationEventSchemaEncodedAny),
13
+ hasMore: Schema.Boolean,
14
+ });
15
+ export const PushBroadcast = Schema.TaggedStruct('WSMessage.PushBroadcast', {
16
+ mutationEventEncoded: mutationEventSchemaEncodedAny,
17
+ persisted: Schema.Boolean,
18
+ });
19
+ export const PushReq = Schema.TaggedStruct('WSMessage.PushReq', {
20
+ requestId: Schema.String,
21
+ mutationEventEncoded: mutationEventSchemaEncodedAny,
22
+ persisted: Schema.Boolean,
23
+ });
24
+ export const PushAck = Schema.TaggedStruct('WSMessage.PushAck', {
25
+ requestId: Schema.String,
26
+ mutationId: Schema.Number,
27
+ });
28
+ export const Error = Schema.TaggedStruct('WSMessage.Error', {
29
+ requestId: Schema.String,
30
+ message: Schema.String,
31
+ });
32
+ export const Ping = Schema.TaggedStruct('WSMessage.Ping', {
33
+ requestId: Schema.Literal('ping'),
34
+ });
35
+ export const Pong = Schema.TaggedStruct('WSMessage.Pong', {
36
+ requestId: Schema.Literal('ping'),
37
+ });
38
+ export const AdminResetRoomReq = Schema.TaggedStruct('WSMessage.AdminResetRoomReq', {
39
+ requestId: Schema.String,
40
+ adminSecret: Schema.String,
41
+ });
42
+ export const AdminResetRoomRes = Schema.TaggedStruct('WSMessage.AdminResetRoomRes', {
43
+ requestId: Schema.String,
44
+ });
45
+ export const AdminInfoReq = Schema.TaggedStruct('WSMessage.AdminInfoReq', {
46
+ requestId: Schema.String,
47
+ adminSecret: Schema.String,
48
+ });
49
+ export const AdminInfoRes = Schema.TaggedStruct('WSMessage.AdminInfoRes', {
50
+ requestId: Schema.String,
51
+ info: Schema.Struct({
52
+ durableObjectId: Schema.String,
53
+ }),
54
+ });
55
+ export const Message = Schema.Union(PullReq, PullRes, PushBroadcast, PushReq, PushAck, Error, Ping, Pong, AdminResetRoomReq, AdminResetRoomRes, AdminInfoReq, AdminInfoRes);
56
+ export const BackendToClientMessage = Schema.Union(PullRes, PushBroadcast, PushAck, AdminResetRoomRes, AdminInfoRes, Error, Pong);
57
+ export const ClientToBackendMessage = Schema.Union(PullReq, PushReq, AdminResetRoomReq, AdminInfoReq, Ping);
58
+ //# sourceMappingURL=ws-message-types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ws-message-types.js","sourceRoot":"","sources":["../../src/common/ws-message-types.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export * from './ws-impl.js';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/sync-impl/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAA"}
@@ -0,0 +1,2 @@
1
+ export * from './ws-impl.js';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/sync-impl/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAA"}
@@ -0,0 +1,18 @@
1
+ import type { SyncBackend, SyncBackendOptionsBase } from '@livestore/common';
2
+ import type { Scope } from '@livestore/utils/effect';
3
+ import { Effect } from '@livestore/utils/effect';
4
+ export interface WsSyncOptions extends SyncBackendOptionsBase {
5
+ type: 'cf';
6
+ url: string;
7
+ roomId: string;
8
+ }
9
+ interface LiveStoreGlobalCf {
10
+ syncBackend: WsSyncOptions;
11
+ }
12
+ declare global {
13
+ interface LiveStoreGlobal extends LiveStoreGlobalCf {
14
+ }
15
+ }
16
+ export declare const makeWsSync: (options: WsSyncOptions) => Effect.Effect<SyncBackend<null>, never, Scope.Scope>;
17
+ export {};
18
+ //# sourceMappingURL=ws-impl.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ws-impl.d.ts","sourceRoot":"","sources":["../../src/sync-impl/ws-impl.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAA;AAE5E,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,yBAAyB,CAAA;AACpD,OAAO,EAAY,MAAM,EAA0D,MAAM,yBAAyB,CAAA;AAKlH,MAAM,WAAW,aAAc,SAAQ,sBAAsB;IAC3D,IAAI,EAAE,IAAI,CAAA;IACV,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,MAAM,CAAA;CACf;AAED,UAAU,iBAAiB;IACzB,WAAW,EAAE,aAAa,CAAA;CAC3B;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,eAAgB,SAAQ,iBAAiB;KAAG;CACvD;AAED,eAAO,MAAM,UAAU,YAAa,aAAa,KAAG,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,CAoFnG,CAAA"}
@@ -0,0 +1,125 @@
1
+ /// <reference lib="dom" />
2
+ import { InvalidPullError, InvalidPushError } from '@livestore/common';
3
+ import { Deferred, Effect, Option, PubSub, Queue, Schema, Stream, SubscriptionRef } from '@livestore/utils/effect';
4
+ import { nanoid } from '@livestore/utils/nanoid';
5
+ import { WSMessage } from '../common/index.js';
6
+ export const makeWsSync = (options) => Effect.gen(function* () {
7
+ const wsUrl = `${options.url}/websocket?room=${options.roomId}`;
8
+ const { isConnected, incomingMessages, send } = yield* connect(wsUrl);
9
+ const metadata = Option.none();
10
+ const api = {
11
+ isConnected,
12
+ pull: (args, { listenForNew }) => listenForNew
13
+ ? Effect.gen(function* () {
14
+ const requestId = nanoid();
15
+ const cursor = Option.getOrUndefined(args)?.cursor.global;
16
+ yield* send(WSMessage.PullReq.make({ cursor, requestId }));
17
+ return Stream.fromPubSub(incomingMessages).pipe(Stream.filter((_) => (_._tag === 'WSMessage.PullRes' ? _.requestId === requestId : true)), Stream.tap((_) => _._tag === 'WSMessage.Error' ? new InvalidPullError({ message: _.message }) : Effect.void), Stream.filter(Schema.is(Schema.Union(WSMessage.PushBroadcast, WSMessage.PullRes))), Stream.map((msg) => msg._tag === 'WSMessage.PushBroadcast'
18
+ ? [{ mutationEventEncoded: msg.mutationEventEncoded, persisted: msg.persisted, metadata }]
19
+ : msg.events.map((_) => ({ mutationEventEncoded: _, metadata, persisted: true }))), Stream.flattenIterables);
20
+ }).pipe(Stream.unwrap)
21
+ : Effect.gen(function* () {
22
+ const requestId = nanoid();
23
+ const cursor = Option.getOrUndefined(args)?.cursor.global;
24
+ yield* send(WSMessage.PullReq.make({ cursor, requestId }));
25
+ return Stream.fromPubSub(incomingMessages).pipe(Stream.filter((_) => _._tag !== 'WSMessage.PushBroadcast' && _.requestId === requestId), Stream.tap((_) => _._tag === 'WSMessage.Error' ? new InvalidPullError({ message: _.message }) : Effect.void), Stream.filter(Schema.is(WSMessage.PullRes)), Stream.takeUntil((_) => _.hasMore === false), Stream.map((_) => _.events), Stream.flattenIterables, Stream.map((mutationEventEncoded) => ({
26
+ mutationEventEncoded,
27
+ metadata,
28
+ persisted: true,
29
+ })));
30
+ }).pipe(Stream.unwrap),
31
+ push: (mutationEventEncoded, persisted) => Effect.gen(function* () {
32
+ const ready = yield* Deferred.make();
33
+ const requestId = nanoid();
34
+ yield* Stream.fromPubSub(incomingMessages).pipe(Stream.filter((_) => _._tag !== 'WSMessage.PushBroadcast' && _.requestId === requestId), Stream.tap((_) => _._tag === 'WSMessage.Error'
35
+ ? Deferred.fail(ready, new InvalidPushError({ message: _.message }))
36
+ : Effect.void), Stream.filter(Schema.is(WSMessage.PushAck)), Stream.filter((_) => _.mutationId === mutationEventEncoded.id.global), Stream.take(1), Stream.tap(() => Deferred.succeed(ready, void 0)), Stream.runDrain, Effect.tapCauseLogPretty, Effect.fork);
37
+ yield* send(WSMessage.PushReq.make({ mutationEventEncoded, requestId, persisted }));
38
+ yield* Deferred.await(ready);
39
+ return { metadata };
40
+ }),
41
+ };
42
+ return api;
43
+ });
44
+ const connect = (wsUrl) => Effect.gen(function* () {
45
+ const isConnected = yield* SubscriptionRef.make(false);
46
+ const wsRef = { current: undefined };
47
+ const incomingMessages = yield* PubSub.unbounded();
48
+ const waitUntilOnline = isConnected.changes.pipe(Stream.filter(Boolean), Stream.take(1), Stream.runDrain);
49
+ const send = (message) => Effect.gen(function* () {
50
+ // Wait first until we're online
51
+ yield* waitUntilOnline;
52
+ wsRef.current.send(Schema.encodeSync(Schema.parseJson(WSMessage.Message))(message));
53
+ });
54
+ const innerConnect = Effect.gen(function* () {
55
+ // If the browser already tells us we're offline, then we'll at least wait until the browser
56
+ // thinks we're online again. (We'll only know for sure once the WS conneciton is established.)
57
+ while (navigator.onLine === false) {
58
+ yield* Effect.sleep(1000);
59
+ }
60
+ // if (navigator.onLine === false) {
61
+ // yield* Effect.async((cb) => self.addEventListener('online', () => cb(Effect.void)))
62
+ // }
63
+ const ws = new WebSocket(wsUrl);
64
+ const connectionClosed = yield* Deferred.make();
65
+ const pongMessages = yield* Queue.unbounded();
66
+ const messageHandler = (event) => {
67
+ const decodedEventRes = Schema.decodeUnknownEither(Schema.parseJson(WSMessage.BackendToClientMessage))(event.data);
68
+ if (decodedEventRes._tag === 'Left') {
69
+ console.error('Sync: Invalid message received', decodedEventRes.left);
70
+ return;
71
+ }
72
+ else {
73
+ if (decodedEventRes.right._tag === 'WSMessage.Pong') {
74
+ Queue.offer(pongMessages, decodedEventRes.right).pipe(Effect.runSync);
75
+ }
76
+ else {
77
+ PubSub.publish(incomingMessages, decodedEventRes.right).pipe(Effect.runSync);
78
+ }
79
+ }
80
+ };
81
+ const offlineHandler = () => {
82
+ Deferred.succeed(connectionClosed, void 0).pipe(Effect.runSync);
83
+ };
84
+ // NOTE it seems that this callback doesn't work reliably on a worker but only via `window.addEventListener`
85
+ // We might need to proxy the event from the main thread to the worker if we want this to work reliably.
86
+ self.addEventListener('offline', offlineHandler);
87
+ yield* Effect.addFinalizer(() => Effect.gen(function* () {
88
+ ws.removeEventListener('message', messageHandler);
89
+ self.removeEventListener('offline', offlineHandler);
90
+ wsRef.current?.close();
91
+ wsRef.current = undefined;
92
+ yield* SubscriptionRef.set(isConnected, false);
93
+ }));
94
+ ws.addEventListener('message', messageHandler);
95
+ if (ws.readyState === WebSocket.OPEN) {
96
+ wsRef.current = ws;
97
+ SubscriptionRef.set(isConnected, true).pipe(Effect.runSync);
98
+ }
99
+ else {
100
+ ws.addEventListener('open', () => {
101
+ wsRef.current = ws;
102
+ SubscriptionRef.set(isConnected, true).pipe(Effect.runSync);
103
+ });
104
+ }
105
+ ws.addEventListener('close', () => {
106
+ Deferred.succeed(connectionClosed, void 0).pipe(Effect.runSync);
107
+ });
108
+ ws.addEventListener('error', () => {
109
+ ws.close();
110
+ Deferred.succeed(connectionClosed, void 0).pipe(Effect.runSync);
111
+ });
112
+ const checkPingPong = Effect.gen(function* () {
113
+ // TODO include pong latency infomation in network status
114
+ yield* send({ _tag: 'WSMessage.Ping', requestId: 'ping' });
115
+ // NOTE those numbers might need more fine-tuning to allow for bad network conditions
116
+ yield* Queue.take(pongMessages).pipe(Effect.timeout(5000));
117
+ yield* Effect.sleep(25_000);
118
+ }).pipe(Effect.withSpan('@livestore/sync-cf:connect:checkPingPong'));
119
+ yield* waitUntilOnline.pipe(Effect.andThen(checkPingPong.pipe(Effect.forever)), Effect.tapErrorCause(() => Deferred.succeed(connectionClosed, void 0)), Effect.forkScoped);
120
+ yield* Deferred.await(connectionClosed);
121
+ }).pipe(Effect.scoped);
122
+ yield* innerConnect.pipe(Effect.forever, Effect.tapCauseLogPretty, Effect.forkScoped);
123
+ return { isConnected, incomingMessages, send };
124
+ });
125
+ //# sourceMappingURL=ws-impl.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ws-impl.js","sourceRoot":"","sources":["../../src/sync-impl/ws-impl.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@livestore/sync-cf",
3
+ "version": "0.0.0-snapshot-909cdd1ac2fd591945c2be2b0f53e14d87f3c9d4",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": {
7
+ "types": "./dist/sync-impl/index.d.ts",
8
+ "default": "./dist/sync-impl/index.js"
9
+ }
10
+ },
11
+ "dependencies": {
12
+ "@livestore/common": "0.0.0-snapshot-909cdd1ac2fd591945c2be2b0f53e14d87f3c9d4",
13
+ "@livestore/utils": "0.0.0-snapshot-909cdd1ac2fd591945c2be2b0f53e14d87f3c9d4"
14
+ },
15
+ "devDependencies": {
16
+ "@cloudflare/workers-types": "4.20241022.0",
17
+ "wrangler": "^3.84.0"
18
+ },
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "scripts": {
23
+ "deploy": "wrangler publish",
24
+ "test": "echo 'No tests yet'"
25
+ }
26
+ }
@@ -0,0 +1,230 @@
1
+ import { makeColumnSpec } from '@livestore/common'
2
+ import { DbSchema, type MutationEvent, mutationEventSchemaAny } from '@livestore/common/schema'
3
+ import { shouldNeverHappen } from '@livestore/utils'
4
+ import { Effect, Schema } from '@livestore/utils/effect'
5
+ import { DurableObject } from 'cloudflare:workers'
6
+
7
+ import { WSMessage } from '../common/index.js'
8
+
9
+ export interface Env {
10
+ WEBSOCKET_SERVER: DurableObjectNamespace<WebSocketServer>
11
+ DB: D1Database
12
+ ADMIN_SECRET: string
13
+ }
14
+
15
+ type WebSocketClient = WebSocket
16
+
17
+ const encodeOutgoingMessage = Schema.encodeSync(Schema.parseJson(WSMessage.BackendToClientMessage))
18
+ const encodeIncomingMessage = Schema.encodeSync(Schema.parseJson(WSMessage.ClientToBackendMessage))
19
+ const decodeIncomingMessage = Schema.decodeUnknownEither(Schema.parseJson(WSMessage.ClientToBackendMessage))
20
+
21
+ export const mutationLogTable = DbSchema.table('__unused', {
22
+ id: DbSchema.integer({ primaryKey: true }),
23
+ parentId: DbSchema.integer({}),
24
+ mutation: DbSchema.text({}),
25
+ args: DbSchema.text({ schema: Schema.parseJson(Schema.Any) }),
26
+ })
27
+
28
+ // Durable Object
29
+ export class WebSocketServer extends DurableObject<Env> {
30
+ dbName = `mutation_log_${this.ctx.id.toString()}`
31
+ storage = makeStorage(this.ctx, this.env, this.dbName)
32
+
33
+ constructor(ctx: DurableObjectState, env: Env) {
34
+ super(ctx, env)
35
+ }
36
+
37
+ fetch = async (_request: Request) =>
38
+ Effect.gen(this, function* () {
39
+ const { 0: client, 1: server } = new WebSocketPair()
40
+
41
+ // See https://developers.cloudflare.com/durable-objects/examples/websocket-hibernation-server
42
+
43
+ this.ctx.acceptWebSocket(server)
44
+
45
+ this.ctx.setWebSocketAutoResponse(
46
+ new WebSocketRequestResponsePair(
47
+ encodeIncomingMessage(WSMessage.Ping.make({ requestId: 'ping' })),
48
+ encodeOutgoingMessage(WSMessage.Pong.make({ requestId: 'ping' })),
49
+ ),
50
+ )
51
+
52
+ const colSpec = makeColumnSpec(mutationLogTable.sqliteDef.ast)
53
+ this.env.DB.exec(`CREATE TABLE IF NOT EXISTS ${this.dbName} (${colSpec}) strict`)
54
+
55
+ return new Response(null, {
56
+ status: 101,
57
+ webSocket: client,
58
+ })
59
+ }).pipe(Effect.tapCauseLogPretty, Effect.runPromise)
60
+
61
+ webSocketMessage = async (ws: WebSocketClient, message: ArrayBuffer | string) => {
62
+ const decodedMessageRes = decodeIncomingMessage(message)
63
+
64
+ if (decodedMessageRes._tag === 'Left') {
65
+ console.error('Invalid message received', decodedMessageRes.left)
66
+ return
67
+ }
68
+
69
+ const decodedMessage = decodedMessageRes.right
70
+ const requestId = decodedMessage.requestId
71
+
72
+ try {
73
+ switch (decodedMessage._tag) {
74
+ case 'WSMessage.PullReq': {
75
+ const cursor = decodedMessage.cursor
76
+ const CHUNK_SIZE = 100
77
+
78
+ // TODO use streaming
79
+ const remainingEvents = [...(await this.storage.getEvents(cursor))]
80
+
81
+ // NOTE we want to make sure the WS server responds at least once with `InitRes` even if `events` is empty
82
+ while (true) {
83
+ const events = remainingEvents.splice(0, CHUNK_SIZE)
84
+ const encodedEvents = Schema.encodeSync(Schema.Array(mutationEventSchemaAny))(events)
85
+ const hasMore = remainingEvents.length > 0
86
+
87
+ ws.send(encodeOutgoingMessage(WSMessage.PullRes.make({ events: encodedEvents, hasMore, requestId })))
88
+
89
+ if (hasMore === false) {
90
+ break
91
+ }
92
+ }
93
+
94
+ break
95
+ }
96
+ case 'WSMessage.PushReq': {
97
+ // TODO check whether we could use the Durable Object storage for this to speed up the lookup
98
+ const latestEvent = await this.storage.getLatestEvent()
99
+ const expectedParentId = latestEvent?.id ?? { global: 0, local: 0 }
100
+
101
+ if (decodedMessage.mutationEventEncoded.parentId !== expectedParentId) {
102
+ ws.send(
103
+ encodeOutgoingMessage(
104
+ WSMessage.Error.make({
105
+ message: `Invalid parent id. Received ${decodedMessage.mutationEventEncoded.parentId} but expected ${expectedParentId}`,
106
+ requestId,
107
+ }),
108
+ ),
109
+ )
110
+ return
111
+ }
112
+
113
+ // TODO handle clientId unique conflict
114
+
115
+ // NOTE we're currently not blocking on this to allow broadcasting right away
116
+ const storePromise = decodedMessage.persisted
117
+ ? this.storage.appendEvent(decodedMessage.mutationEventEncoded)
118
+ : Promise.resolve()
119
+
120
+ ws.send(
121
+ encodeOutgoingMessage(
122
+ WSMessage.PushAck.make({ mutationId: decodedMessage.mutationEventEncoded.id.global, requestId }),
123
+ ),
124
+ )
125
+
126
+ // console.debug(`Broadcasting mutation event to ${this.subscribedWebSockets.size} clients`)
127
+
128
+ const connectedClients = this.ctx.getWebSockets()
129
+
130
+ if (connectedClients.length > 0) {
131
+ const broadcastMessage = encodeOutgoingMessage(
132
+ WSMessage.PushBroadcast.make({
133
+ mutationEventEncoded: decodedMessage.mutationEventEncoded,
134
+ persisted: decodedMessage.persisted,
135
+ }),
136
+ )
137
+
138
+ for (const conn of connectedClients) {
139
+ console.log('Broadcasting to client', conn === ws ? 'self' : 'other')
140
+ // if (conn !== ws) {
141
+ conn.send(broadcastMessage)
142
+ // }
143
+ }
144
+ }
145
+
146
+ await storePromise
147
+
148
+ break
149
+ }
150
+ case 'WSMessage.AdminResetRoomReq': {
151
+ if (decodedMessage.adminSecret !== this.env.ADMIN_SECRET) {
152
+ ws.send(encodeOutgoingMessage(WSMessage.Error.make({ message: 'Invalid admin secret', requestId })))
153
+ return
154
+ }
155
+
156
+ await this.storage.resetRoom()
157
+ ws.send(encodeOutgoingMessage(WSMessage.AdminResetRoomRes.make({ requestId })))
158
+
159
+ break
160
+ }
161
+ case 'WSMessage.AdminInfoReq': {
162
+ if (decodedMessage.adminSecret !== this.env.ADMIN_SECRET) {
163
+ ws.send(encodeOutgoingMessage(WSMessage.Error.make({ message: 'Invalid admin secret', requestId })))
164
+ return
165
+ }
166
+
167
+ ws.send(
168
+ encodeOutgoingMessage(
169
+ WSMessage.AdminInfoRes.make({ requestId, info: { durableObjectId: this.ctx.id.toString() } }),
170
+ ),
171
+ )
172
+
173
+ break
174
+ }
175
+ default: {
176
+ console.error('unsupported message', decodedMessage)
177
+ return shouldNeverHappen()
178
+ }
179
+ }
180
+ } catch (error: any) {
181
+ ws.send(encodeOutgoingMessage(WSMessage.Error.make({ message: error.message, requestId })))
182
+ }
183
+ }
184
+
185
+ webSocketClose = async (ws: WebSocketClient, code: number, _reason: string, _wasClean: boolean) => {
186
+ // If the client closes the connection, the runtime will invoke the webSocketClose() handler.
187
+ ws.close(code, 'Durable Object is closing WebSocket')
188
+ }
189
+ }
190
+
191
+ const makeStorage = (ctx: DurableObjectState, env: Env, dbName: string) => {
192
+ const getLatestEvent = async (): Promise<MutationEvent.Any | undefined> => {
193
+ const rawEvents = await env.DB.prepare(`SELECT * FROM ${dbName} ORDER BY id DESC LIMIT 1`).all()
194
+ if (rawEvents.error) {
195
+ throw new Error(rawEvents.error)
196
+ }
197
+ const events = Schema.decodeUnknownSync(Schema.Array(mutationLogTable.schema))(rawEvents.results).map((e) => ({
198
+ ...e,
199
+ id: { global: e.id, local: 0 },
200
+ parentId: { global: e.parentId, local: 0 },
201
+ }))
202
+ return events[0]
203
+ }
204
+
205
+ const getEvents = async (cursor: number | undefined): Promise<ReadonlyArray<MutationEvent.Any>> => {
206
+ const whereClause = cursor ? `WHERE id > ${cursor}` : ''
207
+ // TODO handle case where `cursor` was not found
208
+ const rawEvents = await env.DB.prepare(`SELECT * FROM ${dbName} ${whereClause} ORDER BY id ASC`).all()
209
+ if (rawEvents.error) {
210
+ throw new Error(rawEvents.error)
211
+ }
212
+ const events = Schema.decodeUnknownSync(Schema.Array(mutationLogTable.schema))(rawEvents.results).map((e) => ({
213
+ ...e,
214
+ id: { global: e.id, local: 0 },
215
+ parentId: { global: e.parentId, local: 0 },
216
+ }))
217
+ return events
218
+ }
219
+
220
+ const appendEvent = async (event: MutationEvent.Any) => {
221
+ const sql = `INSERT INTO ${dbName} (id, parentId, args, mutation) VALUES (?, ?, ?, ?)`
222
+ await env.DB.prepare(sql).bind(event.id, event.parentId, JSON.stringify(event.args), event.mutation).run()
223
+ }
224
+
225
+ const resetRoom = async () => {
226
+ await ctx.storage.deleteAll()
227
+ }
228
+
229
+ return { getLatestEvent, getEvents, appendEvent, resetRoom }
230
+ }
@@ -0,0 +1,84 @@
1
+ /// <reference no-default-lib="true"/>
2
+ /// <reference lib="esnext" />
3
+
4
+ // import { mutationEventSchemaEncodedAny } from '@livestore/common/schema'
5
+ // import { Effect, HttpServer, Schema } from '@livestore/utils/effect'
6
+
7
+ import type { Env } from './durable-object.js'
8
+
9
+ export * from './durable-object.js'
10
+
11
+ // const handleRequest = (request: Request, env: Env) =>
12
+ // HttpServer.router.empty.pipe(
13
+ // HttpServer.router.get(
14
+ // '/websocket',
15
+ // Effect.gen(function* () {
16
+ // // This example will refer to the same Durable Object instance,
17
+ // // since the name "foo" is hardcoded.
18
+ // const id = env.WEBSOCKET_SERVER.idFromName('foo')
19
+ // const durableObject = env.WEBSOCKET_SERVER.get(id)
20
+
21
+ // HttpServer.
22
+
23
+ // // Expect to receive a WebSocket Upgrade request.
24
+ // // If there is one, accept the request and return a WebSocket Response.
25
+ // const headerRes = yield* HttpServer.request
26
+ // .schemaHeaders(
27
+ // Schema.Struct({
28
+ // Upgrade: Schema.Literal('websocket'),
29
+ // }),
30
+ // )
31
+ // .pipe(Effect.either)
32
+
33
+ // if (headerRes._tag === 'Left') {
34
+ // // return new Response('Durable Object expected Upgrade: websocket', { status: 426 })
35
+ // return yield* HttpServer.response.text('Durable Object expected Upgrade: websocket', { status: 426 })
36
+ // }
37
+
38
+ // HttpServer.response.empty
39
+
40
+ // return yield* Effect.promise(() => durableObject.fetch(request))
41
+ // }),
42
+ // ),
43
+ // HttpServer.router.catchAll((e) => {
44
+ // console.log(e)
45
+ // return HttpServer.response.empty({ status: 400 })
46
+ // }),
47
+ // (_) => HttpServer.app.toWebHandler(_)(request),
48
+ // // request
49
+ // )
50
+
51
+ // Worker
52
+ export default {
53
+ fetch: async (request: Request, env: Env, _ctx: ExecutionContext): Promise<Response> => {
54
+ const url = new URL(request.url)
55
+ const searchParams = url.searchParams
56
+ const roomId = searchParams.get('room')
57
+
58
+ if (roomId === null) {
59
+ return new Response('Room ID is required', { status: 400 })
60
+ }
61
+
62
+ // This example will refer to the same Durable Object instance,
63
+ // since the name "foo" is hardcoded.
64
+ const id = env.WEBSOCKET_SERVER.idFromName(roomId)
65
+ const durableObject = env.WEBSOCKET_SERVER.get(id)
66
+
67
+ if (url.pathname.endsWith('/websocket')) {
68
+ const upgradeHeader = request.headers.get('Upgrade')
69
+ if (!upgradeHeader || upgradeHeader !== 'websocket') {
70
+ return new Response('Durable Object expected Upgrade: websocket', { status: 426 })
71
+ }
72
+
73
+ return durableObject.fetch(request)
74
+ }
75
+
76
+ return new Response(null, {
77
+ status: 400,
78
+ statusText: 'Bad Request',
79
+ headers: {
80
+ 'Content-Type': 'text/plain',
81
+ },
82
+ })
83
+ },
84
+ }
@@ -0,0 +1 @@
1
+ export * as WSMessage from './ws-message-types.js'