@derivation/rpc 0.1.5 → 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-message.d.ts +3 -3
- package/dist/client-message.js +3 -3
- package/dist/client.d.ts +3 -2
- package/dist/client.js +8 -8
- 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 +7 -7
- 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
|
@@ -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>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Client } from "./client";
|
|
2
|
+
import { MessagePortTransport } from "./messageport-transport";
|
|
3
|
+
/**
|
|
4
|
+
* Create a client connected to a SharedWorker.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```typescript
|
|
8
|
+
* // main.ts
|
|
9
|
+
* const graph = new Graph();
|
|
10
|
+
* const worker = new SharedWorker('/worker.js');
|
|
11
|
+
*
|
|
12
|
+
* const client = createSharedWorkerClient(worker.port, {
|
|
13
|
+
* streams: {
|
|
14
|
+
* todos: setSink(graph, todoIso),
|
|
15
|
+
* },
|
|
16
|
+
* }, graph);
|
|
17
|
+
*
|
|
18
|
+
* const todos = await client.run('todos', { filter: 'active' });
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export function createSharedWorkerClient(port, sinks, graph) {
|
|
22
|
+
const transport = new MessagePortTransport(port);
|
|
23
|
+
port.start();
|
|
24
|
+
return new Client(transport, sinks, graph);
|
|
25
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Graph } from "derivation";
|
|
2
|
+
import { StreamEndpoints, MutationEndpoints, RPCDefinition } from "./stream-types";
|
|
3
|
+
import { PresenceHandler } from "./presence-manager";
|
|
4
|
+
export type SharedWorkerServerOptions<Defs extends RPCDefinition, Ctx = void> = {
|
|
5
|
+
streams: StreamEndpoints<Defs["streams"], Ctx>;
|
|
6
|
+
mutations: MutationEndpoints<Defs["mutations"], Ctx>;
|
|
7
|
+
createContext: (port: MessagePort) => Ctx | Promise<Ctx>;
|
|
8
|
+
presenceHandler?: PresenceHandler;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Set up a SharedWorker server for RPC communication.
|
|
12
|
+
* This creates a shared Graph that all connected tabs can interact with.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* // worker.ts
|
|
17
|
+
* const { graph } = setupSharedWorker({
|
|
18
|
+
* streams: {
|
|
19
|
+
* todos: async (args, ctx) => new ReactiveSetSourceAdapter(source),
|
|
20
|
+
* },
|
|
21
|
+
* mutations: {
|
|
22
|
+
* addTodo: async (args, ctx) => ({ success: true, value: newTodo }),
|
|
23
|
+
* },
|
|
24
|
+
* createContext: (port) => ({ portId: crypto.randomUUID() }),
|
|
25
|
+
* });
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export declare function setupSharedWorker<Defs extends RPCDefinition, Ctx = void>(options: SharedWorkerServerOptions<Defs, Ctx>, graph: Graph): void;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { SharedWorkerClientHandler } from "./shared-worker-client-handler";
|
|
2
|
+
import WeakList from "./weak-list";
|
|
3
|
+
import { MessagePortTransport } from "./messageport-transport";
|
|
4
|
+
/**
|
|
5
|
+
* Set up a SharedWorker server for RPC communication.
|
|
6
|
+
* This creates a shared Graph that all connected tabs can interact with.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* // worker.ts
|
|
11
|
+
* const { graph } = setupSharedWorker({
|
|
12
|
+
* streams: {
|
|
13
|
+
* todos: async (args, ctx) => new ReactiveSetSourceAdapter(source),
|
|
14
|
+
* },
|
|
15
|
+
* mutations: {
|
|
16
|
+
* addTodo: async (args, ctx) => ({ success: true, value: newTodo }),
|
|
17
|
+
* },
|
|
18
|
+
* createContext: (port) => ({ portId: crypto.randomUUID() }),
|
|
19
|
+
* });
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export function setupSharedWorker(options, graph) {
|
|
23
|
+
const { streams, mutations, createContext, presenceHandler } = options;
|
|
24
|
+
const clients = new WeakList();
|
|
25
|
+
// After each graph step, broadcast deltas to all connected clients
|
|
26
|
+
graph.afterStep(() => {
|
|
27
|
+
for (const client of clients) {
|
|
28
|
+
client.handleStep();
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
// Handle SharedWorker connections
|
|
32
|
+
const globalScope = self;
|
|
33
|
+
globalScope.onconnect = (event) => {
|
|
34
|
+
const port = event.ports[0];
|
|
35
|
+
const messageBuffer = [];
|
|
36
|
+
let client = null;
|
|
37
|
+
// Set up temporary message handler to buffer messages
|
|
38
|
+
const tempMessageHandler = (e) => {
|
|
39
|
+
messageBuffer.push(e.data);
|
|
40
|
+
};
|
|
41
|
+
port.onmessage = tempMessageHandler;
|
|
42
|
+
// Create context (handle both sync and async)
|
|
43
|
+
Promise.resolve(createContext(port))
|
|
44
|
+
.then((context) => {
|
|
45
|
+
// Create transport and client handler
|
|
46
|
+
const transport = new MessagePortTransport(port);
|
|
47
|
+
client = new SharedWorkerClientHandler(transport, context, streams, mutations, presenceHandler);
|
|
48
|
+
clients.add(client);
|
|
49
|
+
// Process buffered messages
|
|
50
|
+
for (const msg of messageBuffer) {
|
|
51
|
+
client.handleMessage(msg);
|
|
52
|
+
}
|
|
53
|
+
messageBuffer.length = 0;
|
|
54
|
+
// Start the port
|
|
55
|
+
port.start();
|
|
56
|
+
})
|
|
57
|
+
.catch((err) => {
|
|
58
|
+
console.error("Error creating context:", err);
|
|
59
|
+
port.close();
|
|
60
|
+
});
|
|
61
|
+
};
|
|
62
|
+
}
|
package/dist/stream-adapter.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { inputValue } from "derivation";
|
|
1
2
|
export class StreamSourceAdapter {
|
|
2
3
|
constructor(stream, iso) {
|
|
3
4
|
this.stream = stream;
|
|
@@ -23,7 +24,7 @@ export class StreamSinkAdapter {
|
|
|
23
24
|
input.push(this.iso.from(change));
|
|
24
25
|
}
|
|
25
26
|
build() {
|
|
26
|
-
const stream = this.graph
|
|
27
|
+
const stream = inputValue(this.graph, this.initialValue);
|
|
27
28
|
return { stream, input: stream };
|
|
28
29
|
}
|
|
29
30
|
}
|
package/dist/stream-types.d.ts
CHANGED
|
@@ -10,15 +10,15 @@ export interface Sink<SinkType, InputType> {
|
|
|
10
10
|
input: InputType;
|
|
11
11
|
};
|
|
12
12
|
}
|
|
13
|
-
export type StreamDefinition<ReturnType extends object, SinkType extends ReturnType, InputType extends object> = {
|
|
14
|
-
args:
|
|
13
|
+
export type StreamDefinition<Args extends object, ReturnType extends object, SinkType extends ReturnType, InputType extends object> = {
|
|
14
|
+
args: Args;
|
|
15
15
|
returnType: ReturnType;
|
|
16
16
|
sinkType: SinkType;
|
|
17
17
|
inputType: InputType;
|
|
18
18
|
};
|
|
19
|
-
export type StreamDefinitions = Record<string, StreamDefinition<object, object, object>>;
|
|
20
|
-
export type StreamEndpoints<Definitions extends StreamDefinitions> = {
|
|
21
|
-
[K in keyof Definitions]: (args: Definitions[K]["args"]) => Source<Definitions[K]["returnType"]
|
|
19
|
+
export type StreamDefinitions = Record<string, StreamDefinition<object, object, object, object>>;
|
|
20
|
+
export type StreamEndpoints<Definitions extends StreamDefinitions, Ctx = void> = {
|
|
21
|
+
[K in keyof Definitions]: (args: Definitions[K]["args"], ctx: Ctx) => Promise<Source<Definitions[K]["returnType"]>>;
|
|
22
22
|
};
|
|
23
23
|
export type StreamSinks<Definitions extends StreamDefinitions> = {
|
|
24
24
|
[K in keyof Definitions]: (snapshot: object) => Sink<Definitions[K]["sinkType"], Definitions[K]["inputType"]>;
|
|
@@ -35,8 +35,8 @@ export type MutationDefinition<Args, Result> = {
|
|
|
35
35
|
result: Result;
|
|
36
36
|
};
|
|
37
37
|
export type MutationDefinitions = Record<string, MutationDefinition<unknown, unknown>>;
|
|
38
|
-
export type MutationEndpoints<Definitions extends MutationDefinitions> = {
|
|
39
|
-
[K in keyof Definitions]: (args: Definitions[K]["args"]) => Promise<MutationResult<Definitions[K]["result"]>>;
|
|
38
|
+
export type MutationEndpoints<Definitions extends MutationDefinitions, Ctx = void> = {
|
|
39
|
+
[K in keyof Definitions]: (args: Definitions[K]["args"], ctx: Ctx) => Promise<MutationResult<Definitions[K]["result"]>>;
|
|
40
40
|
};
|
|
41
41
|
export type RPCDefinition = {
|
|
42
42
|
streams: StreamDefinitions;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transport abstraction for RPC communication.
|
|
3
|
+
* Allows the same RPC system to work over WebSocket or MessagePort.
|
|
4
|
+
*/
|
|
5
|
+
export interface Transport {
|
|
6
|
+
/**
|
|
7
|
+
* Send a string message through the transport.
|
|
8
|
+
*/
|
|
9
|
+
send(data: string): void;
|
|
10
|
+
/**
|
|
11
|
+
* Register a handler for incoming messages.
|
|
12
|
+
*/
|
|
13
|
+
onMessage(handler: (data: string) => void): void;
|
|
14
|
+
/**
|
|
15
|
+
* Register a handler for transport closure/disconnection.
|
|
16
|
+
*/
|
|
17
|
+
onClose(handler: () => void): void;
|
|
18
|
+
/**
|
|
19
|
+
* Close the transport connection.
|
|
20
|
+
*/
|
|
21
|
+
close(): void;
|
|
22
|
+
/**
|
|
23
|
+
* Number of bytes buffered to be sent (optional, used for flow control).
|
|
24
|
+
* MessagePort doesn't provide this, so it's optional.
|
|
25
|
+
*/
|
|
26
|
+
readonly bufferedAmount?: number;
|
|
27
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|