@graphorin/server 0.5.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/CHANGELOG.md +7 -0
- package/LICENSE +21 -0
- package/README.md +116 -0
- package/dist/app.d.ts +174 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +684 -0
- package/dist/app.js.map +1 -0
- package/dist/commentary/audit-bridge.d.ts +57 -0
- package/dist/commentary/audit-bridge.d.ts.map +1 -0
- package/dist/commentary/audit-bridge.js +92 -0
- package/dist/commentary/audit-bridge.js.map +1 -0
- package/dist/commentary/built-in-patterns.d.ts +24 -0
- package/dist/commentary/built-in-patterns.d.ts.map +1 -0
- package/dist/commentary/built-in-patterns.js +62 -0
- package/dist/commentary/built-in-patterns.js.map +1 -0
- package/dist/commentary/index.d.ts +5 -0
- package/dist/commentary/index.js +5 -0
- package/dist/commentary/sanitizer.d.ts +37 -0
- package/dist/commentary/sanitizer.d.ts.map +1 -0
- package/dist/commentary/sanitizer.js +182 -0
- package/dist/commentary/sanitizer.js.map +1 -0
- package/dist/commentary/types.d.ts +120 -0
- package/dist/commentary/types.d.ts.map +1 -0
- package/dist/config.d.ts +760 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +217 -0
- package/dist/config.js.map +1 -0
- package/dist/consolidator/daemon.d.ts +88 -0
- package/dist/consolidator/daemon.d.ts.map +1 -0
- package/dist/consolidator/daemon.js +54 -0
- package/dist/consolidator/daemon.js.map +1 -0
- package/dist/consolidator/index.d.ts +2 -0
- package/dist/consolidator/index.js +3 -0
- package/dist/errors/index.d.ts +104 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +131 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/health/checks.d.ts +120 -0
- package/dist/health/checks.d.ts.map +1 -0
- package/dist/health/checks.js +127 -0
- package/dist/health/checks.js.map +1 -0
- package/dist/health/index.d.ts +3 -0
- package/dist/health/index.js +4 -0
- package/dist/health/routes.d.ts +66 -0
- package/dist/health/routes.d.ts.map +1 -0
- package/dist/health/routes.js +96 -0
- package/dist/health/routes.js.map +1 -0
- package/dist/index.d.ts +88 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +86 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/context.d.ts +66 -0
- package/dist/internal/context.d.ts.map +1 -0
- package/dist/internal/ids.js +21 -0
- package/dist/internal/ids.js.map +1 -0
- package/dist/internal/json.js +46 -0
- package/dist/internal/json.js.map +1 -0
- package/dist/lifecycle/hooks.d.ts +57 -0
- package/dist/lifecycle/hooks.d.ts.map +1 -0
- package/dist/lifecycle/index.d.ts +2 -0
- package/dist/lifecycle/index.js +3 -0
- package/dist/lifecycle/pre-bind.d.ts +38 -0
- package/dist/lifecycle/pre-bind.d.ts.map +1 -0
- package/dist/lifecycle/pre-bind.js +62 -0
- package/dist/lifecycle/pre-bind.js.map +1 -0
- package/dist/metrics/catalog.d.ts +34 -0
- package/dist/metrics/catalog.d.ts.map +1 -0
- package/dist/metrics/catalog.js +61 -0
- package/dist/metrics/catalog.js.map +1 -0
- package/dist/metrics/index.d.ts +3 -0
- package/dist/metrics/index.js +4 -0
- package/dist/metrics/registry.d.ts +63 -0
- package/dist/metrics/registry.d.ts.map +1 -0
- package/dist/metrics/registry.js +222 -0
- package/dist/metrics/registry.js.map +1 -0
- package/dist/middleware/audit.d.ts +47 -0
- package/dist/middleware/audit.d.ts.map +1 -0
- package/dist/middleware/audit.js +71 -0
- package/dist/middleware/audit.js.map +1 -0
- package/dist/middleware/auth.d.ts +50 -0
- package/dist/middleware/auth.d.ts.map +1 -0
- package/dist/middleware/auth.js +164 -0
- package/dist/middleware/auth.js.map +1 -0
- package/dist/middleware/cors.d.ts +15 -0
- package/dist/middleware/cors.d.ts.map +1 -0
- package/dist/middleware/cors.js +45 -0
- package/dist/middleware/cors.js.map +1 -0
- package/dist/middleware/csrf.d.ts +15 -0
- package/dist/middleware/csrf.d.ts.map +1 -0
- package/dist/middleware/csrf.js +77 -0
- package/dist/middleware/csrf.js.map +1 -0
- package/dist/middleware/idempotency.d.ts +41 -0
- package/dist/middleware/idempotency.d.ts.map +1 -0
- package/dist/middleware/idempotency.js +198 -0
- package/dist/middleware/idempotency.js.map +1 -0
- package/dist/middleware/index.d.ts +9 -0
- package/dist/middleware/index.js +10 -0
- package/dist/middleware/rate-limit.d.ts +17 -0
- package/dist/middleware/rate-limit.d.ts.map +1 -0
- package/dist/middleware/rate-limit.js +47 -0
- package/dist/middleware/rate-limit.js.map +1 -0
- package/dist/middleware/request-state.d.ts +27 -0
- package/dist/middleware/request-state.d.ts.map +1 -0
- package/dist/middleware/request-state.js +42 -0
- package/dist/middleware/request-state.js.map +1 -0
- package/dist/middleware/scope.d.ts +24 -0
- package/dist/middleware/scope.d.ts.map +1 -0
- package/dist/middleware/scope.js +50 -0
- package/dist/middleware/scope.js.map +1 -0
- package/dist/registry/index.d.ts +135 -0
- package/dist/registry/index.d.ts.map +1 -0
- package/dist/registry/index.js +96 -0
- package/dist/registry/index.js.map +1 -0
- package/dist/replay/index.d.ts +2 -0
- package/dist/replay/index.js +3 -0
- package/dist/replay/routes.d.ts +46 -0
- package/dist/replay/routes.d.ts.map +1 -0
- package/dist/replay/routes.js +189 -0
- package/dist/replay/routes.js.map +1 -0
- package/dist/routes/agents.d.ts +41 -0
- package/dist/routes/agents.d.ts.map +1 -0
- package/dist/routes/agents.js +213 -0
- package/dist/routes/agents.js.map +1 -0
- package/dist/routes/audit.d.ts +57 -0
- package/dist/routes/audit.d.ts.map +1 -0
- package/dist/routes/audit.js +116 -0
- package/dist/routes/audit.js.map +1 -0
- package/dist/routes/auth.d.ts +26 -0
- package/dist/routes/auth.d.ts.map +1 -0
- package/dist/routes/auth.js +54 -0
- package/dist/routes/auth.js.map +1 -0
- package/dist/routes/health.d.ts +22 -0
- package/dist/routes/health.d.ts.map +1 -0
- package/dist/routes/health.js +33 -0
- package/dist/routes/health.js.map +1 -0
- package/dist/routes/index.d.ts +10 -0
- package/dist/routes/index.js +12 -0
- package/dist/routes/mcp.d.ts +32 -0
- package/dist/routes/mcp.d.ts.map +1 -0
- package/dist/routes/mcp.js +61 -0
- package/dist/routes/mcp.js.map +1 -0
- package/dist/routes/memory.d.ts +141 -0
- package/dist/routes/memory.d.ts.map +1 -0
- package/dist/routes/memory.js +102 -0
- package/dist/routes/memory.js.map +1 -0
- package/dist/routes/sessions.d.ts +54 -0
- package/dist/routes/sessions.d.ts.map +1 -0
- package/dist/routes/sessions.js +135 -0
- package/dist/routes/sessions.js.map +1 -0
- package/dist/routes/skills.d.ts +34 -0
- package/dist/routes/skills.d.ts.map +1 -0
- package/dist/routes/skills.js +54 -0
- package/dist/routes/skills.js.map +1 -0
- package/dist/routes/tokens.d.ts +25 -0
- package/dist/routes/tokens.d.ts.map +1 -0
- package/dist/routes/tokens.js +87 -0
- package/dist/routes/tokens.js.map +1 -0
- package/dist/routes/workflows.d.ts +27 -0
- package/dist/routes/workflows.d.ts.map +1 -0
- package/dist/routes/workflows.js +220 -0
- package/dist/routes/workflows.js.map +1 -0
- package/dist/runtime/run-state.d.ts +158 -0
- package/dist/runtime/run-state.d.ts.map +1 -0
- package/dist/runtime/run-state.js +182 -0
- package/dist/runtime/run-state.js.map +1 -0
- package/dist/sse/events.d.ts +44 -0
- package/dist/sse/events.d.ts.map +1 -0
- package/dist/sse/events.js +200 -0
- package/dist/sse/events.js.map +1 -0
- package/dist/sse/index.d.ts +2 -0
- package/dist/sse/index.js +3 -0
- package/dist/triggers/daemon.d.ts +74 -0
- package/dist/triggers/daemon.d.ts.map +1 -0
- package/dist/triggers/daemon.js +114 -0
- package/dist/triggers/daemon.js.map +1 -0
- package/dist/triggers/index.d.ts +3 -0
- package/dist/triggers/index.js +4 -0
- package/dist/triggers/routes.d.ts +21 -0
- package/dist/triggers/routes.d.ts.map +1 -0
- package/dist/triggers/routes.js +117 -0
- package/dist/triggers/routes.js.map +1 -0
- package/dist/ws/dispatcher.d.ts +169 -0
- package/dist/ws/dispatcher.d.ts.map +1 -0
- package/dist/ws/dispatcher.js +303 -0
- package/dist/ws/dispatcher.js.map +1 -0
- package/dist/ws/index.d.ts +6 -0
- package/dist/ws/index.js +7 -0
- package/dist/ws/replay-buffer.d.ts +47 -0
- package/dist/ws/replay-buffer.d.ts.map +1 -0
- package/dist/ws/replay-buffer.js +88 -0
- package/dist/ws/replay-buffer.js.map +1 -0
- package/dist/ws/subjects.d.ts +71 -0
- package/dist/ws/subjects.d.ts.map +1 -0
- package/dist/ws/subjects.js +112 -0
- package/dist/ws/subjects.js.map +1 -0
- package/dist/ws/ticket.d.ts +74 -0
- package/dist/ws/ticket.d.ts.map +1 -0
- package/dist/ws/ticket.js +112 -0
- package/dist/ws/ticket.js.map +1 -0
- package/dist/ws/upgrade.d.ts +63 -0
- package/dist/ws/upgrade.d.ts.map +1 -0
- package/dist/ws/upgrade.js +324 -0
- package/dist/ws/upgrade.js.map +1 -0
- package/package.json +156 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { createDeliveryCommentarySanitizer } from "../commentary/sanitizer.js";
|
|
2
|
+
import "../commentary/index.js";
|
|
3
|
+
import { createReplayBuffer } from "./replay-buffer.js";
|
|
4
|
+
import { isSubjectAllowed, tryParseSubject } from "./subjects.js";
|
|
5
|
+
import { ServerMessageSchema, isEventFrame } from "@graphorin/protocol";
|
|
6
|
+
|
|
7
|
+
//#region src/ws/dispatcher.ts
|
|
8
|
+
/**
|
|
9
|
+
* Per-server WebSocket dispatcher. Owns the in-memory subscription
|
|
10
|
+
* map, fans out events to matching connections, applies the
|
|
11
|
+
* delivery-layer commentary-phase trace sanitization, validates
|
|
12
|
+
* outbound frames against the `@graphorin/protocol` schema, and
|
|
13
|
+
* exposes a tiny `emit(subject, event)` surface route handlers
|
|
14
|
+
* (and the agent / workflow runtimes) consume.
|
|
15
|
+
*
|
|
16
|
+
* @packageDocumentation
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Build a fresh dispatcher.
|
|
20
|
+
*
|
|
21
|
+
* @stable
|
|
22
|
+
*/
|
|
23
|
+
function createWsDispatcher(options = {}) {
|
|
24
|
+
const sanitizer = createDeliveryCommentarySanitizer(options.commentary);
|
|
25
|
+
const replayBuffer = createReplayBuffer(options.replayBuffer ?? {});
|
|
26
|
+
const now = options.now ?? Date.now;
|
|
27
|
+
const onWarn = options.onWarn;
|
|
28
|
+
const perConnectionQueueLimit = options.perConnectionQueueLimit ?? 1e3;
|
|
29
|
+
const subscribers = /* @__PURE__ */ new Map();
|
|
30
|
+
const subscriptions = /* @__PURE__ */ new Map();
|
|
31
|
+
const subjectIndex = /* @__PURE__ */ new Map();
|
|
32
|
+
let eventCounter = 0;
|
|
33
|
+
function indexSubject(subject, subscriptionId) {
|
|
34
|
+
let set = subjectIndex.get(subject);
|
|
35
|
+
if (set === void 0) {
|
|
36
|
+
set = /* @__PURE__ */ new Set();
|
|
37
|
+
subjectIndex.set(subject, set);
|
|
38
|
+
}
|
|
39
|
+
set.add(subscriptionId);
|
|
40
|
+
}
|
|
41
|
+
function deindexSubject(subject, subscriptionId) {
|
|
42
|
+
const set = subjectIndex.get(subject);
|
|
43
|
+
if (set === void 0) return;
|
|
44
|
+
set.delete(subscriptionId);
|
|
45
|
+
if (set.size === 0) subjectIndex.delete(subject);
|
|
46
|
+
}
|
|
47
|
+
function nextEventId() {
|
|
48
|
+
eventCounter += 1;
|
|
49
|
+
return `evt-${now().toString(36)}-${eventCounter.toString(36)}`;
|
|
50
|
+
}
|
|
51
|
+
function dispatchToSubscriber(record, frame) {
|
|
52
|
+
const subscriber = subscribers.get(record.subscriberId);
|
|
53
|
+
if (subscriber === void 0 || subscriber.closed) return;
|
|
54
|
+
const buffered = typeof subscriber.handle.bufferedAmount === "function" ? subscriber.handle.bufferedAmount() : 0;
|
|
55
|
+
if (subscriber.outstandingEvents >= perConnectionQueueLimit || buffered >= perConnectionQueueLimit * 1024) {
|
|
56
|
+
onWarn?.({
|
|
57
|
+
kind: "queue-overflow",
|
|
58
|
+
subscriptionId: record.subscriptionId
|
|
59
|
+
});
|
|
60
|
+
shutdownSubscriber(subscriber, 4006, "flow.throttled");
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
subscriber.outstandingEvents += 1;
|
|
64
|
+
try {
|
|
65
|
+
subscriber.handle.send(frame);
|
|
66
|
+
} catch (err) {
|
|
67
|
+
onWarn?.({
|
|
68
|
+
kind: "queue-overflow",
|
|
69
|
+
subscriptionId: record.subscriptionId
|
|
70
|
+
});
|
|
71
|
+
shutdownSubscriber(subscriber, 4006, "flow.throttled");
|
|
72
|
+
} finally {
|
|
73
|
+
subscriber.outstandingEvents = Math.max(0, subscriber.outstandingEvents - 1);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function shutdownSubscriber(subscriber, code, reason) {
|
|
77
|
+
if (subscriber.closed) return;
|
|
78
|
+
subscriber.closed = true;
|
|
79
|
+
try {
|
|
80
|
+
subscriber.handle.close(code, reason);
|
|
81
|
+
} catch {}
|
|
82
|
+
for (const id of subscriber.subscriptions) {
|
|
83
|
+
const sub = subscriptions.get(id);
|
|
84
|
+
if (sub !== void 0) {
|
|
85
|
+
deindexSubject(sub.subject, id);
|
|
86
|
+
subscriptions.delete(id);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
subscriber.subscriptions.clear();
|
|
90
|
+
subscribers.delete(subscriber.handle.id);
|
|
91
|
+
}
|
|
92
|
+
function validateAndDispatch(record, frame) {
|
|
93
|
+
const validated = ServerMessageSchema.safeParse(frame);
|
|
94
|
+
if (!validated.success) {
|
|
95
|
+
onWarn?.({
|
|
96
|
+
kind: "invalid-frame",
|
|
97
|
+
subscriptionId: record.subscriptionId,
|
|
98
|
+
issues: validated.error.issues.map((i) => `${i.path.join(".") || "<root>"}: ${i.message}`)
|
|
99
|
+
});
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
dispatchToSubscriber(record, validated.data);
|
|
103
|
+
}
|
|
104
|
+
function registerSubscriber(handle) {
|
|
105
|
+
subscribers.set(handle.id, {
|
|
106
|
+
handle,
|
|
107
|
+
subscriptions: /* @__PURE__ */ new Set(),
|
|
108
|
+
outstandingEvents: 0,
|
|
109
|
+
closed: false
|
|
110
|
+
});
|
|
111
|
+
return { unregister: () => {
|
|
112
|
+
const record = subscribers.get(handle.id);
|
|
113
|
+
if (record === void 0) return;
|
|
114
|
+
for (const id of record.subscriptions) {
|
|
115
|
+
const sub = subscriptions.get(id);
|
|
116
|
+
if (sub !== void 0) {
|
|
117
|
+
deindexSubject(sub.subject, id);
|
|
118
|
+
subscriptions.delete(id);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
subscribers.delete(handle.id);
|
|
122
|
+
} };
|
|
123
|
+
}
|
|
124
|
+
function subscribe(input) {
|
|
125
|
+
const subscriber = subscribers.get(input.subscriberId);
|
|
126
|
+
if (subscriber === void 0) return {
|
|
127
|
+
ok: false,
|
|
128
|
+
reason: "subject-unknown"
|
|
129
|
+
};
|
|
130
|
+
const parseResult = tryParseSubject(input.subject);
|
|
131
|
+
if (!parseResult.ok) {
|
|
132
|
+
const reason = parseResult.reason;
|
|
133
|
+
onWarn?.({
|
|
134
|
+
kind: "subject-rejected",
|
|
135
|
+
subject: input.subject,
|
|
136
|
+
reason
|
|
137
|
+
});
|
|
138
|
+
if (reason === "wildcard-not-supported") return {
|
|
139
|
+
ok: false,
|
|
140
|
+
reason: "subject-wildcard"
|
|
141
|
+
};
|
|
142
|
+
if (reason === "unknown-subject") return {
|
|
143
|
+
ok: false,
|
|
144
|
+
reason: "subject-unknown"
|
|
145
|
+
};
|
|
146
|
+
return {
|
|
147
|
+
ok: false,
|
|
148
|
+
reason: "subject-malformed"
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
if (!isSubjectAllowed(subscriber.handle.grantedScopes, parseResult.subject)) return {
|
|
152
|
+
ok: false,
|
|
153
|
+
reason: "scope-denied"
|
|
154
|
+
};
|
|
155
|
+
const record = {
|
|
156
|
+
subscriberId: input.subscriberId,
|
|
157
|
+
tokenId: subscriber.handle.tokenId,
|
|
158
|
+
subscriptionId: input.subscriptionId,
|
|
159
|
+
subject: input.subject,
|
|
160
|
+
parsed: parseResult.subject,
|
|
161
|
+
createdAt: now(),
|
|
162
|
+
lastEventId: void 0
|
|
163
|
+
};
|
|
164
|
+
subscriptions.set(input.subscriptionId, record);
|
|
165
|
+
subscriber.subscriptions.add(input.subscriptionId);
|
|
166
|
+
indexSubject(input.subject, input.subscriptionId);
|
|
167
|
+
const replay = replayBuffer.replay(input.subject, input.sinceEventId);
|
|
168
|
+
let replayed = 0;
|
|
169
|
+
let snapshotEventId = replay.nextEventIdHint;
|
|
170
|
+
for (const event of replay.events) {
|
|
171
|
+
const frame = {
|
|
172
|
+
...event,
|
|
173
|
+
subscriptionId: input.subscriptionId
|
|
174
|
+
};
|
|
175
|
+
validateAndDispatch(record, frame);
|
|
176
|
+
record.lastEventId = frame.eventId;
|
|
177
|
+
replayed += 1;
|
|
178
|
+
}
|
|
179
|
+
if (replay.droppedCount > 0) {
|
|
180
|
+
validateAndDispatch(record, {
|
|
181
|
+
v: "1",
|
|
182
|
+
kind: "replay-marker",
|
|
183
|
+
subscriptionId: input.subscriptionId,
|
|
184
|
+
eventId: snapshotEventId ?? `evt-replay-${now().toString(36)}`,
|
|
185
|
+
droppedCount: replay.droppedCount,
|
|
186
|
+
note: `Replay buffer dropped ${replay.droppedCount} events before resume.`
|
|
187
|
+
});
|
|
188
|
+
snapshotEventId = snapshotEventId ?? `evt-replay-${now().toString(36)}`;
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
ok: true,
|
|
192
|
+
subscription: snapshotForRecord(record),
|
|
193
|
+
replayedCount: replayed,
|
|
194
|
+
snapshotEventId
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
function unsubscribe(subscriptionId) {
|
|
198
|
+
const record = subscriptions.get(subscriptionId);
|
|
199
|
+
if (record === void 0) return false;
|
|
200
|
+
subscribers.get(record.subscriberId)?.subscriptions.delete(subscriptionId);
|
|
201
|
+
deindexSubject(record.subject, subscriptionId);
|
|
202
|
+
subscriptions.delete(subscriptionId);
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
function emit(subject, bare) {
|
|
206
|
+
const eventId = bare.eventId ?? nextEventId();
|
|
207
|
+
const bufferedFrame = {
|
|
208
|
+
v: "1",
|
|
209
|
+
kind: "event",
|
|
210
|
+
eventId,
|
|
211
|
+
subscriptionId: "__pending__",
|
|
212
|
+
subject,
|
|
213
|
+
type: bare.type,
|
|
214
|
+
payload: bare.payload
|
|
215
|
+
};
|
|
216
|
+
replayBuffer.push(subject, bufferedFrame);
|
|
217
|
+
const subjectSubscribers = subjectIndex.get(subject);
|
|
218
|
+
if (subjectSubscribers === void 0 || subjectSubscribers.size === 0) return;
|
|
219
|
+
for (const subscriptionId of subjectSubscribers) {
|
|
220
|
+
const record = subscriptions.get(subscriptionId);
|
|
221
|
+
if (record === void 0) continue;
|
|
222
|
+
const sanitized = sanitizer.sanitize({
|
|
223
|
+
...bufferedFrame,
|
|
224
|
+
subscriptionId
|
|
225
|
+
}, "ws");
|
|
226
|
+
validateAndDispatch(record, sanitized);
|
|
227
|
+
record.lastEventId = sanitized.eventId;
|
|
228
|
+
if (sanitized.eventId === eventId) continue;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
function emitLifecycle(subscriptionId, status, reason) {
|
|
232
|
+
const record = subscriptions.get(subscriptionId);
|
|
233
|
+
if (record === void 0) return;
|
|
234
|
+
validateAndDispatch(record, {
|
|
235
|
+
v: "1",
|
|
236
|
+
kind: "lifecycle",
|
|
237
|
+
subscriptionId,
|
|
238
|
+
status,
|
|
239
|
+
...reason !== void 0 ? { reason } : {}
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
function listForSubscriber(subscriberId) {
|
|
243
|
+
const subscriber = subscribers.get(subscriberId);
|
|
244
|
+
if (subscriber === void 0) return Object.freeze([]);
|
|
245
|
+
return Object.freeze([...subscriber.subscriptions].map((id) => subscriptions.get(id)).filter((entry) => entry !== void 0).map(snapshotForRecord));
|
|
246
|
+
}
|
|
247
|
+
function snapshotSubscription(subscriptionId) {
|
|
248
|
+
const record = subscriptions.get(subscriptionId);
|
|
249
|
+
if (record === void 0) return void 0;
|
|
250
|
+
return snapshotForRecord(record);
|
|
251
|
+
}
|
|
252
|
+
function size() {
|
|
253
|
+
return Object.freeze({
|
|
254
|
+
subscribers: subscribers.size,
|
|
255
|
+
subscriptions: subscriptions.size
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
function shutdown() {
|
|
259
|
+
for (const subscription of subscriptions.values()) {
|
|
260
|
+
const subscriber = subscribers.get(subscription.subscriberId);
|
|
261
|
+
try {
|
|
262
|
+
subscriber?.handle.send({
|
|
263
|
+
v: "1",
|
|
264
|
+
kind: "lifecycle",
|
|
265
|
+
subscriptionId: subscription.subscriptionId,
|
|
266
|
+
status: "aborted",
|
|
267
|
+
reason: "server-shutdown"
|
|
268
|
+
});
|
|
269
|
+
} catch {}
|
|
270
|
+
}
|
|
271
|
+
subscriptions.clear();
|
|
272
|
+
subjectIndex.clear();
|
|
273
|
+
subscribers.clear();
|
|
274
|
+
}
|
|
275
|
+
return Object.freeze({
|
|
276
|
+
sanitizer,
|
|
277
|
+
replayBuffer,
|
|
278
|
+
registerSubscriber,
|
|
279
|
+
subscribe,
|
|
280
|
+
unsubscribe,
|
|
281
|
+
emit,
|
|
282
|
+
emitLifecycle,
|
|
283
|
+
listForSubscriber,
|
|
284
|
+
snapshotSubscription,
|
|
285
|
+
size,
|
|
286
|
+
shutdown
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
function snapshotForRecord(record) {
|
|
290
|
+
return Object.freeze({
|
|
291
|
+
subscriptionId: record.subscriptionId,
|
|
292
|
+
subject: record.subject,
|
|
293
|
+
subjectKind: record.parsed.kind,
|
|
294
|
+
subscriberId: record.subscriberId,
|
|
295
|
+
tokenId: record.tokenId,
|
|
296
|
+
createdAt: record.createdAt,
|
|
297
|
+
lastEventId: record.lastEventId
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
//#endregion
|
|
302
|
+
export { createWsDispatcher };
|
|
303
|
+
//# sourceMappingURL=dispatcher.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dispatcher.js","names":["record: SubscriptionRecord","snapshotEventId: string | undefined","frame: ServerEventFrame","bufferedFrame: ServerEventFrame"],"sources":["../../src/ws/dispatcher.ts"],"sourcesContent":["/**\n * Per-server WebSocket dispatcher. Owns the in-memory subscription\n * map, fans out events to matching connections, applies the\n * delivery-layer commentary-phase trace sanitization, validates\n * outbound frames against the `@graphorin/protocol` schema, and\n * exposes a tiny `emit(subject, event)` surface route handlers\n * (and the agent / workflow runtimes) consume.\n *\n * @packageDocumentation\n */\n\nimport {\n isEventFrame,\n type ServerEventFrame,\n type ServerLifecycleFrame,\n type ServerMessage,\n ServerMessageSchema,\n} from '@graphorin/protocol';\nimport type { ParsedScope } from '@graphorin/security/auth';\nimport {\n createDeliveryCommentarySanitizer,\n type DeliveryCommentaryConfig,\n type DeliveryCommentarySanitizer,\n} from '../commentary/index.js';\nimport {\n createReplayBuffer,\n type ReplayBuffer,\n type ReplayBufferOptions,\n} from './replay-buffer.js';\nimport { isSubjectAllowed, type ParsedSubject, tryParseSubject } from './subjects.js';\n\n/**\n * Public configuration accepted by {@link createWsDispatcher}.\n *\n * @stable\n */\nexport interface WsDispatcherOptions {\n readonly commentary?: DeliveryCommentaryConfig;\n readonly replayBuffer?: ReplayBufferOptions;\n /**\n * Cap on outstanding events queued for an offline subscriber.\n * Defaults to the same value as the replay buffer per-subject cap.\n */\n readonly perConnectionQueueLimit?: number;\n readonly now?: () => number;\n /**\n * Logger sink for protocol violations + dropped frames. When\n * omitted, the dispatcher is silent (production wiring uses the\n * server's structured logger).\n */\n readonly onWarn?: (event: WsDispatcherWarning) => void;\n}\n\n/**\n * Discriminator surfaced to the optional warn sink.\n *\n * @stable\n */\nexport type WsDispatcherWarning =\n | {\n readonly kind: 'invalid-frame';\n readonly subscriptionId: string;\n readonly issues: ReadonlyArray<string>;\n }\n | { readonly kind: 'queue-overflow'; readonly subscriptionId: string }\n | { readonly kind: 'subject-rejected'; readonly subject: string; readonly reason: string };\n\n/**\n * Subscriber surface used by the dispatcher. Each WebSocket\n * connection wraps its `WSContext.send` in this interface so the\n * dispatcher does not depend on `@hono/node-ws` types directly.\n *\n * @stable\n */\nexport interface WsSubscriberHandle {\n readonly id: string;\n readonly tokenId: string;\n readonly grantedScopes: ReadonlyArray<ParsedScope>;\n /** Send a server frame; the dispatcher already validated it. */\n send(frame: ServerMessage): void;\n /** Close the underlying WebSocket with a Graphorin close code. */\n close(code: number, reason: string): void;\n /**\n * Optional buffered-byte sample. The dispatcher reads this on every\n * emit to detect sustained backpressure and close the connection\n * with the Graphorin `flow.throttled` code (4006) before the OS-\n * level send buffer collapses. Consumers backed by `@hono/node-ws`\n * can return the underlying `ws.bufferedAmount`. When the field is\n * not implemented, the dispatcher falls back to the per-connection\n * outstanding-event counter.\n */\n bufferedAmount?(): number;\n}\n\n/**\n * Snapshot of an active subscription. Surfaced via\n * {@link WsDispatcher.snapshotSubscription}.\n *\n * @stable\n */\nexport interface WsSubscriptionSnapshot {\n readonly subscriptionId: string;\n readonly subject: string;\n readonly subjectKind: ParsedSubject['kind'];\n readonly subscriberId: string;\n readonly tokenId: string;\n readonly createdAt: number;\n readonly lastEventId: string | undefined;\n}\n\n/**\n * Result of {@link WsDispatcher.subscribe}.\n *\n * @stable\n */\nexport type SubscribeResult =\n | {\n readonly ok: true;\n readonly subscription: WsSubscriptionSnapshot;\n readonly replayedCount: number;\n readonly snapshotEventId: string | undefined;\n }\n | {\n readonly ok: false;\n readonly reason:\n | 'subject-malformed'\n | 'subject-unknown'\n | 'subject-wildcard'\n | 'scope-denied';\n };\n\n/**\n * Public surface of the dispatcher.\n *\n * @stable\n */\nexport interface WsDispatcher {\n readonly sanitizer: DeliveryCommentarySanitizer;\n readonly replayBuffer: ReplayBuffer;\n /**\n * Register a subscriber (one per WebSocket connection).\n * `unregister` is called on close.\n */\n registerSubscriber(handle: WsSubscriberHandle): { unregister(): void };\n /**\n * Open a new subscription for an active subscriber. Returns either\n * the subscription snapshot + replayed-event count or a typed\n * failure reason the caller maps to the appropriate close code /\n * RPC error.\n */\n subscribe(input: {\n readonly subscriberId: string;\n readonly subject: string;\n readonly subscriptionId: string;\n readonly sinceEventId?: string;\n }): SubscribeResult;\n unsubscribe(subscriptionId: string): boolean;\n /**\n * Push an event into the dispatcher. Goes through the sanitizer,\n * validates against the protocol schema, persists into the replay\n * buffer, and fans out to every matching active subscription.\n */\n emit(subject: string, frame: BareEventFrame): void;\n /** Push a lifecycle frame to a single subscription. */\n emitLifecycle(\n subscriptionId: string,\n status: ServerLifecycleFrame['status'],\n reason?: string,\n ): void;\n /** List active subscriptions for a given subscriber (diagnostics). */\n listForSubscriber(subscriberId: string): ReadonlyArray<WsSubscriptionSnapshot>;\n snapshotSubscription(subscriptionId: string): WsSubscriptionSnapshot | undefined;\n size(): { readonly subscribers: number; readonly subscriptions: number };\n /** Clear in-memory state (used during graceful shutdown). */\n shutdown(): void;\n}\n\n/**\n * Frame argument accepted by {@link WsDispatcher.emit}. The\n * dispatcher fills in `subscriptionId`, `subject`, and `eventId`.\n *\n * @stable\n */\nexport interface BareEventFrame {\n readonly type: string;\n readonly payload: unknown;\n readonly eventId?: string;\n}\n\ninterface SubscriberRecord {\n readonly handle: WsSubscriberHandle;\n readonly subscriptions: Set<string>;\n outstandingEvents: number;\n closed: boolean;\n}\n\ninterface SubscriptionRecord {\n readonly subscriberId: string;\n /** IP-18: the authenticating principal's token id (not the connection id). */\n readonly tokenId: string;\n readonly subscriptionId: string;\n readonly subject: string;\n readonly parsed: ParsedSubject;\n readonly createdAt: number;\n lastEventId: string | undefined;\n}\n\n/**\n * Build a fresh dispatcher.\n *\n * @stable\n */\nexport function createWsDispatcher(options: WsDispatcherOptions = {}): WsDispatcher {\n const sanitizer = createDeliveryCommentarySanitizer(options.commentary);\n const replayBuffer = createReplayBuffer(options.replayBuffer ?? {});\n const now = options.now ?? Date.now;\n const onWarn = options.onWarn;\n const perConnectionQueueLimit = options.perConnectionQueueLimit ?? 1_000;\n const subscribers = new Map<string, SubscriberRecord>();\n const subscriptions = new Map<string, SubscriptionRecord>();\n const subjectIndex = new Map<string, Set<string>>();\n let eventCounter = 0;\n\n function indexSubject(subject: string, subscriptionId: string): void {\n let set = subjectIndex.get(subject);\n if (set === undefined) {\n set = new Set();\n subjectIndex.set(subject, set);\n }\n set.add(subscriptionId);\n }\n\n function deindexSubject(subject: string, subscriptionId: string): void {\n const set = subjectIndex.get(subject);\n if (set === undefined) return;\n set.delete(subscriptionId);\n if (set.size === 0) subjectIndex.delete(subject);\n }\n\n function nextEventId(): string {\n eventCounter += 1;\n return `evt-${now().toString(36)}-${eventCounter.toString(36)}`;\n }\n\n function dispatchToSubscriber(record: SubscriptionRecord, frame: ServerMessage): void {\n const subscriber = subscribers.get(record.subscriberId);\n if (subscriber === undefined || subscriber.closed) return;\n // Per-connection backpressure: track the outstanding-event counter\n // and consult the optional `bufferedAmount()` sample. The\n // dispatcher closes the connection with the Graphorin\n // `flow.throttled` code (4006) when either signal exceeds the\n // configured limit so the client can reconnect against the\n // replay buffer instead of accumulating an unbounded send queue.\n const buffered =\n typeof subscriber.handle.bufferedAmount === 'function'\n ? subscriber.handle.bufferedAmount()\n : 0;\n if (\n subscriber.outstandingEvents >= perConnectionQueueLimit ||\n buffered >= perConnectionQueueLimit * 1024\n ) {\n onWarn?.({ kind: 'queue-overflow', subscriptionId: record.subscriptionId });\n shutdownSubscriber(subscriber, 4006, 'flow.throttled');\n return;\n }\n subscriber.outstandingEvents += 1;\n try {\n subscriber.handle.send(frame);\n } catch (err) {\n onWarn?.({ kind: 'queue-overflow', subscriptionId: record.subscriptionId });\n shutdownSubscriber(subscriber, 4006, 'flow.throttled');\n void err;\n } finally {\n subscriber.outstandingEvents = Math.max(0, subscriber.outstandingEvents - 1);\n }\n }\n\n function shutdownSubscriber(subscriber: SubscriberRecord, code: number, reason: string): void {\n if (subscriber.closed) return;\n subscriber.closed = true;\n try {\n subscriber.handle.close(code, reason);\n } catch {\n // ignore — we are tearing the subscriber down.\n }\n for (const id of subscriber.subscriptions) {\n const sub = subscriptions.get(id);\n if (sub !== undefined) {\n deindexSubject(sub.subject, id);\n subscriptions.delete(id);\n }\n }\n subscriber.subscriptions.clear();\n subscribers.delete(subscriber.handle.id);\n }\n\n function validateAndDispatch(record: SubscriptionRecord, frame: ServerMessage): void {\n const validated = ServerMessageSchema.safeParse(frame);\n if (!validated.success) {\n onWarn?.({\n kind: 'invalid-frame',\n subscriptionId: record.subscriptionId,\n issues: validated.error.issues.map((i) => `${i.path.join('.') || '<root>'}: ${i.message}`),\n });\n return;\n }\n dispatchToSubscriber(record, validated.data);\n }\n\n function registerSubscriber(handle: WsSubscriberHandle): { unregister(): void } {\n subscribers.set(handle.id, {\n handle,\n subscriptions: new Set(),\n outstandingEvents: 0,\n closed: false,\n });\n return {\n unregister: () => {\n const record = subscribers.get(handle.id);\n if (record === undefined) return;\n for (const id of record.subscriptions) {\n const sub = subscriptions.get(id);\n if (sub !== undefined) {\n deindexSubject(sub.subject, id);\n subscriptions.delete(id);\n }\n }\n subscribers.delete(handle.id);\n },\n };\n }\n\n function subscribe(input: {\n readonly subscriberId: string;\n readonly subject: string;\n readonly subscriptionId: string;\n readonly sinceEventId?: string;\n }): SubscribeResult {\n const subscriber = subscribers.get(input.subscriberId);\n if (subscriber === undefined) {\n return { ok: false, reason: 'subject-unknown' };\n }\n const parseResult = tryParseSubject(input.subject);\n if (!parseResult.ok) {\n const reason = parseResult.reason;\n onWarn?.({ kind: 'subject-rejected', subject: input.subject, reason });\n if (reason === 'wildcard-not-supported') return { ok: false, reason: 'subject-wildcard' };\n if (reason === 'unknown-subject') return { ok: false, reason: 'subject-unknown' };\n return { ok: false, reason: 'subject-malformed' };\n }\n if (!isSubjectAllowed(subscriber.handle.grantedScopes, parseResult.subject)) {\n return { ok: false, reason: 'scope-denied' };\n }\n const record: SubscriptionRecord = {\n subscriberId: input.subscriberId,\n tokenId: subscriber.handle.tokenId,\n subscriptionId: input.subscriptionId,\n subject: input.subject,\n parsed: parseResult.subject,\n createdAt: now(),\n lastEventId: undefined,\n };\n subscriptions.set(input.subscriptionId, record);\n subscriber.subscriptions.add(input.subscriptionId);\n indexSubject(input.subject, input.subscriptionId);\n const replay = replayBuffer.replay(input.subject, input.sinceEventId);\n let replayed = 0;\n let snapshotEventId: string | undefined = replay.nextEventIdHint;\n for (const event of replay.events) {\n const frame: ServerEventFrame = {\n ...event,\n subscriptionId: input.subscriptionId,\n };\n validateAndDispatch(record, frame);\n record.lastEventId = frame.eventId;\n replayed += 1;\n }\n if (replay.droppedCount > 0) {\n validateAndDispatch(record, {\n v: '1',\n kind: 'replay-marker',\n subscriptionId: input.subscriptionId,\n eventId: snapshotEventId ?? `evt-replay-${now().toString(36)}`,\n droppedCount: replay.droppedCount,\n note: `Replay buffer dropped ${replay.droppedCount} events before resume.`,\n });\n snapshotEventId = snapshotEventId ?? `evt-replay-${now().toString(36)}`;\n }\n return {\n ok: true,\n subscription: snapshotForRecord(record),\n replayedCount: replayed,\n snapshotEventId,\n };\n }\n\n function unsubscribe(subscriptionId: string): boolean {\n const record = subscriptions.get(subscriptionId);\n if (record === undefined) return false;\n const subscriber = subscribers.get(record.subscriberId);\n subscriber?.subscriptions.delete(subscriptionId);\n deindexSubject(record.subject, subscriptionId);\n subscriptions.delete(subscriptionId);\n return true;\n }\n\n function emit(subject: string, bare: BareEventFrame): void {\n const eventId = bare.eventId ?? nextEventId();\n // Buffer first so the replay layer captures every event, even\n // when there is no live subscriber. The buffered frame is\n // sanitizer-neutral (the per-subscription dispatch sanitizes\n // again on the wire so the second pass is idempotent — by\n // construction matching wraps are not re-wrapped).\n const bufferedFrame: ServerEventFrame = {\n v: '1',\n kind: 'event',\n eventId,\n subscriptionId: '__pending__',\n subject,\n type: bare.type,\n payload: bare.payload,\n };\n replayBuffer.push(subject, bufferedFrame);\n const subjectSubscribers = subjectIndex.get(subject);\n if (subjectSubscribers === undefined || subjectSubscribers.size === 0) return;\n for (const subscriptionId of subjectSubscribers) {\n const record = subscriptions.get(subscriptionId);\n if (record === undefined) continue;\n const sanitized = sanitizer.sanitize({ ...bufferedFrame, subscriptionId }, 'ws');\n validateAndDispatch(record, sanitized);\n record.lastEventId = sanitized.eventId;\n if (sanitized.eventId === eventId) continue;\n }\n void isEventFrame; // keep import warm\n }\n\n function emitLifecycle(\n subscriptionId: string,\n status: ServerLifecycleFrame['status'],\n reason?: string,\n ): void {\n const record = subscriptions.get(subscriptionId);\n if (record === undefined) return;\n const frame: ServerLifecycleFrame = {\n v: '1',\n kind: 'lifecycle',\n subscriptionId,\n status,\n ...(reason !== undefined ? { reason } : {}),\n };\n validateAndDispatch(record, frame);\n }\n\n function listForSubscriber(subscriberId: string): ReadonlyArray<WsSubscriptionSnapshot> {\n const subscriber = subscribers.get(subscriberId);\n if (subscriber === undefined) return Object.freeze([]);\n return Object.freeze(\n [...subscriber.subscriptions]\n .map((id) => subscriptions.get(id))\n .filter((entry): entry is SubscriptionRecord => entry !== undefined)\n .map(snapshotForRecord),\n );\n }\n\n function snapshotSubscription(subscriptionId: string): WsSubscriptionSnapshot | undefined {\n const record = subscriptions.get(subscriptionId);\n if (record === undefined) return undefined;\n return snapshotForRecord(record);\n }\n\n function size(): { readonly subscribers: number; readonly subscriptions: number } {\n return Object.freeze({\n subscribers: subscribers.size,\n subscriptions: subscriptions.size,\n });\n }\n\n function shutdown(): void {\n for (const subscription of subscriptions.values()) {\n const subscriber = subscribers.get(subscription.subscriberId);\n try {\n subscriber?.handle.send({\n v: '1',\n kind: 'lifecycle',\n subscriptionId: subscription.subscriptionId,\n status: 'aborted',\n reason: 'server-shutdown',\n });\n } catch {\n // swallow — we are tearing down.\n }\n }\n subscriptions.clear();\n subjectIndex.clear();\n subscribers.clear();\n }\n\n return Object.freeze({\n sanitizer,\n replayBuffer,\n registerSubscriber,\n subscribe,\n unsubscribe,\n emit,\n emitLifecycle,\n listForSubscriber,\n snapshotSubscription,\n size,\n shutdown,\n });\n}\n\nfunction snapshotForRecord(record: SubscriptionRecord): WsSubscriptionSnapshot {\n return Object.freeze({\n subscriptionId: record.subscriptionId,\n subject: record.subject,\n subjectKind: record.parsed.kind,\n subscriberId: record.subscriberId,\n tokenId: record.tokenId,\n createdAt: record.createdAt,\n lastEventId: record.lastEventId,\n });\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { ReplayBuffer, ReplayBufferOptions, ReplayBufferSlice, createReplayBuffer } from "./replay-buffer.js";
|
|
2
|
+
import { ParseSubjectResult, ParsedSubject, isSubjectAllowed, requiredScopeFor, tryParseSubject } from "./subjects.js";
|
|
3
|
+
import { BareEventFrame, SubscribeResult, WsDispatcher, WsDispatcherOptions, WsDispatcherWarning, WsSubscriberHandle, WsSubscriptionSnapshot, createWsDispatcher } from "./dispatcher.js";
|
|
4
|
+
import { WsTicket, WsTicketConsumeResult, WsTicketStore, WsTicketStoreOptions, createWsTicketStore } from "./ticket.js";
|
|
5
|
+
import { WsUpgradeOptions, createWsUpgradeEvents } from "./upgrade.js";
|
|
6
|
+
export { type BareEventFrame, type ParseSubjectResult, type ParsedSubject, type ReplayBuffer, type ReplayBufferOptions, type ReplayBufferSlice, type SubscribeResult, type WsDispatcher, type WsDispatcherOptions, type WsDispatcherWarning, type WsSubscriberHandle, type WsSubscriptionSnapshot, type WsTicket, type WsTicketConsumeResult, type WsTicketStore, type WsTicketStoreOptions, type WsUpgradeOptions, createReplayBuffer, createWsDispatcher, createWsTicketStore, createWsUpgradeEvents, isSubjectAllowed, requiredScopeFor, tryParseSubject };
|
package/dist/ws/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { createReplayBuffer } from "./replay-buffer.js";
|
|
2
|
+
import { isSubjectAllowed, requiredScopeFor, tryParseSubject } from "./subjects.js";
|
|
3
|
+
import { createWsDispatcher } from "./dispatcher.js";
|
|
4
|
+
import { createWsTicketStore } from "./ticket.js";
|
|
5
|
+
import { createWsUpgradeEvents } from "./upgrade.js";
|
|
6
|
+
|
|
7
|
+
export { createReplayBuffer, createWsDispatcher, createWsTicketStore, createWsUpgradeEvents, isSubjectAllowed, requiredScopeFor, tryParseSubject };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { ServerEventFrame } from "@graphorin/protocol";
|
|
2
|
+
|
|
3
|
+
//#region src/ws/replay-buffer.d.ts
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Public configuration accepted by {@link createReplayBuffer}.
|
|
7
|
+
*
|
|
8
|
+
* @stable
|
|
9
|
+
*/
|
|
10
|
+
interface ReplayBufferOptions {
|
|
11
|
+
readonly maxEvents?: number;
|
|
12
|
+
readonly ttlMs?: number;
|
|
13
|
+
readonly now?: () => number;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Snapshot returned by {@link ReplayBuffer.replay}.
|
|
17
|
+
*
|
|
18
|
+
* @stable
|
|
19
|
+
*/
|
|
20
|
+
interface ReplayBufferSlice {
|
|
21
|
+
readonly events: ReadonlyArray<ServerEventFrame>;
|
|
22
|
+
readonly droppedCount: number;
|
|
23
|
+
readonly nextEventIdHint: string | undefined;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Per-subject replay buffer. Stores up to `maxEvents` per subject
|
|
27
|
+
* with a TTL; thread-safe under the single-writer Node event loop
|
|
28
|
+
* model.
|
|
29
|
+
*
|
|
30
|
+
* @stable
|
|
31
|
+
*/
|
|
32
|
+
interface ReplayBuffer {
|
|
33
|
+
push(subject: string, event: ServerEventFrame): void;
|
|
34
|
+
replay(subject: string, sinceEventId: string | undefined): ReplayBufferSlice;
|
|
35
|
+
size(subject: string): number;
|
|
36
|
+
forget(subject: string): void;
|
|
37
|
+
prune(): void;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Build an in-memory replay buffer.
|
|
41
|
+
*
|
|
42
|
+
* @stable
|
|
43
|
+
*/
|
|
44
|
+
declare function createReplayBuffer(options?: ReplayBufferOptions): ReplayBuffer;
|
|
45
|
+
//#endregion
|
|
46
|
+
export { ReplayBuffer, ReplayBufferOptions, ReplayBufferSlice, createReplayBuffer };
|
|
47
|
+
//# sourceMappingURL=replay-buffer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"replay-buffer.d.ts","names":[],"sources":["../../src/ws/replay-buffer.ts"],"sourcesContent":[],"mappings":";;;;;;;;;UAoBiB,mBAAA;;;;;;;;;;UAWA,iBAAA;mBACE,cAAc;;;;;;;;;;;UAYhB,YAAA;+BACc;6DAC8B;;;;;;;;;;iBAgB7C,kBAAA,WAA4B,sBAA2B"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
//#region src/ws/replay-buffer.ts
|
|
2
|
+
/**
|
|
3
|
+
* Build an in-memory replay buffer.
|
|
4
|
+
*
|
|
5
|
+
* @stable
|
|
6
|
+
*/
|
|
7
|
+
function createReplayBuffer(options = {}) {
|
|
8
|
+
const maxEvents = options.maxEvents ?? 1e3;
|
|
9
|
+
const ttlMs = options.ttlMs ?? 5 * 6e4;
|
|
10
|
+
const now = options.now ?? Date.now;
|
|
11
|
+
const buffers = /* @__PURE__ */ new Map();
|
|
12
|
+
const dropped = /* @__PURE__ */ new Map();
|
|
13
|
+
function pruneSubject(subject) {
|
|
14
|
+
const list = buffers.get(subject);
|
|
15
|
+
if (list === void 0 || list.length === 0) return;
|
|
16
|
+
const cutoff = now() - ttlMs;
|
|
17
|
+
let cutoffIndex = 0;
|
|
18
|
+
while (cutoffIndex < list.length) {
|
|
19
|
+
const entry = list[cutoffIndex];
|
|
20
|
+
if (entry === void 0 || entry.bufferedAt >= cutoff) break;
|
|
21
|
+
cutoffIndex += 1;
|
|
22
|
+
}
|
|
23
|
+
if (cutoffIndex > 0) {
|
|
24
|
+
list.splice(0, cutoffIndex);
|
|
25
|
+
dropped.set(subject, (dropped.get(subject) ?? 0) + cutoffIndex);
|
|
26
|
+
}
|
|
27
|
+
if (list.length === 0) buffers.delete(subject);
|
|
28
|
+
}
|
|
29
|
+
function push(subject, event) {
|
|
30
|
+
pruneSubject(subject);
|
|
31
|
+
let list = buffers.get(subject);
|
|
32
|
+
if (list === void 0) {
|
|
33
|
+
list = [];
|
|
34
|
+
buffers.set(subject, list);
|
|
35
|
+
}
|
|
36
|
+
list.push({
|
|
37
|
+
event,
|
|
38
|
+
bufferedAt: now()
|
|
39
|
+
});
|
|
40
|
+
if (list.length > maxEvents) {
|
|
41
|
+
const overflow = list.length - maxEvents;
|
|
42
|
+
list.splice(0, overflow);
|
|
43
|
+
dropped.set(subject, (dropped.get(subject) ?? 0) + overflow);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function replay(subject, sinceEventId) {
|
|
47
|
+
pruneSubject(subject);
|
|
48
|
+
const list = buffers.get(subject) ?? [];
|
|
49
|
+
const droppedCount = dropped.get(subject) ?? 0;
|
|
50
|
+
const last = list.at(-1);
|
|
51
|
+
if (sinceEventId === void 0) return {
|
|
52
|
+
events: list.map((entry) => entry.event),
|
|
53
|
+
droppedCount,
|
|
54
|
+
nextEventIdHint: last?.event.eventId
|
|
55
|
+
};
|
|
56
|
+
const startIndex = list.findIndex((entry) => entry.event.eventId === sinceEventId);
|
|
57
|
+
if (startIndex < 0) return {
|
|
58
|
+
events: list.map((entry) => entry.event),
|
|
59
|
+
droppedCount,
|
|
60
|
+
nextEventIdHint: last?.event.eventId
|
|
61
|
+
};
|
|
62
|
+
const replayed = list.slice(startIndex + 1).map((entry) => entry.event);
|
|
63
|
+
return {
|
|
64
|
+
events: replayed,
|
|
65
|
+
droppedCount: 0,
|
|
66
|
+
nextEventIdHint: replayed.at(-1)?.eventId
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
return Object.freeze({
|
|
70
|
+
push,
|
|
71
|
+
replay,
|
|
72
|
+
size(subject) {
|
|
73
|
+
pruneSubject(subject);
|
|
74
|
+
return buffers.get(subject)?.length ?? 0;
|
|
75
|
+
},
|
|
76
|
+
forget(subject) {
|
|
77
|
+
buffers.delete(subject);
|
|
78
|
+
dropped.delete(subject);
|
|
79
|
+
},
|
|
80
|
+
prune() {
|
|
81
|
+
for (const subject of [...buffers.keys()]) pruneSubject(subject);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
//#endregion
|
|
87
|
+
export { createReplayBuffer };
|
|
88
|
+
//# sourceMappingURL=replay-buffer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"replay-buffer.js","names":[],"sources":["../../src/ws/replay-buffer.ts"],"sourcesContent":["/**\n * Per-subscription replay buffer used by the dispatcher to support\n * short-disconnect resume. When a client reconnects with a\n * `lastEventId`, the dispatcher walks the buffer from the matching\n * cursor and re-emits every event the client missed.\n *\n * Defaults (configurable per-server):\n * - `maxEvents`: 1000 (per subscription)\n * - `ttlMs`: 5 minutes\n *\n * @packageDocumentation\n */\n\nimport type { ServerEventFrame } from '@graphorin/protocol';\n\n/**\n * Public configuration accepted by {@link createReplayBuffer}.\n *\n * @stable\n */\nexport interface ReplayBufferOptions {\n readonly maxEvents?: number;\n readonly ttlMs?: number;\n readonly now?: () => number;\n}\n\n/**\n * Snapshot returned by {@link ReplayBuffer.replay}.\n *\n * @stable\n */\nexport interface ReplayBufferSlice {\n readonly events: ReadonlyArray<ServerEventFrame>;\n readonly droppedCount: number;\n readonly nextEventIdHint: string | undefined;\n}\n\n/**\n * Per-subject replay buffer. Stores up to `maxEvents` per subject\n * with a TTL; thread-safe under the single-writer Node event loop\n * model.\n *\n * @stable\n */\nexport interface ReplayBuffer {\n push(subject: string, event: ServerEventFrame): void;\n replay(subject: string, sinceEventId: string | undefined): ReplayBufferSlice;\n size(subject: string): number;\n forget(subject: string): void;\n prune(): void;\n}\n\ninterface BufferedEvent {\n readonly event: ServerEventFrame;\n readonly bufferedAt: number;\n}\n\n/**\n * Build an in-memory replay buffer.\n *\n * @stable\n */\nexport function createReplayBuffer(options: ReplayBufferOptions = {}): ReplayBuffer {\n const maxEvents = options.maxEvents ?? 1_000;\n const ttlMs = options.ttlMs ?? 5 * 60_000;\n const now = options.now ?? Date.now;\n const buffers = new Map<string, BufferedEvent[]>();\n const dropped = new Map<string, number>();\n\n function pruneSubject(subject: string): void {\n const list = buffers.get(subject);\n if (list === undefined || list.length === 0) return;\n const cutoff = now() - ttlMs;\n let cutoffIndex = 0;\n while (cutoffIndex < list.length) {\n const entry = list[cutoffIndex];\n if (entry === undefined || entry.bufferedAt >= cutoff) break;\n cutoffIndex += 1;\n }\n if (cutoffIndex > 0) {\n list.splice(0, cutoffIndex);\n dropped.set(subject, (dropped.get(subject) ?? 0) + cutoffIndex);\n }\n if (list.length === 0) buffers.delete(subject);\n }\n\n function push(subject: string, event: ServerEventFrame): void {\n pruneSubject(subject);\n let list = buffers.get(subject);\n if (list === undefined) {\n list = [];\n buffers.set(subject, list);\n }\n list.push({ event, bufferedAt: now() });\n if (list.length > maxEvents) {\n const overflow = list.length - maxEvents;\n list.splice(0, overflow);\n dropped.set(subject, (dropped.get(subject) ?? 0) + overflow);\n }\n }\n\n function replay(subject: string, sinceEventId: string | undefined): ReplayBufferSlice {\n pruneSubject(subject);\n const list = buffers.get(subject) ?? [];\n const droppedCount = dropped.get(subject) ?? 0;\n const last = list.at(-1);\n if (sinceEventId === undefined) {\n const slice: ReplayBufferSlice = {\n events: list.map((entry) => entry.event),\n droppedCount,\n nextEventIdHint: last?.event.eventId,\n };\n return slice;\n }\n const startIndex = list.findIndex((entry) => entry.event.eventId === sinceEventId);\n if (startIndex < 0) {\n // Cursor predates the buffer; return everything we still have +\n // signal the gap via `droppedCount`.\n return {\n events: list.map((entry) => entry.event),\n droppedCount,\n nextEventIdHint: last?.event.eventId,\n };\n }\n const replayed = list.slice(startIndex + 1).map((entry) => entry.event);\n return {\n events: replayed,\n droppedCount: 0,\n nextEventIdHint: replayed.at(-1)?.eventId,\n };\n }\n\n return Object.freeze({\n push,\n replay,\n size(subject: string): number {\n pruneSubject(subject);\n return buffers.get(subject)?.length ?? 0;\n },\n forget(subject: string): void {\n buffers.delete(subject);\n dropped.delete(subject);\n },\n prune(): void {\n for (const subject of [...buffers.keys()]) pruneSubject(subject);\n },\n });\n}\n"],"mappings":";;;;;;AA8DA,SAAgB,mBAAmB,UAA+B,EAAE,EAAgB;CAClF,MAAM,YAAY,QAAQ,aAAa;CACvC,MAAM,QAAQ,QAAQ,SAAS,IAAI;CACnC,MAAM,MAAM,QAAQ,OAAO,KAAK;CAChC,MAAM,0BAAU,IAAI,KAA8B;CAClD,MAAM,0BAAU,IAAI,KAAqB;CAEzC,SAAS,aAAa,SAAuB;EAC3C,MAAM,OAAO,QAAQ,IAAI,QAAQ;AACjC,MAAI,SAAS,UAAa,KAAK,WAAW,EAAG;EAC7C,MAAM,SAAS,KAAK,GAAG;EACvB,IAAI,cAAc;AAClB,SAAO,cAAc,KAAK,QAAQ;GAChC,MAAM,QAAQ,KAAK;AACnB,OAAI,UAAU,UAAa,MAAM,cAAc,OAAQ;AACvD,kBAAe;;AAEjB,MAAI,cAAc,GAAG;AACnB,QAAK,OAAO,GAAG,YAAY;AAC3B,WAAQ,IAAI,UAAU,QAAQ,IAAI,QAAQ,IAAI,KAAK,YAAY;;AAEjE,MAAI,KAAK,WAAW,EAAG,SAAQ,OAAO,QAAQ;;CAGhD,SAAS,KAAK,SAAiB,OAA+B;AAC5D,eAAa,QAAQ;EACrB,IAAI,OAAO,QAAQ,IAAI,QAAQ;AAC/B,MAAI,SAAS,QAAW;AACtB,UAAO,EAAE;AACT,WAAQ,IAAI,SAAS,KAAK;;AAE5B,OAAK,KAAK;GAAE;GAAO,YAAY,KAAK;GAAE,CAAC;AACvC,MAAI,KAAK,SAAS,WAAW;GAC3B,MAAM,WAAW,KAAK,SAAS;AAC/B,QAAK,OAAO,GAAG,SAAS;AACxB,WAAQ,IAAI,UAAU,QAAQ,IAAI,QAAQ,IAAI,KAAK,SAAS;;;CAIhE,SAAS,OAAO,SAAiB,cAAqD;AACpF,eAAa,QAAQ;EACrB,MAAM,OAAO,QAAQ,IAAI,QAAQ,IAAI,EAAE;EACvC,MAAM,eAAe,QAAQ,IAAI,QAAQ,IAAI;EAC7C,MAAM,OAAO,KAAK,GAAG,GAAG;AACxB,MAAI,iBAAiB,OAMnB,QALiC;GAC/B,QAAQ,KAAK,KAAK,UAAU,MAAM,MAAM;GACxC;GACA,iBAAiB,MAAM,MAAM;GAC9B;EAGH,MAAM,aAAa,KAAK,WAAW,UAAU,MAAM,MAAM,YAAY,aAAa;AAClF,MAAI,aAAa,EAGf,QAAO;GACL,QAAQ,KAAK,KAAK,UAAU,MAAM,MAAM;GACxC;GACA,iBAAiB,MAAM,MAAM;GAC9B;EAEH,MAAM,WAAW,KAAK,MAAM,aAAa,EAAE,CAAC,KAAK,UAAU,MAAM,MAAM;AACvE,SAAO;GACL,QAAQ;GACR,cAAc;GACd,iBAAiB,SAAS,GAAG,GAAG,EAAE;GACnC;;AAGH,QAAO,OAAO,OAAO;EACnB;EACA;EACA,KAAK,SAAyB;AAC5B,gBAAa,QAAQ;AACrB,UAAO,QAAQ,IAAI,QAAQ,EAAE,UAAU;;EAEzC,OAAO,SAAuB;AAC5B,WAAQ,OAAO,QAAQ;AACvB,WAAQ,OAAO,QAAQ;;EAEzB,QAAc;AACZ,QAAK,MAAM,WAAW,CAAC,GAAG,QAAQ,MAAM,CAAC,CAAE,cAAa,QAAQ;;EAEnE,CAAC"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { ParsedScope } from "@graphorin/security/auth";
|
|
2
|
+
|
|
3
|
+
//#region src/ws/subjects.d.ts
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Discriminated union of every recognised subject form. Surfaced on
|
|
7
|
+
* audit log entries + diagnostics; the wire still carries the raw
|
|
8
|
+
* string.
|
|
9
|
+
*
|
|
10
|
+
* @stable
|
|
11
|
+
*/
|
|
12
|
+
type ParsedSubject = {
|
|
13
|
+
readonly kind: 'session-events';
|
|
14
|
+
readonly sessionId: string;
|
|
15
|
+
} | {
|
|
16
|
+
readonly kind: 'session-run-events';
|
|
17
|
+
readonly sessionId: string;
|
|
18
|
+
readonly runId: string;
|
|
19
|
+
} | {
|
|
20
|
+
readonly kind: 'agent-run-events';
|
|
21
|
+
readonly agentId: string;
|
|
22
|
+
readonly runId: string;
|
|
23
|
+
} | {
|
|
24
|
+
readonly kind: 'workflow-events';
|
|
25
|
+
readonly workflowId: string;
|
|
26
|
+
} | {
|
|
27
|
+
readonly kind: 'workflow-run-events';
|
|
28
|
+
readonly workflowId: string;
|
|
29
|
+
readonly runId: string;
|
|
30
|
+
} | {
|
|
31
|
+
readonly kind: 'memory-conflicts';
|
|
32
|
+
} | {
|
|
33
|
+
readonly kind: 'audit-events';
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Result of {@link tryParseSubject}.
|
|
37
|
+
*
|
|
38
|
+
* @stable
|
|
39
|
+
*/
|
|
40
|
+
type ParseSubjectResult = {
|
|
41
|
+
readonly ok: true;
|
|
42
|
+
readonly subject: ParsedSubject;
|
|
43
|
+
} | {
|
|
44
|
+
readonly ok: false;
|
|
45
|
+
readonly reason: 'wildcard-not-supported' | 'unknown-subject' | 'malformed';
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Parse a subject string into the {@link ParsedSubject} discriminator.
|
|
49
|
+
*
|
|
50
|
+
* @stable
|
|
51
|
+
*/
|
|
52
|
+
declare function tryParseSubject(raw: string): ParseSubjectResult;
|
|
53
|
+
/**
|
|
54
|
+
* Required scope literal for every subject kind, expressed as a
|
|
55
|
+
* `ParsedScope`. The matcher `scopeMatches(granted, required)` uses
|
|
56
|
+
* the standard wildcard rules from `@graphorin/security/auth`
|
|
57
|
+
* (e.g. `agents:*` matches `agents:invoke:foo`).
|
|
58
|
+
*
|
|
59
|
+
* @stable
|
|
60
|
+
*/
|
|
61
|
+
declare function requiredScopeFor(subject: ParsedSubject): ParsedScope;
|
|
62
|
+
/**
|
|
63
|
+
* Compatibility shim — re-exports `scopeMatches` so consumers don't
|
|
64
|
+
* have to learn the security package's surface.
|
|
65
|
+
*
|
|
66
|
+
* @stable
|
|
67
|
+
*/
|
|
68
|
+
declare function isSubjectAllowed(granted: ReadonlyArray<ParsedScope>, subject: ParsedSubject): boolean;
|
|
69
|
+
//#endregion
|
|
70
|
+
export { ParseSubjectResult, ParsedSubject, isSubjectAllowed, requiredScopeFor, tryParseSubject };
|
|
71
|
+
//# sourceMappingURL=subjects.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"subjects.d.ts","names":[],"sources":["../../src/ws/subjects.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;KAwBY,aAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;KA0BA,kBAAA;;oBAC+B;;;;;;;;;;iBAiB3B,eAAA,eAA8B;;;;;;;;;iBAqD9B,gBAAA,UAA0B,gBAAgB;;;;;;;iBAyB1C,gBAAA,UACL,cAAc,uBACd"}
|