@warmdrift/kgauto-compiler 2.0.0-alpha.16 → 2.0.0-alpha.18
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/chunk-NUTC7NUC.mjs +298 -0
- package/dist/chunk-VZGMWKRT.mjs +19 -0
- package/dist/glassbox/index.d.mts +42 -0
- package/dist/glassbox/index.d.ts +42 -0
- package/dist/glassbox/index.js +300 -0
- package/dist/glassbox/index.mjs +10 -0
- package/dist/glassbox-routes/index.d.mts +73 -0
- package/dist/glassbox-routes/index.d.ts +73 -0
- package/dist/glassbox-routes/index.js +560 -0
- package/dist/glassbox-routes/index.mjs +269 -0
- package/dist/index.d.mts +4 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +357 -2
- package/dist/index.mjs +77 -2
- package/dist/{profiles-BoLYdl7F.d.mts → ir-C3P4gDt0.d.mts} +13 -134
- package/dist/{profiles-CVB2_5C8.d.ts → ir-CFHU3BUT.d.ts} +13 -134
- package/dist/profiles.d.mts +137 -2
- package/dist/profiles.d.ts +137 -2
- package/dist/types-DWF6mPGg.d.mts +122 -0
- package/dist/types-xeklorHU.d.ts +122 -0
- package/package.json +12 -2
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
// src/glassbox/types.ts
|
|
2
|
+
var GLASSBOX_STREAM_TTL_MS = 6e4;
|
|
3
|
+
|
|
4
|
+
// src/glassbox/pubsub-memory.ts
|
|
5
|
+
var MemoryPubSub = class {
|
|
6
|
+
subscribers = /* @__PURE__ */ new Map();
|
|
7
|
+
async publish(traceId, event) {
|
|
8
|
+
const subs = this.subscribers.get(traceId);
|
|
9
|
+
if (!subs || subs.size === 0) return;
|
|
10
|
+
for (const sub of subs) {
|
|
11
|
+
if (sub.closed) continue;
|
|
12
|
+
try {
|
|
13
|
+
sub.controller.enqueue(event);
|
|
14
|
+
} catch {
|
|
15
|
+
sub.closed = true;
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
this.refreshTtl(traceId, sub);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
subscribe(traceId) {
|
|
22
|
+
const self = this;
|
|
23
|
+
let sub;
|
|
24
|
+
return new ReadableStream({
|
|
25
|
+
start(controller) {
|
|
26
|
+
sub = {
|
|
27
|
+
controller,
|
|
28
|
+
ttlTimer: setTimeout(() => {
|
|
29
|
+
self.closeSubscriber(traceId, sub);
|
|
30
|
+
}, GLASSBOX_STREAM_TTL_MS),
|
|
31
|
+
closed: false
|
|
32
|
+
};
|
|
33
|
+
let set = self.subscribers.get(traceId);
|
|
34
|
+
if (!set) {
|
|
35
|
+
set = /* @__PURE__ */ new Set();
|
|
36
|
+
self.subscribers.set(traceId, set);
|
|
37
|
+
}
|
|
38
|
+
set.add(sub);
|
|
39
|
+
},
|
|
40
|
+
cancel() {
|
|
41
|
+
if (sub) self.removeSubscriber(traceId, sub);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Refresh the rolling TTL for a subscriber after an event lands. Replaces
|
|
47
|
+
* the existing timer with a fresh 60s one.
|
|
48
|
+
*/
|
|
49
|
+
refreshTtl(traceId, sub) {
|
|
50
|
+
clearTimeout(sub.ttlTimer);
|
|
51
|
+
sub.ttlTimer = setTimeout(() => {
|
|
52
|
+
this.closeSubscriber(traceId, sub);
|
|
53
|
+
}, GLASSBOX_STREAM_TTL_MS);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Close the subscriber's stream cleanly and remove from the fan-out set.
|
|
57
|
+
* Idempotent — safe to call multiple times.
|
|
58
|
+
*/
|
|
59
|
+
closeSubscriber(traceId, sub) {
|
|
60
|
+
if (sub.closed) return;
|
|
61
|
+
sub.closed = true;
|
|
62
|
+
clearTimeout(sub.ttlTimer);
|
|
63
|
+
try {
|
|
64
|
+
sub.controller.close();
|
|
65
|
+
} catch {
|
|
66
|
+
}
|
|
67
|
+
this.removeSubscriber(traceId, sub);
|
|
68
|
+
}
|
|
69
|
+
removeSubscriber(traceId, sub) {
|
|
70
|
+
clearTimeout(sub.ttlTimer);
|
|
71
|
+
const set = this.subscribers.get(traceId);
|
|
72
|
+
if (!set) return;
|
|
73
|
+
set.delete(sub);
|
|
74
|
+
if (set.size === 0) this.subscribers.delete(traceId);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Test-only reset. Tears down all subscribers, clears all state. Calling
|
|
78
|
+
* outside of tests is harmless but cancels every active stream.
|
|
79
|
+
*/
|
|
80
|
+
_reset() {
|
|
81
|
+
for (const [, set] of this.subscribers) {
|
|
82
|
+
for (const sub of set) {
|
|
83
|
+
this.closeSubscriber("", sub);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
this.subscribers.clear();
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// src/glassbox/pubsub-upstash.ts
|
|
91
|
+
var UpstashPubSub = class {
|
|
92
|
+
url;
|
|
93
|
+
token;
|
|
94
|
+
fetchImpl;
|
|
95
|
+
blockMs;
|
|
96
|
+
maxLen;
|
|
97
|
+
constructor(cfg) {
|
|
98
|
+
this.url = cfg.url.replace(/\/$/, "");
|
|
99
|
+
this.token = cfg.token;
|
|
100
|
+
this.fetchImpl = cfg.fetchImpl ?? globalThis.fetch.bind(globalThis);
|
|
101
|
+
this.blockMs = cfg.blockMs ?? 100;
|
|
102
|
+
this.maxLen = cfg.maxLen ?? 100;
|
|
103
|
+
}
|
|
104
|
+
async publish(traceId, event) {
|
|
105
|
+
const key = streamKey(traceId);
|
|
106
|
+
const payload = JSON.stringify(event);
|
|
107
|
+
await this.cmd([
|
|
108
|
+
"XADD",
|
|
109
|
+
key,
|
|
110
|
+
"MAXLEN",
|
|
111
|
+
"~",
|
|
112
|
+
String(this.maxLen),
|
|
113
|
+
"*",
|
|
114
|
+
"event",
|
|
115
|
+
payload
|
|
116
|
+
]);
|
|
117
|
+
await this.cmd(["EXPIRE", key, String(Math.ceil(GLASSBOX_STREAM_TTL_MS / 1e3))]);
|
|
118
|
+
}
|
|
119
|
+
subscribe(traceId) {
|
|
120
|
+
const key = streamKey(traceId);
|
|
121
|
+
const self = this;
|
|
122
|
+
let cursor = "$";
|
|
123
|
+
let cancelled = false;
|
|
124
|
+
let ttlDeadline = Date.now() + GLASSBOX_STREAM_TTL_MS;
|
|
125
|
+
return new ReadableStream({
|
|
126
|
+
async start(controller) {
|
|
127
|
+
try {
|
|
128
|
+
while (!cancelled && Date.now() < ttlDeadline) {
|
|
129
|
+
const resp = await self.cmd([
|
|
130
|
+
"XREAD",
|
|
131
|
+
"BLOCK",
|
|
132
|
+
String(self.blockMs),
|
|
133
|
+
"STREAMS",
|
|
134
|
+
key,
|
|
135
|
+
cursor
|
|
136
|
+
]);
|
|
137
|
+
if (cancelled) break;
|
|
138
|
+
const parsed = parseXReadResult(resp.result);
|
|
139
|
+
if (parsed.entries.length === 0) {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
for (const entry of parsed.entries) {
|
|
143
|
+
const evt = decodeEvent(entry.fields);
|
|
144
|
+
if (evt) {
|
|
145
|
+
try {
|
|
146
|
+
controller.enqueue(evt);
|
|
147
|
+
} catch {
|
|
148
|
+
cancelled = true;
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
cursor = entry.id;
|
|
153
|
+
}
|
|
154
|
+
ttlDeadline = Date.now() + GLASSBOX_STREAM_TTL_MS;
|
|
155
|
+
}
|
|
156
|
+
} catch (err) {
|
|
157
|
+
if (!cancelled) {
|
|
158
|
+
try {
|
|
159
|
+
controller.error(err);
|
|
160
|
+
} catch {
|
|
161
|
+
}
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
controller.close();
|
|
167
|
+
} catch {
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
cancel() {
|
|
171
|
+
cancelled = true;
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
async cmd(args) {
|
|
176
|
+
const res = await this.fetchImpl(this.url, {
|
|
177
|
+
method: "POST",
|
|
178
|
+
headers: {
|
|
179
|
+
Authorization: `Bearer ${this.token}`,
|
|
180
|
+
"Content-Type": "application/json"
|
|
181
|
+
},
|
|
182
|
+
body: JSON.stringify(args)
|
|
183
|
+
});
|
|
184
|
+
if (!res.ok) {
|
|
185
|
+
throw new Error(`Upstash ${args[0]} failed: HTTP ${res.status}`);
|
|
186
|
+
}
|
|
187
|
+
const json = await res.json();
|
|
188
|
+
if (json.error) {
|
|
189
|
+
throw new Error(`Upstash ${args[0]} failed: ${json.error}`);
|
|
190
|
+
}
|
|
191
|
+
return json;
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
function streamKey(traceId) {
|
|
195
|
+
return `glassbox:trace:${traceId}`;
|
|
196
|
+
}
|
|
197
|
+
function decodeEvent(fields) {
|
|
198
|
+
const raw = fields["event"];
|
|
199
|
+
if (!raw) return void 0;
|
|
200
|
+
try {
|
|
201
|
+
const parsed = JSON.parse(raw);
|
|
202
|
+
if (typeof parsed.kind === "string" && typeof parsed.at === "number") {
|
|
203
|
+
return parsed;
|
|
204
|
+
}
|
|
205
|
+
return void 0;
|
|
206
|
+
} catch {
|
|
207
|
+
return void 0;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
function parseXReadResult(raw) {
|
|
211
|
+
if (!Array.isArray(raw)) return { entries: [] };
|
|
212
|
+
const entries = [];
|
|
213
|
+
for (const stream of raw) {
|
|
214
|
+
if (!Array.isArray(stream) || stream.length < 2) continue;
|
|
215
|
+
const streamEntries = stream[1];
|
|
216
|
+
if (!Array.isArray(streamEntries)) continue;
|
|
217
|
+
for (const entry of streamEntries) {
|
|
218
|
+
if (!Array.isArray(entry) || entry.length < 2) continue;
|
|
219
|
+
const id = String(entry[0]);
|
|
220
|
+
const flat = entry[1];
|
|
221
|
+
if (!Array.isArray(flat)) continue;
|
|
222
|
+
const fields = {};
|
|
223
|
+
for (let i = 0; i < flat.length; i += 2) {
|
|
224
|
+
const k = flat[i];
|
|
225
|
+
const v = flat[i + 1];
|
|
226
|
+
if (typeof k === "string") fields[k] = String(v ?? "");
|
|
227
|
+
}
|
|
228
|
+
entries.push({ id, fields });
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return { entries };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// src/glassbox/emit.ts
|
|
235
|
+
var activePubSub;
|
|
236
|
+
function getPubSub() {
|
|
237
|
+
if (activePubSub) return activePubSub;
|
|
238
|
+
const url = readEnv("UPSTASH_REDIS_URL");
|
|
239
|
+
const token = readEnv("UPSTASH_REDIS_TOKEN");
|
|
240
|
+
if (url && token) {
|
|
241
|
+
activePubSub = new UpstashPubSub({ url, token });
|
|
242
|
+
} else {
|
|
243
|
+
activePubSub = new MemoryPubSub();
|
|
244
|
+
}
|
|
245
|
+
return activePubSub;
|
|
246
|
+
}
|
|
247
|
+
function readEnv(key) {
|
|
248
|
+
try {
|
|
249
|
+
if (typeof process !== "undefined" && process.env) {
|
|
250
|
+
const v = process.env[key];
|
|
251
|
+
return v && v.trim() !== "" ? v : void 0;
|
|
252
|
+
}
|
|
253
|
+
} catch {
|
|
254
|
+
}
|
|
255
|
+
return void 0;
|
|
256
|
+
}
|
|
257
|
+
function emitGlassboxEvent(traceId, kind, data) {
|
|
258
|
+
if (!traceId) return;
|
|
259
|
+
const event = { kind, at: Date.now(), data };
|
|
260
|
+
const ps = getPubSub();
|
|
261
|
+
try {
|
|
262
|
+
const p = ps.publish(traceId, event);
|
|
263
|
+
if (p && typeof p.then === "function") {
|
|
264
|
+
p.catch(() => {
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
} catch {
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
function emitCompileStart(traceId, data) {
|
|
271
|
+
emitGlassboxEvent(traceId, "compile.start", data);
|
|
272
|
+
}
|
|
273
|
+
function emitCompileDone(traceId, data) {
|
|
274
|
+
emitGlassboxEvent(traceId, "compile.done", data);
|
|
275
|
+
}
|
|
276
|
+
function emitExecuteAttempt(traceId, data) {
|
|
277
|
+
emitGlassboxEvent(traceId, "execute.attempt", data);
|
|
278
|
+
}
|
|
279
|
+
function emitExecuteSuccess(traceId, data) {
|
|
280
|
+
emitGlassboxEvent(traceId, "execute.success", data);
|
|
281
|
+
}
|
|
282
|
+
function emitAdvisoryFired(traceId, data) {
|
|
283
|
+
emitGlassboxEvent(traceId, "advisory.fired", data);
|
|
284
|
+
}
|
|
285
|
+
function emitFallbackWalked(traceId, data) {
|
|
286
|
+
emitGlassboxEvent(traceId, "fallback.walked", data);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export {
|
|
290
|
+
GLASSBOX_STREAM_TTL_MS,
|
|
291
|
+
getPubSub,
|
|
292
|
+
emitCompileStart,
|
|
293
|
+
emitCompileDone,
|
|
294
|
+
emitExecuteAttempt,
|
|
295
|
+
emitExecuteSuccess,
|
|
296
|
+
emitAdvisoryFired,
|
|
297
|
+
emitFallbackWalked
|
|
298
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getPubSub
|
|
3
|
+
} from "./chunk-NUTC7NUC.mjs";
|
|
4
|
+
|
|
5
|
+
// src/glassbox/subscribe.ts
|
|
6
|
+
function subscribe(traceId) {
|
|
7
|
+
if (!traceId) {
|
|
8
|
+
return new ReadableStream({
|
|
9
|
+
start(controller) {
|
|
10
|
+
controller.close();
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
return getPubSub().subscribe(traceId);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export {
|
|
18
|
+
subscribe
|
|
19
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { G as GlassboxEvent } from '../types-DWF6mPGg.mjs';
|
|
2
|
+
export { A as AdvisoryFiredData, C as CompileDoneData, a as CompileStartData, E as ExecuteAttemptData, b as ExecuteSuccessData, F as FallbackWalkedData, c as GLASSBOX_STREAM_TTL_MS, d as GlassboxEventKind, e as GlassboxPubSub } from '../types-DWF6mPGg.mjs';
|
|
3
|
+
import '../ir-C3P4gDt0.mjs';
|
|
4
|
+
import '../dialect.mjs';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* subscribe(traceId) — public Glass-Box subscription export.
|
|
8
|
+
*
|
|
9
|
+
* Consumer route handlers (e.g. Vercel Edge `/api/glassbox/stream`) import
|
|
10
|
+
* this and bridge the ReadableStream to SSE. The browser-side panel then
|
|
11
|
+
* renders events in real time:
|
|
12
|
+
*
|
|
13
|
+
* import { subscribe } from '@warmdrift/kgauto-compiler/glassbox';
|
|
14
|
+
*
|
|
15
|
+
* export async function GET(req: Request) {
|
|
16
|
+
* const { searchParams } = new URL(req.url);
|
|
17
|
+
* const traceId = searchParams.get('traceId');
|
|
18
|
+
* if (!traceId) return new Response('missing traceId', { status: 400 });
|
|
19
|
+
* const stream = subscribe(traceId);
|
|
20
|
+
* // ... bridge ReadableStream<GlassboxEvent> → SSE here ...
|
|
21
|
+
* }
|
|
22
|
+
*
|
|
23
|
+
* Stream behavior:
|
|
24
|
+
* - Emits events for `traceId` as they're published.
|
|
25
|
+
* - Closes cleanly 60s after the last event (rolling TTL).
|
|
26
|
+
* - If no events arrive within 60s of subscription, closes empty.
|
|
27
|
+
* - Multiple subscribers on the same traceId all fan out.
|
|
28
|
+
*
|
|
29
|
+
* No replay: subscribe() picks up only events published AFTER subscription.
|
|
30
|
+
* Replay is the brain-poll surface's job (see design doc).
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Subscribe to Glass-Box events for a traceId. Returns a ReadableStream
|
|
35
|
+
* that yields GlassboxEvent objects until the per-trace TTL elapses.
|
|
36
|
+
*
|
|
37
|
+
* Cancelling the stream (consumer disconnect, AbortController, etc.) tears
|
|
38
|
+
* down the subscription cleanly via the underlying adapter.
|
|
39
|
+
*/
|
|
40
|
+
declare function subscribe(traceId: string): ReadableStream<GlassboxEvent>;
|
|
41
|
+
|
|
42
|
+
export { GlassboxEvent, subscribe };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { G as GlassboxEvent } from '../types-xeklorHU.js';
|
|
2
|
+
export { A as AdvisoryFiredData, C as CompileDoneData, a as CompileStartData, E as ExecuteAttemptData, b as ExecuteSuccessData, F as FallbackWalkedData, c as GLASSBOX_STREAM_TTL_MS, d as GlassboxEventKind, e as GlassboxPubSub } from '../types-xeklorHU.js';
|
|
3
|
+
import '../ir-CFHU3BUT.js';
|
|
4
|
+
import '../dialect.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* subscribe(traceId) — public Glass-Box subscription export.
|
|
8
|
+
*
|
|
9
|
+
* Consumer route handlers (e.g. Vercel Edge `/api/glassbox/stream`) import
|
|
10
|
+
* this and bridge the ReadableStream to SSE. The browser-side panel then
|
|
11
|
+
* renders events in real time:
|
|
12
|
+
*
|
|
13
|
+
* import { subscribe } from '@warmdrift/kgauto-compiler/glassbox';
|
|
14
|
+
*
|
|
15
|
+
* export async function GET(req: Request) {
|
|
16
|
+
* const { searchParams } = new URL(req.url);
|
|
17
|
+
* const traceId = searchParams.get('traceId');
|
|
18
|
+
* if (!traceId) return new Response('missing traceId', { status: 400 });
|
|
19
|
+
* const stream = subscribe(traceId);
|
|
20
|
+
* // ... bridge ReadableStream<GlassboxEvent> → SSE here ...
|
|
21
|
+
* }
|
|
22
|
+
*
|
|
23
|
+
* Stream behavior:
|
|
24
|
+
* - Emits events for `traceId` as they're published.
|
|
25
|
+
* - Closes cleanly 60s after the last event (rolling TTL).
|
|
26
|
+
* - If no events arrive within 60s of subscription, closes empty.
|
|
27
|
+
* - Multiple subscribers on the same traceId all fan out.
|
|
28
|
+
*
|
|
29
|
+
* No replay: subscribe() picks up only events published AFTER subscription.
|
|
30
|
+
* Replay is the brain-poll surface's job (see design doc).
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Subscribe to Glass-Box events for a traceId. Returns a ReadableStream
|
|
35
|
+
* that yields GlassboxEvent objects until the per-trace TTL elapses.
|
|
36
|
+
*
|
|
37
|
+
* Cancelling the stream (consumer disconnect, AbortController, etc.) tears
|
|
38
|
+
* down the subscription cleanly via the underlying adapter.
|
|
39
|
+
*/
|
|
40
|
+
declare function subscribe(traceId: string): ReadableStream<GlassboxEvent>;
|
|
41
|
+
|
|
42
|
+
export { GlassboxEvent, subscribe };
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/glassbox/index.ts
|
|
21
|
+
var glassbox_exports = {};
|
|
22
|
+
__export(glassbox_exports, {
|
|
23
|
+
GLASSBOX_STREAM_TTL_MS: () => GLASSBOX_STREAM_TTL_MS,
|
|
24
|
+
subscribe: () => subscribe
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(glassbox_exports);
|
|
27
|
+
|
|
28
|
+
// src/glassbox/types.ts
|
|
29
|
+
var GLASSBOX_STREAM_TTL_MS = 6e4;
|
|
30
|
+
|
|
31
|
+
// src/glassbox/pubsub-memory.ts
|
|
32
|
+
var MemoryPubSub = class {
|
|
33
|
+
subscribers = /* @__PURE__ */ new Map();
|
|
34
|
+
async publish(traceId, event) {
|
|
35
|
+
const subs = this.subscribers.get(traceId);
|
|
36
|
+
if (!subs || subs.size === 0) return;
|
|
37
|
+
for (const sub of subs) {
|
|
38
|
+
if (sub.closed) continue;
|
|
39
|
+
try {
|
|
40
|
+
sub.controller.enqueue(event);
|
|
41
|
+
} catch {
|
|
42
|
+
sub.closed = true;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
this.refreshTtl(traceId, sub);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
subscribe(traceId) {
|
|
49
|
+
const self = this;
|
|
50
|
+
let sub;
|
|
51
|
+
return new ReadableStream({
|
|
52
|
+
start(controller) {
|
|
53
|
+
sub = {
|
|
54
|
+
controller,
|
|
55
|
+
ttlTimer: setTimeout(() => {
|
|
56
|
+
self.closeSubscriber(traceId, sub);
|
|
57
|
+
}, GLASSBOX_STREAM_TTL_MS),
|
|
58
|
+
closed: false
|
|
59
|
+
};
|
|
60
|
+
let set = self.subscribers.get(traceId);
|
|
61
|
+
if (!set) {
|
|
62
|
+
set = /* @__PURE__ */ new Set();
|
|
63
|
+
self.subscribers.set(traceId, set);
|
|
64
|
+
}
|
|
65
|
+
set.add(sub);
|
|
66
|
+
},
|
|
67
|
+
cancel() {
|
|
68
|
+
if (sub) self.removeSubscriber(traceId, sub);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Refresh the rolling TTL for a subscriber after an event lands. Replaces
|
|
74
|
+
* the existing timer with a fresh 60s one.
|
|
75
|
+
*/
|
|
76
|
+
refreshTtl(traceId, sub) {
|
|
77
|
+
clearTimeout(sub.ttlTimer);
|
|
78
|
+
sub.ttlTimer = setTimeout(() => {
|
|
79
|
+
this.closeSubscriber(traceId, sub);
|
|
80
|
+
}, GLASSBOX_STREAM_TTL_MS);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Close the subscriber's stream cleanly and remove from the fan-out set.
|
|
84
|
+
* Idempotent — safe to call multiple times.
|
|
85
|
+
*/
|
|
86
|
+
closeSubscriber(traceId, sub) {
|
|
87
|
+
if (sub.closed) return;
|
|
88
|
+
sub.closed = true;
|
|
89
|
+
clearTimeout(sub.ttlTimer);
|
|
90
|
+
try {
|
|
91
|
+
sub.controller.close();
|
|
92
|
+
} catch {
|
|
93
|
+
}
|
|
94
|
+
this.removeSubscriber(traceId, sub);
|
|
95
|
+
}
|
|
96
|
+
removeSubscriber(traceId, sub) {
|
|
97
|
+
clearTimeout(sub.ttlTimer);
|
|
98
|
+
const set = this.subscribers.get(traceId);
|
|
99
|
+
if (!set) return;
|
|
100
|
+
set.delete(sub);
|
|
101
|
+
if (set.size === 0) this.subscribers.delete(traceId);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Test-only reset. Tears down all subscribers, clears all state. Calling
|
|
105
|
+
* outside of tests is harmless but cancels every active stream.
|
|
106
|
+
*/
|
|
107
|
+
_reset() {
|
|
108
|
+
for (const [, set] of this.subscribers) {
|
|
109
|
+
for (const sub of set) {
|
|
110
|
+
this.closeSubscriber("", sub);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
this.subscribers.clear();
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// src/glassbox/pubsub-upstash.ts
|
|
118
|
+
var UpstashPubSub = class {
|
|
119
|
+
url;
|
|
120
|
+
token;
|
|
121
|
+
fetchImpl;
|
|
122
|
+
blockMs;
|
|
123
|
+
maxLen;
|
|
124
|
+
constructor(cfg) {
|
|
125
|
+
this.url = cfg.url.replace(/\/$/, "");
|
|
126
|
+
this.token = cfg.token;
|
|
127
|
+
this.fetchImpl = cfg.fetchImpl ?? globalThis.fetch.bind(globalThis);
|
|
128
|
+
this.blockMs = cfg.blockMs ?? 100;
|
|
129
|
+
this.maxLen = cfg.maxLen ?? 100;
|
|
130
|
+
}
|
|
131
|
+
async publish(traceId, event) {
|
|
132
|
+
const key = streamKey(traceId);
|
|
133
|
+
const payload = JSON.stringify(event);
|
|
134
|
+
await this.cmd([
|
|
135
|
+
"XADD",
|
|
136
|
+
key,
|
|
137
|
+
"MAXLEN",
|
|
138
|
+
"~",
|
|
139
|
+
String(this.maxLen),
|
|
140
|
+
"*",
|
|
141
|
+
"event",
|
|
142
|
+
payload
|
|
143
|
+
]);
|
|
144
|
+
await this.cmd(["EXPIRE", key, String(Math.ceil(GLASSBOX_STREAM_TTL_MS / 1e3))]);
|
|
145
|
+
}
|
|
146
|
+
subscribe(traceId) {
|
|
147
|
+
const key = streamKey(traceId);
|
|
148
|
+
const self = this;
|
|
149
|
+
let cursor = "$";
|
|
150
|
+
let cancelled = false;
|
|
151
|
+
let ttlDeadline = Date.now() + GLASSBOX_STREAM_TTL_MS;
|
|
152
|
+
return new ReadableStream({
|
|
153
|
+
async start(controller) {
|
|
154
|
+
try {
|
|
155
|
+
while (!cancelled && Date.now() < ttlDeadline) {
|
|
156
|
+
const resp = await self.cmd([
|
|
157
|
+
"XREAD",
|
|
158
|
+
"BLOCK",
|
|
159
|
+
String(self.blockMs),
|
|
160
|
+
"STREAMS",
|
|
161
|
+
key,
|
|
162
|
+
cursor
|
|
163
|
+
]);
|
|
164
|
+
if (cancelled) break;
|
|
165
|
+
const parsed = parseXReadResult(resp.result);
|
|
166
|
+
if (parsed.entries.length === 0) {
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
for (const entry of parsed.entries) {
|
|
170
|
+
const evt = decodeEvent(entry.fields);
|
|
171
|
+
if (evt) {
|
|
172
|
+
try {
|
|
173
|
+
controller.enqueue(evt);
|
|
174
|
+
} catch {
|
|
175
|
+
cancelled = true;
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
cursor = entry.id;
|
|
180
|
+
}
|
|
181
|
+
ttlDeadline = Date.now() + GLASSBOX_STREAM_TTL_MS;
|
|
182
|
+
}
|
|
183
|
+
} catch (err) {
|
|
184
|
+
if (!cancelled) {
|
|
185
|
+
try {
|
|
186
|
+
controller.error(err);
|
|
187
|
+
} catch {
|
|
188
|
+
}
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
try {
|
|
193
|
+
controller.close();
|
|
194
|
+
} catch {
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
cancel() {
|
|
198
|
+
cancelled = true;
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
async cmd(args) {
|
|
203
|
+
const res = await this.fetchImpl(this.url, {
|
|
204
|
+
method: "POST",
|
|
205
|
+
headers: {
|
|
206
|
+
Authorization: `Bearer ${this.token}`,
|
|
207
|
+
"Content-Type": "application/json"
|
|
208
|
+
},
|
|
209
|
+
body: JSON.stringify(args)
|
|
210
|
+
});
|
|
211
|
+
if (!res.ok) {
|
|
212
|
+
throw new Error(`Upstash ${args[0]} failed: HTTP ${res.status}`);
|
|
213
|
+
}
|
|
214
|
+
const json = await res.json();
|
|
215
|
+
if (json.error) {
|
|
216
|
+
throw new Error(`Upstash ${args[0]} failed: ${json.error}`);
|
|
217
|
+
}
|
|
218
|
+
return json;
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
function streamKey(traceId) {
|
|
222
|
+
return `glassbox:trace:${traceId}`;
|
|
223
|
+
}
|
|
224
|
+
function decodeEvent(fields) {
|
|
225
|
+
const raw = fields["event"];
|
|
226
|
+
if (!raw) return void 0;
|
|
227
|
+
try {
|
|
228
|
+
const parsed = JSON.parse(raw);
|
|
229
|
+
if (typeof parsed.kind === "string" && typeof parsed.at === "number") {
|
|
230
|
+
return parsed;
|
|
231
|
+
}
|
|
232
|
+
return void 0;
|
|
233
|
+
} catch {
|
|
234
|
+
return void 0;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
function parseXReadResult(raw) {
|
|
238
|
+
if (!Array.isArray(raw)) return { entries: [] };
|
|
239
|
+
const entries = [];
|
|
240
|
+
for (const stream of raw) {
|
|
241
|
+
if (!Array.isArray(stream) || stream.length < 2) continue;
|
|
242
|
+
const streamEntries = stream[1];
|
|
243
|
+
if (!Array.isArray(streamEntries)) continue;
|
|
244
|
+
for (const entry of streamEntries) {
|
|
245
|
+
if (!Array.isArray(entry) || entry.length < 2) continue;
|
|
246
|
+
const id = String(entry[0]);
|
|
247
|
+
const flat = entry[1];
|
|
248
|
+
if (!Array.isArray(flat)) continue;
|
|
249
|
+
const fields = {};
|
|
250
|
+
for (let i = 0; i < flat.length; i += 2) {
|
|
251
|
+
const k = flat[i];
|
|
252
|
+
const v = flat[i + 1];
|
|
253
|
+
if (typeof k === "string") fields[k] = String(v ?? "");
|
|
254
|
+
}
|
|
255
|
+
entries.push({ id, fields });
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return { entries };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// src/glassbox/emit.ts
|
|
262
|
+
var activePubSub;
|
|
263
|
+
function getPubSub() {
|
|
264
|
+
if (activePubSub) return activePubSub;
|
|
265
|
+
const url = readEnv("UPSTASH_REDIS_URL");
|
|
266
|
+
const token = readEnv("UPSTASH_REDIS_TOKEN");
|
|
267
|
+
if (url && token) {
|
|
268
|
+
activePubSub = new UpstashPubSub({ url, token });
|
|
269
|
+
} else {
|
|
270
|
+
activePubSub = new MemoryPubSub();
|
|
271
|
+
}
|
|
272
|
+
return activePubSub;
|
|
273
|
+
}
|
|
274
|
+
function readEnv(key) {
|
|
275
|
+
try {
|
|
276
|
+
if (typeof process !== "undefined" && process.env) {
|
|
277
|
+
const v = process.env[key];
|
|
278
|
+
return v && v.trim() !== "" ? v : void 0;
|
|
279
|
+
}
|
|
280
|
+
} catch {
|
|
281
|
+
}
|
|
282
|
+
return void 0;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// src/glassbox/subscribe.ts
|
|
286
|
+
function subscribe(traceId) {
|
|
287
|
+
if (!traceId) {
|
|
288
|
+
return new ReadableStream({
|
|
289
|
+
start(controller) {
|
|
290
|
+
controller.close();
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
return getPubSub().subscribe(traceId);
|
|
295
|
+
}
|
|
296
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
297
|
+
0 && (module.exports = {
|
|
298
|
+
GLASSBOX_STREAM_TTL_MS,
|
|
299
|
+
subscribe
|
|
300
|
+
});
|