@statelyai/sdk 0.14.2 → 0.16.0

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/relay.mjs CHANGED
@@ -1,6 +1,29 @@
1
1
  import { INSPECTION_PROTOCOL_VERSION, PREFIX } from "./protocol.mjs";
2
2
 
3
3
  //#region src/relay.ts
4
+ const REPLAYABLE_MESSAGE_TYPES = new Set([
5
+ "@statelyai.init",
6
+ "@statelyai.inspectSnapshot",
7
+ "@statelyai.system.init",
8
+ "@statelyai.system.actorRegistered",
9
+ "@statelyai.system.actorEvent",
10
+ "@statelyai.system.actorSnapshot",
11
+ "@statelyai.system.actorStopped"
12
+ ]);
13
+ const FORWARD_ONLY_MESSAGE_TYPES = new Set([
14
+ "@statelyai.system.selectActor",
15
+ "@statelyai.retrieve",
16
+ "@statelyai.retrieved",
17
+ "@statelyai.error",
18
+ "@statelyai.ready",
19
+ "@statelyai.loaded",
20
+ "@statelyai.capabilityDenied",
21
+ "@statelyai.register",
22
+ "@statelyai.registered"
23
+ ]);
24
+ function isSupportedMessageType(type) {
25
+ return REPLAYABLE_MESSAGE_TYPES.has(type) || FORWARD_ONLY_MESSAGE_TYPES.has(type);
26
+ }
4
27
  function parseMessage(raw) {
5
28
  if (raw instanceof ArrayBuffer) raw = new TextDecoder().decode(raw);
6
29
  if (typeof raw === "string") try {
@@ -12,6 +35,22 @@ function parseMessage(raw) {
12
35
  const message = raw;
13
36
  return typeof message.type === "string" && message.type.startsWith(PREFIX) ? message : null;
14
37
  }
38
+ function validationError(message) {
39
+ if (!isSupportedMessageType(message.type)) return `Unsupported inspection message type ${String(message.type)}`;
40
+ const hasString = (key) => typeof message[key] === "string" && message[key] !== "";
41
+ switch (message.type) {
42
+ case "@statelyai.system.init": return message.protocolVersion === INSPECTION_PROTOCOL_VERSION && Array.isArray(message.actors) ? null : "system.init requires the current protocolVersion and actors array";
43
+ case "@statelyai.system.selectActor":
44
+ case "@statelyai.system.actorStopped": return hasString("sessionId") ? null : `${message.type} requires sessionId`;
45
+ case "@statelyai.system.actorRegistered": return hasString("sessionId") && hasString("actorId") ? null : "actorRegistered requires sessionId and actorId";
46
+ case "@statelyai.system.actorEvent": return hasString("sessionId") && "event" in message ? null : "actorEvent requires sessionId and event";
47
+ case "@statelyai.system.actorSnapshot": return hasString("sessionId") && "snapshot" in message ? null : "actorSnapshot requires sessionId and snapshot";
48
+ case "@statelyai.retrieve":
49
+ case "@statelyai.retrieved": return hasString("requestId") ? null : `${message.type} requires requestId`;
50
+ case "@statelyai.error": return hasString("code") && hasString("message") ? null : "error requires code and message";
51
+ default: return null;
52
+ }
53
+ }
15
54
  function errorMessage(code, message) {
16
55
  return {
17
56
  type: "@statelyai.error",
@@ -30,10 +69,10 @@ function compactReplay(replay, message, max) {
30
69
  const index = replay.findIndex((entry) => entry.type === "@statelyai.init");
31
70
  if (index >= 0) replay.splice(index, 1);
32
71
  } else if (type === "@statelyai.system.actorSnapshot" || type === "@statelyai.inspectSnapshot") {
33
- const actorId = record.actorId;
72
+ const sessionId = record.sessionId;
34
73
  const index = replay.findIndex((entry) => {
35
74
  const candidate = entry;
36
- return candidate.type === type && candidate.actorId === actorId;
75
+ return candidate.type === type && candidate.sessionId === sessionId;
37
76
  });
38
77
  if (index >= 0) replay.splice(index, 1);
39
78
  }
@@ -41,110 +80,92 @@ function compactReplay(replay, message, max) {
41
80
  if (replay.length > max) replay.splice(0, replay.length - max);
42
81
  }
43
82
  /**
44
- * Runtime-neutral WebSocket inspection room.
83
+ * Runtime-neutral implementation of one inspection room.
45
84
  *
46
- * It owns protocol registration, routing, multiplexed session membership, and
47
- * bounded late-join replay. Node and Durable Object adapters only translate
48
- * `send` effects to their native WebSocket APIs and persist `snapshot()`.
85
+ * The hosting adapter selects the room from the WebSocket URL. This module
86
+ * owns protocol registration, multi-producer/viewer routing, and bounded
87
+ * per-producer replay.
49
88
  */
50
89
  function createInspectionRelay(options = {}) {
51
90
  const maxReplayMessages = options.maxReplayMessages ?? 200;
52
- const maxReplaySessions = Math.max(1, options.maxReplaySessions ?? 100);
91
+ const maxReplayProducers = Math.max(1, options.maxReplayProducers ?? 100);
53
92
  const peers = /* @__PURE__ */ new Map();
54
- const sessions = new Map(Object.entries(options.snapshot?.sessions ?? {}).slice(-maxReplaySessions).map(([id, replay]) => [id, [...replay]]));
93
+ const producers = new Map(Object.entries(options.snapshot?.producers ?? {}).slice(-maxReplayProducers).map(([id, replay]) => [id, [...replay]]));
55
94
  const send = (peerId, message) => ({
56
95
  type: "send",
57
96
  peerId,
58
97
  message
59
98
  });
60
- function replayTo(peerId, sessionId) {
61
- return (sessions.get(sessionId) ?? []).map((message) => send(peerId, message));
62
- }
63
- function oppositePeers(sender, sessionId) {
64
- return Array.from(peers.values()).filter((peer) => peer.id !== sender.id && peer.role !== sender.role && peer.sessionIds.includes(sessionId));
65
- }
66
- function retainReplay(sessionId, replay) {
67
- sessions.delete(sessionId);
68
- sessions.set(sessionId, replay);
69
- while (sessions.size > maxReplaySessions) {
70
- const oldestSessionId = sessions.keys().next().value;
71
- if (oldestSessionId === void 0) break;
72
- sessions.delete(oldestSessionId);
99
+ function retainReplay(producerId, replay) {
100
+ producers.delete(producerId);
101
+ producers.set(producerId, replay);
102
+ while (producers.size > maxReplayProducers) {
103
+ const oldestProducerId = producers.keys().next().value;
104
+ if (oldestProducerId === void 0) break;
105
+ producers.delete(oldestProducerId);
73
106
  }
74
107
  }
75
108
  return {
76
109
  connect(peer) {
77
- peers.set(peer.id, {
78
- ...peer,
79
- sessionIds: Array.from(new Set([peer.primarySessionId, ...peer.sessionIds]))
80
- });
110
+ peers.set(peer.id, { ...peer });
81
111
  },
82
112
  disconnect(peerId) {
83
113
  peers.delete(peerId);
84
114
  },
85
115
  clear() {
86
116
  peers.clear();
87
- sessions.clear();
117
+ producers.clear();
88
118
  },
89
119
  peer(peerId) {
90
120
  const peer = peers.get(peerId);
91
- return peer ? {
92
- ...peer,
93
- sessionIds: [...peer.sessionIds]
94
- } : void 0;
121
+ return peer ? { ...peer } : void 0;
95
122
  },
96
123
  receive(peerId, raw) {
97
124
  const message = parseMessage(raw);
98
- if (!message) return [];
125
+ if (!message) return [send(peerId, errorMessage("invalid_message", "Expected a JSON protocol message"))];
126
+ const invalid = validationError(message);
127
+ if (invalid) return [send(peerId, errorMessage("invalid_message", invalid))];
99
128
  if (message.type === "@statelyai.register") {
100
129
  const role = message.role;
101
- const sessionId = message.sessionId;
102
130
  const protocolVersion = message.protocolVersion;
103
- if (role !== "client" && role !== "viz" || typeof sessionId !== "string" || sessionId.length === 0) return [send(peerId, errorMessage("invalid_registration", "Invalid relay registration"))];
104
- if (typeof protocolVersion === "number" && protocolVersion > INSPECTION_PROTOCOL_VERSION) return [send(peerId, errorMessage("unsupported_protocol", `Protocol ${protocolVersion} is not supported`))];
131
+ const producerId = message.producerId;
132
+ if (role !== "producer" && role !== "viewer" || role === "producer" && (typeof producerId !== "string" || producerId.length === 0)) return [send(peerId, errorMessage("invalid_registration", "Invalid relay registration"))];
133
+ if (protocolVersion !== INSPECTION_PROTOCOL_VERSION) return [send(peerId, errorMessage("unsupported_protocol", `Protocol ${String(protocolVersion)} is not supported; expected ${INSPECTION_PROTOCOL_VERSION}`))];
105
134
  const peer = {
106
135
  id: peerId,
107
136
  role,
108
- primarySessionId: sessionId,
109
- sessionIds: [sessionId]
137
+ ...role === "producer" && { producerId }
110
138
  };
111
139
  peers.set(peerId, peer);
112
140
  const effects = [send(peerId, {
113
141
  type: "@statelyai.registered",
114
142
  protocolVersion: INSPECTION_PROTOCOL_VERSION,
115
- sessionId
143
+ ...peer.producerId && { producerId: peer.producerId }
116
144
  })];
117
- if (role === "viz") effects.push(...replayTo(peerId, sessionId));
145
+ if (role === "viewer") for (const replay of producers.values()) effects.push(...replay.map((entry) => send(peerId, entry)));
118
146
  return effects;
119
147
  }
120
148
  const sender = peers.get(peerId);
121
149
  if (!sender) return [send(peerId, errorMessage("not_registered", "Register before sending messages"))];
122
- if (message.type === "@statelyai.addSession") {
123
- const sessionId = message.sessionId;
124
- if (typeof sessionId !== "string" || sessionId.length === 0) return [send(peerId, errorMessage("invalid_session", "Invalid session id"))];
125
- if (!sender.sessionIds.includes(sessionId)) sender.sessionIds.push(sessionId);
126
- const effects = [send(peerId, {
127
- type: "@statelyai.sessionAdded",
128
- sessionId
129
- })];
130
- if (sender.role === "viz") effects.push(...replayTo(peerId, sessionId));
131
- return effects;
132
- }
133
- const sessionId = (typeof message.sessionId === "string" ? message.sessionId : void 0) ?? sender.primarySessionId;
134
- if (!sender.sessionIds.includes(sessionId)) return [send(peerId, errorMessage("unknown_session", "Peer is not registered for session"))];
135
- const stamped = {
136
- ...message,
137
- sessionId
138
- };
139
- if (sender.role === "client") {
140
- const replay = sessions.get(sessionId) ?? [];
141
- compactReplay(replay, stamped, maxReplayMessages);
142
- retainReplay(sessionId, replay);
150
+ if (sender.role === "producer") {
151
+ const producerId = sender.producerId;
152
+ const stamped = {
153
+ ...message,
154
+ producerId
155
+ };
156
+ if (REPLAYABLE_MESSAGE_TYPES.has(message.type)) {
157
+ const replay = producers.get(producerId) ?? [];
158
+ compactReplay(replay, stamped, maxReplayMessages);
159
+ retainReplay(producerId, replay);
160
+ }
161
+ return Array.from(peers.values()).filter((peer) => peer.role === "viewer").map((peer) => send(peer.id, stamped));
143
162
  }
144
- return oppositePeers(sender, sessionId).map((peer) => send(peer.id, stamped));
163
+ const targetProducerId = typeof message.producerId === "string" ? message.producerId : void 0;
164
+ return Array.from(peers.values()).filter((peer) => peer.role === "producer" && (!targetProducerId || peer.producerId === targetProducerId)).map((peer) => send(peer.id, message));
145
165
  },
146
- snapshot() {
147
- return { sessions: Object.fromEntries(Array.from(sessions, ([sessionId, replay]) => [sessionId, [...replay]])) };
166
+ snapshot(producerId) {
167
+ const entries = producerId ? producers.has(producerId) ? [[producerId, producers.get(producerId)]] : [] : Array.from(producers.entries());
168
+ return { producers: Object.fromEntries(entries.map(([producerId, replay]) => [producerId, [...replay]])) };
148
169
  }
149
170
  };
150
171
  }