@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.
@@ -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, StreamDefinitions } from "./stream-types";
5
- export declare class ClientHandler<Defs extends StreamDefinitions> {
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 endpoints;
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 interval;
11
- constructor(ws: WebSocket, endpoints: StreamEndpoints<Defs>);
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;
@@ -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, endpoints) {
5
+ constructor(ws, streamEndpoints, mutationEndpoints, presenceHandler) {
5
6
  this.closed = false;
6
7
  this.streams = new Map();
7
8
  this.ws = ws;
8
- this.endpoints = endpoints;
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.interval) {
14
- clearInterval(this.interval);
18
+ if (this.heartbeatTimeout) {
19
+ clearTimeout(this.heartbeatTimeout);
15
20
  }
16
- this.interval = setInterval(() => {
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.endpoints)) {
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.endpoints[name];
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
- changes[id] = source.LastChange;
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
- clearInterval(this.interval);
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
  }
@@ -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
  };
@@ -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 { StreamDefinitions, StreamSinks } from "./stream-types";
3
- export declare class Client<Defs extends StreamDefinitions> {
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 pending;
8
+ private pendingStreams;
9
+ private pendingMutations;
9
10
  private activeStreams;
11
+ private heartbeatTimeout;
12
+ private inactivityTimeout;
10
13
  private registry;
11
- constructor(ws: WebSocket, sinks: StreamSinks<Defs>, graph: Graph);
14
+ private resetHeartbeat;
15
+ private resetInactivity;
16
+ constructor(ws: WebSocket, sinks: StreamSinks<Defs["streams"]>, graph: Graph);
12
17
  private handleMessage;
13
- private send;
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, stream) {
2
+ function changer(sink, input) {
3
3
  return (change) => {
4
- const s = stream.deref();
5
- if (s) {
6
- sink.apply(change, s);
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.pending = new Map();
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.send(ClientMessage.unsubscribe(id));
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.handleMessage(message);
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.pending.get(message.id);
51
+ const resolve = this.pendingStreams.get(message.id);
32
52
  if (resolve) {
33
53
  resolve(message.snapshot);
34
- this.pending.delete(message.id);
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} GCd — auto-unsubscribing`);
47
- this.send(ClientMessage.unsubscribe(id));
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
- send(message) {
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.send(ClientMessage.subscribe(id, String(key), args));
101
+ this.sendMessage(ClientMessage.subscribe(id, String(key), args));
65
102
  const snapshot = await new Promise((resolve) => {
66
- this.pending.set(id, resolve);
103
+ this.pendingStreams.set(id, resolve);
67
104
  });
68
105
  const endpoint = this.sinks[key];
69
- const sink = endpoint(snapshot);
70
- const stream = sink.build();
71
- const ref = new WeakRef(stream);
72
- this.activeStreams.set(id, changer(sink, ref));
73
- this.registry.register(stream, [id, String(key)]);
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,5 @@
1
+ export interface PresenceHandler {
2
+ add(value: Record<string, unknown>): void;
3
+ remove(value: Record<string, unknown>): void;
4
+ update(oldValue: Record<string, unknown>, newValue: Record<string, unknown>): void;
5
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,7 @@
1
+ export declare class RateLimiter {
2
+ private timestamps;
3
+ private readonly maxOccurrences;
4
+ private readonly windowNanos;
5
+ constructor(maxOccurrences: number, windowSeconds: number);
6
+ trigger(): boolean;
7
+ }
@@ -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
- constructor(graph: Graph, keyIso: Iso<K, unknown>, valueIso: Iso<V, unknown>);
16
- apply(change: object, stream: ReactiveMapSource<K, V>): void;
17
- build(): ReactiveMapSource<K, V>;
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
- return this.iso.to(this.map.changes.value);
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, stream) {
26
+ apply(change, input) {
24
27
  const zmapChange = this.iso.from(change);
25
28
  for (const [key, value, weight] of zmapChange.getEntries()) {
26
- stream.add(key, value, weight);
29
+ input.add(key, value, weight);
27
30
  }
28
31
  }
29
32
  build() {
30
- return this.graph.inputMap(new ZMap());
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
- const initial = wholeIso.from(snapshot);
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
- constructor(graph: Graph, iso: Iso<ZSet<T>, unknown>);
16
- apply(change: object, stream: ReactiveSetSource<T>): void;
17
- build(): ReactiveSetSource<T>;
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
- return this.iso.to(this.set.changes.value);
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, stream) {
24
- stream.push(this.iso.from(change));
26
+ apply(change, input) {
27
+ input.push(this.iso.from(change));
25
28
  }
26
29
  build() {
27
- return this.graph.inputSet(new ZSet([]));
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
- const set = wholeIso.from(snapshot);
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
  }