@derivation/rpc 0.1.6 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/client-handler.d.ts +12 -11
  2. package/dist/client-handler.js +19 -13
  3. package/dist/client.d.ts +4 -3
  4. package/dist/client.js +8 -8
  5. package/dist/index.d.ts +7 -7
  6. package/dist/index.js +6 -6
  7. package/dist/iso.d.ts +1 -1
  8. package/dist/iso.js +1 -1
  9. package/dist/messageport-transport.d.ts +13 -0
  10. package/dist/messageport-transport.js +28 -0
  11. package/dist/node-web-socket-transport.d.ts +16 -0
  12. package/dist/node-web-socket-transport.js +33 -0
  13. package/dist/queue.d.ts +9 -0
  14. package/dist/queue.js +32 -0
  15. package/dist/rate-limiter.d.ts +1 -1
  16. package/dist/rate-limiter.js +13 -8
  17. package/dist/reactive-map-adapter.d.ts +4 -3
  18. package/dist/reactive-map-adapter.js +3 -2
  19. package/dist/reactive-set-adapter.d.ts +4 -3
  20. package/dist/reactive-set-adapter.js +3 -2
  21. package/dist/shared-worker-client-handler.d.ts +27 -0
  22. package/dist/shared-worker-client-handler.js +149 -0
  23. package/dist/shared-worker-client.d.ts +22 -0
  24. package/dist/shared-worker-client.js +25 -0
  25. package/dist/shared-worker-server.d.ts +28 -0
  26. package/dist/shared-worker-server.js +62 -0
  27. package/dist/stream-adapter.d.ts +2 -2
  28. package/dist/stream-adapter.js +2 -1
  29. package/dist/stream-types.d.ts +4 -4
  30. package/dist/transport.d.ts +27 -0
  31. package/dist/transport.js +1 -0
  32. package/dist/tsconfig.tsbuildinfo +1 -1
  33. package/dist/web-socket-server.d.ts +11 -0
  34. package/dist/web-socket-server.js +59 -0
  35. package/dist/web-socket-transport.d.ts +13 -0
  36. package/dist/web-socket-transport.js +25 -0
  37. package/dist/websocket-server.d.ts +8 -2
  38. package/dist/websocket-server.js +29 -5
  39. package/dist/websocket-transport.d.ts +28 -0
  40. package/dist/websocket-transport.js +58 -0
  41. package/package.json +23 -2
@@ -1,10 +1,11 @@
1
- import { RawData, WebSocket } from "ws";
2
- import { ClientMessage } from "./client-message";
3
- import { ServerMessage } from "./server-message";
4
- import { StreamEndpoints, MutationEndpoints, RPCDefinition } from "./stream-types";
5
- import { PresenceHandler } from "./presence-manager";
6
- export declare class ClientHandler<Defs extends RPCDefinition> {
7
- private readonly ws;
1
+ import { ClientMessage } from "./client-message.js";
2
+ import { ServerMessage } from "./server-message.js";
3
+ import { StreamEndpoints, MutationEndpoints, RPCDefinition } from "./stream-types.js";
4
+ import { PresenceHandler } from "./presence-manager.js";
5
+ import { Transport } from "./transport.js";
6
+ export declare class ClientHandler<Defs extends RPCDefinition, Ctx = void> {
7
+ private readonly transport;
8
+ private readonly context;
8
9
  private readonly streamEndpoints;
9
10
  private readonly mutationEndpoints;
10
11
  private readonly presenceHandler?;
@@ -14,13 +15,13 @@ export declare class ClientHandler<Defs extends RPCDefinition> {
14
15
  private heartbeatTimeout;
15
16
  private inactivityTimeout;
16
17
  private readonly rateLimiter;
17
- constructor(ws: WebSocket, streamEndpoints: StreamEndpoints<Defs["streams"]>, mutationEndpoints: MutationEndpoints<Defs["mutations"]>, presenceHandler?: PresenceHandler);
18
+ constructor(transport: Transport, context: Ctx, streamEndpoints: StreamEndpoints<Defs["streams"], Ctx>, mutationEndpoints: MutationEndpoints<Defs["mutations"], Ctx>, presenceHandler?: PresenceHandler);
18
19
  private resetHeartbeat;
19
20
  private resetInactivity;
20
- handleMessage(message: RawData): void;
21
- handleClientMessage(message: ClientMessage): void;
21
+ handleMessage(message: string): void;
22
+ handleClientMessage(message: ClientMessage): Promise<void>;
22
23
  handleStep(): void;
23
24
  sendMessage(message: ServerMessage): void;
24
- handleDisconnect(): void;
25
+ private handleDisconnect;
25
26
  close(): void;
26
27
  }
@@ -1,16 +1,20 @@
1
- import { parseClientMessage } from "./client-message";
2
- import { ServerMessage } from "./server-message";
3
- import { RateLimiter } from "./rate-limiter";
1
+ import { parseClientMessage } from "./client-message.js";
2
+ import { ServerMessage } from "./server-message.js";
3
+ import { RateLimiter } from "./rate-limiter.js";
4
4
  export class ClientHandler {
5
- constructor(ws, streamEndpoints, mutationEndpoints, presenceHandler) {
5
+ constructor(transport, context, streamEndpoints, mutationEndpoints, presenceHandler) {
6
6
  this.closed = false;
7
7
  this.streams = new Map();
8
- this.ws = ws;
8
+ this.transport = transport;
9
+ this.context = context;
9
10
  this.streamEndpoints = streamEndpoints;
10
11
  this.mutationEndpoints = mutationEndpoints;
11
12
  this.presenceHandler = presenceHandler;
12
13
  this.rateLimiter = new RateLimiter(100, 300); // 100 messages over 5 minutes
13
14
  console.log("new client connected");
15
+ // Set up transport handlers
16
+ this.transport.onMessage((data) => this.handleMessage(data));
17
+ this.transport.onClose(() => this.handleDisconnect());
14
18
  this.resetHeartbeat();
15
19
  this.resetInactivity();
16
20
  }
@@ -40,10 +44,10 @@ export class ClientHandler {
40
44
  }
41
45
  let data;
42
46
  try {
43
- data = JSON.parse(message.toString());
47
+ data = JSON.parse(message);
44
48
  }
45
49
  catch (_a) {
46
- console.error("Invalid JSON received:", message.toString());
50
+ console.error("Invalid JSON received:", message);
47
51
  return this.close();
48
52
  }
49
53
  let parsed;
@@ -56,7 +60,7 @@ export class ClientHandler {
56
60
  }
57
61
  this.handleClientMessage(parsed);
58
62
  }
59
- handleClientMessage(message) {
63
+ async handleClientMessage(message) {
60
64
  switch (message.type) {
61
65
  case "subscribe": {
62
66
  const { id, name, args } = message;
@@ -67,7 +71,7 @@ export class ClientHandler {
67
71
  }
68
72
  const endpoint = this.streamEndpoints[name];
69
73
  try {
70
- const source = endpoint(args);
74
+ const source = await endpoint(args, this.context);
71
75
  this.streams.set(id, source);
72
76
  this.sendMessage(ServerMessage.subscribed(id, source.Snapshot));
73
77
  console.log(`Client subscribed to \"${name}\" (${id})`);
@@ -92,7 +96,7 @@ export class ClientHandler {
92
96
  return;
93
97
  }
94
98
  const endpoint = this.mutationEndpoints[name];
95
- endpoint(args)
99
+ endpoint(args, this.context)
96
100
  .then((result) => {
97
101
  if (result.success) {
98
102
  this.sendMessage(ServerMessage.resultSuccess(id, result.value));
@@ -146,13 +150,15 @@ export class ClientHandler {
146
150
  sendMessage(message) {
147
151
  this.resetHeartbeat();
148
152
  if (!this.closed) {
149
- if (this.ws.bufferedAmount > 100 * 1024) {
153
+ // Check buffer if available (WebSocket provides this, MessagePort doesn't)
154
+ if (this.transport.bufferedAmount !== undefined &&
155
+ this.transport.bufferedAmount > 100 * 1024) {
150
156
  console.log("Send buffer exceeded 100KB, closing connection");
151
157
  this.close();
152
158
  return;
153
159
  }
154
160
  try {
155
- this.ws.send(JSON.stringify(message));
161
+ this.transport.send(JSON.stringify(message));
156
162
  }
157
163
  catch (err) {
158
164
  console.error("Failed to send message:", err);
@@ -174,7 +180,7 @@ export class ClientHandler {
174
180
  this.presenceHandler.remove(this.currentPresence);
175
181
  }
176
182
  try {
177
- this.ws.close();
183
+ this.transport.close();
178
184
  }
179
185
  catch (_a) { }
180
186
  }
package/dist/client.d.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import type { Graph } from "derivation";
2
- import type { StreamSinks, RPCDefinition, MutationResult } from "./stream-types";
2
+ import type { StreamSinks, RPCDefinition, MutationResult } from "./stream-types.js";
3
+ import { Transport } from "./transport.js";
3
4
  export declare class Client<Defs extends RPCDefinition> {
4
- private ws;
5
+ private transport;
5
6
  private sinks;
6
7
  private graph;
7
8
  private nextId;
@@ -13,7 +14,7 @@ export declare class Client<Defs extends RPCDefinition> {
13
14
  private registry;
14
15
  private resetHeartbeat;
15
16
  private resetInactivity;
16
- constructor(ws: WebSocket, sinks: StreamSinks<Defs["streams"]>, graph: Graph);
17
+ constructor(transport: Transport, sinks: StreamSinks<Defs["streams"]>, graph: Graph);
17
18
  private handleMessage;
18
19
  private sendMessage;
19
20
  run<Key extends keyof Defs["streams"]>(key: Key, args: Defs["streams"][Key]["args"]): Promise<Defs["streams"][Key]["returnType"]>;
package/dist/client.js CHANGED
@@ -1,4 +1,4 @@
1
- import { ClientMessage } from "./client-message";
1
+ import { ClientMessage } from "./client-message.js";
2
2
  function changer(sink, input) {
3
3
  return (change) => {
4
4
  const i = input.deref();
@@ -24,8 +24,8 @@ export class Client {
24
24
  this.close();
25
25
  }, 30000);
26
26
  }
27
- constructor(ws, sinks, graph) {
28
- this.ws = ws;
27
+ constructor(transport, sinks, graph) {
28
+ this.transport = transport;
29
29
  this.sinks = sinks;
30
30
  this.graph = graph;
31
31
  this.nextId = 1;
@@ -37,10 +37,10 @@ export class Client {
37
37
  this.sendMessage(ClientMessage.unsubscribe(id));
38
38
  this.activeStreams.delete(id);
39
39
  });
40
- this.ws.onmessage = (event) => {
41
- const message = JSON.parse(event.data);
40
+ this.transport.onMessage((data) => {
41
+ const message = JSON.parse(data);
42
42
  this.handleMessage(message);
43
- };
43
+ });
44
44
  this.resetHeartbeat();
45
45
  this.resetInactivity();
46
46
  }
@@ -93,7 +93,7 @@ export class Client {
93
93
  }
94
94
  sendMessage(message) {
95
95
  this.resetHeartbeat();
96
- this.ws.send(JSON.stringify(message));
96
+ this.transport.send(JSON.stringify(message));
97
97
  }
98
98
  async run(key, args) {
99
99
  console.log(`Running stream ${String(key)} with args ${JSON.stringify(args)}`);
@@ -123,7 +123,7 @@ export class Client {
123
123
  clearTimeout(this.heartbeatTimeout);
124
124
  clearTimeout(this.inactivityTimeout);
125
125
  try {
126
- this.ws.close();
126
+ this.transport.close();
127
127
  }
128
128
  catch (_a) { }
129
129
  }
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- export * from "./client";
2
- export * from "./stream-types";
3
- export { ReactiveSetSourceAdapter, ReactiveSetSinkAdapter, sink as setSink } from "./reactive-set-adapter";
4
- export { ReactiveMapSourceAdapter, ReactiveMapSinkAdapter, sink as mapSink } from "./reactive-map-adapter";
5
- export { StreamSourceAdapter, StreamSinkAdapter, sink as streamSink } from "./stream-adapter";
6
- export { setupWebSocketServer } from "./websocket-server";
7
- export type { PresenceHandler } from "./presence-manager";
1
+ export * from "./client.js";
2
+ export * from "./stream-types.js";
3
+ export { ReactiveSetSourceAdapter, ReactiveSetSinkAdapter, sink as setSink, } from "./reactive-set-adapter.js";
4
+ export { ReactiveMapSourceAdapter, ReactiveMapSinkAdapter, sink as mapSink, } from "./reactive-map-adapter.js";
5
+ export { StreamSourceAdapter, StreamSinkAdapter, sink as streamSink, } from "./stream-adapter.js";
6
+ export type { Transport } from "./transport.js";
7
+ export type { PresenceHandler } from "./presence-manager.js";
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
- export * from "./client";
2
- export * from "./stream-types";
3
- export { ReactiveSetSourceAdapter, ReactiveSetSinkAdapter, sink as setSink } from "./reactive-set-adapter";
4
- export { ReactiveMapSourceAdapter, ReactiveMapSinkAdapter, sink as mapSink } from "./reactive-map-adapter";
5
- export { StreamSourceAdapter, StreamSinkAdapter, sink as streamSink } from "./stream-adapter";
6
- export { setupWebSocketServer } from "./websocket-server";
1
+ // Main index only exports isomorphic code that works in both browser and Node.js
2
+ export * from "./client.js";
3
+ export * from "./stream-types.js";
4
+ export { ReactiveSetSourceAdapter, ReactiveSetSinkAdapter, sink as setSink, } from "./reactive-set-adapter.js";
5
+ export { ReactiveMapSourceAdapter, ReactiveMapSinkAdapter, sink as mapSink, } from "./reactive-map-adapter.js";
6
+ export { StreamSourceAdapter, StreamSinkAdapter, sink as streamSink, } from "./stream-adapter.js";
package/dist/iso.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { RecordOf } from "immutable";
2
- import { ZSet, ZMap } from "derivation";
2
+ import { ZSet, ZMap } from "@derivation/relational";
3
3
  export interface Iso<In, Out> {
4
4
  to(x: In): Out;
5
5
  from(x: Out): In;
package/dist/iso.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Record as ImmutableRecord } from "immutable";
2
- import { ZSet, ZMap } from "derivation";
2
+ import { ZSet, ZMap } from "@derivation/relational";
3
3
  export function id() {
4
4
  return {
5
5
  to: (x) => x,
@@ -0,0 +1,13 @@
1
+ import { Transport } from "./transport.js";
2
+ /**
3
+ * Transport implementation for MessagePort (SharedWorker communication).
4
+ */
5
+ export declare class MessagePortTransport implements Transport {
6
+ private port;
7
+ constructor(port: MessagePort);
8
+ send(data: string): void;
9
+ onMessage(handler: (data: string) => void): void;
10
+ onClose(handler: () => void): void;
11
+ close(): void;
12
+ get bufferedAmount(): undefined;
13
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Transport implementation for MessagePort (SharedWorker communication).
3
+ */
4
+ export class MessagePortTransport {
5
+ constructor(port) {
6
+ this.port = port;
7
+ }
8
+ send(data) {
9
+ this.port.postMessage(data);
10
+ }
11
+ onMessage(handler) {
12
+ this.port.onmessage = (event) => {
13
+ handler(event.data);
14
+ };
15
+ }
16
+ onClose(handler) {
17
+ // MessagePort doesn't have a reliable close event
18
+ // We'll use messageerror as a signal, though it's not perfect
19
+ this.port.onmessageerror = handler;
20
+ }
21
+ close() {
22
+ this.port.close();
23
+ }
24
+ // MessagePort doesn't provide bufferedAmount
25
+ get bufferedAmount() {
26
+ return undefined;
27
+ }
28
+ }
@@ -0,0 +1,16 @@
1
+ import { WebSocket } from "ws";
2
+ import { Transport } from "./transport.js";
3
+ /**
4
+ * Transport implementation for Node.js WebSocket (server-side using 'ws' library).
5
+ */
6
+ export declare class NodeWebSocketTransport implements Transport {
7
+ private ws;
8
+ private messageHandlerSet;
9
+ private closeHandlerSet;
10
+ constructor(ws: WebSocket);
11
+ send(data: string): void;
12
+ onMessage(handler: (data: string) => void): void;
13
+ onClose(handler: () => void): void;
14
+ close(): void;
15
+ get bufferedAmount(): number;
16
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Transport implementation for Node.js WebSocket (server-side using 'ws' library).
3
+ */
4
+ export class NodeWebSocketTransport {
5
+ constructor(ws) {
6
+ this.ws = ws;
7
+ this.messageHandlerSet = false;
8
+ this.closeHandlerSet = false;
9
+ }
10
+ send(data) {
11
+ this.ws.send(data);
12
+ }
13
+ onMessage(handler) {
14
+ if (!this.messageHandlerSet) {
15
+ this.ws.on("message", (data) => {
16
+ handler(data.toString());
17
+ });
18
+ this.messageHandlerSet = true;
19
+ }
20
+ }
21
+ onClose(handler) {
22
+ if (!this.closeHandlerSet) {
23
+ this.ws.on("close", handler);
24
+ this.closeHandlerSet = true;
25
+ }
26
+ }
27
+ close() {
28
+ this.ws.close();
29
+ }
30
+ get bufferedAmount() {
31
+ return this.ws.bufferedAmount;
32
+ }
33
+ }
@@ -0,0 +1,9 @@
1
+ export declare class Queue<T> {
2
+ private front;
3
+ private back;
4
+ get length(): number;
5
+ isEmpty(): boolean;
6
+ push(item: T): void;
7
+ peek(): T | undefined;
8
+ pop(): T | undefined;
9
+ }
package/dist/queue.js ADDED
@@ -0,0 +1,32 @@
1
+ export class Queue {
2
+ constructor() {
3
+ this.front = [];
4
+ this.back = [];
5
+ }
6
+ get length() {
7
+ return this.front.length + this.back.length;
8
+ }
9
+ isEmpty() {
10
+ return this.front.length === 0 && this.back.length === 0;
11
+ }
12
+ push(item) {
13
+ this.front.push(item);
14
+ }
15
+ peek() {
16
+ if (this.back.length === 0) {
17
+ if (this.front.length === 0) {
18
+ return undefined;
19
+ }
20
+ return this.front[0];
21
+ }
22
+ return this.back[this.back.length - 1];
23
+ }
24
+ pop() {
25
+ if (this.back.length === 0) {
26
+ // Reverse front and swap to back
27
+ this.back = this.front.reverse();
28
+ this.front = [];
29
+ }
30
+ return this.back.pop();
31
+ }
32
+ }
@@ -1,5 +1,5 @@
1
1
  export declare class RateLimiter {
2
- private timestamps;
2
+ private readonly timestamps;
3
3
  private readonly maxOccurrences;
4
4
  private readonly windowNanos;
5
5
  constructor(maxOccurrences: number, windowSeconds: number);
@@ -1,19 +1,24 @@
1
+ import { Queue } from "./queue.js";
1
2
  export class RateLimiter {
2
3
  constructor(maxOccurrences, windowSeconds) {
3
- this.timestamps = [];
4
+ this.timestamps = new Queue();
4
5
  this.maxOccurrences = maxOccurrences;
5
6
  this.windowNanos = BigInt(windowSeconds) * BigInt(1000000000);
6
7
  }
7
8
  trigger() {
8
9
  const now = process.hrtime.bigint();
9
- // Add current timestamp
10
- this.timestamps.push(now);
11
- // Remove expired timestamps from front
12
10
  const cutoff = now - this.windowNanos;
13
- while (this.timestamps.length > 0 && this.timestamps[0] < cutoff) {
14
- this.timestamps.shift();
11
+ while (!this.timestamps.isEmpty()) {
12
+ const oldest = this.timestamps.peek();
13
+ if (oldest === undefined || oldest >= cutoff) {
14
+ break;
15
+ }
16
+ this.timestamps.pop();
17
+ }
18
+ if (this.timestamps.length >= this.maxOccurrences) {
19
+ return true;
15
20
  }
16
- // Return true if we've exceeded the limit
17
- return this.timestamps.length > this.maxOccurrences;
21
+ this.timestamps.push(now);
22
+ return false;
18
23
  }
19
24
  }
@@ -1,6 +1,7 @@
1
- import { Graph, ReactiveMap, ReactiveMapSource, ZMapChangeInput } from "derivation";
2
- import { Source, Sink } from "./stream-types";
3
- import { Iso } from "./iso";
1
+ import { Graph } from "derivation";
2
+ import { ReactiveMap, ReactiveMapSource, ZMapChangeInput } from "@derivation/relational";
3
+ import { Source, Sink } from "./stream-types.js";
4
+ import { Iso } from "./iso.js";
4
5
  export declare class ReactiveMapSourceAdapter<K, V> implements Source<ReactiveMap<K, V>> {
5
6
  private readonly map;
6
7
  private readonly iso;
@@ -1,4 +1,5 @@
1
- import { zmap } from "./iso";
1
+ import { inputMap, } from "@derivation/relational";
2
+ import { zmap } from "./iso.js";
2
3
  export class ReactiveMapSourceAdapter {
3
4
  constructor(map, keyIso, valueIso) {
4
5
  this.map = map;
@@ -30,7 +31,7 @@ export class ReactiveMapSinkAdapter {
30
31
  }
31
32
  }
32
33
  build() {
33
- const stream = this.graph.inputMap(this.initialMap);
34
+ const stream = inputMap(this.graph, this.initialMap);
34
35
  return { stream, input: stream.changes };
35
36
  }
36
37
  }
@@ -1,6 +1,7 @@
1
- import { ZSet, Graph, ReactiveSet, ReactiveSetSource, ZSetChangeInput } from "derivation";
2
- import { Source, Sink } from "./stream-types";
3
- import { Iso } from "./iso";
1
+ import { Graph } from "derivation";
2
+ import { ZSet, ReactiveSet, ReactiveSetSource, ZSetChangeInput } from "@derivation/relational";
3
+ import { Source, Sink } from "./stream-types.js";
4
+ import { Iso } from "./iso.js";
4
5
  export declare class ReactiveSetSourceAdapter<T> implements Source<ReactiveSet<T>> {
5
6
  private readonly set;
6
7
  private readonly iso;
@@ -1,4 +1,5 @@
1
- import { zset, zsetToArray, compose } from "./iso";
1
+ import { inputSet, } from "@derivation/relational";
2
+ import { zset, zsetToArray, compose } from "./iso.js";
2
3
  export class ReactiveSetSourceAdapter {
3
4
  constructor(set, iso) {
4
5
  this.set = set;
@@ -27,7 +28,7 @@ export class ReactiveSetSinkAdapter {
27
28
  input.push(this.iso.from(change));
28
29
  }
29
30
  build() {
30
- const stream = this.graph.inputSet(this.initialSet);
31
+ const stream = inputSet(this.graph, this.initialSet);
31
32
  return { stream, input: stream.changes };
32
33
  }
33
34
  }
@@ -0,0 +1,27 @@
1
+ import { ClientMessage } from "./client-message.js";
2
+ import { ServerMessage } from "./server-message.js";
3
+ import { StreamEndpoints, MutationEndpoints, RPCDefinition } from "./stream-types.js";
4
+ import { PresenceHandler } from "./presence-manager.js";
5
+ import { Transport } from "./transport.js";
6
+ /**
7
+ * Client handler for SharedWorker connections (browser-only).
8
+ * Simplified version without rate limiting, heartbeats, or inactivity timeouts.
9
+ * Designed for same-origin trusted connections.
10
+ */
11
+ export declare class SharedWorkerClientHandler<Defs extends RPCDefinition, Ctx = void> {
12
+ private readonly transport;
13
+ private readonly context;
14
+ private readonly streamEndpoints;
15
+ private readonly mutationEndpoints;
16
+ private readonly presenceHandler?;
17
+ private currentPresence?;
18
+ private closed;
19
+ private readonly streams;
20
+ constructor(transport: Transport, context: Ctx, streamEndpoints: StreamEndpoints<Defs["streams"], Ctx>, mutationEndpoints: MutationEndpoints<Defs["mutations"], Ctx>, presenceHandler?: PresenceHandler);
21
+ handleMessage(message: string): void;
22
+ handleClientMessage(message: ClientMessage): Promise<void>;
23
+ handleStep(): void;
24
+ sendMessage(message: ServerMessage): void;
25
+ private handleDisconnect;
26
+ close(): void;
27
+ }
@@ -0,0 +1,149 @@
1
+ import { parseClientMessage } from "./client-message.js";
2
+ import { ServerMessage } from "./server-message.js";
3
+ /**
4
+ * Client handler for SharedWorker connections (browser-only).
5
+ * Simplified version without rate limiting, heartbeats, or inactivity timeouts.
6
+ * Designed for same-origin trusted connections.
7
+ */
8
+ export class SharedWorkerClientHandler {
9
+ constructor(transport, context, streamEndpoints, mutationEndpoints, presenceHandler) {
10
+ this.closed = false;
11
+ this.streams = new Map();
12
+ this.transport = transport;
13
+ this.context = context;
14
+ this.streamEndpoints = streamEndpoints;
15
+ this.mutationEndpoints = mutationEndpoints;
16
+ this.presenceHandler = presenceHandler;
17
+ console.log("new client connected");
18
+ // Set up transport handlers
19
+ this.transport.onMessage((data) => this.handleMessage(data));
20
+ this.transport.onClose(() => this.handleDisconnect());
21
+ }
22
+ handleMessage(message) {
23
+ let data;
24
+ try {
25
+ data = JSON.parse(message);
26
+ }
27
+ catch (_a) {
28
+ console.error("Invalid JSON received:", message);
29
+ return this.close();
30
+ }
31
+ let parsed;
32
+ try {
33
+ parsed = parseClientMessage(data);
34
+ }
35
+ catch (error) {
36
+ console.error("Invalid client message:", error);
37
+ return this.close();
38
+ }
39
+ this.handleClientMessage(parsed);
40
+ }
41
+ async handleClientMessage(message) {
42
+ switch (message.type) {
43
+ case "subscribe": {
44
+ const { id, name, args } = message;
45
+ if (!(name in this.streamEndpoints)) {
46
+ console.error(`Unknown stream: ${name}`);
47
+ this.close();
48
+ return;
49
+ }
50
+ const endpoint = this.streamEndpoints[name];
51
+ try {
52
+ const source = await endpoint(args, this.context);
53
+ this.streams.set(id, source);
54
+ this.sendMessage(ServerMessage.subscribed(id, source.Snapshot));
55
+ console.log(`Client subscribed to "${name}" (${id})`);
56
+ }
57
+ catch (err) {
58
+ console.error(`Error building stream ${name}:`, err);
59
+ this.close();
60
+ }
61
+ break;
62
+ }
63
+ case "unsubscribe": {
64
+ const { id } = message;
65
+ this.streams.delete(id);
66
+ console.log(`Client unsubscribed from ${id}`);
67
+ break;
68
+ }
69
+ case "call": {
70
+ const { id, name, args } = message;
71
+ if (!(name in this.mutationEndpoints)) {
72
+ console.error(`Unknown mutation: ${name}`);
73
+ this.close();
74
+ return;
75
+ }
76
+ const endpoint = this.mutationEndpoints[name];
77
+ endpoint(args, this.context)
78
+ .then((result) => {
79
+ if (result.success) {
80
+ this.sendMessage(ServerMessage.resultSuccess(id, result.value));
81
+ console.log(`Mutation "${name}" (${id}) completed successfully`);
82
+ }
83
+ else {
84
+ this.sendMessage(ServerMessage.resultError(id, result.error));
85
+ console.log(`Mutation "${name}" (${id}) returned error: ${result.error}`);
86
+ }
87
+ })
88
+ .catch((err) => {
89
+ console.error(`Unhandled exception in mutation "${name}" (${id}):`, err);
90
+ this.close();
91
+ });
92
+ break;
93
+ }
94
+ case "heartbeat":
95
+ break;
96
+ case "presence": {
97
+ if (!this.presenceHandler) {
98
+ console.error("Presence not configured");
99
+ this.close();
100
+ return;
101
+ }
102
+ const { data } = message;
103
+ if (this.currentPresence !== undefined) {
104
+ this.presenceHandler.update(this.currentPresence, data);
105
+ }
106
+ else {
107
+ this.presenceHandler.add(data);
108
+ }
109
+ this.currentPresence = data;
110
+ break;
111
+ }
112
+ }
113
+ }
114
+ handleStep() {
115
+ if (this.closed)
116
+ return;
117
+ const changes = {};
118
+ for (const [id, source] of this.streams) {
119
+ const change = source.LastChange;
120
+ if (change === null)
121
+ continue;
122
+ changes[id] = change;
123
+ }
124
+ if (Object.keys(changes).length > 0) {
125
+ this.sendMessage(ServerMessage.delta(changes));
126
+ }
127
+ }
128
+ sendMessage(message) {
129
+ if (this.closed)
130
+ return;
131
+ this.transport.send(JSON.stringify(message));
132
+ }
133
+ handleDisconnect() {
134
+ this.close();
135
+ }
136
+ close() {
137
+ if (this.closed)
138
+ return;
139
+ this.closed = true;
140
+ if (this.presenceHandler && this.currentPresence) {
141
+ this.presenceHandler.remove(this.currentPresence);
142
+ }
143
+ this.streams.clear();
144
+ try {
145
+ this.transport.close();
146
+ }
147
+ catch (_a) { }
148
+ }
149
+ }