@derivation/rpc 0.1.6 → 0.1.7
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/client-handler.d.ts +8 -7
- package/dist/client-handler.js +16 -10
- package/dist/client.d.ts +3 -2
- package/dist/client.js +7 -7
- package/dist/index.d.ts +4 -4
- package/dist/index.js +4 -4
- package/dist/iso.d.ts +1 -1
- package/dist/iso.js +1 -1
- package/dist/messageport-transport.d.ts +13 -0
- package/dist/messageport-transport.js +28 -0
- package/dist/node-web-socket-transport.d.ts +16 -0
- package/dist/node-web-socket-transport.js +33 -0
- package/dist/queue.d.ts +9 -0
- package/dist/queue.js +32 -0
- package/dist/rate-limiter.d.ts +1 -1
- package/dist/rate-limiter.js +13 -8
- package/dist/reactive-map-adapter.d.ts +2 -1
- package/dist/reactive-map-adapter.js +2 -1
- package/dist/reactive-set-adapter.d.ts +2 -1
- package/dist/reactive-set-adapter.js +2 -1
- package/dist/shared-worker-client-handler.d.ts +27 -0
- package/dist/shared-worker-client-handler.js +149 -0
- package/dist/shared-worker-client.d.ts +22 -0
- package/dist/shared-worker-client.js +25 -0
- package/dist/shared-worker-server.d.ts +28 -0
- package/dist/shared-worker-server.js +62 -0
- package/dist/stream-adapter.js +2 -1
- package/dist/stream-types.d.ts +4 -4
- package/dist/transport.d.ts +27 -0
- package/dist/transport.js +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/web-socket-server.d.ts +11 -0
- package/dist/web-socket-server.js +59 -0
- package/dist/web-socket-transport.d.ts +13 -0
- package/dist/web-socket-transport.js +25 -0
- package/dist/websocket-server.d.ts +8 -2
- package/dist/websocket-server.js +29 -5
- package/dist/websocket-transport.d.ts +28 -0
- package/dist/websocket-transport.js +58 -0
- package/package.json +23 -2
package/dist/client-handler.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { RawData, WebSocket } from "ws";
|
|
2
1
|
import { ClientMessage } from "./client-message";
|
|
3
2
|
import { ServerMessage } from "./server-message";
|
|
4
3
|
import { StreamEndpoints, MutationEndpoints, RPCDefinition } from "./stream-types";
|
|
5
4
|
import { PresenceHandler } from "./presence-manager";
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
import { Transport } from "./transport";
|
|
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(
|
|
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:
|
|
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
|
|
25
|
+
private handleDisconnect;
|
|
25
26
|
close(): void;
|
|
26
27
|
}
|
package/dist/client-handler.js
CHANGED
|
@@ -2,15 +2,19 @@ import { parseClientMessage } from "./client-message";
|
|
|
2
2
|
import { ServerMessage } from "./server-message";
|
|
3
3
|
import { RateLimiter } from "./rate-limiter";
|
|
4
4
|
export class ClientHandler {
|
|
5
|
-
constructor(
|
|
5
|
+
constructor(transport, context, streamEndpoints, mutationEndpoints, presenceHandler) {
|
|
6
6
|
this.closed = false;
|
|
7
7
|
this.streams = new Map();
|
|
8
|
-
this.
|
|
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
|
|
47
|
+
data = JSON.parse(message);
|
|
44
48
|
}
|
|
45
49
|
catch (_a) {
|
|
46
|
-
console.error("Invalid JSON received:", message
|
|
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 (
|
|
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.
|
|
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.
|
|
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
2
|
import type { StreamSinks, RPCDefinition, MutationResult } from "./stream-types";
|
|
3
|
+
import { Transport } from "./transport";
|
|
3
4
|
export declare class Client<Defs extends RPCDefinition> {
|
|
4
|
-
private
|
|
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(
|
|
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
|
@@ -24,8 +24,8 @@ export class Client {
|
|
|
24
24
|
this.close();
|
|
25
25
|
}, 30000);
|
|
26
26
|
}
|
|
27
|
-
constructor(
|
|
28
|
-
this.
|
|
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.
|
|
41
|
-
const message = JSON.parse(
|
|
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.
|
|
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.
|
|
126
|
+
this.transport.close();
|
|
127
127
|
}
|
|
128
128
|
catch (_a) { }
|
|
129
129
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export * from "./client";
|
|
2
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 {
|
|
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 type { Transport } from "./transport";
|
|
7
7
|
export type { PresenceHandler } from "./presence-manager";
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
// Main index only exports isomorphic code that works in both browser and Node.js
|
|
1
2
|
export * from "./client";
|
|
2
3
|
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";
|
|
4
|
+
export { ReactiveSetSourceAdapter, ReactiveSetSinkAdapter, sink as setSink, } from "./reactive-set-adapter";
|
|
5
|
+
export { ReactiveMapSourceAdapter, ReactiveMapSinkAdapter, sink as mapSink, } from "./reactive-map-adapter";
|
|
6
|
+
export { StreamSourceAdapter, StreamSinkAdapter, sink as streamSink, } from "./stream-adapter";
|
package/dist/iso.d.ts
CHANGED
package/dist/iso.js
CHANGED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Transport } from "./transport";
|
|
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";
|
|
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
|
+
}
|
package/dist/queue.d.ts
ADDED
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
|
+
}
|
package/dist/rate-limiter.d.ts
CHANGED
package/dist/rate-limiter.js
CHANGED
|
@@ -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.
|
|
14
|
-
this.timestamps.
|
|
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
|
-
|
|
17
|
-
return
|
|
21
|
+
this.timestamps.push(now);
|
|
22
|
+
return false;
|
|
18
23
|
}
|
|
19
24
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { Graph
|
|
1
|
+
import { Graph } from "derivation";
|
|
2
|
+
import { ReactiveMap, ReactiveMapSource, ZMapChangeInput } from "@derivation/relational";
|
|
2
3
|
import { Source, Sink } from "./stream-types";
|
|
3
4
|
import { Iso } from "./iso";
|
|
4
5
|
export declare class ReactiveMapSourceAdapter<K, V> implements Source<ReactiveMap<K, V>> {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { inputMap, } from "@derivation/relational";
|
|
1
2
|
import { zmap } from "./iso";
|
|
2
3
|
export class ReactiveMapSourceAdapter {
|
|
3
4
|
constructor(map, keyIso, valueIso) {
|
|
@@ -30,7 +31,7 @@ export class ReactiveMapSinkAdapter {
|
|
|
30
31
|
}
|
|
31
32
|
}
|
|
32
33
|
build() {
|
|
33
|
-
const stream = this.graph
|
|
34
|
+
const stream = inputMap(this.graph, this.initialMap);
|
|
34
35
|
return { stream, input: stream.changes };
|
|
35
36
|
}
|
|
36
37
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Graph } from "derivation";
|
|
2
|
+
import { ZSet, ReactiveSet, ReactiveSetSource, ZSetChangeInput } from "@derivation/relational";
|
|
2
3
|
import { Source, Sink } from "./stream-types";
|
|
3
4
|
import { Iso } from "./iso";
|
|
4
5
|
export declare class ReactiveSetSourceAdapter<T> implements Source<ReactiveSet<T>> {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { inputSet, } from "@derivation/relational";
|
|
1
2
|
import { zset, zsetToArray, compose } from "./iso";
|
|
2
3
|
export class ReactiveSetSourceAdapter {
|
|
3
4
|
constructor(set, iso) {
|
|
@@ -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
|
|
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";
|
|
2
|
+
import { ServerMessage } from "./server-message";
|
|
3
|
+
import { StreamEndpoints, MutationEndpoints, RPCDefinition } from "./stream-types";
|
|
4
|
+
import { PresenceHandler } from "./presence-manager";
|
|
5
|
+
import { Transport } from "./transport";
|
|
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";
|
|
2
|
+
import { ServerMessage } from "./server-message";
|
|
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
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Graph } from "derivation";
|
|
2
|
+
import { Client } from "./client";
|
|
3
|
+
import { StreamSinks, RPCDefinition } from "./stream-types";
|
|
4
|
+
/**
|
|
5
|
+
* Create a client connected to a SharedWorker.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* // main.ts
|
|
10
|
+
* const graph = new Graph();
|
|
11
|
+
* const worker = new SharedWorker('/worker.js');
|
|
12
|
+
*
|
|
13
|
+
* const client = createSharedWorkerClient(worker.port, {
|
|
14
|
+
* streams: {
|
|
15
|
+
* todos: setSink(graph, todoIso),
|
|
16
|
+
* },
|
|
17
|
+
* }, graph);
|
|
18
|
+
*
|
|
19
|
+
* const todos = await client.run('todos', { filter: 'active' });
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export declare function createSharedWorkerClient<Defs extends RPCDefinition>(port: MessagePort, sinks: StreamSinks<Defs["streams"]>, graph: Graph): Client<Defs>;
|