@derivation/rpc 0.1.2 → 0.1.5
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 +12 -5
- package/dist/client-handler.js +82 -9
- package/dist/client-message.d.ts +22 -0
- package/dist/client-message.js +22 -0
- package/dist/client.d.ts +14 -6
- package/dist/client.js +76 -19
- package/dist/index.d.ts +1 -0
- package/dist/presence-manager.d.ts +5 -0
- package/dist/presence-manager.js +1 -0
- package/dist/rate-limiter.d.ts +7 -0
- package/dist/rate-limiter.js +19 -0
- package/dist/reactive-map-adapter.d.ts +11 -7
- package/dist/reactive-map-adapter.js +11 -15
- package/dist/reactive-set-adapter.d.ts +11 -7
- package/dist/reactive-set-adapter.js +11 -14
- package/dist/server-message.d.ts +16 -0
- package/dist/server-message.js +20 -0
- package/dist/stream-adapter.d.ts +10 -6
- package/dist/stream-adapter.js +7 -12
- package/dist/stream-types.d.ts +30 -7
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/websocket-server.d.ts +3 -2
- package/dist/websocket-server.js +3 -3
- package/package.json +7 -4
package/dist/client-handler.d.ts
CHANGED
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
import { RawData, WebSocket } from "ws";
|
|
2
2
|
import { ClientMessage } from "./client-message";
|
|
3
3
|
import { ServerMessage } from "./server-message";
|
|
4
|
-
import { StreamEndpoints,
|
|
5
|
-
|
|
4
|
+
import { StreamEndpoints, MutationEndpoints, RPCDefinition } from "./stream-types";
|
|
5
|
+
import { PresenceHandler } from "./presence-manager";
|
|
6
|
+
export declare class ClientHandler<Defs extends RPCDefinition> {
|
|
6
7
|
private readonly ws;
|
|
7
|
-
private readonly
|
|
8
|
+
private readonly streamEndpoints;
|
|
9
|
+
private readonly mutationEndpoints;
|
|
10
|
+
private readonly presenceHandler?;
|
|
11
|
+
private currentPresence?;
|
|
8
12
|
private closed;
|
|
9
13
|
private readonly streams;
|
|
10
|
-
private
|
|
11
|
-
|
|
14
|
+
private heartbeatTimeout;
|
|
15
|
+
private inactivityTimeout;
|
|
16
|
+
private readonly rateLimiter;
|
|
17
|
+
constructor(ws: WebSocket, streamEndpoints: StreamEndpoints<Defs["streams"]>, mutationEndpoints: MutationEndpoints<Defs["mutations"]>, presenceHandler?: PresenceHandler);
|
|
12
18
|
private resetHeartbeat;
|
|
19
|
+
private resetInactivity;
|
|
13
20
|
handleMessage(message: RawData): void;
|
|
14
21
|
handleClientMessage(message: ClientMessage): void;
|
|
15
22
|
handleStep(): void;
|
package/dist/client-handler.js
CHANGED
|
@@ -1,23 +1,43 @@
|
|
|
1
1
|
import { parseClientMessage } from "./client-message";
|
|
2
2
|
import { ServerMessage } from "./server-message";
|
|
3
|
+
import { RateLimiter } from "./rate-limiter";
|
|
3
4
|
export class ClientHandler {
|
|
4
|
-
constructor(ws,
|
|
5
|
+
constructor(ws, streamEndpoints, mutationEndpoints, presenceHandler) {
|
|
5
6
|
this.closed = false;
|
|
6
7
|
this.streams = new Map();
|
|
7
8
|
this.ws = ws;
|
|
8
|
-
this.
|
|
9
|
+
this.streamEndpoints = streamEndpoints;
|
|
10
|
+
this.mutationEndpoints = mutationEndpoints;
|
|
11
|
+
this.presenceHandler = presenceHandler;
|
|
12
|
+
this.rateLimiter = new RateLimiter(100, 300); // 100 messages over 5 minutes
|
|
9
13
|
console.log("new client connected");
|
|
10
14
|
this.resetHeartbeat();
|
|
15
|
+
this.resetInactivity();
|
|
11
16
|
}
|
|
12
17
|
resetHeartbeat() {
|
|
13
|
-
if (this.
|
|
14
|
-
|
|
18
|
+
if (this.heartbeatTimeout) {
|
|
19
|
+
clearTimeout(this.heartbeatTimeout);
|
|
15
20
|
}
|
|
16
|
-
this.
|
|
21
|
+
this.heartbeatTimeout = setTimeout(() => {
|
|
17
22
|
this.sendMessage(ServerMessage.heartbeat());
|
|
18
23
|
}, 10000);
|
|
19
24
|
}
|
|
25
|
+
resetInactivity() {
|
|
26
|
+
if (this.inactivityTimeout) {
|
|
27
|
+
clearTimeout(this.inactivityTimeout);
|
|
28
|
+
}
|
|
29
|
+
this.inactivityTimeout = setTimeout(() => {
|
|
30
|
+
this.close();
|
|
31
|
+
}, 30000);
|
|
32
|
+
}
|
|
20
33
|
handleMessage(message) {
|
|
34
|
+
this.resetInactivity();
|
|
35
|
+
// Check rate limit
|
|
36
|
+
if (this.rateLimiter.trigger()) {
|
|
37
|
+
console.log("Rate limit exceeded, closing connection");
|
|
38
|
+
this.close();
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
21
41
|
let data;
|
|
22
42
|
try {
|
|
23
43
|
data = JSON.parse(message.toString());
|
|
@@ -40,12 +60,12 @@ export class ClientHandler {
|
|
|
40
60
|
switch (message.type) {
|
|
41
61
|
case "subscribe": {
|
|
42
62
|
const { id, name, args } = message;
|
|
43
|
-
if (!(name in this.
|
|
63
|
+
if (!(name in this.streamEndpoints)) {
|
|
44
64
|
console.error(`Unknown stream: ${name}`);
|
|
45
65
|
this.close();
|
|
46
66
|
return;
|
|
47
67
|
}
|
|
48
|
-
const endpoint = this.
|
|
68
|
+
const endpoint = this.streamEndpoints[name];
|
|
49
69
|
try {
|
|
50
70
|
const source = endpoint(args);
|
|
51
71
|
this.streams.set(id, source);
|
|
@@ -64,8 +84,49 @@ export class ClientHandler {
|
|
|
64
84
|
console.log(`Client unsubscribed from ${id}`);
|
|
65
85
|
break;
|
|
66
86
|
}
|
|
87
|
+
case "call": {
|
|
88
|
+
const { id, name, args } = message;
|
|
89
|
+
if (!(name in this.mutationEndpoints)) {
|
|
90
|
+
console.error(`Unknown mutation: ${name}`);
|
|
91
|
+
this.close();
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const endpoint = this.mutationEndpoints[name];
|
|
95
|
+
endpoint(args)
|
|
96
|
+
.then((result) => {
|
|
97
|
+
if (result.success) {
|
|
98
|
+
this.sendMessage(ServerMessage.resultSuccess(id, result.value));
|
|
99
|
+
console.log(`Mutation \"${name}\" (${id}) completed successfully`);
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
this.sendMessage(ServerMessage.resultError(id, result.error));
|
|
103
|
+
console.log(`Mutation \"${name}\" (${id}) returned error: ${result.error}`);
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
.catch((err) => {
|
|
107
|
+
console.error(`Unhandled exception in mutation \"${name}\" (${id}):`, err);
|
|
108
|
+
this.close();
|
|
109
|
+
});
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
67
112
|
case "heartbeat":
|
|
68
113
|
break;
|
|
114
|
+
case "presence": {
|
|
115
|
+
if (!this.presenceHandler) {
|
|
116
|
+
console.error("Presence not configured");
|
|
117
|
+
this.close();
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const { data } = message;
|
|
121
|
+
if (this.currentPresence !== undefined) {
|
|
122
|
+
this.presenceHandler.update(this.currentPresence, data);
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
this.presenceHandler.add(data);
|
|
126
|
+
}
|
|
127
|
+
this.currentPresence = data;
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
69
130
|
}
|
|
70
131
|
}
|
|
71
132
|
handleStep() {
|
|
@@ -73,7 +134,10 @@ export class ClientHandler {
|
|
|
73
134
|
return;
|
|
74
135
|
const changes = {};
|
|
75
136
|
for (const [id, source] of this.streams) {
|
|
76
|
-
|
|
137
|
+
const change = source.LastChange;
|
|
138
|
+
if (change === null)
|
|
139
|
+
continue;
|
|
140
|
+
changes[id] = change;
|
|
77
141
|
}
|
|
78
142
|
if (Object.keys(changes).length > 0) {
|
|
79
143
|
this.sendMessage(ServerMessage.delta(changes));
|
|
@@ -82,6 +146,11 @@ export class ClientHandler {
|
|
|
82
146
|
sendMessage(message) {
|
|
83
147
|
this.resetHeartbeat();
|
|
84
148
|
if (!this.closed) {
|
|
149
|
+
if (this.ws.bufferedAmount > 100 * 1024) {
|
|
150
|
+
console.log("Send buffer exceeded 100KB, closing connection");
|
|
151
|
+
this.close();
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
85
154
|
try {
|
|
86
155
|
this.ws.send(JSON.stringify(message));
|
|
87
156
|
}
|
|
@@ -99,7 +168,11 @@ export class ClientHandler {
|
|
|
99
168
|
if (this.closed)
|
|
100
169
|
return;
|
|
101
170
|
this.closed = true;
|
|
102
|
-
|
|
171
|
+
clearTimeout(this.heartbeatTimeout);
|
|
172
|
+
clearTimeout(this.inactivityTimeout);
|
|
173
|
+
if (this.presenceHandler && this.currentPresence !== undefined) {
|
|
174
|
+
this.presenceHandler.remove(this.currentPresence);
|
|
175
|
+
}
|
|
103
176
|
try {
|
|
104
177
|
this.ws.close();
|
|
105
178
|
}
|
package/dist/client-message.d.ts
CHANGED
|
@@ -15,6 +15,18 @@ export declare const HeartbeatMessageSchema: z.ZodObject<{
|
|
|
15
15
|
type: z.ZodLiteral<"heartbeat">;
|
|
16
16
|
}, z.core.$strip>;
|
|
17
17
|
export type HeartbeatMessage = z.infer<typeof HeartbeatMessageSchema>;
|
|
18
|
+
export declare const CallMessageSchema: z.ZodObject<{
|
|
19
|
+
type: z.ZodLiteral<"call">;
|
|
20
|
+
id: z.ZodNumber;
|
|
21
|
+
name: z.ZodString;
|
|
22
|
+
args: z.ZodObject<{}, z.core.$loose>;
|
|
23
|
+
}, z.core.$strip>;
|
|
24
|
+
export type CallMessage = z.infer<typeof CallMessageSchema>;
|
|
25
|
+
export declare const PresenceMessageSchema: z.ZodObject<{
|
|
26
|
+
type: z.ZodLiteral<"presence">;
|
|
27
|
+
data: z.ZodObject<{}, z.core.$loose>;
|
|
28
|
+
}, z.core.$strip>;
|
|
29
|
+
export type PresenceMessage = z.infer<typeof PresenceMessageSchema>;
|
|
18
30
|
export declare const ClientMessageSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
19
31
|
type: z.ZodLiteral<"subscribe">;
|
|
20
32
|
id: z.ZodNumber;
|
|
@@ -25,11 +37,21 @@ export declare const ClientMessageSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
25
37
|
id: z.ZodNumber;
|
|
26
38
|
}, z.core.$strip>, z.ZodObject<{
|
|
27
39
|
type: z.ZodLiteral<"heartbeat">;
|
|
40
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
41
|
+
type: z.ZodLiteral<"call">;
|
|
42
|
+
id: z.ZodNumber;
|
|
43
|
+
name: z.ZodString;
|
|
44
|
+
args: z.ZodObject<{}, z.core.$loose>;
|
|
45
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
46
|
+
type: z.ZodLiteral<"presence">;
|
|
47
|
+
data: z.ZodObject<{}, z.core.$loose>;
|
|
28
48
|
}, z.core.$strip>], "type">;
|
|
29
49
|
export type ClientMessage = z.infer<typeof ClientMessageSchema>;
|
|
30
50
|
export declare function parseClientMessage(data: unknown): ClientMessage;
|
|
31
51
|
export declare const ClientMessage: {
|
|
32
52
|
subscribe: (id: number, name: string, args: Record<string, unknown>) => SubscribeMessage;
|
|
33
53
|
unsubscribe: (id: number) => UnsubscribeMessage;
|
|
54
|
+
call: (id: number, name: string, args: Record<string, unknown>) => CallMessage;
|
|
34
55
|
heartbeat: () => HeartbeatMessage;
|
|
56
|
+
presence: (data: Record<string, unknown>) => PresenceMessage;
|
|
35
57
|
};
|
package/dist/client-message.js
CHANGED
|
@@ -12,10 +12,22 @@ export const UnsubscribeMessageSchema = z.object({
|
|
|
12
12
|
export const HeartbeatMessageSchema = z.object({
|
|
13
13
|
type: z.literal("heartbeat"),
|
|
14
14
|
});
|
|
15
|
+
export const CallMessageSchema = z.object({
|
|
16
|
+
type: z.literal("call"),
|
|
17
|
+
id: z.number(),
|
|
18
|
+
name: z.string(),
|
|
19
|
+
args: z.looseObject({}),
|
|
20
|
+
});
|
|
21
|
+
export const PresenceMessageSchema = z.object({
|
|
22
|
+
type: z.literal("presence"),
|
|
23
|
+
data: z.looseObject({}),
|
|
24
|
+
});
|
|
15
25
|
export const ClientMessageSchema = z.discriminatedUnion("type", [
|
|
16
26
|
SubscribeMessageSchema,
|
|
17
27
|
UnsubscribeMessageSchema,
|
|
18
28
|
HeartbeatMessageSchema,
|
|
29
|
+
CallMessageSchema,
|
|
30
|
+
PresenceMessageSchema,
|
|
19
31
|
]);
|
|
20
32
|
export function parseClientMessage(data) {
|
|
21
33
|
return ClientMessageSchema.parse(data);
|
|
@@ -31,7 +43,17 @@ export const ClientMessage = {
|
|
|
31
43
|
type: "unsubscribe",
|
|
32
44
|
id,
|
|
33
45
|
}),
|
|
46
|
+
call: (id, name, args) => ({
|
|
47
|
+
type: "call",
|
|
48
|
+
id,
|
|
49
|
+
name,
|
|
50
|
+
args,
|
|
51
|
+
}),
|
|
34
52
|
heartbeat: () => ({
|
|
35
53
|
type: "heartbeat",
|
|
36
54
|
}),
|
|
55
|
+
presence: (data) => ({
|
|
56
|
+
type: "presence",
|
|
57
|
+
data,
|
|
58
|
+
}),
|
|
37
59
|
};
|
package/dist/client.d.ts
CHANGED
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
import type { Graph } from "derivation";
|
|
2
|
-
import type {
|
|
3
|
-
export declare class Client<Defs extends
|
|
2
|
+
import type { StreamSinks, RPCDefinition, MutationResult } from "./stream-types";
|
|
3
|
+
export declare class Client<Defs extends RPCDefinition> {
|
|
4
4
|
private ws;
|
|
5
5
|
private sinks;
|
|
6
6
|
private graph;
|
|
7
7
|
private nextId;
|
|
8
|
-
private
|
|
8
|
+
private pendingStreams;
|
|
9
|
+
private pendingMutations;
|
|
9
10
|
private activeStreams;
|
|
11
|
+
private heartbeatTimeout;
|
|
12
|
+
private inactivityTimeout;
|
|
10
13
|
private registry;
|
|
11
|
-
|
|
14
|
+
private resetHeartbeat;
|
|
15
|
+
private resetInactivity;
|
|
16
|
+
constructor(ws: WebSocket, sinks: StreamSinks<Defs["streams"]>, graph: Graph);
|
|
12
17
|
private handleMessage;
|
|
13
|
-
private
|
|
14
|
-
run<Key extends keyof Defs>(key: Key, args: Defs[Key]["args"]): Promise<Defs[Key]["returnType"]>;
|
|
18
|
+
private sendMessage;
|
|
19
|
+
run<Key extends keyof Defs["streams"]>(key: Key, args: Defs["streams"][Key]["args"]): Promise<Defs["streams"][Key]["returnType"]>;
|
|
20
|
+
call<Key extends keyof Defs["mutations"]>(key: Key, args: Defs["mutations"][Key]["args"]): Promise<MutationResult<Defs["mutations"][Key]["result"]>>;
|
|
21
|
+
close(): void;
|
|
22
|
+
setPresence(value: Record<string, unknown>): void;
|
|
15
23
|
}
|
package/dist/client.js
CHANGED
|
@@ -1,37 +1,57 @@
|
|
|
1
1
|
import { ClientMessage } from "./client-message";
|
|
2
|
-
function changer(sink,
|
|
2
|
+
function changer(sink, input) {
|
|
3
3
|
return (change) => {
|
|
4
|
-
const
|
|
5
|
-
if (
|
|
6
|
-
sink.apply(change,
|
|
4
|
+
const i = input.deref();
|
|
5
|
+
if (i) {
|
|
6
|
+
sink.apply(change, i);
|
|
7
7
|
}
|
|
8
8
|
};
|
|
9
9
|
}
|
|
10
10
|
export class Client {
|
|
11
|
+
resetHeartbeat() {
|
|
12
|
+
if (this.heartbeatTimeout) {
|
|
13
|
+
clearTimeout(this.heartbeatTimeout);
|
|
14
|
+
}
|
|
15
|
+
this.heartbeatTimeout = setTimeout(() => {
|
|
16
|
+
this.sendMessage(ClientMessage.heartbeat());
|
|
17
|
+
}, 10000);
|
|
18
|
+
}
|
|
19
|
+
resetInactivity() {
|
|
20
|
+
if (this.inactivityTimeout) {
|
|
21
|
+
clearTimeout(this.inactivityTimeout);
|
|
22
|
+
}
|
|
23
|
+
this.inactivityTimeout = setTimeout(() => {
|
|
24
|
+
this.close();
|
|
25
|
+
}, 30000);
|
|
26
|
+
}
|
|
11
27
|
constructor(ws, sinks, graph) {
|
|
12
28
|
this.ws = ws;
|
|
13
29
|
this.sinks = sinks;
|
|
14
30
|
this.graph = graph;
|
|
15
31
|
this.nextId = 1;
|
|
16
|
-
this.
|
|
32
|
+
this.pendingStreams = new Map();
|
|
33
|
+
this.pendingMutations = new Map();
|
|
17
34
|
this.activeStreams = new Map();
|
|
18
35
|
this.registry = new FinalizationRegistry(([id, name]) => {
|
|
19
36
|
console.log(`🧹 Stream ${id} (${name}) collected — unsubscribing`);
|
|
20
|
-
this.
|
|
37
|
+
this.sendMessage(ClientMessage.unsubscribe(id));
|
|
21
38
|
this.activeStreams.delete(id);
|
|
22
39
|
});
|
|
23
40
|
this.ws.onmessage = (event) => {
|
|
24
41
|
const message = JSON.parse(event.data);
|
|
25
|
-
this.
|
|
42
|
+
this.ws.send(JSON.stringify(message));
|
|
26
43
|
};
|
|
44
|
+
this.resetHeartbeat();
|
|
45
|
+
this.resetInactivity();
|
|
27
46
|
}
|
|
28
47
|
handleMessage(message) {
|
|
48
|
+
this.resetInactivity();
|
|
29
49
|
switch (message.type) {
|
|
30
50
|
case "snapshot": {
|
|
31
|
-
const resolve = this.
|
|
51
|
+
const resolve = this.pendingStreams.get(message.id);
|
|
32
52
|
if (resolve) {
|
|
33
53
|
resolve(message.snapshot);
|
|
34
|
-
this.
|
|
54
|
+
this.pendingStreams.delete(message.id);
|
|
35
55
|
}
|
|
36
56
|
break;
|
|
37
57
|
}
|
|
@@ -43,34 +63,71 @@ export class Client {
|
|
|
43
63
|
sink(change);
|
|
44
64
|
}
|
|
45
65
|
else if (!sink) {
|
|
46
|
-
console.log(`🧹 Sink ${id} GC
|
|
47
|
-
this.
|
|
66
|
+
console.log(`🧹 Sink ${id} GC'd — auto-unsubscribing`);
|
|
67
|
+
this.sendMessage(ClientMessage.unsubscribe(id));
|
|
48
68
|
this.activeStreams.delete(id);
|
|
49
69
|
}
|
|
50
70
|
}
|
|
51
71
|
this.graph.step();
|
|
52
72
|
break;
|
|
53
73
|
}
|
|
74
|
+
case "result": {
|
|
75
|
+
const resolve = this.pendingMutations.get(message.id);
|
|
76
|
+
if (resolve) {
|
|
77
|
+
if (message.success) {
|
|
78
|
+
resolve({ success: true, value: message.value });
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
resolve({
|
|
82
|
+
success: false,
|
|
83
|
+
error: message.error || "Unknown error",
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
this.pendingMutations.delete(message.id);
|
|
87
|
+
}
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
54
90
|
case "heartbeat":
|
|
55
91
|
break;
|
|
56
92
|
}
|
|
57
93
|
}
|
|
58
|
-
|
|
94
|
+
sendMessage(message) {
|
|
95
|
+
this.resetHeartbeat();
|
|
59
96
|
this.ws.send(JSON.stringify(message));
|
|
60
97
|
}
|
|
61
98
|
async run(key, args) {
|
|
62
99
|
console.log(`Running stream ${String(key)} with args ${JSON.stringify(args)}`);
|
|
63
100
|
const id = this.nextId++;
|
|
64
|
-
this.
|
|
101
|
+
this.sendMessage(ClientMessage.subscribe(id, String(key), args));
|
|
65
102
|
const snapshot = await new Promise((resolve) => {
|
|
66
|
-
this.
|
|
103
|
+
this.pendingStreams.set(id, resolve);
|
|
67
104
|
});
|
|
68
105
|
const endpoint = this.sinks[key];
|
|
69
|
-
const
|
|
70
|
-
const stream =
|
|
71
|
-
const
|
|
72
|
-
this.activeStreams.set(id, changer(
|
|
73
|
-
this.registry.register(
|
|
106
|
+
const sinkAdapter = endpoint(snapshot);
|
|
107
|
+
const { stream, input } = sinkAdapter.build();
|
|
108
|
+
const inputRef = new WeakRef(input);
|
|
109
|
+
this.activeStreams.set(id, changer(sinkAdapter, inputRef));
|
|
110
|
+
this.registry.register(input, [id, String(key)]);
|
|
74
111
|
return stream;
|
|
75
112
|
}
|
|
113
|
+
async call(key, args) {
|
|
114
|
+
console.log(`Calling mutation ${String(key)} with args ${JSON.stringify(args)}`);
|
|
115
|
+
const id = this.nextId++;
|
|
116
|
+
this.sendMessage(ClientMessage.call(id, String(key), args));
|
|
117
|
+
const result = await new Promise((resolve) => {
|
|
118
|
+
this.pendingMutations.set(id, resolve);
|
|
119
|
+
});
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
122
|
+
close() {
|
|
123
|
+
clearTimeout(this.heartbeatTimeout);
|
|
124
|
+
clearTimeout(this.inactivityTimeout);
|
|
125
|
+
try {
|
|
126
|
+
this.ws.close();
|
|
127
|
+
}
|
|
128
|
+
catch (_a) { }
|
|
129
|
+
}
|
|
130
|
+
setPresence(value) {
|
|
131
|
+
this.sendMessage(ClientMessage.presence(value));
|
|
132
|
+
}
|
|
76
133
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -4,3 +4,4 @@ export { ReactiveSetSourceAdapter, ReactiveSetSinkAdapter, sink as setSink } fro
|
|
|
4
4
|
export { ReactiveMapSourceAdapter, ReactiveMapSinkAdapter, sink as mapSink } from "./reactive-map-adapter";
|
|
5
5
|
export { StreamSourceAdapter, StreamSinkAdapter, sink as streamSink } from "./stream-adapter";
|
|
6
6
|
export { setupWebSocketServer } from "./websocket-server";
|
|
7
|
+
export type { PresenceHandler } from "./presence-manager";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export class RateLimiter {
|
|
2
|
+
constructor(maxOccurrences, windowSeconds) {
|
|
3
|
+
this.timestamps = [];
|
|
4
|
+
this.maxOccurrences = maxOccurrences;
|
|
5
|
+
this.windowNanos = BigInt(windowSeconds) * BigInt(1000000000);
|
|
6
|
+
}
|
|
7
|
+
trigger() {
|
|
8
|
+
const now = process.hrtime.bigint();
|
|
9
|
+
// Add current timestamp
|
|
10
|
+
this.timestamps.push(now);
|
|
11
|
+
// Remove expired timestamps from front
|
|
12
|
+
const cutoff = now - this.windowNanos;
|
|
13
|
+
while (this.timestamps.length > 0 && this.timestamps[0] < cutoff) {
|
|
14
|
+
this.timestamps.shift();
|
|
15
|
+
}
|
|
16
|
+
// Return true if we've exceeded the limit
|
|
17
|
+
return this.timestamps.length > this.maxOccurrences;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Graph, ReactiveMap, ReactiveMapSource } from "derivation";
|
|
1
|
+
import { Graph, ReactiveMap, ReactiveMapSource, ZMapChangeInput } from "derivation";
|
|
2
2
|
import { Source, Sink } from "./stream-types";
|
|
3
3
|
import { Iso } from "./iso";
|
|
4
4
|
export declare class ReactiveMapSourceAdapter<K, V> implements Source<ReactiveMap<K, V>> {
|
|
@@ -6,14 +6,18 @@ export declare class ReactiveMapSourceAdapter<K, V> implements Source<ReactiveMa
|
|
|
6
6
|
private readonly iso;
|
|
7
7
|
constructor(map: ReactiveMap<K, V>, keyIso: Iso<K, unknown>, valueIso: Iso<V, unknown>);
|
|
8
8
|
get Snapshot(): object;
|
|
9
|
-
get LastChange(): object;
|
|
9
|
+
get LastChange(): object | null;
|
|
10
10
|
get Stream(): ReactiveMap<K, V>;
|
|
11
11
|
}
|
|
12
|
-
export declare class ReactiveMapSinkAdapter<K, V> implements Sink<ReactiveMapSource<K, V>> {
|
|
12
|
+
export declare class ReactiveMapSinkAdapter<K, V> implements Sink<ReactiveMapSource<K, V>, ZMapChangeInput<K, V>> {
|
|
13
13
|
private readonly graph;
|
|
14
14
|
private readonly iso;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
private readonly initialMap;
|
|
16
|
+
constructor(graph: Graph, keyIso: Iso<K, unknown>, valueIso: Iso<V, unknown>, snapshot: object);
|
|
17
|
+
apply(change: object, input: ZMapChangeInput<K, V>): void;
|
|
18
|
+
build(): {
|
|
19
|
+
stream: ReactiveMapSource<K, V>;
|
|
20
|
+
input: ZMapChangeInput<K, V>;
|
|
21
|
+
};
|
|
18
22
|
}
|
|
19
|
-
export declare function sink<K, V>(graph: Graph, keyIso: Iso<K, unknown>, valueIso: Iso<V, unknown>): (snapshot: object) => Sink<ReactiveMapSource<K, V>>;
|
|
23
|
+
export declare function sink<K, V>(graph: Graph, keyIso: Iso<K, unknown>, valueIso: Iso<V, unknown>): (snapshot: object) => Sink<ReactiveMapSource<K, V>, ZMapChangeInput<K, V>>;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { ZMap } from "derivation";
|
|
2
1
|
import { zmap } from "./iso";
|
|
3
2
|
export class ReactiveMapSourceAdapter {
|
|
4
3
|
constructor(map, keyIso, valueIso) {
|
|
@@ -9,37 +8,34 @@ export class ReactiveMapSourceAdapter {
|
|
|
9
8
|
return this.iso.to(this.map.snapshot);
|
|
10
9
|
}
|
|
11
10
|
get LastChange() {
|
|
12
|
-
|
|
11
|
+
const change = this.iso.to(this.map.changes.value);
|
|
12
|
+
if (change.length === 0)
|
|
13
|
+
return null;
|
|
14
|
+
return change;
|
|
13
15
|
}
|
|
14
16
|
get Stream() {
|
|
15
17
|
return this.map;
|
|
16
18
|
}
|
|
17
19
|
}
|
|
18
20
|
export class ReactiveMapSinkAdapter {
|
|
19
|
-
constructor(graph, keyIso, valueIso) {
|
|
21
|
+
constructor(graph, keyIso, valueIso, snapshot) {
|
|
20
22
|
this.graph = graph;
|
|
21
23
|
this.iso = zmap(keyIso, valueIso);
|
|
24
|
+
this.initialMap = this.iso.from(snapshot);
|
|
22
25
|
}
|
|
23
|
-
apply(change,
|
|
26
|
+
apply(change, input) {
|
|
24
27
|
const zmapChange = this.iso.from(change);
|
|
25
28
|
for (const [key, value, weight] of zmapChange.getEntries()) {
|
|
26
|
-
|
|
29
|
+
input.add(key, value, weight);
|
|
27
30
|
}
|
|
28
31
|
}
|
|
29
32
|
build() {
|
|
30
|
-
|
|
33
|
+
const stream = this.graph.inputMap(this.initialMap);
|
|
34
|
+
return { stream, input: stream.changes };
|
|
31
35
|
}
|
|
32
36
|
}
|
|
33
37
|
export function sink(graph, keyIso, valueIso) {
|
|
34
|
-
const wholeIso = zmap(keyIso, valueIso);
|
|
35
38
|
return (snapshot) => {
|
|
36
|
-
|
|
37
|
-
const g = graph;
|
|
38
|
-
const adapter = new ReactiveMapSinkAdapter(g, keyIso, valueIso);
|
|
39
|
-
const stream = g.inputMap(initial);
|
|
40
|
-
return {
|
|
41
|
-
apply: adapter.apply.bind(adapter),
|
|
42
|
-
build: () => stream,
|
|
43
|
-
};
|
|
39
|
+
return new ReactiveMapSinkAdapter(graph, keyIso, valueIso, snapshot);
|
|
44
40
|
};
|
|
45
41
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ZSet, Graph, ReactiveSet, ReactiveSetSource } from "derivation";
|
|
1
|
+
import { ZSet, Graph, ReactiveSet, ReactiveSetSource, ZSetChangeInput } from "derivation";
|
|
2
2
|
import { Source, Sink } from "./stream-types";
|
|
3
3
|
import { Iso } from "./iso";
|
|
4
4
|
export declare class ReactiveSetSourceAdapter<T> implements Source<ReactiveSet<T>> {
|
|
@@ -6,14 +6,18 @@ export declare class ReactiveSetSourceAdapter<T> implements Source<ReactiveSet<T
|
|
|
6
6
|
private readonly iso;
|
|
7
7
|
constructor(set: ReactiveSet<T>, iso: Iso<T, unknown>);
|
|
8
8
|
get Snapshot(): object;
|
|
9
|
-
get LastChange(): object;
|
|
9
|
+
get LastChange(): object | null;
|
|
10
10
|
get Stream(): ReactiveSet<T>;
|
|
11
11
|
}
|
|
12
|
-
export declare class ReactiveSetSinkAdapter<T> implements Sink<ReactiveSetSource<T>> {
|
|
12
|
+
export declare class ReactiveSetSinkAdapter<T> implements Sink<ReactiveSetSource<T>, ZSetChangeInput<T>> {
|
|
13
13
|
private readonly graph;
|
|
14
14
|
private readonly iso;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
private readonly initialSet;
|
|
16
|
+
constructor(graph: Graph, iso: Iso<ZSet<T>, unknown>, snapshot: object);
|
|
17
|
+
apply(change: object, input: ZSetChangeInput<T>): void;
|
|
18
|
+
build(): {
|
|
19
|
+
stream: ReactiveSetSource<T>;
|
|
20
|
+
input: ZSetChangeInput<T>;
|
|
21
|
+
};
|
|
18
22
|
}
|
|
19
|
-
export declare function sink<T>(graph: Graph, iso: Iso<T, unknown>): (snapshot: object) => Sink<ReactiveSetSource<T>>;
|
|
23
|
+
export declare function sink<T>(graph: Graph, iso: Iso<T, unknown>): (snapshot: object) => Sink<ReactiveSetSource<T>, ZSetChangeInput<T>>;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { ZSet } from "derivation";
|
|
2
1
|
import { zset, zsetToArray, compose } from "./iso";
|
|
3
2
|
export class ReactiveSetSourceAdapter {
|
|
4
3
|
constructor(set, iso) {
|
|
@@ -9,34 +8,32 @@ export class ReactiveSetSourceAdapter {
|
|
|
9
8
|
return this.iso.to(this.set.snapshot);
|
|
10
9
|
}
|
|
11
10
|
get LastChange() {
|
|
12
|
-
|
|
11
|
+
const change = this.iso.to(this.set.changes.value);
|
|
12
|
+
if (change.length === 0)
|
|
13
|
+
return null;
|
|
14
|
+
return change;
|
|
13
15
|
}
|
|
14
16
|
get Stream() {
|
|
15
17
|
return this.set;
|
|
16
18
|
}
|
|
17
19
|
}
|
|
18
20
|
export class ReactiveSetSinkAdapter {
|
|
19
|
-
constructor(graph, iso) {
|
|
21
|
+
constructor(graph, iso, snapshot) {
|
|
20
22
|
this.graph = graph;
|
|
21
23
|
this.iso = iso;
|
|
24
|
+
this.initialSet = iso.from(snapshot);
|
|
22
25
|
}
|
|
23
|
-
apply(change,
|
|
24
|
-
|
|
26
|
+
apply(change, input) {
|
|
27
|
+
input.push(this.iso.from(change));
|
|
25
28
|
}
|
|
26
29
|
build() {
|
|
27
|
-
|
|
30
|
+
const stream = this.graph.inputSet(this.initialSet);
|
|
31
|
+
return { stream, input: stream.changes };
|
|
28
32
|
}
|
|
29
33
|
}
|
|
30
34
|
export function sink(graph, iso) {
|
|
31
35
|
const wholeIso = compose(zset(iso), zsetToArray());
|
|
32
36
|
return (snapshot) => {
|
|
33
|
-
|
|
34
|
-
const g = graph;
|
|
35
|
-
const adapter = new ReactiveSetSinkAdapter(g, wholeIso);
|
|
36
|
-
const stream = g.inputSet(set);
|
|
37
|
-
return {
|
|
38
|
-
apply: adapter.apply.bind(adapter),
|
|
39
|
-
build: () => stream,
|
|
40
|
-
};
|
|
37
|
+
return new ReactiveSetSinkAdapter(graph, wholeIso, snapshot);
|
|
41
38
|
};
|
|
42
39
|
}
|