@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.
- package/dist/.tsbuildinfo +1 -0
- package/dist/cf-worker/durable-object.d.ts +69 -0
- package/dist/cf-worker/durable-object.d.ts.map +1 -0
- package/dist/cf-worker/durable-object.js +164 -0
- package/dist/cf-worker/durable-object.js.map +1 -0
- package/dist/cf-worker/index.d.ts +8 -0
- package/dist/cf-worker/index.d.ts.map +1 -0
- package/dist/cf-worker/index.js +67 -0
- package/dist/cf-worker/index.js.map +1 -0
- package/dist/common/index.d.ts +2 -0
- package/dist/common/index.d.ts.map +1 -0
- package/dist/common/index.js +2 -0
- package/dist/common/index.js.map +1 -0
- package/dist/common/ws-message-types.d.ts +344 -0
- package/dist/common/ws-message-types.d.ts.map +1 -0
- package/dist/common/ws-message-types.js +58 -0
- package/dist/common/ws-message-types.js.map +1 -0
- package/dist/sync-impl/index.d.ts +2 -0
- package/dist/sync-impl/index.d.ts.map +1 -0
- package/dist/sync-impl/index.js +2 -0
- package/dist/sync-impl/index.js.map +1 -0
- package/dist/sync-impl/ws-impl.d.ts +18 -0
- package/dist/sync-impl/ws-impl.d.ts.map +1 -0
- package/dist/sync-impl/ws-impl.js +125 -0
- package/dist/sync-impl/ws-impl.js.map +1 -0
- package/package.json +26 -0
- package/src/cf-worker/durable-object.ts +230 -0
- package/src/cf-worker/index.ts +84 -0
- package/src/common/index.ts +1 -0
- package/src/common/ws-message-types.ts +119 -0
- package/src/sync-impl/index.ts +1 -0
- package/src/sync-impl/ws-impl.ts +221 -0
- package/tsconfig.json +12 -0
- package/wrangler.toml +21 -0
|
@@ -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 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/sync-impl/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAA"}
|
|
@@ -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":"AAAA,2BAA2B;AAG3B,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AAEtE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAA;AAClH,OAAO,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAA;AAEhD,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAgB9C,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,OAAsB,EAAwD,EAAE,CACzG,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;IAClB,MAAM,KAAK,GAAG,GAAG,OAAO,CAAC,GAAG,mBAAmB,OAAO,CAAC,MAAM,EAAE,CAAA;IAE/D,MAAM,EAAE,WAAW,EAAE,gBAAgB,EAAE,IAAI,EAAE,GAAG,KAAK,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAA;IAErE,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,EAAE,CAAA;IAE9B,MAAM,GAAG,GAAG;QACV,WAAW;QACX,IAAI,EAAE,CAAC,IAAI,EAAE,EAAE,YAAY,EAAE,EAAE,EAAE,CAC/B,YAAY;YACV,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;gBAClB,MAAM,SAAS,GAAG,MAAM,EAAE,CAAA;gBAC1B,MAAM,MAAM,GAAG,MAAM,CAAC,cAAc,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,CAAA;gBAEzD,KAAK,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC,CAAA;gBAE1D,OAAO,MAAM,CAAC,UAAU,CAAC,gBAAgB,CAAC,CAAC,IAAI,CAC7C,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,mBAAmB,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,EACzF,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CACf,CAAC,CAAC,IAAI,KAAK,iBAAiB,CAAC,CAAC,CAAC,IAAI,gBAAgB,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAC1F,EACD,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,aAAa,EAAE,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,EAClF,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CACjB,GAAG,CAAC,IAAI,KAAK,yBAAyB;oBACpC,CAAC,CAAC,CAAC,EAAE,oBAAoB,EAAE,GAAG,CAAC,oBAAoB,EAAE,SAAS,EAAE,GAAG,CAAC,SAAS,EAAE,QAAQ,EAAE,CAAC;oBAC1F,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,oBAAoB,EAAE,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC,CACpF,EACD,MAAM,CAAC,gBAAgB,CACxB,CAAA;YACH,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC;YACxB,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;gBAClB,MAAM,SAAS,GAAG,MAAM,EAAE,CAAA;gBAC1B,MAAM,MAAM,GAAG,MAAM,CAAC,cAAc,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,CAAA;gBAEzD,KAAK,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC,CAAA;gBAE1D,OAAO,MAAM,CAAC,UAAU,CAAC,gBAAgB,CAAC,CAAC,IAAI,CAC7C,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,yBAAyB,IAAI,CAAC,CAAC,SAAS,KAAK,SAAS,CAAC,EACvF,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CACf,CAAC,CAAC,IAAI,KAAK,iBAAiB,CAAC,CAAC,CAAC,IAAI,gBAAgB,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAC1F,EACD,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,EAC3C,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,KAAK,KAAK,CAAC,EAC5C,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,EAC3B,MAAM,CAAC,gBAAgB,EACvB,MAAM,CAAC,GAAG,CAAC,CAAC,oBAAoB,EAAE,EAAE,CAAC,CAAC;oBACpC,oBAAoB;oBACpB,QAAQ;oBACR,SAAS,EAAE,IAAI;iBAChB,CAAC,CAAC,CACJ,CAAA;YACH,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC;QAC5B,IAAI,EAAE,CAAC,oBAAoB,EAAE,SAAS,EAAE,EAAE,CACxC,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;YAClB,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,QAAQ,CAAC,IAAI,EAA0B,CAAA;YAC5D,MAAM,SAAS,GAAG,MAAM,EAAE,CAAA;YAE1B,KAAK,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC,gBAAgB,CAAC,CAAC,IAAI,CAC7C,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,yBAAyB,IAAI,CAAC,CAAC,SAAS,KAAK,SAAS,CAAC,EACvF,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CACf,CAAC,CAAC,IAAI,KAAK,iBAAiB;gBAC1B,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,gBAAgB,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;gBACpE,CAAC,CAAC,MAAM,CAAC,IAAI,CAChB,EACD,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,EAC3C,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,KAAK,oBAAoB,CAAC,EAAE,CAAC,MAAM,CAAC,EACrE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EACd,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,EACjD,MAAM,CAAC,QAAQ,EACf,MAAM,CAAC,iBAAiB,EACxB,MAAM,CAAC,IAAI,CACZ,CAAA;YAED,KAAK,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,oBAAoB,EAAE,SAAS,EAAE,SAAS,EAAE,CAAC,CAAC,CAAA;YAEnF,KAAK,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;YAE5B,OAAO,EAAE,QAAQ,EAAE,CAAA;QACrB,CAAC,CAAC;KACuB,CAAA;IAE7B,OAAO,GAAG,CAAA;AACZ,CAAC,CAAC,CAAA;AAEJ,MAAM,OAAO,GAAG,CAAC,KAAa,EAAE,EAAE,CAChC,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;IAClB,MAAM,WAAW,GAAG,KAAK,CAAC,CAAC,eAAe,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IACtD,MAAM,KAAK,GAAuC,EAAE,OAAO,EAAE,SAAS,EAAE,CAAA;IAExE,MAAM,gBAAgB,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,SAAS,EAA6D,CAAA;IAE7G,MAAM,eAAe,GAAG,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAA;IAEzG,MAAM,IAAI,GAAG,CAAC,OAA0B,EAAE,EAAE,CAC1C,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;QAClB,gCAAgC;QAChC,KAAK,CAAC,CAAC,eAAe,CAAA;QAEtB,KAAK,CAAC,OAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,SAAS,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAA;IACtF,CAAC,CAAC,CAAA;IAEJ,MAAM,YAAY,GAAG,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;QACvC,4FAA4F;QAC5F,+FAA+F;QAC/F,OAAO,SAAS,CAAC,MAAM,KAAK,KAAK,EAAE,CAAC;YAClC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;QAC3B,CAAC;QACD,oCAAoC;QACpC,wFAAwF;QACxF,IAAI;QAEJ,MAAM,EAAE,GAAG,IAAI,SAAS,CAAC,KAAK,CAAC,CAAA;QAC/B,MAAM,gBAAgB,GAAG,KAAK,CAAC,CAAC,QAAQ,CAAC,IAAI,EAAQ,CAAA;QAErD,MAAM,YAAY,GAAG,KAAK,CAAC,CAAC,KAAK,CAAC,SAAS,EAAkB,CAAA;QAE7D,MAAM,cAAc,GAAG,CAAC,KAAwB,EAAQ,EAAE;YACxD,MAAM,eAAe,GAAG,MAAM,CAAC,mBAAmB,CAAC,MAAM,CAAC,SAAS,CAAC,SAAS,CAAC,sBAAsB,CAAC,CAAC,CACpG,KAAK,CAAC,IAAI,CACX,CAAA;YAED,IAAI,eAAe,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;gBACpC,OAAO,CAAC,KAAK,CAAC,gCAAgC,EAAE,eAAe,CAAC,IAAI,CAAC,CAAA;gBACrE,OAAM;YACR,CAAC;iBAAM,CAAC;gBACN,IAAI,eAAe,CAAC,KAAK,CAAC,IAAI,KAAK,gBAAgB,EAAE,CAAC;oBACpD,KAAK,CAAC,KAAK,CAAC,YAAY,EAAE,eAAe,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;gBACvE,CAAC;qBAAM,CAAC;oBACN,MAAM,CAAC,OAAO,CAAC,gBAAgB,EAAE,eAAe,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;gBAC9E,CAAC;YACH,CAAC;QACH,CAAC,CAAA;QAED,MAAM,cAAc,GAAG,GAAG,EAAE;YAC1B,QAAQ,CAAC,OAAO,CAAC,gBAAgB,EAAE,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;QACjE,CAAC,CAAA;QAED,4GAA4G;QAC5G,wGAAwG;QACxG,IAAI,CAAC,gBAAgB,CAAC,SAAS,EAAE,cAAc,CAAC,CAAA;QAEhD,KAAK,CAAC,CAAC,MAAM,CAAC,YAAY,CAAC,GAAG,EAAE,CAC9B,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;YAClB,EAAE,CAAC,mBAAmB,CAAC,SAAS,EAAE,cAAc,CAAC,CAAA;YACjD,IAAI,CAAC,mBAAmB,CAAC,SAAS,EAAE,cAAc,CAAC,CAAA;YACnD,KAAK,CAAC,OAAO,EAAE,KAAK,EAAE,CAAA;YACtB,KAAK,CAAC,OAAO,GAAG,SAAS,CAAA;YACzB,KAAK,CAAC,CAAC,eAAe,CAAC,GAAG,CAAC,WAAW,EAAE,KAAK,CAAC,CAAA;QAChD,CAAC,CAAC,CACH,CAAA;QAED,EAAE,CAAC,gBAAgB,CAAC,SAAS,EAAE,cAAc,CAAC,CAAA;QAE9C,IAAI,EAAE,CAAC,UAAU,KAAK,SAAS,CAAC,IAAI,EAAE,CAAC;YACrC,KAAK,CAAC,OAAO,GAAG,EAAE,CAAA;YAClB,eAAe,CAAC,GAAG,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;QAC7D,CAAC;aAAM,CAAC;YACN,EAAE,CAAC,gBAAgB,CAAC,MAAM,EAAE,GAAG,EAAE;gBAC/B,KAAK,CAAC,OAAO,GAAG,EAAE,CAAA;gBAClB,eAAe,CAAC,GAAG,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;YAC7D,CAAC,CAAC,CAAA;QACJ,CAAC;QAED,EAAE,CAAC,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE;YAChC,QAAQ,CAAC,OAAO,CAAC,gBAAgB,EAAE,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;QACjE,CAAC,CAAC,CAAA;QAEF,EAAE,CAAC,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE;YAChC,EAAE,CAAC,KAAK,EAAE,CAAA;YACV,QAAQ,CAAC,OAAO,CAAC,gBAAgB,EAAE,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;QACjE,CAAC,CAAC,CAAA;QAEF,MAAM,aAAa,GAAG,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;YACxC,yDAAyD;YACzD,KAAK,CAAC,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC,CAAA;YAE1D,qFAAqF;YACrF,KAAK,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAA;YAE1D,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAA;QAC7B,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,0CAA0C,CAAC,CAAC,CAAA;QAEpE,KAAK,CAAC,CAAC,eAAe,CAAC,IAAI,CACzB,MAAM,CAAC,OAAO,CAAC,aAAa,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,EAClD,MAAM,CAAC,aAAa,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAC,gBAAgB,EAAE,KAAK,CAAC,CAAC,CAAC,EACtE,MAAM,CAAC,UAAU,CAClB,CAAA;QAED,KAAK,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAA;IACzC,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;IAEtB,KAAK,CAAC,CAAC,YAAY,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,iBAAiB,EAAE,MAAM,CAAC,UAAU,CAAC,CAAA;IAErF,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,IAAI,EAAE,CAAA;AAChD,CAAC,CAAC,CAAA"}
|
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'
|