@dreb/dashboard 2.45.2 → 2.45.3
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/README.md +39 -13
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -1
- package/dist/index.js.map +1 -1
- package/dist/server/dashboard-images.d.ts +56 -0
- package/dist/server/dashboard-images.d.ts.map +1 -0
- package/dist/server/dashboard-images.js +317 -0
- package/dist/server/dashboard-images.js.map +1 -0
- package/dist/server/event-hub.d.ts +3 -0
- package/dist/server/event-hub.d.ts.map +1 -1
- package/dist/server/event-hub.js +7 -1
- package/dist/server/event-hub.js.map +1 -1
- package/dist/server/image-preview-worker.d.ts +7 -0
- package/dist/server/image-preview-worker.d.ts.map +1 -0
- package/dist/server/image-preview-worker.js +150 -0
- package/dist/server/image-preview-worker.js.map +1 -0
- package/dist/server/image-preview.d.ts +29 -0
- package/dist/server/image-preview.d.ts.map +1 -0
- package/dist/server/image-preview.js +76 -0
- package/dist/server/image-preview.js.map +1 -0
- package/dist/server/server.d.ts +8 -1
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +85 -4
- package/dist/server/server.js.map +1 -1
- package/dist/shared/protocol.d.ts +16 -0
- package/dist/shared/protocol.d.ts.map +1 -1
- package/dist/shared/protocol.js +1 -0
- package/dist/shared/protocol.js.map +1 -1
- package/dist/static/assets/index-BcGevXt-.css +1 -0
- package/dist/static/assets/index-d9zGbh25.js +79 -0
- package/dist/static/index.html +2 -2
- package/dist/static/sw.js +1 -1
- package/package.json +2 -1
- package/dist/static/assets/index-BJcyaSXg.js +0 -79
- package/dist/static/assets/index-DdcUWCjw.css +0 -1
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { MAX_PREVIEW_BYTES, MAX_PREVIEW_HEIGHT, MAX_PREVIEW_WIDTH, } from "./image-preview.js";
|
|
3
|
+
export const DASHBOARD_IMAGE_CACHE_BYTES = 64 * 1024 * 1024;
|
|
4
|
+
export const DASHBOARD_IMAGE_CACHE_RECORDS = 2000;
|
|
5
|
+
export const DASHBOARD_IMAGE_ID_PATTERN = /^[0-9a-f]{64}$/;
|
|
6
|
+
export const DASHBOARD_IMAGE_MIME_TYPES = new Set([
|
|
7
|
+
"image/png",
|
|
8
|
+
"image/jpeg",
|
|
9
|
+
"image/gif",
|
|
10
|
+
"image/webp",
|
|
11
|
+
]);
|
|
12
|
+
const DROP = Symbol("drop-dashboard-image");
|
|
13
|
+
const JPEG_EOI = Buffer.from([0xff, 0xd9]);
|
|
14
|
+
function scopeKey(scope) {
|
|
15
|
+
return `${scope.runtimeKey}\0${scope.agentId ?? ""}`;
|
|
16
|
+
}
|
|
17
|
+
function isRecord(value) {
|
|
18
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
19
|
+
}
|
|
20
|
+
function hasRasterSignature(mimeType, bytes) {
|
|
21
|
+
const buffer = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
22
|
+
switch (mimeType) {
|
|
23
|
+
case "image/png": {
|
|
24
|
+
const signature = bytes.length >= 24 &&
|
|
25
|
+
[0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a].every((byte, i) => bytes[i] === byte);
|
|
26
|
+
return (signature &&
|
|
27
|
+
buffer.subarray(12, 16).toString("ascii") === "IHDR" &&
|
|
28
|
+
buffer.readUInt32BE(16) > 0 &&
|
|
29
|
+
buffer.readUInt32BE(20) > 0 &&
|
|
30
|
+
buffer.indexOf(Buffer.from("IEND"), 24) >= 0);
|
|
31
|
+
}
|
|
32
|
+
case "image/jpeg": {
|
|
33
|
+
if (bytes.length < 4 || bytes[0] !== 0xff || bytes[1] !== 0xd8 || bytes[2] !== 0xff)
|
|
34
|
+
return false;
|
|
35
|
+
// EOI does not have to be the final byte. Phone JPEGs commonly append
|
|
36
|
+
// auxiliary gain-map, motion-photo, or vendor metadata after the primary
|
|
37
|
+
// image. Browsers and Photon decode the JPEG through EOI and preserve the
|
|
38
|
+
// trailing bytes when the exact original is requested.
|
|
39
|
+
return buffer.lastIndexOf(JPEG_EOI) >= 2;
|
|
40
|
+
}
|
|
41
|
+
case "image/gif": {
|
|
42
|
+
const header = buffer.subarray(0, 6).toString("ascii");
|
|
43
|
+
return (bytes.length >= 14 &&
|
|
44
|
+
(header === "GIF87a" || header === "GIF89a") &&
|
|
45
|
+
buffer.readUInt16LE(6) > 0 &&
|
|
46
|
+
buffer.readUInt16LE(8) > 0 &&
|
|
47
|
+
bytes[bytes.length - 1] === 0x3b);
|
|
48
|
+
}
|
|
49
|
+
case "image/webp": {
|
|
50
|
+
if (bytes.length < 20 ||
|
|
51
|
+
buffer.subarray(0, 4).toString("ascii") !== "RIFF" ||
|
|
52
|
+
buffer.subarray(8, 12).toString("ascii") !== "WEBP") {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
const chunk = buffer.subarray(12, 16).toString("ascii");
|
|
56
|
+
return (buffer.readUInt32LE(4) + 8 <= bytes.length && (chunk === "VP8 " || chunk === "VP8L" || chunk === "VP8X"));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/** Strict standard base64 decode: whitespace, data URLs, and non-canonical padding are rejected. */
|
|
61
|
+
export function decodeDashboardImage(value, mimeType) {
|
|
62
|
+
if (typeof value !== "string" || typeof mimeType !== "string")
|
|
63
|
+
return undefined;
|
|
64
|
+
if (!DASHBOARD_IMAGE_MIME_TYPES.has(mimeType))
|
|
65
|
+
return undefined;
|
|
66
|
+
if (value.length === 0 || value.length % 4 !== 0 || !/^[A-Za-z0-9+/]+={0,2}$/.test(value))
|
|
67
|
+
return undefined;
|
|
68
|
+
const bytes = Buffer.from(value, "base64");
|
|
69
|
+
if (bytes.toString("base64") !== value)
|
|
70
|
+
return undefined;
|
|
71
|
+
const exactMimeType = mimeType;
|
|
72
|
+
if (!hasRasterSignature(exactMimeType, bytes))
|
|
73
|
+
return undefined;
|
|
74
|
+
return { bytes, mimeType: exactMimeType };
|
|
75
|
+
}
|
|
76
|
+
export function dashboardImageId(mimeType, bytes) {
|
|
77
|
+
return createHash("sha256").update(mimeType).update(Uint8Array.of(0)).update(bytes).digest("hex");
|
|
78
|
+
}
|
|
79
|
+
export function isDashboardImageReference(value) {
|
|
80
|
+
if (!isRecord(value))
|
|
81
|
+
return false;
|
|
82
|
+
return (value.type === "image_reference" &&
|
|
83
|
+
typeof value.id === "string" &&
|
|
84
|
+
DASHBOARD_IMAGE_ID_PATTERN.test(value.id) &&
|
|
85
|
+
typeof value.mimeType === "string" &&
|
|
86
|
+
DASHBOARD_IMAGE_MIME_TYPES.has(value.mimeType) &&
|
|
87
|
+
typeof value.size === "number" &&
|
|
88
|
+
Number.isSafeInteger(value.size) &&
|
|
89
|
+
value.size > 0);
|
|
90
|
+
}
|
|
91
|
+
export class DashboardImageNotFoundError extends Error {
|
|
92
|
+
}
|
|
93
|
+
export class DashboardImagePreviewError extends Error {
|
|
94
|
+
}
|
|
95
|
+
/** Synchronous projection + bounded original/preview repository. */
|
|
96
|
+
export class DashboardImageService {
|
|
97
|
+
previews;
|
|
98
|
+
images = new Map();
|
|
99
|
+
previewFlights = new Map();
|
|
100
|
+
maxBytes;
|
|
101
|
+
maxRecords;
|
|
102
|
+
usedBytes = 0;
|
|
103
|
+
usedRecords = 0;
|
|
104
|
+
clock = 0;
|
|
105
|
+
constructor(previews, options = {}) {
|
|
106
|
+
this.previews = previews;
|
|
107
|
+
this.maxBytes = options.maxBytes ?? DASHBOARD_IMAGE_CACHE_BYTES;
|
|
108
|
+
this.maxRecords = options.maxRecords ?? DASHBOARD_IMAGE_CACHE_RECORDS;
|
|
109
|
+
}
|
|
110
|
+
get byteSize() {
|
|
111
|
+
return this.usedBytes;
|
|
112
|
+
}
|
|
113
|
+
get recordCount() {
|
|
114
|
+
return this.usedRecords;
|
|
115
|
+
}
|
|
116
|
+
/** Project a browser-facing event without mutating the authoritative source. */
|
|
117
|
+
projectEvent(event, scope) {
|
|
118
|
+
return this.projectNode(event, scope);
|
|
119
|
+
}
|
|
120
|
+
/** Project messages/snapshots before JSON serialization. */
|
|
121
|
+
project(value, scope) {
|
|
122
|
+
const projected = this.projectNode(value, scope);
|
|
123
|
+
return (projected === DROP ? undefined : projected);
|
|
124
|
+
}
|
|
125
|
+
async original(scope, id, loadAuthoritative) {
|
|
126
|
+
const cached = this.cachedVariant(scope, id, "original");
|
|
127
|
+
if (cached)
|
|
128
|
+
return cached;
|
|
129
|
+
return this.recoverOriginal(scope, id, loadAuthoritative);
|
|
130
|
+
}
|
|
131
|
+
async preview(scope, id, loadAuthoritative) {
|
|
132
|
+
const cached = this.cachedVariant(scope, id, "preview");
|
|
133
|
+
if (cached)
|
|
134
|
+
return cached;
|
|
135
|
+
// Authorize/recover this scope before joining a content-global flight. A
|
|
136
|
+
// guessed ID from another runtime must not piggyback its in-flight preview.
|
|
137
|
+
const original = await this.original(scope, id, loadAuthoritative);
|
|
138
|
+
const recoveredPreview = this.cachedVariant(scope, id, "preview");
|
|
139
|
+
if (recoveredPreview)
|
|
140
|
+
return recoveredPreview;
|
|
141
|
+
const flight = this.previewFlights.get(id);
|
|
142
|
+
if (flight)
|
|
143
|
+
return flight;
|
|
144
|
+
const promise = (async () => {
|
|
145
|
+
let generated;
|
|
146
|
+
try {
|
|
147
|
+
generated = await this.previews.generate(original.bytes, original.mimeType);
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
throw new DashboardImagePreviewError(`Preview generation failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
151
|
+
}
|
|
152
|
+
if (generated.bytes.byteLength > MAX_PREVIEW_BYTES ||
|
|
153
|
+
generated.width < 1 ||
|
|
154
|
+
generated.height < 1 ||
|
|
155
|
+
generated.width > MAX_PREVIEW_WIDTH ||
|
|
156
|
+
generated.height > MAX_PREVIEW_HEIGHT ||
|
|
157
|
+
(generated.mimeType !== "image/png" && generated.mimeType !== "image/jpeg")) {
|
|
158
|
+
throw new DashboardImagePreviewError("Preview worker returned an invalid or over-budget image");
|
|
159
|
+
}
|
|
160
|
+
const binary = { bytes: generated.bytes, mimeType: generated.mimeType };
|
|
161
|
+
this.putVariant(id, scope, "preview", binary);
|
|
162
|
+
return binary;
|
|
163
|
+
})();
|
|
164
|
+
this.previewFlights.set(id, promise);
|
|
165
|
+
try {
|
|
166
|
+
return await promise;
|
|
167
|
+
}
|
|
168
|
+
finally {
|
|
169
|
+
this.previewFlights.delete(id);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
removeRuntime(runtimeKey) {
|
|
173
|
+
for (const image of [...this.images.values()]) {
|
|
174
|
+
for (const key of image.scopes) {
|
|
175
|
+
if (key === runtimeKey || key.startsWith(`${runtimeKey}\0`))
|
|
176
|
+
image.scopes.delete(key);
|
|
177
|
+
}
|
|
178
|
+
if (image.scopes.size === 0)
|
|
179
|
+
this.deleteImage(image);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
close() {
|
|
183
|
+
return this.previews.close();
|
|
184
|
+
}
|
|
185
|
+
projectNode(value, scope) {
|
|
186
|
+
if (Array.isArray(value)) {
|
|
187
|
+
const projected = [];
|
|
188
|
+
for (const item of value) {
|
|
189
|
+
const next = this.projectNode(item, scope);
|
|
190
|
+
if (next !== DROP)
|
|
191
|
+
projected.push(next);
|
|
192
|
+
}
|
|
193
|
+
return projected;
|
|
194
|
+
}
|
|
195
|
+
if (!isRecord(value))
|
|
196
|
+
return value;
|
|
197
|
+
if (value.type === "image") {
|
|
198
|
+
const image = decodeDashboardImage(value.data, value.mimeType);
|
|
199
|
+
if (!image)
|
|
200
|
+
return DROP;
|
|
201
|
+
return this.insertOriginal(scope, image);
|
|
202
|
+
}
|
|
203
|
+
if (value.type === "image_reference")
|
|
204
|
+
return isDashboardImageReference(value) ? { ...value } : DROP;
|
|
205
|
+
const childScope = value.type === "background_agent_event" && typeof value.agentId === "string"
|
|
206
|
+
? { runtimeKey: scope.runtimeKey, agentId: value.agentId }
|
|
207
|
+
: scope;
|
|
208
|
+
const copy = {};
|
|
209
|
+
for (const [key, item] of Object.entries(value)) {
|
|
210
|
+
const next = this.projectNode(item, key === "event" ? childScope : scope);
|
|
211
|
+
if (next !== DROP)
|
|
212
|
+
copy[key] = next;
|
|
213
|
+
}
|
|
214
|
+
return copy;
|
|
215
|
+
}
|
|
216
|
+
insertOriginal(scope, binary) {
|
|
217
|
+
const id = dashboardImageId(binary.mimeType, binary.bytes);
|
|
218
|
+
let image = this.images.get(id);
|
|
219
|
+
if (!image) {
|
|
220
|
+
image = { id, mimeType: binary.mimeType, size: binary.bytes.byteLength, scopes: new Set() };
|
|
221
|
+
this.images.set(id, image);
|
|
222
|
+
}
|
|
223
|
+
image.scopes.add(scopeKey(scope));
|
|
224
|
+
if (!image.original)
|
|
225
|
+
this.putVariant(id, scope, "original", binary);
|
|
226
|
+
else
|
|
227
|
+
image.original.lastUsed = ++this.clock;
|
|
228
|
+
return { type: "image_reference", id, mimeType: binary.mimeType, size: binary.bytes.byteLength };
|
|
229
|
+
}
|
|
230
|
+
cachedVariant(scope, id, variant) {
|
|
231
|
+
const image = this.images.get(id);
|
|
232
|
+
if (!image || !image.scopes.has(scopeKey(scope)))
|
|
233
|
+
return undefined;
|
|
234
|
+
const cached = image[variant];
|
|
235
|
+
if (!cached)
|
|
236
|
+
return undefined;
|
|
237
|
+
cached.lastUsed = ++this.clock;
|
|
238
|
+
return { bytes: cached.bytes, mimeType: cached.mimeType };
|
|
239
|
+
}
|
|
240
|
+
async recoverOriginal(scope, id, loadAuthoritative) {
|
|
241
|
+
const source = await loadAuthoritative();
|
|
242
|
+
let recovered;
|
|
243
|
+
const visit = (value) => {
|
|
244
|
+
if (recovered)
|
|
245
|
+
return;
|
|
246
|
+
if (Array.isArray(value)) {
|
|
247
|
+
for (const item of value)
|
|
248
|
+
visit(item);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (!isRecord(value))
|
|
252
|
+
return;
|
|
253
|
+
if (value.type === "image") {
|
|
254
|
+
const image = decodeDashboardImage(value.data, value.mimeType);
|
|
255
|
+
if (image && dashboardImageId(image.mimeType, image.bytes) === id)
|
|
256
|
+
recovered = image;
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
for (const item of Object.values(value))
|
|
260
|
+
visit(item);
|
|
261
|
+
};
|
|
262
|
+
visit(source);
|
|
263
|
+
if (!recovered)
|
|
264
|
+
throw new DashboardImageNotFoundError("Image is no longer available from this transcript");
|
|
265
|
+
this.insertOriginal(scope, recovered);
|
|
266
|
+
return recovered;
|
|
267
|
+
}
|
|
268
|
+
putVariant(id, scope, variant, binary) {
|
|
269
|
+
let image = this.images.get(id);
|
|
270
|
+
if (!image) {
|
|
271
|
+
image = { id, mimeType: binary.mimeType, size: binary.bytes.byteLength, scopes: new Set() };
|
|
272
|
+
this.images.set(id, image);
|
|
273
|
+
}
|
|
274
|
+
image.scopes.add(scopeKey(scope));
|
|
275
|
+
const previous = image[variant];
|
|
276
|
+
if (previous) {
|
|
277
|
+
this.usedBytes -= previous.bytes.byteLength;
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
this.usedRecords += 1;
|
|
281
|
+
}
|
|
282
|
+
image[variant] = { bytes: binary.bytes, mimeType: binary.mimeType, lastUsed: ++this.clock };
|
|
283
|
+
this.usedBytes += binary.bytes.byteLength;
|
|
284
|
+
this.evict();
|
|
285
|
+
}
|
|
286
|
+
evict() {
|
|
287
|
+
while (this.usedBytes > this.maxBytes || this.usedRecords > this.maxRecords) {
|
|
288
|
+
let oldest;
|
|
289
|
+
for (const image of this.images.values()) {
|
|
290
|
+
for (const variant of ["original", "preview"]) {
|
|
291
|
+
const candidate = image[variant];
|
|
292
|
+
if (candidate && (!oldest || candidate.lastUsed < oldest.lastUsed)) {
|
|
293
|
+
oldest = { image, variant, lastUsed: candidate.lastUsed };
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (!oldest)
|
|
298
|
+
break;
|
|
299
|
+
const removed = oldest.image[oldest.variant];
|
|
300
|
+
this.usedBytes -= removed.bytes.byteLength;
|
|
301
|
+
this.usedRecords -= 1;
|
|
302
|
+
delete oldest.image[oldest.variant];
|
|
303
|
+
if (!oldest.image.original && !oldest.image.preview)
|
|
304
|
+
this.images.delete(oldest.image.id);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
deleteImage(image) {
|
|
308
|
+
for (const variant of [image.original, image.preview]) {
|
|
309
|
+
if (!variant)
|
|
310
|
+
continue;
|
|
311
|
+
this.usedBytes -= variant.bytes.byteLength;
|
|
312
|
+
this.usedRecords -= 1;
|
|
313
|
+
}
|
|
314
|
+
this.images.delete(image.id);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
//# sourceMappingURL=dashboard-images.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dashboard-images.js","sourceRoot":"","sources":["../../src/server/dashboard-images.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash } from \"node:crypto\";\nimport type { DashboardImageReferenceDto } from \"../shared/protocol.js\";\nimport {\n\ttype GeneratedImagePreview,\n\ttype ImagePreviewGenerator,\n\tMAX_PREVIEW_BYTES,\n\tMAX_PREVIEW_HEIGHT,\n\tMAX_PREVIEW_WIDTH,\n} from \"./image-preview.js\";\n\nexport const DASHBOARD_IMAGE_CACHE_BYTES = 64 * 1024 * 1024;\nexport const DASHBOARD_IMAGE_CACHE_RECORDS = 2000;\nexport const DASHBOARD_IMAGE_ID_PATTERN = /^[0-9a-f]{64}$/;\nexport const DASHBOARD_IMAGE_MIME_TYPES = new Set<DashboardImageReferenceDto[\"mimeType\"]>([\n\t\"image/png\",\n\t\"image/jpeg\",\n\t\"image/gif\",\n\t\"image/webp\",\n]);\n\nexport interface DashboardImageScope {\n\truntimeKey: string;\n\tagentId?: string;\n}\n\nexport interface DashboardImageBinary {\n\tbytes: Uint8Array;\n\tmimeType: DashboardImageReferenceDto[\"mimeType\"];\n}\n\nexport interface DashboardImageRepositoryOptions {\n\tmaxBytes?: number;\n\tmaxRecords?: number;\n}\n\ninterface CachedVariant extends DashboardImageBinary {\n\tlastUsed: number;\n}\n\ninterface CachedImage {\n\tid: string;\n\tmimeType: DashboardImageReferenceDto[\"mimeType\"];\n\tsize: number;\n\tscopes: Set<string>;\n\toriginal?: CachedVariant;\n\tpreview?: CachedVariant;\n}\n\nconst DROP = Symbol(\"drop-dashboard-image\");\nconst JPEG_EOI = Buffer.from([0xff, 0xd9]);\n\nfunction scopeKey(scope: DashboardImageScope): string {\n\treturn `${scope.runtimeKey}\\0${scope.agentId ?? \"\"}`;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn Boolean(value) && typeof value === \"object\" && !Array.isArray(value);\n}\n\nfunction hasRasterSignature(mimeType: DashboardImageReferenceDto[\"mimeType\"], bytes: Uint8Array): boolean {\n\tconst buffer = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);\n\tswitch (mimeType) {\n\t\tcase \"image/png\": {\n\t\t\tconst signature =\n\t\t\t\tbytes.length >= 24 &&\n\t\t\t\t[0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a].every((byte, i) => bytes[i] === byte);\n\t\t\treturn (\n\t\t\t\tsignature &&\n\t\t\t\tbuffer.subarray(12, 16).toString(\"ascii\") === \"IHDR\" &&\n\t\t\t\tbuffer.readUInt32BE(16) > 0 &&\n\t\t\t\tbuffer.readUInt32BE(20) > 0 &&\n\t\t\t\tbuffer.indexOf(Buffer.from(\"IEND\"), 24) >= 0\n\t\t\t);\n\t\t}\n\t\tcase \"image/jpeg\": {\n\t\t\tif (bytes.length < 4 || bytes[0] !== 0xff || bytes[1] !== 0xd8 || bytes[2] !== 0xff) return false;\n\t\t\t// EOI does not have to be the final byte. Phone JPEGs commonly append\n\t\t\t// auxiliary gain-map, motion-photo, or vendor metadata after the primary\n\t\t\t// image. Browsers and Photon decode the JPEG through EOI and preserve the\n\t\t\t// trailing bytes when the exact original is requested.\n\t\t\treturn buffer.lastIndexOf(JPEG_EOI) >= 2;\n\t\t}\n\t\tcase \"image/gif\": {\n\t\t\tconst header = buffer.subarray(0, 6).toString(\"ascii\");\n\t\t\treturn (\n\t\t\t\tbytes.length >= 14 &&\n\t\t\t\t(header === \"GIF87a\" || header === \"GIF89a\") &&\n\t\t\t\tbuffer.readUInt16LE(6) > 0 &&\n\t\t\t\tbuffer.readUInt16LE(8) > 0 &&\n\t\t\t\tbytes[bytes.length - 1] === 0x3b\n\t\t\t);\n\t\t}\n\t\tcase \"image/webp\": {\n\t\t\tif (\n\t\t\t\tbytes.length < 20 ||\n\t\t\t\tbuffer.subarray(0, 4).toString(\"ascii\") !== \"RIFF\" ||\n\t\t\t\tbuffer.subarray(8, 12).toString(\"ascii\") !== \"WEBP\"\n\t\t\t) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tconst chunk = buffer.subarray(12, 16).toString(\"ascii\");\n\t\t\treturn (\n\t\t\t\tbuffer.readUInt32LE(4) + 8 <= bytes.length && (chunk === \"VP8 \" || chunk === \"VP8L\" || chunk === \"VP8X\")\n\t\t\t);\n\t\t}\n\t}\n}\n\n/** Strict standard base64 decode: whitespace, data URLs, and non-canonical padding are rejected. */\nexport function decodeDashboardImage(value: unknown, mimeType: unknown): DashboardImageBinary | undefined {\n\tif (typeof value !== \"string\" || typeof mimeType !== \"string\") return undefined;\n\tif (!DASHBOARD_IMAGE_MIME_TYPES.has(mimeType as DashboardImageReferenceDto[\"mimeType\"])) return undefined;\n\tif (value.length === 0 || value.length % 4 !== 0 || !/^[A-Za-z0-9+/]+={0,2}$/.test(value)) return undefined;\n\tconst bytes = Buffer.from(value, \"base64\");\n\tif (bytes.toString(\"base64\") !== value) return undefined;\n\tconst exactMimeType = mimeType as DashboardImageReferenceDto[\"mimeType\"];\n\tif (!hasRasterSignature(exactMimeType, bytes)) return undefined;\n\treturn { bytes, mimeType: exactMimeType };\n}\n\nexport function dashboardImageId(mimeType: string, bytes: Uint8Array): string {\n\treturn createHash(\"sha256\").update(mimeType).update(Uint8Array.of(0)).update(bytes).digest(\"hex\");\n}\n\nexport function isDashboardImageReference(value: unknown): value is DashboardImageReferenceDto {\n\tif (!isRecord(value)) return false;\n\treturn (\n\t\tvalue.type === \"image_reference\" &&\n\t\ttypeof value.id === \"string\" &&\n\t\tDASHBOARD_IMAGE_ID_PATTERN.test(value.id) &&\n\t\ttypeof value.mimeType === \"string\" &&\n\t\tDASHBOARD_IMAGE_MIME_TYPES.has(value.mimeType as DashboardImageReferenceDto[\"mimeType\"]) &&\n\t\ttypeof value.size === \"number\" &&\n\t\tNumber.isSafeInteger(value.size) &&\n\t\tvalue.size > 0\n\t);\n}\n\nexport class DashboardImageNotFoundError extends Error {}\nexport class DashboardImagePreviewError extends Error {}\n\n/** Synchronous projection + bounded original/preview repository. */\nexport class DashboardImageService {\n\tprivate readonly images = new Map<string, CachedImage>();\n\tprivate readonly previewFlights = new Map<string, Promise<DashboardImageBinary>>();\n\tprivate readonly maxBytes: number;\n\tprivate readonly maxRecords: number;\n\tprivate usedBytes = 0;\n\tprivate usedRecords = 0;\n\tprivate clock = 0;\n\n\tconstructor(\n\t\tprivate readonly previews: ImagePreviewGenerator,\n\t\toptions: DashboardImageRepositoryOptions = {},\n\t) {\n\t\tthis.maxBytes = options.maxBytes ?? DASHBOARD_IMAGE_CACHE_BYTES;\n\t\tthis.maxRecords = options.maxRecords ?? DASHBOARD_IMAGE_CACHE_RECORDS;\n\t}\n\n\tget byteSize(): number {\n\t\treturn this.usedBytes;\n\t}\n\n\tget recordCount(): number {\n\t\treturn this.usedRecords;\n\t}\n\n\t/** Project a browser-facing event without mutating the authoritative source. */\n\tprojectEvent(event: Record<string, unknown>, scope: DashboardImageScope): Record<string, unknown> {\n\t\treturn this.projectNode(event, scope) as Record<string, unknown>;\n\t}\n\n\t/** Project messages/snapshots before JSON serialization. */\n\tproject<T>(value: T, scope: DashboardImageScope): T {\n\t\tconst projected = this.projectNode(value, scope);\n\t\treturn (projected === DROP ? undefined : projected) as T;\n\t}\n\n\tasync original(\n\t\tscope: DashboardImageScope,\n\t\tid: string,\n\t\tloadAuthoritative: () => Promise<unknown>,\n\t): Promise<DashboardImageBinary> {\n\t\tconst cached = this.cachedVariant(scope, id, \"original\");\n\t\tif (cached) return cached;\n\t\treturn this.recoverOriginal(scope, id, loadAuthoritative);\n\t}\n\n\tasync preview(\n\t\tscope: DashboardImageScope,\n\t\tid: string,\n\t\tloadAuthoritative: () => Promise<unknown>,\n\t): Promise<DashboardImageBinary> {\n\t\tconst cached = this.cachedVariant(scope, id, \"preview\");\n\t\tif (cached) return cached;\n\t\t// Authorize/recover this scope before joining a content-global flight. A\n\t\t// guessed ID from another runtime must not piggyback its in-flight preview.\n\t\tconst original = await this.original(scope, id, loadAuthoritative);\n\t\tconst recoveredPreview = this.cachedVariant(scope, id, \"preview\");\n\t\tif (recoveredPreview) return recoveredPreview;\n\t\tconst flight = this.previewFlights.get(id);\n\t\tif (flight) return flight;\n\t\tconst promise = (async () => {\n\t\t\tlet generated: GeneratedImagePreview;\n\t\t\ttry {\n\t\t\t\tgenerated = await this.previews.generate(original.bytes, original.mimeType);\n\t\t\t} catch (error) {\n\t\t\t\tthrow new DashboardImagePreviewError(\n\t\t\t\t\t`Preview generation failed: ${error instanceof Error ? error.message : String(error)}`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (\n\t\t\t\tgenerated.bytes.byteLength > MAX_PREVIEW_BYTES ||\n\t\t\t\tgenerated.width < 1 ||\n\t\t\t\tgenerated.height < 1 ||\n\t\t\t\tgenerated.width > MAX_PREVIEW_WIDTH ||\n\t\t\t\tgenerated.height > MAX_PREVIEW_HEIGHT ||\n\t\t\t\t(generated.mimeType !== \"image/png\" && generated.mimeType !== \"image/jpeg\")\n\t\t\t) {\n\t\t\t\tthrow new DashboardImagePreviewError(\"Preview worker returned an invalid or over-budget image\");\n\t\t\t}\n\t\t\tconst binary: DashboardImageBinary = { bytes: generated.bytes, mimeType: generated.mimeType };\n\t\t\tthis.putVariant(id, scope, \"preview\", binary);\n\t\t\treturn binary;\n\t\t})();\n\t\tthis.previewFlights.set(id, promise);\n\t\ttry {\n\t\t\treturn await promise;\n\t\t} finally {\n\t\t\tthis.previewFlights.delete(id);\n\t\t}\n\t}\n\n\tremoveRuntime(runtimeKey: string): void {\n\t\tfor (const image of [...this.images.values()]) {\n\t\t\tfor (const key of image.scopes) {\n\t\t\t\tif (key === runtimeKey || key.startsWith(`${runtimeKey}\\0`)) image.scopes.delete(key);\n\t\t\t}\n\t\t\tif (image.scopes.size === 0) this.deleteImage(image);\n\t\t}\n\t}\n\n\tclose(): Promise<void> {\n\t\treturn this.previews.close();\n\t}\n\n\tprivate projectNode(value: unknown, scope: DashboardImageScope): unknown | typeof DROP {\n\t\tif (Array.isArray(value)) {\n\t\t\tconst projected: unknown[] = [];\n\t\t\tfor (const item of value) {\n\t\t\t\tconst next = this.projectNode(item, scope);\n\t\t\t\tif (next !== DROP) projected.push(next);\n\t\t\t}\n\t\t\treturn projected;\n\t\t}\n\t\tif (!isRecord(value)) return value;\n\t\tif (value.type === \"image\") {\n\t\t\tconst image = decodeDashboardImage(value.data, value.mimeType);\n\t\t\tif (!image) return DROP;\n\t\t\treturn this.insertOriginal(scope, image);\n\t\t}\n\t\tif (value.type === \"image_reference\") return isDashboardImageReference(value) ? { ...value } : DROP;\n\t\tconst childScope =\n\t\t\tvalue.type === \"background_agent_event\" && typeof value.agentId === \"string\"\n\t\t\t\t? { runtimeKey: scope.runtimeKey, agentId: value.agentId }\n\t\t\t\t: scope;\n\t\tconst copy: Record<string, unknown> = {};\n\t\tfor (const [key, item] of Object.entries(value)) {\n\t\t\tconst next = this.projectNode(item, key === \"event\" ? childScope : scope);\n\t\t\tif (next !== DROP) copy[key] = next;\n\t\t}\n\t\treturn copy;\n\t}\n\n\tprivate insertOriginal(scope: DashboardImageScope, binary: DashboardImageBinary): DashboardImageReferenceDto {\n\t\tconst id = dashboardImageId(binary.mimeType, binary.bytes);\n\t\tlet image = this.images.get(id);\n\t\tif (!image) {\n\t\t\timage = { id, mimeType: binary.mimeType, size: binary.bytes.byteLength, scopes: new Set() };\n\t\t\tthis.images.set(id, image);\n\t\t}\n\t\timage.scopes.add(scopeKey(scope));\n\t\tif (!image.original) this.putVariant(id, scope, \"original\", binary);\n\t\telse image.original.lastUsed = ++this.clock;\n\t\treturn { type: \"image_reference\", id, mimeType: binary.mimeType, size: binary.bytes.byteLength };\n\t}\n\n\tprivate cachedVariant(\n\t\tscope: DashboardImageScope,\n\t\tid: string,\n\t\tvariant: \"original\" | \"preview\",\n\t): DashboardImageBinary | undefined {\n\t\tconst image = this.images.get(id);\n\t\tif (!image || !image.scopes.has(scopeKey(scope))) return undefined;\n\t\tconst cached = image[variant];\n\t\tif (!cached) return undefined;\n\t\tcached.lastUsed = ++this.clock;\n\t\treturn { bytes: cached.bytes, mimeType: cached.mimeType };\n\t}\n\n\tprivate async recoverOriginal(\n\t\tscope: DashboardImageScope,\n\t\tid: string,\n\t\tloadAuthoritative: () => Promise<unknown>,\n\t): Promise<DashboardImageBinary> {\n\t\tconst source = await loadAuthoritative();\n\t\tlet recovered: DashboardImageBinary | undefined;\n\t\tconst visit = (value: unknown): void => {\n\t\t\tif (recovered) return;\n\t\t\tif (Array.isArray(value)) {\n\t\t\t\tfor (const item of value) visit(item);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (!isRecord(value)) return;\n\t\t\tif (value.type === \"image\") {\n\t\t\t\tconst image = decodeDashboardImage(value.data, value.mimeType);\n\t\t\t\tif (image && dashboardImageId(image.mimeType, image.bytes) === id) recovered = image;\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tfor (const item of Object.values(value)) visit(item);\n\t\t};\n\t\tvisit(source);\n\t\tif (!recovered) throw new DashboardImageNotFoundError(\"Image is no longer available from this transcript\");\n\t\tthis.insertOriginal(scope, recovered);\n\t\treturn recovered;\n\t}\n\n\tprivate putVariant(\n\t\tid: string,\n\t\tscope: DashboardImageScope,\n\t\tvariant: \"original\" | \"preview\",\n\t\tbinary: DashboardImageBinary,\n\t): void {\n\t\tlet image = this.images.get(id);\n\t\tif (!image) {\n\t\t\timage = { id, mimeType: binary.mimeType, size: binary.bytes.byteLength, scopes: new Set() };\n\t\t\tthis.images.set(id, image);\n\t\t}\n\t\timage.scopes.add(scopeKey(scope));\n\t\tconst previous = image[variant];\n\t\tif (previous) {\n\t\t\tthis.usedBytes -= previous.bytes.byteLength;\n\t\t} else {\n\t\t\tthis.usedRecords += 1;\n\t\t}\n\t\timage[variant] = { bytes: binary.bytes, mimeType: binary.mimeType, lastUsed: ++this.clock };\n\t\tthis.usedBytes += binary.bytes.byteLength;\n\t\tthis.evict();\n\t}\n\n\tprivate evict(): void {\n\t\twhile (this.usedBytes > this.maxBytes || this.usedRecords > this.maxRecords) {\n\t\t\tlet oldest: { image: CachedImage; variant: \"original\" | \"preview\"; lastUsed: number } | undefined;\n\t\t\tfor (const image of this.images.values()) {\n\t\t\t\tfor (const variant of [\"original\", \"preview\"] as const) {\n\t\t\t\t\tconst candidate = image[variant];\n\t\t\t\t\tif (candidate && (!oldest || candidate.lastUsed < oldest.lastUsed)) {\n\t\t\t\t\t\toldest = { image, variant, lastUsed: candidate.lastUsed };\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (!oldest) break;\n\t\t\tconst removed = oldest.image[oldest.variant]!;\n\t\t\tthis.usedBytes -= removed.bytes.byteLength;\n\t\t\tthis.usedRecords -= 1;\n\t\t\tdelete oldest.image[oldest.variant];\n\t\t\tif (!oldest.image.original && !oldest.image.preview) this.images.delete(oldest.image.id);\n\t\t}\n\t}\n\n\tprivate deleteImage(image: CachedImage): void {\n\t\tfor (const variant of [image.original, image.preview]) {\n\t\t\tif (!variant) continue;\n\t\t\tthis.usedBytes -= variant.bytes.byteLength;\n\t\t\tthis.usedRecords -= 1;\n\t\t}\n\t\tthis.images.delete(image.id);\n\t}\n}\n"]}
|
|
@@ -52,7 +52,10 @@ export declare class EventHub {
|
|
|
52
52
|
private readonly buffer;
|
|
53
53
|
private readonly clients;
|
|
54
54
|
private readonly options;
|
|
55
|
+
private eventProjector?;
|
|
55
56
|
constructor(options?: number | EventHubOptions);
|
|
57
|
+
/** Install the server-owned browser projection before frame sizing/replay retention. */
|
|
58
|
+
setEventProjector(projector: (key: string, event: Record<string, unknown>) => Record<string, unknown>): void;
|
|
56
59
|
/** Publish an event from a runtime; assigns a sequence number and fans out. */
|
|
57
60
|
publish(key: string, rawEvent: Record<string, unknown>): EventEnvelope;
|
|
58
61
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"event-hub.d.ts","sourceRoot":"","sources":["../../src/server/event-hub.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAE3D,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAExD,mEAAmE;AACnE,MAAM,WAAW,gBAAgB;IAChC,IAAI,EAAE,YAAY,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,2EAA2E;IAC3E,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,gBAAgB;IAChC,IAAI,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,qEAAqE;AACrE,MAAM,WAAW,SAAS;IACzB,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,gBAAgB,GAAG,OAAO,GAAG,SAAS,CAAC;CACvE;AAED,MAAM,WAAW,eAAe;IAC/B,yCAAyC;IACzC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,2DAA2D;IAC3D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,4DAA4D;IAC5D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,oEAAoE;IACpE,UAAU,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,eAAO,MAAM,mBAAmB,OAAO,CAAC;AACxC,eAAO,MAAM,oBAAoB,QAAkB,CAAC;AACpD,gEAAgE;AAChE,eAAO,MAAM,oBAAoB,QAAkB,CAAC;AACpD,eAAO,MAAM,mBAAmB,QAAc,CAAC;AAgB/C;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAuB7F;AAED,qBAAa,QAAQ;IACpB,OAAO,CAAC,GAAG,CAAK;IAChB,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA4B;IACnD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAwB;IAChD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA4B;IAEpD,YAAY,OAAO,GAAE,MAAM,GAAG,eAAoB,EASjD;IAED,+EAA+E;IAC/E,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,aAAa,CAUrE;IAED;;;;OAIG;IACH,MAAM,CAAC,MAAM,EAAE,SAAS,EAAE,WAAW,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,CAAC,UAAU,EAAE,gBAAgB,KAAK,IAAI,GAAG,MAAM,IAAI,CA0B7G;IAED,IAAI,WAAW,IAAI,MAAM,CAExB;IAED,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED,8EAA8E;IAC9E,IAAI,eAAe,IAAI,MAAM,CAE5B;IAED,OAAO,CAAC,SAAS;IAMjB,OAAO,CAAC,MAAM;IAUd,OAAO,CAAC,WAAW;IASnB,OAAO,CAAC,YAAY;IAQpB,iGAAiG;IACjG,OAAO,CAAC,aAAa;IASrB;;;OAGG;IACH,OAAO,CAAC,cAAc;IAOtB,OAAO,CAAC,MAAM;IAMd,OAAO,CAAC,KAAK;CAeb;AAED,+EAA+E;AAC/E,wBAAgB,cAAc,CAAC,QAAQ,EAAE,aAAa,GAAG,MAAM,CAE9D;AAED,+EAA+E;AAC/E,wBAAgB,oBAAoB,IAAI,MAAM,CAE7C","sourcesContent":["/**\n * SSE event hub — projects runtime events for the dashboard, keeps a\n * byte-bounded replay history, and fans serialized frames out to browsers.\n */\n\nimport type { EventEnvelope } from \"../shared/protocol.js\";\n\nexport type SseWriteKind = \"live\" | \"replay\" | \"resync\";\n\n/** Metadata deliberately excludes the serialized event payload. */\nexport interface SseWriteMetadata {\n\tkind: SseWriteKind;\n\tseq: number;\n\ttype: string;\n\tframeBytes: number;\n\t/** Safe synthesized-barrier classification, never runtime payload data. */\n\treason?: string;\n}\n\nexport interface ReplayDiagnostic {\n\tkind: \"replay\" | \"resync\";\n\tcount: number;\n\tbytes: number;\n\tfromSeq?: number;\n\ttoSeq?: number;\n\treason?: string;\n}\n\n/** Return false when this connection can no longer accept frames. */\nexport interface SseClient {\n\twrite(chunk: string, metadata?: SseWriteMetadata): boolean | undefined;\n}\n\nexport interface EventHubOptions {\n\t/** Maximum number of retained frames. */\n\tbufferSize?: number;\n\t/** Maximum encoded bytes retained for reconnect replay. */\n\tbufferBytes?: number;\n\t/** Maximum encoded bytes written during a single replay. */\n\treplayBytes?: number;\n\t/** Largest projected event frame that may be delivered directly. */\n\teventBytes?: number;\n}\n\nexport const DEFAULT_BUFFER_SIZE = 2000;\nexport const DEFAULT_BUFFER_BYTES = 8 * 1024 * 1024;\n/** Kept below the response destruction ceiling in server.ts. */\nexport const DEFAULT_REPLAY_BYTES = 3 * 1024 * 1024;\nexport const DEFAULT_EVENT_BYTES = 1024 * 1024;\n\ninterface SerializedEnvelope {\n\tenvelope: EventEnvelope;\n\tframe: string;\n\tbytes: number;\n\t/** Present only for barriers synthesized by this hub, never runtime data. */\n\tresyncReason?: string;\n}\n\nfunction omit<T extends Record<string, unknown>>(event: T, ...keys: string[]): Record<string, unknown> {\n\tconst copy = { ...event };\n\tfor (const key of keys) delete copy[key];\n\treturn copy;\n}\n\n/**\n * Dashboard-only transport projection. Each removed field is cumulative data\n * that the dashboard reducer does not read. Unknown event types are returned\n * exactly as received so extensions and future runtimes remain forward-safe.\n */\nexport function projectDashboardEvent(event: Record<string, unknown>): Record<string, unknown> {\n\tswitch (event.type) {\n\t\tcase \"agent_end\":\n\t\t\treturn omit(event, \"messages\");\n\t\tcase \"turn_end\":\n\t\t\treturn omit(event, \"message\", \"toolResults\");\n\t\tcase \"message_update\":\n\t\t\treturn omit(event, \"message\");\n\t\tcase \"tool_execution_update\":\n\t\t\treturn omit(event, \"args\");\n\t\tcase \"stream_retry\":\n\t\t\treturn omit(event, \"discardedPartial\");\n\t\tcase \"length_retry\":\n\t\t\treturn omit(event, \"discardedPartial\");\n\t\tcase \"background_agent_event\": {\n\t\t\tconst child = event.event;\n\t\t\treturn child && typeof child === \"object\" && !Array.isArray(child)\n\t\t\t\t? { ...event, event: projectDashboardEvent(child as Record<string, unknown>) }\n\t\t\t\t: event;\n\t\t}\n\t\tdefault:\n\t\t\treturn event;\n\t}\n}\n\nexport class EventHub {\n\tprivate seq = 0;\n\tprivate bufferedBytes = 0;\n\tprivate readonly buffer: SerializedEnvelope[] = [];\n\tprivate readonly clients = new Set<SseClient>();\n\tprivate readonly options: Required<EventHubOptions>;\n\n\tconstructor(options: number | EventHubOptions = {}) {\n\t\tthis.options = {\n\t\t\tbufferSize: typeof options === \"number\" ? options : (options.bufferSize ?? DEFAULT_BUFFER_SIZE),\n\t\t\tbufferBytes:\n\t\t\t\ttypeof options === \"number\" ? DEFAULT_BUFFER_BYTES : (options.bufferBytes ?? DEFAULT_BUFFER_BYTES),\n\t\t\treplayBytes:\n\t\t\t\ttypeof options === \"number\" ? DEFAULT_REPLAY_BYTES : (options.replayBytes ?? DEFAULT_REPLAY_BYTES),\n\t\t\teventBytes: typeof options === \"number\" ? DEFAULT_EVENT_BYTES : (options.eventBytes ?? DEFAULT_EVENT_BYTES),\n\t\t};\n\t}\n\n\t/** Publish an event from a runtime; assigns a sequence number and fans out. */\n\tpublish(key: string, rawEvent: Record<string, unknown>): EventEnvelope {\n\t\tconst event = projectDashboardEvent(rawEvent);\n\t\tconst serialized = this.serialize(this.seq + 1, key, event);\n\t\tif (serialized.bytes > this.options.eventBytes) {\n\t\t\treturn this.publishResync(\"oversized_event\").envelope;\n\t\t}\n\t\tthis.seq += 1;\n\t\tthis.retain(serialized);\n\t\tthis.fanout(serialized, \"live\");\n\t\treturn serialized.envelope;\n\t}\n\n\t/**\n\t * Attach a client. Replays only a complete, bounded range. A gap or replay\n\t * over budget receives a recovery frame only on this connection; it never\n\t * consumes a global sequence or disturbs healthy clients' ordered stream.\n\t */\n\tattach(client: SseClient, lastEventId?: number, onReplay?: (diagnostic: ReplayDiagnostic) => void): () => void {\n\t\tif (lastEventId !== undefined) {\n\t\t\tconst replay = this.replayAfter(lastEventId);\n\t\t\tif (!replay) {\n\t\t\t\tconst reason = this.resyncReason(lastEventId);\n\t\t\t\tconst barrier = this.targetedResync(reason);\n\t\t\t\tonReplay?.({ kind: \"resync\", count: 1, bytes: barrier.bytes, toSeq: barrier.envelope.seq, reason });\n\t\t\t\tif (!this.write(client, barrier, \"resync\")) return () => {};\n\t\t\t} else {\n\t\t\t\tconst bytes = replay.reduce((total, item) => total + item.bytes, 0);\n\t\t\t\tonReplay?.({\n\t\t\t\t\tkind: \"replay\",\n\t\t\t\t\tcount: replay.length,\n\t\t\t\t\tbytes,\n\t\t\t\t\tfromSeq: replay[0]?.envelope.seq,\n\t\t\t\t\ttoSeq: replay[replay.length - 1]?.envelope.seq,\n\t\t\t\t});\n\t\t\t\tfor (const serialized of replay) {\n\t\t\t\t\tif (!this.write(client, serialized, \"replay\")) return () => {};\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tthis.clients.add(client);\n\t\treturn () => {\n\t\t\tthis.clients.delete(client);\n\t\t};\n\t}\n\n\tget clientCount(): number {\n\t\treturn this.clients.size;\n\t}\n\n\tget historyBytes(): number {\n\t\treturn this.bufferedBytes;\n\t}\n\n\tget historyCount(): number {\n\t\treturn this.buffer.length;\n\t}\n\n\t/** Current sequence, captured without emitting, retaining, or fanning out. */\n\tget currentSequence(): number {\n\t\treturn this.seq;\n\t}\n\n\tprivate serialize(seq: number, key: string, event: Record<string, unknown>): SerializedEnvelope {\n\t\tconst envelope: EventEnvelope = { seq, key, event };\n\t\tconst frame = formatSseFrame(envelope);\n\t\treturn { envelope, frame, bytes: Buffer.byteLength(frame) };\n\t}\n\n\tprivate retain(serialized: SerializedEnvelope): void {\n\t\tthis.buffer.push(serialized);\n\t\tthis.bufferedBytes += serialized.bytes;\n\t\twhile (this.buffer.length > this.options.bufferSize || this.bufferedBytes > this.options.bufferBytes) {\n\t\t\tconst evicted = this.buffer.shift();\n\t\t\tif (!evicted) break;\n\t\t\tthis.bufferedBytes -= evicted.bytes;\n\t\t}\n\t}\n\n\tprivate replayAfter(lastEventId: number): SerializedEnvelope[] | undefined {\n\t\tconst oldest = this.buffer[0]?.envelope.seq;\n\t\tconst newest = this.buffer[this.buffer.length - 1]?.envelope.seq;\n\t\tif (oldest === undefined || newest === undefined || lastEventId < oldest - 1 || lastEventId > newest)\n\t\t\treturn undefined;\n\t\tconst replay = this.buffer.filter((item) => item.envelope.seq > lastEventId);\n\t\treturn replay.reduce((bytes, item) => bytes + item.bytes, 0) <= this.options.replayBytes ? replay : undefined;\n\t}\n\n\tprivate resyncReason(lastEventId: number): string {\n\t\tconst oldest = this.buffer[0]?.envelope.seq;\n\t\tconst newest = this.buffer[this.buffer.length - 1]?.envelope.seq;\n\t\tif (oldest === undefined) return \"empty_buffer\";\n\t\tif (lastEventId < oldest - 1 || lastEventId > newest!) return \"buffer_gap\";\n\t\treturn \"replay_over_budget\";\n\t}\n\n\t/** Explicit oversized events are globally unrecoverable, so retain and fan out their barrier. */\n\tprivate publishResync(reason: string): SerializedEnvelope {\n\t\tthis.seq += 1;\n\t\tconst barrier = this.serialize(this.seq, \"\", { type: \"dashboard_resync\", reason });\n\t\tbarrier.resyncReason = reason;\n\t\tthis.retain(barrier);\n\t\tthis.fanout(barrier, \"resync\");\n\t\treturn barrier;\n\t}\n\n\t/**\n\t * A stale reconnect needs the current ordering cursor, not a new global\n\t * event. With no events yet, establish sequence 1 so it has a usable cursor.\n\t */\n\tprivate targetedResync(reason: string): SerializedEnvelope {\n\t\tif (this.seq === 0) this.seq = 1;\n\t\tconst barrier = this.serialize(this.seq, \"\", { type: \"dashboard_resync\", reason });\n\t\tbarrier.resyncReason = reason;\n\t\treturn barrier;\n\t}\n\n\tprivate fanout(serialized: SerializedEnvelope, kind: SseWriteKind): void {\n\t\tfor (const client of this.clients) {\n\t\t\tif (!this.write(client, serialized, kind)) this.clients.delete(client);\n\t\t}\n\t}\n\n\tprivate write(client: SseClient, serialized: SerializedEnvelope, kind: SseWriteKind): boolean {\n\t\ttry {\n\t\t\treturn (\n\t\t\t\tclient.write(serialized.frame, {\n\t\t\t\t\tkind,\n\t\t\t\t\tseq: serialized.envelope.seq,\n\t\t\t\t\ttype: String(serialized.envelope.event.type ?? \"unknown\"),\n\t\t\t\t\tframeBytes: serialized.bytes,\n\t\t\t\t\t...(serialized.resyncReason ? { reason: serialized.resyncReason } : {}),\n\t\t\t\t}) !== false\n\t\t\t);\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t}\n}\n\n/** Format an envelope as an SSE frame with the sequence number as event id. */\nexport function formatSseFrame(envelope: EventEnvelope): string {\n\treturn `id: ${envelope.seq}\\ndata: ${JSON.stringify(envelope)}\\n\\n`;\n}\n\n/** Observable liveness signal; intentionally unnumbered and never buffered. */\nexport function formatHeartbeatFrame(): string {\n\treturn \"event: heartbeat\\ndata: {}\\n\\n\";\n}\n"]}
|
|
1
|
+
{"version":3,"file":"event-hub.d.ts","sourceRoot":"","sources":["../../src/server/event-hub.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * SSE event hub — projects runtime events for the dashboard, keeps a\n * byte-bounded replay history, and fans serialized frames out to browsers.\n */\n\nimport type { EventEnvelope } from \"../shared/protocol.js\";\n\nexport type SseWriteKind = \"live\" | \"replay\" | \"resync\";\n\n/** Metadata deliberately excludes the serialized event payload. */\nexport interface SseWriteMetadata {\n\tkind: SseWriteKind;\n\tseq: number;\n\ttype: string;\n\tframeBytes: number;\n\t/** Safe synthesized-barrier classification, never runtime payload data. */\n\treason?: string;\n}\n\nexport interface ReplayDiagnostic {\n\tkind: \"replay\" | \"resync\";\n\tcount: number;\n\tbytes: number;\n\tfromSeq?: number;\n\ttoSeq?: number;\n\treason?: string;\n}\n\n/** Return false when this connection can no longer accept frames. */\nexport interface SseClient {\n\twrite(chunk: string, metadata?: SseWriteMetadata): boolean | undefined;\n}\n\nexport interface EventHubOptions {\n\t/** Maximum number of retained frames. */\n\tbufferSize?: number;\n\t/** Maximum encoded bytes retained for reconnect replay. */\n\tbufferBytes?: number;\n\t/** Maximum encoded bytes written during a single replay. */\n\treplayBytes?: number;\n\t/** Largest projected event frame that may be delivered directly. */\n\teventBytes?: number;\n}\n\nexport const DEFAULT_BUFFER_SIZE = 2000;\nexport const DEFAULT_BUFFER_BYTES = 8 * 1024 * 1024;\n/** Kept below the response destruction ceiling in server.ts. */\nexport const DEFAULT_REPLAY_BYTES = 3 * 1024 * 1024;\nexport const DEFAULT_EVENT_BYTES = 1024 * 1024;\n\ninterface SerializedEnvelope {\n\tenvelope: EventEnvelope;\n\tframe: string;\n\tbytes: number;\n\t/** Present only for barriers synthesized by this hub, never runtime data. */\n\tresyncReason?: string;\n}\n\nfunction omit<T extends Record<string, unknown>>(event: T, ...keys: string[]): Record<string, unknown> {\n\tconst copy = { ...event };\n\tfor (const key of keys) delete copy[key];\n\treturn copy;\n}\n\n/**\n * Dashboard-only transport projection. Each removed field is cumulative data\n * that the dashboard reducer does not read. Unknown event types are returned\n * exactly as received so extensions and future runtimes remain forward-safe.\n */\nexport function projectDashboardEvent(event: Record<string, unknown>): Record<string, unknown> {\n\tswitch (event.type) {\n\t\tcase \"agent_end\":\n\t\t\treturn omit(event, \"messages\");\n\t\tcase \"turn_end\":\n\t\t\treturn omit(event, \"message\", \"toolResults\");\n\t\tcase \"message_update\":\n\t\t\treturn omit(event, \"message\");\n\t\tcase \"tool_execution_update\":\n\t\t\treturn omit(event, \"args\");\n\t\tcase \"stream_retry\":\n\t\t\treturn omit(event, \"discardedPartial\");\n\t\tcase \"length_retry\":\n\t\t\treturn omit(event, \"discardedPartial\");\n\t\tcase \"background_agent_event\": {\n\t\t\tconst child = event.event;\n\t\t\treturn child && typeof child === \"object\" && !Array.isArray(child)\n\t\t\t\t? { ...event, event: projectDashboardEvent(child as Record<string, unknown>) }\n\t\t\t\t: event;\n\t\t}\n\t\tdefault:\n\t\t\treturn event;\n\t}\n}\n\nexport class EventHub {\n\tprivate seq = 0;\n\tprivate bufferedBytes = 0;\n\tprivate readonly buffer: SerializedEnvelope[] = [];\n\tprivate readonly clients = new Set<SseClient>();\n\tprivate readonly options: Required<EventHubOptions>;\n\tprivate eventProjector?: (key: string, event: Record<string, unknown>) => Record<string, unknown>;\n\n\tconstructor(options: number | EventHubOptions = {}) {\n\t\tthis.options = {\n\t\t\tbufferSize: typeof options === \"number\" ? options : (options.bufferSize ?? DEFAULT_BUFFER_SIZE),\n\t\t\tbufferBytes:\n\t\t\t\ttypeof options === \"number\" ? DEFAULT_BUFFER_BYTES : (options.bufferBytes ?? DEFAULT_BUFFER_BYTES),\n\t\t\treplayBytes:\n\t\t\t\ttypeof options === \"number\" ? DEFAULT_REPLAY_BYTES : (options.replayBytes ?? DEFAULT_REPLAY_BYTES),\n\t\t\teventBytes: typeof options === \"number\" ? DEFAULT_EVENT_BYTES : (options.eventBytes ?? DEFAULT_EVENT_BYTES),\n\t\t};\n\t}\n\n\t/** Install the server-owned browser projection before frame sizing/replay retention. */\n\tsetEventProjector(projector: (key: string, event: Record<string, unknown>) => Record<string, unknown>): void {\n\t\tthis.eventProjector = projector;\n\t}\n\n\t/** Publish an event from a runtime; assigns a sequence number and fans out. */\n\tpublish(key: string, rawEvent: Record<string, unknown>): EventEnvelope {\n\t\tconst baseProjection = projectDashboardEvent(rawEvent);\n\t\tconst event = this.eventProjector ? this.eventProjector(key, baseProjection) : baseProjection;\n\t\tconst serialized = this.serialize(this.seq + 1, key, event);\n\t\tif (serialized.bytes > this.options.eventBytes) {\n\t\t\treturn this.publishResync(\"oversized_event\").envelope;\n\t\t}\n\t\tthis.seq += 1;\n\t\tthis.retain(serialized);\n\t\tthis.fanout(serialized, \"live\");\n\t\treturn serialized.envelope;\n\t}\n\n\t/**\n\t * Attach a client. Replays only a complete, bounded range. A gap or replay\n\t * over budget receives a recovery frame only on this connection; it never\n\t * consumes a global sequence or disturbs healthy clients' ordered stream.\n\t */\n\tattach(client: SseClient, lastEventId?: number, onReplay?: (diagnostic: ReplayDiagnostic) => void): () => void {\n\t\tif (lastEventId !== undefined) {\n\t\t\tconst replay = this.replayAfter(lastEventId);\n\t\t\tif (!replay) {\n\t\t\t\tconst reason = this.resyncReason(lastEventId);\n\t\t\t\tconst barrier = this.targetedResync(reason);\n\t\t\t\tonReplay?.({ kind: \"resync\", count: 1, bytes: barrier.bytes, toSeq: barrier.envelope.seq, reason });\n\t\t\t\tif (!this.write(client, barrier, \"resync\")) return () => {};\n\t\t\t} else {\n\t\t\t\tconst bytes = replay.reduce((total, item) => total + item.bytes, 0);\n\t\t\t\tonReplay?.({\n\t\t\t\t\tkind: \"replay\",\n\t\t\t\t\tcount: replay.length,\n\t\t\t\t\tbytes,\n\t\t\t\t\tfromSeq: replay[0]?.envelope.seq,\n\t\t\t\t\ttoSeq: replay[replay.length - 1]?.envelope.seq,\n\t\t\t\t});\n\t\t\t\tfor (const serialized of replay) {\n\t\t\t\t\tif (!this.write(client, serialized, \"replay\")) return () => {};\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tthis.clients.add(client);\n\t\treturn () => {\n\t\t\tthis.clients.delete(client);\n\t\t};\n\t}\n\n\tget clientCount(): number {\n\t\treturn this.clients.size;\n\t}\n\n\tget historyBytes(): number {\n\t\treturn this.bufferedBytes;\n\t}\n\n\tget historyCount(): number {\n\t\treturn this.buffer.length;\n\t}\n\n\t/** Current sequence, captured without emitting, retaining, or fanning out. */\n\tget currentSequence(): number {\n\t\treturn this.seq;\n\t}\n\n\tprivate serialize(seq: number, key: string, event: Record<string, unknown>): SerializedEnvelope {\n\t\tconst envelope: EventEnvelope = { seq, key, event };\n\t\tconst frame = formatSseFrame(envelope);\n\t\treturn { envelope, frame, bytes: Buffer.byteLength(frame) };\n\t}\n\n\tprivate retain(serialized: SerializedEnvelope): void {\n\t\tthis.buffer.push(serialized);\n\t\tthis.bufferedBytes += serialized.bytes;\n\t\twhile (this.buffer.length > this.options.bufferSize || this.bufferedBytes > this.options.bufferBytes) {\n\t\t\tconst evicted = this.buffer.shift();\n\t\t\tif (!evicted) break;\n\t\t\tthis.bufferedBytes -= evicted.bytes;\n\t\t}\n\t}\n\n\tprivate replayAfter(lastEventId: number): SerializedEnvelope[] | undefined {\n\t\tconst oldest = this.buffer[0]?.envelope.seq;\n\t\tconst newest = this.buffer[this.buffer.length - 1]?.envelope.seq;\n\t\tif (oldest === undefined || newest === undefined || lastEventId < oldest - 1 || lastEventId > newest)\n\t\t\treturn undefined;\n\t\tconst replay = this.buffer.filter((item) => item.envelope.seq > lastEventId);\n\t\treturn replay.reduce((bytes, item) => bytes + item.bytes, 0) <= this.options.replayBytes ? replay : undefined;\n\t}\n\n\tprivate resyncReason(lastEventId: number): string {\n\t\tconst oldest = this.buffer[0]?.envelope.seq;\n\t\tconst newest = this.buffer[this.buffer.length - 1]?.envelope.seq;\n\t\tif (oldest === undefined) return \"empty_buffer\";\n\t\tif (lastEventId < oldest - 1 || lastEventId > newest!) return \"buffer_gap\";\n\t\treturn \"replay_over_budget\";\n\t}\n\n\t/** Explicit oversized events are globally unrecoverable, so retain and fan out their barrier. */\n\tprivate publishResync(reason: string): SerializedEnvelope {\n\t\tthis.seq += 1;\n\t\tconst barrier = this.serialize(this.seq, \"\", { type: \"dashboard_resync\", reason });\n\t\tbarrier.resyncReason = reason;\n\t\tthis.retain(barrier);\n\t\tthis.fanout(barrier, \"resync\");\n\t\treturn barrier;\n\t}\n\n\t/**\n\t * A stale reconnect needs the current ordering cursor, not a new global\n\t * event. With no events yet, establish sequence 1 so it has a usable cursor.\n\t */\n\tprivate targetedResync(reason: string): SerializedEnvelope {\n\t\tif (this.seq === 0) this.seq = 1;\n\t\tconst barrier = this.serialize(this.seq, \"\", { type: \"dashboard_resync\", reason });\n\t\tbarrier.resyncReason = reason;\n\t\treturn barrier;\n\t}\n\n\tprivate fanout(serialized: SerializedEnvelope, kind: SseWriteKind): void {\n\t\tfor (const client of this.clients) {\n\t\t\tif (!this.write(client, serialized, kind)) this.clients.delete(client);\n\t\t}\n\t}\n\n\tprivate write(client: SseClient, serialized: SerializedEnvelope, kind: SseWriteKind): boolean {\n\t\ttry {\n\t\t\treturn (\n\t\t\t\tclient.write(serialized.frame, {\n\t\t\t\t\tkind,\n\t\t\t\t\tseq: serialized.envelope.seq,\n\t\t\t\t\ttype: String(serialized.envelope.event.type ?? \"unknown\"),\n\t\t\t\t\tframeBytes: serialized.bytes,\n\t\t\t\t\t...(serialized.resyncReason ? { reason: serialized.resyncReason } : {}),\n\t\t\t\t}) !== false\n\t\t\t);\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t}\n}\n\n/** Format an envelope as an SSE frame with the sequence number as event id. */\nexport function formatSseFrame(envelope: EventEnvelope): string {\n\treturn `id: ${envelope.seq}\\ndata: ${JSON.stringify(envelope)}\\n\\n`;\n}\n\n/** Observable liveness signal; intentionally unnumbered and never buffered. */\nexport function formatHeartbeatFrame(): string {\n\treturn \"event: heartbeat\\ndata: {}\\n\\n\";\n}\n"]}
|
package/dist/server/event-hub.js
CHANGED
|
@@ -48,6 +48,7 @@ export class EventHub {
|
|
|
48
48
|
buffer = [];
|
|
49
49
|
clients = new Set();
|
|
50
50
|
options;
|
|
51
|
+
eventProjector;
|
|
51
52
|
constructor(options = {}) {
|
|
52
53
|
this.options = {
|
|
53
54
|
bufferSize: typeof options === "number" ? options : (options.bufferSize ?? DEFAULT_BUFFER_SIZE),
|
|
@@ -56,9 +57,14 @@ export class EventHub {
|
|
|
56
57
|
eventBytes: typeof options === "number" ? DEFAULT_EVENT_BYTES : (options.eventBytes ?? DEFAULT_EVENT_BYTES),
|
|
57
58
|
};
|
|
58
59
|
}
|
|
60
|
+
/** Install the server-owned browser projection before frame sizing/replay retention. */
|
|
61
|
+
setEventProjector(projector) {
|
|
62
|
+
this.eventProjector = projector;
|
|
63
|
+
}
|
|
59
64
|
/** Publish an event from a runtime; assigns a sequence number and fans out. */
|
|
60
65
|
publish(key, rawEvent) {
|
|
61
|
-
const
|
|
66
|
+
const baseProjection = projectDashboardEvent(rawEvent);
|
|
67
|
+
const event = this.eventProjector ? this.eventProjector(key, baseProjection) : baseProjection;
|
|
62
68
|
const serialized = this.serialize(this.seq + 1, key, event);
|
|
63
69
|
if (serialized.bytes > this.options.eventBytes) {
|
|
64
70
|
return this.publishResync("oversized_event").envelope;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"event-hub.js","sourceRoot":"","sources":["../../src/server/event-hub.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * SSE event hub — projects runtime events for the dashboard, keeps a\n * byte-bounded replay history, and fans serialized frames out to browsers.\n */\n\nimport type { EventEnvelope } from \"../shared/protocol.js\";\n\nexport type SseWriteKind = \"live\" | \"replay\" | \"resync\";\n\n/** Metadata deliberately excludes the serialized event payload. */\nexport interface SseWriteMetadata {\n\tkind: SseWriteKind;\n\tseq: number;\n\ttype: string;\n\tframeBytes: number;\n\t/** Safe synthesized-barrier classification, never runtime payload data. */\n\treason?: string;\n}\n\nexport interface ReplayDiagnostic {\n\tkind: \"replay\" | \"resync\";\n\tcount: number;\n\tbytes: number;\n\tfromSeq?: number;\n\ttoSeq?: number;\n\treason?: string;\n}\n\n/** Return false when this connection can no longer accept frames. */\nexport interface SseClient {\n\twrite(chunk: string, metadata?: SseWriteMetadata): boolean | undefined;\n}\n\nexport interface EventHubOptions {\n\t/** Maximum number of retained frames. */\n\tbufferSize?: number;\n\t/** Maximum encoded bytes retained for reconnect replay. */\n\tbufferBytes?: number;\n\t/** Maximum encoded bytes written during a single replay. */\n\treplayBytes?: number;\n\t/** Largest projected event frame that may be delivered directly. */\n\teventBytes?: number;\n}\n\nexport const DEFAULT_BUFFER_SIZE = 2000;\nexport const DEFAULT_BUFFER_BYTES = 8 * 1024 * 1024;\n/** Kept below the response destruction ceiling in server.ts. */\nexport const DEFAULT_REPLAY_BYTES = 3 * 1024 * 1024;\nexport const DEFAULT_EVENT_BYTES = 1024 * 1024;\n\ninterface SerializedEnvelope {\n\tenvelope: EventEnvelope;\n\tframe: string;\n\tbytes: number;\n\t/** Present only for barriers synthesized by this hub, never runtime data. */\n\tresyncReason?: string;\n}\n\nfunction omit<T extends Record<string, unknown>>(event: T, ...keys: string[]): Record<string, unknown> {\n\tconst copy = { ...event };\n\tfor (const key of keys) delete copy[key];\n\treturn copy;\n}\n\n/**\n * Dashboard-only transport projection. Each removed field is cumulative data\n * that the dashboard reducer does not read. Unknown event types are returned\n * exactly as received so extensions and future runtimes remain forward-safe.\n */\nexport function projectDashboardEvent(event: Record<string, unknown>): Record<string, unknown> {\n\tswitch (event.type) {\n\t\tcase \"agent_end\":\n\t\t\treturn omit(event, \"messages\");\n\t\tcase \"turn_end\":\n\t\t\treturn omit(event, \"message\", \"toolResults\");\n\t\tcase \"message_update\":\n\t\t\treturn omit(event, \"message\");\n\t\tcase \"tool_execution_update\":\n\t\t\treturn omit(event, \"args\");\n\t\tcase \"stream_retry\":\n\t\t\treturn omit(event, \"discardedPartial\");\n\t\tcase \"length_retry\":\n\t\t\treturn omit(event, \"discardedPartial\");\n\t\tcase \"background_agent_event\": {\n\t\t\tconst child = event.event;\n\t\t\treturn child && typeof child === \"object\" && !Array.isArray(child)\n\t\t\t\t? { ...event, event: projectDashboardEvent(child as Record<string, unknown>) }\n\t\t\t\t: event;\n\t\t}\n\t\tdefault:\n\t\t\treturn event;\n\t}\n}\n\nexport class EventHub {\n\tprivate seq = 0;\n\tprivate bufferedBytes = 0;\n\tprivate readonly buffer: SerializedEnvelope[] = [];\n\tprivate readonly clients = new Set<SseClient>();\n\tprivate readonly options: Required<EventHubOptions>;\n\n\tconstructor(options: number | EventHubOptions = {}) {\n\t\tthis.options = {\n\t\t\tbufferSize: typeof options === \"number\" ? options : (options.bufferSize ?? DEFAULT_BUFFER_SIZE),\n\t\t\tbufferBytes:\n\t\t\t\ttypeof options === \"number\" ? DEFAULT_BUFFER_BYTES : (options.bufferBytes ?? DEFAULT_BUFFER_BYTES),\n\t\t\treplayBytes:\n\t\t\t\ttypeof options === \"number\" ? DEFAULT_REPLAY_BYTES : (options.replayBytes ?? DEFAULT_REPLAY_BYTES),\n\t\t\teventBytes: typeof options === \"number\" ? DEFAULT_EVENT_BYTES : (options.eventBytes ?? DEFAULT_EVENT_BYTES),\n\t\t};\n\t}\n\n\t/** Publish an event from a runtime; assigns a sequence number and fans out. */\n\tpublish(key: string, rawEvent: Record<string, unknown>): EventEnvelope {\n\t\tconst event = projectDashboardEvent(rawEvent);\n\t\tconst serialized = this.serialize(this.seq + 1, key, event);\n\t\tif (serialized.bytes > this.options.eventBytes) {\n\t\t\treturn this.publishResync(\"oversized_event\").envelope;\n\t\t}\n\t\tthis.seq += 1;\n\t\tthis.retain(serialized);\n\t\tthis.fanout(serialized, \"live\");\n\t\treturn serialized.envelope;\n\t}\n\n\t/**\n\t * Attach a client. Replays only a complete, bounded range. A gap or replay\n\t * over budget receives a recovery frame only on this connection; it never\n\t * consumes a global sequence or disturbs healthy clients' ordered stream.\n\t */\n\tattach(client: SseClient, lastEventId?: number, onReplay?: (diagnostic: ReplayDiagnostic) => void): () => void {\n\t\tif (lastEventId !== undefined) {\n\t\t\tconst replay = this.replayAfter(lastEventId);\n\t\t\tif (!replay) {\n\t\t\t\tconst reason = this.resyncReason(lastEventId);\n\t\t\t\tconst barrier = this.targetedResync(reason);\n\t\t\t\tonReplay?.({ kind: \"resync\", count: 1, bytes: barrier.bytes, toSeq: barrier.envelope.seq, reason });\n\t\t\t\tif (!this.write(client, barrier, \"resync\")) return () => {};\n\t\t\t} else {\n\t\t\t\tconst bytes = replay.reduce((total, item) => total + item.bytes, 0);\n\t\t\t\tonReplay?.({\n\t\t\t\t\tkind: \"replay\",\n\t\t\t\t\tcount: replay.length,\n\t\t\t\t\tbytes,\n\t\t\t\t\tfromSeq: replay[0]?.envelope.seq,\n\t\t\t\t\ttoSeq: replay[replay.length - 1]?.envelope.seq,\n\t\t\t\t});\n\t\t\t\tfor (const serialized of replay) {\n\t\t\t\t\tif (!this.write(client, serialized, \"replay\")) return () => {};\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tthis.clients.add(client);\n\t\treturn () => {\n\t\t\tthis.clients.delete(client);\n\t\t};\n\t}\n\n\tget clientCount(): number {\n\t\treturn this.clients.size;\n\t}\n\n\tget historyBytes(): number {\n\t\treturn this.bufferedBytes;\n\t}\n\n\tget historyCount(): number {\n\t\treturn this.buffer.length;\n\t}\n\n\t/** Current sequence, captured without emitting, retaining, or fanning out. */\n\tget currentSequence(): number {\n\t\treturn this.seq;\n\t}\n\n\tprivate serialize(seq: number, key: string, event: Record<string, unknown>): SerializedEnvelope {\n\t\tconst envelope: EventEnvelope = { seq, key, event };\n\t\tconst frame = formatSseFrame(envelope);\n\t\treturn { envelope, frame, bytes: Buffer.byteLength(frame) };\n\t}\n\n\tprivate retain(serialized: SerializedEnvelope): void {\n\t\tthis.buffer.push(serialized);\n\t\tthis.bufferedBytes += serialized.bytes;\n\t\twhile (this.buffer.length > this.options.bufferSize || this.bufferedBytes > this.options.bufferBytes) {\n\t\t\tconst evicted = this.buffer.shift();\n\t\t\tif (!evicted) break;\n\t\t\tthis.bufferedBytes -= evicted.bytes;\n\t\t}\n\t}\n\n\tprivate replayAfter(lastEventId: number): SerializedEnvelope[] | undefined {\n\t\tconst oldest = this.buffer[0]?.envelope.seq;\n\t\tconst newest = this.buffer[this.buffer.length - 1]?.envelope.seq;\n\t\tif (oldest === undefined || newest === undefined || lastEventId < oldest - 1 || lastEventId > newest)\n\t\t\treturn undefined;\n\t\tconst replay = this.buffer.filter((item) => item.envelope.seq > lastEventId);\n\t\treturn replay.reduce((bytes, item) => bytes + item.bytes, 0) <= this.options.replayBytes ? replay : undefined;\n\t}\n\n\tprivate resyncReason(lastEventId: number): string {\n\t\tconst oldest = this.buffer[0]?.envelope.seq;\n\t\tconst newest = this.buffer[this.buffer.length - 1]?.envelope.seq;\n\t\tif (oldest === undefined) return \"empty_buffer\";\n\t\tif (lastEventId < oldest - 1 || lastEventId > newest!) return \"buffer_gap\";\n\t\treturn \"replay_over_budget\";\n\t}\n\n\t/** Explicit oversized events are globally unrecoverable, so retain and fan out their barrier. */\n\tprivate publishResync(reason: string): SerializedEnvelope {\n\t\tthis.seq += 1;\n\t\tconst barrier = this.serialize(this.seq, \"\", { type: \"dashboard_resync\", reason });\n\t\tbarrier.resyncReason = reason;\n\t\tthis.retain(barrier);\n\t\tthis.fanout(barrier, \"resync\");\n\t\treturn barrier;\n\t}\n\n\t/**\n\t * A stale reconnect needs the current ordering cursor, not a new global\n\t * event. With no events yet, establish sequence 1 so it has a usable cursor.\n\t */\n\tprivate targetedResync(reason: string): SerializedEnvelope {\n\t\tif (this.seq === 0) this.seq = 1;\n\t\tconst barrier = this.serialize(this.seq, \"\", { type: \"dashboard_resync\", reason });\n\t\tbarrier.resyncReason = reason;\n\t\treturn barrier;\n\t}\n\n\tprivate fanout(serialized: SerializedEnvelope, kind: SseWriteKind): void {\n\t\tfor (const client of this.clients) {\n\t\t\tif (!this.write(client, serialized, kind)) this.clients.delete(client);\n\t\t}\n\t}\n\n\tprivate write(client: SseClient, serialized: SerializedEnvelope, kind: SseWriteKind): boolean {\n\t\ttry {\n\t\t\treturn (\n\t\t\t\tclient.write(serialized.frame, {\n\t\t\t\t\tkind,\n\t\t\t\t\tseq: serialized.envelope.seq,\n\t\t\t\t\ttype: String(serialized.envelope.event.type ?? \"unknown\"),\n\t\t\t\t\tframeBytes: serialized.bytes,\n\t\t\t\t\t...(serialized.resyncReason ? { reason: serialized.resyncReason } : {}),\n\t\t\t\t}) !== false\n\t\t\t);\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t}\n}\n\n/** Format an envelope as an SSE frame with the sequence number as event id. */\nexport function formatSseFrame(envelope: EventEnvelope): string {\n\treturn `id: ${envelope.seq}\\ndata: ${JSON.stringify(envelope)}\\n\\n`;\n}\n\n/** Observable liveness signal; intentionally unnumbered and never buffered. */\nexport function formatHeartbeatFrame(): string {\n\treturn \"event: heartbeat\\ndata: {}\\n\\n\";\n}\n"]}
|
|
1
|
+
{"version":3,"file":"event-hub.js","sourceRoot":"","sources":["../../src/server/event-hub.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * SSE event hub — projects runtime events for the dashboard, keeps a\n * byte-bounded replay history, and fans serialized frames out to browsers.\n */\n\nimport type { EventEnvelope } from \"../shared/protocol.js\";\n\nexport type SseWriteKind = \"live\" | \"replay\" | \"resync\";\n\n/** Metadata deliberately excludes the serialized event payload. */\nexport interface SseWriteMetadata {\n\tkind: SseWriteKind;\n\tseq: number;\n\ttype: string;\n\tframeBytes: number;\n\t/** Safe synthesized-barrier classification, never runtime payload data. */\n\treason?: string;\n}\n\nexport interface ReplayDiagnostic {\n\tkind: \"replay\" | \"resync\";\n\tcount: number;\n\tbytes: number;\n\tfromSeq?: number;\n\ttoSeq?: number;\n\treason?: string;\n}\n\n/** Return false when this connection can no longer accept frames. */\nexport interface SseClient {\n\twrite(chunk: string, metadata?: SseWriteMetadata): boolean | undefined;\n}\n\nexport interface EventHubOptions {\n\t/** Maximum number of retained frames. */\n\tbufferSize?: number;\n\t/** Maximum encoded bytes retained for reconnect replay. */\n\tbufferBytes?: number;\n\t/** Maximum encoded bytes written during a single replay. */\n\treplayBytes?: number;\n\t/** Largest projected event frame that may be delivered directly. */\n\teventBytes?: number;\n}\n\nexport const DEFAULT_BUFFER_SIZE = 2000;\nexport const DEFAULT_BUFFER_BYTES = 8 * 1024 * 1024;\n/** Kept below the response destruction ceiling in server.ts. */\nexport const DEFAULT_REPLAY_BYTES = 3 * 1024 * 1024;\nexport const DEFAULT_EVENT_BYTES = 1024 * 1024;\n\ninterface SerializedEnvelope {\n\tenvelope: EventEnvelope;\n\tframe: string;\n\tbytes: number;\n\t/** Present only for barriers synthesized by this hub, never runtime data. */\n\tresyncReason?: string;\n}\n\nfunction omit<T extends Record<string, unknown>>(event: T, ...keys: string[]): Record<string, unknown> {\n\tconst copy = { ...event };\n\tfor (const key of keys) delete copy[key];\n\treturn copy;\n}\n\n/**\n * Dashboard-only transport projection. Each removed field is cumulative data\n * that the dashboard reducer does not read. Unknown event types are returned\n * exactly as received so extensions and future runtimes remain forward-safe.\n */\nexport function projectDashboardEvent(event: Record<string, unknown>): Record<string, unknown> {\n\tswitch (event.type) {\n\t\tcase \"agent_end\":\n\t\t\treturn omit(event, \"messages\");\n\t\tcase \"turn_end\":\n\t\t\treturn omit(event, \"message\", \"toolResults\");\n\t\tcase \"message_update\":\n\t\t\treturn omit(event, \"message\");\n\t\tcase \"tool_execution_update\":\n\t\t\treturn omit(event, \"args\");\n\t\tcase \"stream_retry\":\n\t\t\treturn omit(event, \"discardedPartial\");\n\t\tcase \"length_retry\":\n\t\t\treturn omit(event, \"discardedPartial\");\n\t\tcase \"background_agent_event\": {\n\t\t\tconst child = event.event;\n\t\t\treturn child && typeof child === \"object\" && !Array.isArray(child)\n\t\t\t\t? { ...event, event: projectDashboardEvent(child as Record<string, unknown>) }\n\t\t\t\t: event;\n\t\t}\n\t\tdefault:\n\t\t\treturn event;\n\t}\n}\n\nexport class EventHub {\n\tprivate seq = 0;\n\tprivate bufferedBytes = 0;\n\tprivate readonly buffer: SerializedEnvelope[] = [];\n\tprivate readonly clients = new Set<SseClient>();\n\tprivate readonly options: Required<EventHubOptions>;\n\tprivate eventProjector?: (key: string, event: Record<string, unknown>) => Record<string, unknown>;\n\n\tconstructor(options: number | EventHubOptions = {}) {\n\t\tthis.options = {\n\t\t\tbufferSize: typeof options === \"number\" ? options : (options.bufferSize ?? DEFAULT_BUFFER_SIZE),\n\t\t\tbufferBytes:\n\t\t\t\ttypeof options === \"number\" ? DEFAULT_BUFFER_BYTES : (options.bufferBytes ?? DEFAULT_BUFFER_BYTES),\n\t\t\treplayBytes:\n\t\t\t\ttypeof options === \"number\" ? DEFAULT_REPLAY_BYTES : (options.replayBytes ?? DEFAULT_REPLAY_BYTES),\n\t\t\teventBytes: typeof options === \"number\" ? DEFAULT_EVENT_BYTES : (options.eventBytes ?? DEFAULT_EVENT_BYTES),\n\t\t};\n\t}\n\n\t/** Install the server-owned browser projection before frame sizing/replay retention. */\n\tsetEventProjector(projector: (key: string, event: Record<string, unknown>) => Record<string, unknown>): void {\n\t\tthis.eventProjector = projector;\n\t}\n\n\t/** Publish an event from a runtime; assigns a sequence number and fans out. */\n\tpublish(key: string, rawEvent: Record<string, unknown>): EventEnvelope {\n\t\tconst baseProjection = projectDashboardEvent(rawEvent);\n\t\tconst event = this.eventProjector ? this.eventProjector(key, baseProjection) : baseProjection;\n\t\tconst serialized = this.serialize(this.seq + 1, key, event);\n\t\tif (serialized.bytes > this.options.eventBytes) {\n\t\t\treturn this.publishResync(\"oversized_event\").envelope;\n\t\t}\n\t\tthis.seq += 1;\n\t\tthis.retain(serialized);\n\t\tthis.fanout(serialized, \"live\");\n\t\treturn serialized.envelope;\n\t}\n\n\t/**\n\t * Attach a client. Replays only a complete, bounded range. A gap or replay\n\t * over budget receives a recovery frame only on this connection; it never\n\t * consumes a global sequence or disturbs healthy clients' ordered stream.\n\t */\n\tattach(client: SseClient, lastEventId?: number, onReplay?: (diagnostic: ReplayDiagnostic) => void): () => void {\n\t\tif (lastEventId !== undefined) {\n\t\t\tconst replay = this.replayAfter(lastEventId);\n\t\t\tif (!replay) {\n\t\t\t\tconst reason = this.resyncReason(lastEventId);\n\t\t\t\tconst barrier = this.targetedResync(reason);\n\t\t\t\tonReplay?.({ kind: \"resync\", count: 1, bytes: barrier.bytes, toSeq: barrier.envelope.seq, reason });\n\t\t\t\tif (!this.write(client, barrier, \"resync\")) return () => {};\n\t\t\t} else {\n\t\t\t\tconst bytes = replay.reduce((total, item) => total + item.bytes, 0);\n\t\t\t\tonReplay?.({\n\t\t\t\t\tkind: \"replay\",\n\t\t\t\t\tcount: replay.length,\n\t\t\t\t\tbytes,\n\t\t\t\t\tfromSeq: replay[0]?.envelope.seq,\n\t\t\t\t\ttoSeq: replay[replay.length - 1]?.envelope.seq,\n\t\t\t\t});\n\t\t\t\tfor (const serialized of replay) {\n\t\t\t\t\tif (!this.write(client, serialized, \"replay\")) return () => {};\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tthis.clients.add(client);\n\t\treturn () => {\n\t\t\tthis.clients.delete(client);\n\t\t};\n\t}\n\n\tget clientCount(): number {\n\t\treturn this.clients.size;\n\t}\n\n\tget historyBytes(): number {\n\t\treturn this.bufferedBytes;\n\t}\n\n\tget historyCount(): number {\n\t\treturn this.buffer.length;\n\t}\n\n\t/** Current sequence, captured without emitting, retaining, or fanning out. */\n\tget currentSequence(): number {\n\t\treturn this.seq;\n\t}\n\n\tprivate serialize(seq: number, key: string, event: Record<string, unknown>): SerializedEnvelope {\n\t\tconst envelope: EventEnvelope = { seq, key, event };\n\t\tconst frame = formatSseFrame(envelope);\n\t\treturn { envelope, frame, bytes: Buffer.byteLength(frame) };\n\t}\n\n\tprivate retain(serialized: SerializedEnvelope): void {\n\t\tthis.buffer.push(serialized);\n\t\tthis.bufferedBytes += serialized.bytes;\n\t\twhile (this.buffer.length > this.options.bufferSize || this.bufferedBytes > this.options.bufferBytes) {\n\t\t\tconst evicted = this.buffer.shift();\n\t\t\tif (!evicted) break;\n\t\t\tthis.bufferedBytes -= evicted.bytes;\n\t\t}\n\t}\n\n\tprivate replayAfter(lastEventId: number): SerializedEnvelope[] | undefined {\n\t\tconst oldest = this.buffer[0]?.envelope.seq;\n\t\tconst newest = this.buffer[this.buffer.length - 1]?.envelope.seq;\n\t\tif (oldest === undefined || newest === undefined || lastEventId < oldest - 1 || lastEventId > newest)\n\t\t\treturn undefined;\n\t\tconst replay = this.buffer.filter((item) => item.envelope.seq > lastEventId);\n\t\treturn replay.reduce((bytes, item) => bytes + item.bytes, 0) <= this.options.replayBytes ? replay : undefined;\n\t}\n\n\tprivate resyncReason(lastEventId: number): string {\n\t\tconst oldest = this.buffer[0]?.envelope.seq;\n\t\tconst newest = this.buffer[this.buffer.length - 1]?.envelope.seq;\n\t\tif (oldest === undefined) return \"empty_buffer\";\n\t\tif (lastEventId < oldest - 1 || lastEventId > newest!) return \"buffer_gap\";\n\t\treturn \"replay_over_budget\";\n\t}\n\n\t/** Explicit oversized events are globally unrecoverable, so retain and fan out their barrier. */\n\tprivate publishResync(reason: string): SerializedEnvelope {\n\t\tthis.seq += 1;\n\t\tconst barrier = this.serialize(this.seq, \"\", { type: \"dashboard_resync\", reason });\n\t\tbarrier.resyncReason = reason;\n\t\tthis.retain(barrier);\n\t\tthis.fanout(barrier, \"resync\");\n\t\treturn barrier;\n\t}\n\n\t/**\n\t * A stale reconnect needs the current ordering cursor, not a new global\n\t * event. With no events yet, establish sequence 1 so it has a usable cursor.\n\t */\n\tprivate targetedResync(reason: string): SerializedEnvelope {\n\t\tif (this.seq === 0) this.seq = 1;\n\t\tconst barrier = this.serialize(this.seq, \"\", { type: \"dashboard_resync\", reason });\n\t\tbarrier.resyncReason = reason;\n\t\treturn barrier;\n\t}\n\n\tprivate fanout(serialized: SerializedEnvelope, kind: SseWriteKind): void {\n\t\tfor (const client of this.clients) {\n\t\t\tif (!this.write(client, serialized, kind)) this.clients.delete(client);\n\t\t}\n\t}\n\n\tprivate write(client: SseClient, serialized: SerializedEnvelope, kind: SseWriteKind): boolean {\n\t\ttry {\n\t\t\treturn (\n\t\t\t\tclient.write(serialized.frame, {\n\t\t\t\t\tkind,\n\t\t\t\t\tseq: serialized.envelope.seq,\n\t\t\t\t\ttype: String(serialized.envelope.event.type ?? \"unknown\"),\n\t\t\t\t\tframeBytes: serialized.bytes,\n\t\t\t\t\t...(serialized.resyncReason ? { reason: serialized.resyncReason } : {}),\n\t\t\t\t}) !== false\n\t\t\t);\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t}\n}\n\n/** Format an envelope as an SSE frame with the sequence number as event id. */\nexport function formatSseFrame(envelope: EventEnvelope): string {\n\treturn `id: ${envelope.seq}\\ndata: ${JSON.stringify(envelope)}\\n\\n`;\n}\n\n/** Observable liveness signal; intentionally unnumbered and never buffered. */\nexport function formatHeartbeatFrame(): string {\n\treturn \"event: heartbeat\\ndata: {}\\n\\n\";\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"image-preview-worker.d.ts","sourceRoot":"","sources":["../../src/server/image-preview-worker.ts"],"names":[],"mappings":"AAiGA,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,UAAU,GAAG;IACxD,KAAK,EAAE,UAAU,CAAC;IAClB,QAAQ,EAAE,WAAW,GAAG,YAAY,CAAC;IACrC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CACf,CAoCA","sourcesContent":["import { parentPort } from \"node:worker_threads\";\nimport * as photon from \"@silvia-odwyer/photon-node\";\nimport { MAX_PREVIEW_BYTES, MAX_PREVIEW_HEIGHT, MAX_PREVIEW_WIDTH } from \"./image-preview.js\";\n\ntype PhotonImage = ReturnType<typeof photon.PhotonImage.new_from_byteslice>;\n\ninterface PreviewRequest {\n\tid: number;\n\tbytes: ArrayBuffer;\n\tmimeType: string;\n}\n\nfunction readExifOrientation(bytes: Uint8Array): number {\n\tlet tiff = -1;\n\tif (bytes.length >= 4 && bytes[0] === 0xff && bytes[1] === 0xd8) {\n\t\tlet offset = 2;\n\t\twhile (offset + 4 <= bytes.length && bytes[offset] === 0xff) {\n\t\t\tconst marker = bytes[offset + 1];\n\t\t\tconst length = (bytes[offset + 2] << 8) | bytes[offset + 3];\n\t\t\tif (marker === 0xe1 && offset + 10 <= bytes.length) {\n\t\t\t\tconst start = offset + 4;\n\t\t\t\tif (String.fromCharCode(...bytes.slice(start, start + 6)) === \"Exif\\0\\0\") tiff = start + 6;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif (length < 2) break;\n\t\t\toffset += 2 + length;\n\t\t}\n\t} else if (\n\t\tbytes.length >= 12 &&\n\t\tString.fromCharCode(...bytes.slice(0, 4)) === \"RIFF\" &&\n\t\tString.fromCharCode(...bytes.slice(8, 12)) === \"WEBP\"\n\t) {\n\t\tlet offset = 12;\n\t\twhile (offset + 8 <= bytes.length) {\n\t\t\tconst chunk = String.fromCharCode(...bytes.slice(offset, offset + 4));\n\t\t\tconst size =\n\t\t\t\tbytes[offset + 4] | (bytes[offset + 5] << 8) | (bytes[offset + 6] << 16) | (bytes[offset + 7] << 24);\n\t\t\tconst start = offset + 8;\n\t\t\tif (chunk === \"EXIF\") {\n\t\t\t\ttiff = String.fromCharCode(...bytes.slice(start, start + 6)) === \"Exif\\0\\0\" ? start + 6 : start;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\toffset = start + size + (size % 2);\n\t\t}\n\t}\n\tif (tiff < 0 || tiff + 8 > bytes.length) return 1;\n\tconst little = bytes[tiff] === 0x49 && bytes[tiff + 1] === 0x49;\n\tconst read16 = (at: number) => (little ? bytes[at] | (bytes[at + 1] << 8) : (bytes[at] << 8) | bytes[at + 1]);\n\tconst read32 = (at: number) =>\n\t\tlittle\n\t\t\t? (bytes[at] | (bytes[at + 1] << 8) | (bytes[at + 2] << 16) | (bytes[at + 3] << 24)) >>> 0\n\t\t\t: ((bytes[at] << 24) | (bytes[at + 1] << 16) | (bytes[at + 2] << 8) | bytes[at + 3]) >>> 0;\n\tconst ifd = tiff + read32(tiff + 4);\n\tif (ifd + 2 > bytes.length) return 1;\n\tconst count = read16(ifd);\n\tfor (let index = 0; index < count; index++) {\n\t\tconst entry = ifd + 2 + index * 12;\n\t\tif (entry + 12 > bytes.length) break;\n\t\tif (read16(entry) === 0x0112) {\n\t\t\tconst value = read16(entry + 8);\n\t\t\treturn value >= 1 && value <= 8 ? value : 1;\n\t\t}\n\t}\n\treturn 1;\n}\n\nfunction rotate(image: PhotonImage, clockwise: boolean): PhotonImage {\n\tconst width = image.get_width();\n\tconst height = image.get_height();\n\tconst source = image.get_raw_pixels();\n\tconst target = new Uint8Array(source.length);\n\tfor (let y = 0; y < height; y++) {\n\t\tfor (let x = 0; x < width; x++) {\n\t\t\tconst sourceIndex = (y * width + x) * 4;\n\t\t\tconst destinationPixel = clockwise ? x * height + (height - 1 - y) : (width - 1 - x) * height + y;\n\t\t\ttarget.set(source.slice(sourceIndex, sourceIndex + 4), destinationPixel * 4);\n\t\t}\n\t}\n\treturn new photon.PhotonImage(target, height, width);\n}\n\nfunction orient(image: PhotonImage, bytes: Uint8Array): PhotonImage {\n\tconst orientation = readExifOrientation(bytes);\n\tif (orientation === 2) photon.fliph(image);\n\telse if (orientation === 3) {\n\t\tphoton.fliph(image);\n\t\tphoton.flipv(image);\n\t} else if (orientation === 4) photon.flipv(image);\n\telse if (orientation >= 5) {\n\t\tconst rotated = rotate(image, orientation === 5 || orientation === 6);\n\t\timage.free();\n\t\timage = rotated;\n\t\tif (orientation === 5 || orientation === 7) photon.fliph(image);\n\t}\n\treturn image;\n}\n\nexport function generateImagePreview(bytes: Uint8Array): {\n\tbytes: Uint8Array;\n\tmimeType: \"image/png\" | \"image/jpeg\";\n\twidth: number;\n\theight: number;\n} {\n\tlet image = photon.PhotonImage.new_from_byteslice(bytes);\n\ttry {\n\t\timage = orient(image, bytes);\n\t\tconst originalWidth = image.get_width();\n\t\tconst originalHeight = image.get_height();\n\t\tif (originalWidth < 1 || originalHeight < 1) throw new Error(\"Image has invalid dimensions\");\n\t\tconst scale = Math.min(1, MAX_PREVIEW_WIDTH / originalWidth, MAX_PREVIEW_HEIGHT / originalHeight);\n\t\tlet width = Math.max(1, Math.round(originalWidth * scale));\n\t\tlet height = Math.max(1, Math.round(originalHeight * scale));\n\t\twhile (true) {\n\t\t\tconst resized = photon.resize(image, width, height, photon.SamplingFilter.Lanczos3);\n\t\t\ttry {\n\t\t\t\tconst candidates: Array<{ bytes: Uint8Array; mimeType: \"image/png\" | \"image/jpeg\" }> = [\n\t\t\t\t\t{ bytes: resized.get_bytes(), mimeType: \"image/png\" },\n\t\t\t\t\t...([85, 70, 55, 40] as const).map((quality) => ({\n\t\t\t\t\t\tbytes: resized.get_bytes_jpeg(quality),\n\t\t\t\t\t\tmimeType: \"image/jpeg\" as const,\n\t\t\t\t\t})),\n\t\t\t\t];\n\t\t\t\tconst acceptable = candidates.filter((candidate) => candidate.bytes.byteLength <= MAX_PREVIEW_BYTES);\n\t\t\t\tif (acceptable.length > 0) {\n\t\t\t\t\tacceptable.sort((left, right) => left.bytes.byteLength - right.bytes.byteLength);\n\t\t\t\t\treturn { ...acceptable[0]!, width, height };\n\t\t\t\t}\n\t\t\t} finally {\n\t\t\t\tresized.free();\n\t\t\t}\n\t\t\tif (width === 1 && height === 1) break;\n\t\t\twidth = Math.max(1, Math.floor(width * 0.75));\n\t\t\theight = Math.max(1, Math.floor(height * 0.75));\n\t\t}\n\t\tthrow new Error(\"Could not encode image within the 256 KiB preview limit\");\n\t} finally {\n\t\timage.free();\n\t}\n}\n\nconst port = parentPort;\nif (port) {\n\tport.on(\"message\", (request: PreviewRequest) => {\n\t\ttry {\n\t\t\tconst preview = generateImagePreview(new Uint8Array(request.bytes));\n\t\t\tconst transferable = Uint8Array.from(preview.bytes);\n\t\t\tport.postMessage({ id: request.id, ok: true, ...preview, bytes: transferable.buffer }, [transferable.buffer]);\n\t\t} catch (error) {\n\t\t\tport.postMessage({\n\t\t\t\tid: request.id,\n\t\t\t\tok: false,\n\t\t\t\terror: error instanceof Error ? error.message : String(error),\n\t\t\t});\n\t\t}\n\t});\n}\n"]}
|