@alvin0/ai-agent-sdk-observability-browser 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 alvin0 (chaulamdinhai) <chaulamdinhai@gmail.com>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,34 @@
1
+ # @alvin0/ai-agent-sdk-observability-browser
2
+
3
+ Runtime: **Browser** (IndexedDB and optional page lifecycle APIs).
4
+
5
+ ```sh
6
+ pnpm add @alvin0/ai-agent-sdk-core @alvin0/ai-agent-sdk-observability-browser
7
+ ```
8
+
9
+ Browser-only local durability for the structured observation bus. The exporter
10
+ stages privacy-processed events in IndexedDB during synchronous capture and
11
+ confirms `local-durable` only after the transaction commits at flush/checkpoint.
12
+
13
+ ```ts
14
+ import { createObservability } from '@alvin0/ai-agent-sdk-core/observability'
15
+ import { indexedDbObservationExporter } from '@alvin0/ai-agent-sdk-observability-browser'
16
+
17
+ const queue = indexedDbObservationExporter()
18
+
19
+ const observability = createObservability({
20
+ mode: 'reliable',
21
+ exporters: [{ exporter: queue, requirement: 'required', boundary: 'local-durable' }],
22
+ })
23
+ ```
24
+
25
+ The database defaults to `ai-agent-sdk-observability`, schema version 1, with
26
+ `events`, `batches`, and `meta` stores. Stored events remain unacknowledged until
27
+ the host calls `acknowledgeBatch()` after its own remote sink confirms delivery.
28
+ `recoverEvents()` exposes crash/reopen recovery without importing a network
29
+ exporter. `installBrowserObservabilityLifecycle()` is opt-in and flushes on
30
+ hidden visibility and `pagehide`; it makes no unload-durability claim.
31
+
32
+ Composition: `runtime.observability.exporters`. Lifecycle:
33
+ `explicit-owned-or-borrowed`; normal browser runtime composition registers this
34
+ exporter as owned at the `local-durable` boundary.
@@ -0,0 +1,75 @@
1
+ import { ObservationBoundary, ObservationEvent } from "@alvin0/ai-agent-sdk-core";
2
+ import { ExportAck, Observability, ObservationBatch, ObservationExportItem, ObservationExporter, ObservationExporterPlugin } from "@alvin0/ai-agent-sdk-core/observability";
3
+ //#region src/storage/indexeddb-exporter.d.ts
4
+ declare const BROWSER_OBSERVATION_ERROR_CODES: Readonly<{
5
+ readonly quota: 'OBSERVABILITY_BROWSER_QUOTA';
6
+ readonly unavailable: 'OBSERVABILITY_EXPORT_FAILED';
7
+ }>;
8
+ type BrowserObservationErrorCode = typeof BROWSER_OBSERVATION_ERROR_CODES[keyof typeof BROWSER_OBSERVATION_ERROR_CODES];
9
+ declare class BrowserObservationError extends Error {
10
+ readonly name = "BrowserObservationError";
11
+ readonly code: BrowserObservationErrorCode;
12
+ constructor(code: BrowserObservationErrorCode, message: string, options?: ErrorOptions);
13
+ }
14
+ interface IndexedDbObservationExporterOptions {
15
+ readonly id?: string;
16
+ readonly databaseName?: string;
17
+ readonly indexedDB?: IDBFactory;
18
+ readonly maxEvents?: number;
19
+ readonly maxBytes?: number;
20
+ readonly openTimeoutMs?: number;
21
+ }
22
+ interface BrowserQueueStats {
23
+ readonly eventCount: number;
24
+ readonly totalBytes: number;
25
+ readonly batchCount: number;
26
+ }
27
+ /** Browser-local durable exporter backed by IndexedDB schema version 1. */
28
+ declare class IndexedDbObservationExporter implements ObservationExporter {
29
+ readonly id: string;
30
+ readonly supportedBoundaries: readonly ObservationBoundary[];
31
+ private readonly maxEvents;
32
+ private readonly maxBytes;
33
+ private readonly databasePromise;
34
+ private database;
35
+ private readonly pendingStages;
36
+ private closing;
37
+ constructor(options?: IndexedDbObservationExporterOptions);
38
+ ready(): Promise<void>;
39
+ stage(event: ObservationEvent): Promise<void>;
40
+ protected stageItem(item: ObservationExportItem): Promise<void>;
41
+ export(batch: ObservationBatch, signal: AbortSignal): Promise<ExportAck>;
42
+ protected exportItems(batchId: string, createdAt: string, items: readonly ObservationExportItem[], signal: AbortSignal): Promise<void>;
43
+ recoverEvents(): Promise<readonly ObservationEvent[]>;
44
+ acknowledgeBatch(batchId: string): Promise<number>;
45
+ stats(): Promise<BrowserQueueStats>;
46
+ pendingBatchIds(): Promise<readonly string[]>;
47
+ shutdown(_signal: AbortSignal): Promise<void>;
48
+ private writeItem;
49
+ private evictForCapacity;
50
+ private assignBatch;
51
+ }
52
+ //#endregion
53
+ //#region src/lifecycle/browser.d.ts
54
+ interface LifecycleTarget {
55
+ readonly visibilityState?: string;
56
+ addEventListener(type: string, listener: EventListener): void;
57
+ removeEventListener(type: string, listener: EventListener): void;
58
+ }
59
+ interface BrowserLifecycleOptions {
60
+ readonly document?: LifecycleTarget;
61
+ readonly page?: LifecycleTarget;
62
+ readonly onFlushFailure?: (error: unknown) => void;
63
+ }
64
+ /**
65
+ * Opt into best-effort browser lifecycle flushing. Page lifecycle events do not
66
+ * provide a durability extension, so callers must inspect queue recovery later.
67
+ */
68
+ declare function installBrowserObservabilityLifecycle(observation: Pick<Observability, 'flush'>, options?: BrowserLifecycleOptions): () => void;
69
+ //#endregion
70
+ //#region src/runtime/indexeddb-plugin.d.ts
71
+ /** Inert runtime adapter; IndexedDB acquisition starts only in explicit readiness. */
72
+ declare function indexedDbObservationExporter(value?: IndexedDbObservationExporterOptions): ObservationExporterPlugin;
73
+ //#endregion
74
+ export { BROWSER_OBSERVATION_ERROR_CODES, type BrowserLifecycleOptions, BrowserObservationError, type BrowserObservationErrorCode, type BrowserQueueStats, IndexedDbObservationExporter, type IndexedDbObservationExporterOptions, indexedDbObservationExporter, installBrowserObservabilityLifecycle };
75
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/storage/indexeddb-exporter.ts","../src/lifecycle/browser.ts","../src/runtime/indexeddb-plugin.ts"],"mappings":";;;cAkBa,iCAA+B;WACnC;WACM;;KAGH,qCAAqC,6CAA6C;cAEjF,gCAAgC;WACzB;WACT,MAAM;EAEf,YAAY,MAAM,6BAA6B,iBAAiB,UAAU;;UAM3D;WACN;WACA;WACA,YAAY;WACZ;WACA;WACA;;UAGM;WACN;WACA;WACA;;;cAmLE,wCAAwC;WAC1C;WACA,8BAA8B;mBACtB;mBACA;mBACA;UACT;mBACS;UACT;EAER,YAAY,UAAS;EA0Bf,SAAS;EAIf,MAAM,OAAO,mBAAmB;YAItB,UAAU,MAAM,wBAAwB;EAoB5C,OAAO,OAAO,kBAAkB,QAAQ,cAAc,QAAQ;YAKpD,YACd,iBACA,mBACA,gBAAgB,yBAChB,QAAQ,cACP;EAQG,iBAAiB,iBAAiB;EAWlC,iBAAiB,kBAAkB;EAmCnC,SAAS,QAAQ;EAWjB,mBAAmB;EASnB,SAAS,SAAS,cAAc;UASxB;UA4DA;UAiDA;;;;UC1eN;WACC;EACT,iBAAiB,cAAc,UAAU;EACzC,oBAAoB,cAAc,UAAU;;UAG7B;WACN,WAAW;WACX,OAAO;WACP,kBAAkB;;;;;;iBAOb,qCACd,aAAa,KAAK,yBAClB,UAAS;;;;iBCoCK,6BACd,QAAQ,sCACP"}
package/dist/index.js ADDED
@@ -0,0 +1,560 @@
1
+ import { deepFreeze } from "@alvin0/ai-agent-sdk-core";
2
+ import { defineObservationExporter } from "@alvin0/ai-agent-sdk-core/observability";
3
+
4
+ //#region src/storage/indexeddb-exporter.ts
5
+ const DATABASE_VERSION = 1;
6
+ const EVENTS_STORE = "events";
7
+ const BATCHES_STORE = "batches";
8
+ const META_STORE = "meta";
9
+ const USAGE_KEY = "usage";
10
+ const DEFAULT_DATABASE_NAME = "ai-agent-sdk-observability";
11
+ const DEFAULT_MAX_EVENTS = 5e4;
12
+ const DEFAULT_MAX_BYTES = 67108864;
13
+ const DEFAULT_OPEN_TIMEOUT_MS = 1e4;
14
+ const BROWSER_OBSERVATION_ERROR_CODES = Object.freeze({
15
+ quota: "OBSERVABILITY_BROWSER_QUOTA",
16
+ unavailable: "OBSERVABILITY_EXPORT_FAILED"
17
+ });
18
+ var BrowserObservationError = class extends Error {
19
+ name = "BrowserObservationError";
20
+ code;
21
+ constructor(code, message, options) {
22
+ super(message, options);
23
+ this.code = code;
24
+ }
25
+ };
26
+ function positiveSafeInteger(value, field) {
27
+ if (!Number.isSafeInteger(value) || value < 1) throw new RangeError(`${field} must be a positive safe integer`);
28
+ return value;
29
+ }
30
+ function safeDatabaseName(value) {
31
+ const normalized = value.trim();
32
+ if (normalized.length < 1 || normalized.length > 128) throw new TypeError("IndexedDB observation databaseName must contain 1-128 characters");
33
+ return normalized;
34
+ }
35
+ function requestValue(request) {
36
+ return new Promise((resolve, reject) => {
37
+ request.onsuccess = () => resolve(request.result);
38
+ request.onerror = () => reject(request.error ?? /* @__PURE__ */ new Error("IndexedDB request failed"));
39
+ });
40
+ }
41
+ function transactionDone(transaction) {
42
+ return new Promise((resolve, reject) => {
43
+ transaction.oncomplete = () => resolve();
44
+ transaction.onabort = () => reject(transaction.error ?? new DOMException("IndexedDB transaction aborted", "AbortError"));
45
+ transaction.onerror = () => {};
46
+ });
47
+ }
48
+ function mapDatabaseError(error, message) {
49
+ if ((typeof error === "object" && error !== null ? Reflect.get(error, "name") : void 0) === "QuotaExceededError") return new BrowserObservationError(BROWSER_OBSERVATION_ERROR_CODES.quota, "browser observation quota exceeded");
50
+ if (error instanceof BrowserObservationError) return error;
51
+ return new BrowserObservationError(BROWSER_OBSERVATION_ERROR_CODES.unavailable, message);
52
+ }
53
+ function raceAbort(pending, signal) {
54
+ if (signal.aborted) return Promise.reject(signal.reason ?? new DOMException("browser observation aborted", "AbortError"));
55
+ return new Promise((resolve, reject) => {
56
+ const abort = () => {
57
+ cleanup();
58
+ reject(signal.reason ?? new DOMException("browser observation aborted", "AbortError"));
59
+ };
60
+ const cleanup = () => signal.removeEventListener("abort", abort);
61
+ signal.addEventListener("abort", abort, { once: true });
62
+ pending.then((value) => {
63
+ cleanup();
64
+ resolve(value);
65
+ }, (error) => {
66
+ cleanup();
67
+ reject(error);
68
+ });
69
+ });
70
+ }
71
+ function hasSchema(database) {
72
+ return database.objectStoreNames.contains(EVENTS_STORE) && database.objectStoreNames.contains(BATCHES_STORE) && database.objectStoreNames.contains(META_STORE);
73
+ }
74
+ function createSchema(database) {
75
+ const events = database.createObjectStore(EVENTS_STORE, { keyPath: ["runId", "sequence"] });
76
+ events.createIndex("eventId", "eventId", { unique: true });
77
+ events.createIndex("priorityOrder", ["priority", "order"]);
78
+ events.createIndex("order", "order", { unique: true });
79
+ events.createIndex("batchId", "batchId");
80
+ database.createObjectStore(BATCHES_STORE, { keyPath: "batchId" }).createIndex("order", "order", { unique: true });
81
+ database.createObjectStore(META_STORE, { keyPath: "key" });
82
+ }
83
+ function openDatabase(factory, name, timeoutMs) {
84
+ return new Promise((resolve, reject) => {
85
+ let settled = false;
86
+ let blocked = false;
87
+ const request = factory.open(name, DATABASE_VERSION);
88
+ const timer = setTimeout(() => finishError(new BrowserObservationError(BROWSER_OBSERVATION_ERROR_CODES.unavailable, "browser observation database open timed out")), timeoutMs);
89
+ const finishError = (error) => {
90
+ if (settled) return;
91
+ settled = true;
92
+ clearTimeout(timer);
93
+ reject(mapDatabaseError(error, blocked ? "browser observation database upgrade is blocked" : "browser observation database is unavailable"));
94
+ };
95
+ request.onblocked = () => {
96
+ blocked = true;
97
+ finishError(new BrowserObservationError(BROWSER_OBSERVATION_ERROR_CODES.unavailable, "browser observation database upgrade is blocked"));
98
+ };
99
+ request.onerror = () => finishError(request.error);
100
+ request.onupgradeneeded = (event) => {
101
+ try {
102
+ if (event.oldVersion === 0) createSchema(request.result);
103
+ } catch (error) {
104
+ try {
105
+ request.transaction?.abort();
106
+ } catch {}
107
+ finishError(error);
108
+ }
109
+ };
110
+ request.onsuccess = () => {
111
+ const database = request.result;
112
+ if (settled) {
113
+ database.close();
114
+ return;
115
+ }
116
+ if (!hasSchema(database)) {
117
+ database.close();
118
+ finishError(new BrowserObservationError(BROWSER_OBSERVATION_ERROR_CODES.unavailable, "browser observation database schema is invalid"));
119
+ return;
120
+ }
121
+ settled = true;
122
+ clearTimeout(timer);
123
+ database.onversionchange = () => database.close();
124
+ resolve(database);
125
+ };
126
+ });
127
+ }
128
+ function defaultUsage() {
129
+ return {
130
+ key: USAGE_KEY,
131
+ eventCount: 0,
132
+ totalBytes: 0,
133
+ nextOrder: 1
134
+ };
135
+ }
136
+ function serializedBytes(value) {
137
+ return new TextEncoder().encode(value).byteLength;
138
+ }
139
+ function itemIdentity(item) {
140
+ if ("eventId" in item) return {
141
+ itemKind: "event",
142
+ runId: item.correlation.runId,
143
+ sequence: item.sequence,
144
+ id: item.eventId,
145
+ priority: item.priority
146
+ };
147
+ return {
148
+ itemKind: "run-record",
149
+ runId: item.runId,
150
+ sequence: -1,
151
+ id: `run:${item.runId}`,
152
+ priority: "critical"
153
+ };
154
+ }
155
+ /** Browser-local durable exporter backed by IndexedDB schema version 1. */
156
+ var IndexedDbObservationExporter = class {
157
+ id;
158
+ supportedBoundaries = Object.freeze(["local-durable"]);
159
+ maxEvents;
160
+ maxBytes;
161
+ databasePromise;
162
+ database;
163
+ pendingStages = /* @__PURE__ */ new Map();
164
+ closing = false;
165
+ constructor(options = {}) {
166
+ this.id = options.id ?? "indexeddb";
167
+ if (!/^[A-Za-z0-9](?:[A-Za-z0-9._-]{0,63})$/.test(this.id)) throw new TypeError("IndexedDB observation exporter id must be a safe 1-64 character identifier");
168
+ this.maxEvents = positiveSafeInteger(options.maxEvents ?? DEFAULT_MAX_EVENTS, "maxEvents");
169
+ this.maxBytes = positiveSafeInteger(options.maxBytes ?? DEFAULT_MAX_BYTES, "maxBytes");
170
+ const factory = options.indexedDB ?? globalThis.indexedDB;
171
+ if (factory === void 0 || typeof factory.open !== "function") throw new TypeError("IndexedDB observation exporter requires indexedDB");
172
+ const databaseName = safeDatabaseName(options.databaseName ?? DEFAULT_DATABASE_NAME);
173
+ const timeoutMs = positiveSafeInteger(options.openTimeoutMs ?? DEFAULT_OPEN_TIMEOUT_MS, "openTimeoutMs");
174
+ this.databasePromise = openDatabase(factory, databaseName, timeoutMs).then((database) => {
175
+ if (this.closing) {
176
+ database.close();
177
+ throw new BrowserObservationError(BROWSER_OBSERVATION_ERROR_CODES.unavailable, "browser observation exporter is closed");
178
+ }
179
+ this.database = database;
180
+ return database;
181
+ });
182
+ this.databasePromise.catch(() => void 0);
183
+ }
184
+ async ready() {
185
+ await this.databasePromise;
186
+ }
187
+ stage(event) {
188
+ return this.stageItem(event);
189
+ }
190
+ stageItem(item) {
191
+ if (this.closing) throw new BrowserObservationError(BROWSER_OBSERVATION_ERROR_CODES.unavailable, "browser observation exporter is closed");
192
+ const identity = itemIdentity(item);
193
+ const payloadJson = JSON.stringify(item);
194
+ const existing = this.pendingStages.get(identity.id);
195
+ if (existing !== void 0) {
196
+ if (existing.payloadJson !== payloadJson) throw new BrowserObservationError(BROWSER_OBSERVATION_ERROR_CODES.unavailable, "duplicate browser observation identity has different data");
197
+ return existing.promise;
198
+ }
199
+ const start = (database) => this.writeItem(database, item, identity, payloadJson);
200
+ const promise = this.database === void 0 ? this.databasePromise.then(start) : start(this.database);
201
+ this.pendingStages.set(identity.id, {
202
+ payloadJson,
203
+ promise
204
+ });
205
+ promise.catch(() => void 0);
206
+ return promise;
207
+ }
208
+ async export(batch, signal) {
209
+ await this.exportItems(batch.batchId, batch.createdAt, batch.events, signal);
210
+ return deepFreeze({
211
+ batchId: batch.batchId,
212
+ accepted: true,
213
+ retryable: false
214
+ });
215
+ }
216
+ async exportItems(batchId, createdAt, items, signal) {
217
+ if (signal.aborted) throw signal.reason ?? new DOMException("browser observation export aborted", "AbortError");
218
+ await raceAbort(Promise.all(items.map((item) => this.stageItem(item))).then(() => void 0), signal);
219
+ const database = await raceAbort(this.databasePromise, signal);
220
+ await raceAbort(this.assignBatch(database, batchId, createdAt, items), signal);
221
+ for (const item of items) this.pendingStages.delete(itemIdentity(item).id);
222
+ }
223
+ async recoverEvents() {
224
+ const transaction = (await this.databasePromise).transaction(EVENTS_STORE, "readonly");
225
+ const done = transactionDone(transaction);
226
+ const records = await requestValue(transaction.objectStore(EVENTS_STORE).index("order").getAll());
227
+ await done;
228
+ return deepFreeze(records.filter((record) => record.itemKind === void 0 || record.itemKind === "event").map((record) => JSON.parse(record.payloadJson)));
229
+ }
230
+ async acknowledgeBatch(batchId) {
231
+ if (typeof batchId !== "string" || batchId.length === 0) throw new TypeError("batchId must be non-empty");
232
+ const transaction = (await this.databasePromise).transaction([
233
+ EVENTS_STORE,
234
+ BATCHES_STORE,
235
+ META_STORE
236
+ ], "readwrite");
237
+ const done = transactionDone(transaction);
238
+ try {
239
+ const events = transaction.objectStore(EVENTS_STORE);
240
+ const batches = transaction.objectStore(BATCHES_STORE);
241
+ const meta = transaction.objectStore(META_STORE);
242
+ const batch = await requestValue(batches.get(batchId));
243
+ if (batch === void 0) {
244
+ await done;
245
+ return 0;
246
+ }
247
+ const usage = await requestValue(meta.get(USAGE_KEY)) ?? defaultUsage();
248
+ let removed = 0;
249
+ for (const eventId of batch.eventIds) {
250
+ const record = await requestValue(events.index("eventId").get(eventId));
251
+ if (record === void 0 || record.batchId !== batchId) continue;
252
+ events.delete([record.runId, record.sequence]);
253
+ usage.eventCount--;
254
+ usage.totalBytes -= record.bytes;
255
+ removed++;
256
+ }
257
+ batches.delete(batchId);
258
+ meta.put(usage);
259
+ await done;
260
+ return removed;
261
+ } catch (error) {
262
+ try {
263
+ transaction.abort();
264
+ } catch {}
265
+ await done.catch(() => void 0);
266
+ throw mapDatabaseError(error, "browser observation acknowledgment failed");
267
+ }
268
+ }
269
+ async stats() {
270
+ const transaction = (await this.databasePromise).transaction([META_STORE, BATCHES_STORE], "readonly");
271
+ const done = transactionDone(transaction);
272
+ const usage = await requestValue(transaction.objectStore(META_STORE).get(USAGE_KEY)) ?? defaultUsage();
273
+ const batchCount = await requestValue(transaction.objectStore(BATCHES_STORE).count());
274
+ await done;
275
+ return deepFreeze({
276
+ eventCount: usage.eventCount,
277
+ totalBytes: usage.totalBytes,
278
+ batchCount
279
+ });
280
+ }
281
+ async pendingBatchIds() {
282
+ const transaction = (await this.databasePromise).transaction(BATCHES_STORE, "readonly");
283
+ const done = transactionDone(transaction);
284
+ const records = await requestValue(transaction.objectStore(BATCHES_STORE).index("order").getAll());
285
+ await done;
286
+ return Object.freeze(records.map((record) => record.batchId));
287
+ }
288
+ async shutdown(_signal) {
289
+ if (this.closing) return;
290
+ this.closing = true;
291
+ await Promise.allSettled([...this.pendingStages.values()].map((stage) => stage.promise));
292
+ (await this.databasePromise.catch(() => void 0))?.close();
293
+ this.database = void 0;
294
+ }
295
+ async writeItem(database, _item, identity, payloadJson) {
296
+ const transaction = database.transaction([
297
+ EVENTS_STORE,
298
+ BATCHES_STORE,
299
+ META_STORE
300
+ ], "readwrite");
301
+ const done = transactionDone(transaction);
302
+ try {
303
+ const events = transaction.objectStore(EVENTS_STORE);
304
+ const meta = transaction.objectStore(META_STORE);
305
+ const [sameId, sameKey, storedUsage] = await Promise.all([
306
+ requestValue(events.index("eventId").get(identity.id)),
307
+ requestValue(events.get([identity.runId, identity.sequence])),
308
+ requestValue(meta.get(USAGE_KEY))
309
+ ]);
310
+ const duplicate = sameId ?? sameKey;
311
+ if (duplicate !== void 0) {
312
+ if (duplicate.eventId !== identity.id || duplicate.payloadJson !== payloadJson || duplicate.runId !== identity.runId || duplicate.sequence !== identity.sequence) throw new BrowserObservationError(BROWSER_OBSERVATION_ERROR_CODES.unavailable, "duplicate browser observation identity has different data");
313
+ await done;
314
+ return;
315
+ }
316
+ const usage = storedUsage ?? defaultUsage();
317
+ const bytes = serializedBytes(payloadJson);
318
+ await this.evictForCapacity(transaction, usage, bytes);
319
+ if (usage.eventCount + 1 > this.maxEvents || usage.totalBytes + bytes > this.maxBytes) throw new BrowserObservationError(BROWSER_OBSERVATION_ERROR_CODES.quota, "browser observation capacity contains only protected critical events");
320
+ const record = {
321
+ itemKind: identity.itemKind,
322
+ runId: identity.runId,
323
+ sequence: identity.sequence,
324
+ eventId: identity.id,
325
+ priority: identity.priority,
326
+ order: usage.nextOrder,
327
+ bytes,
328
+ payloadJson
329
+ };
330
+ events.add(record);
331
+ usage.eventCount++;
332
+ usage.totalBytes += bytes;
333
+ usage.nextOrder++;
334
+ meta.put(usage);
335
+ await done;
336
+ } catch (error) {
337
+ try {
338
+ transaction.abort();
339
+ } catch {}
340
+ await done.catch(() => void 0);
341
+ throw mapDatabaseError(error, "browser observation staging failed");
342
+ }
343
+ }
344
+ async evictForCapacity(transaction, usage, incomingBytes) {
345
+ for (const priority of ["verbose", "normal"]) {
346
+ if (usage.eventCount + 1 <= this.maxEvents && usage.totalBytes + incomingBytes <= this.maxBytes) return;
347
+ await new Promise((resolve, reject) => {
348
+ const events = transaction.objectStore(EVENTS_STORE);
349
+ const batches = transaction.objectStore(BATCHES_STORE);
350
+ const range = IDBKeyRange.bound([priority, 0], [priority, Number.MAX_SAFE_INTEGER]);
351
+ const request = events.index("priorityOrder").openCursor(range);
352
+ request.onerror = () => reject(request.error ?? /* @__PURE__ */ new Error("IndexedDB eviction cursor failed"));
353
+ request.onsuccess = () => {
354
+ const cursor = request.result;
355
+ if (cursor === null || usage.eventCount + 1 <= this.maxEvents && usage.totalBytes + incomingBytes <= this.maxBytes) {
356
+ resolve();
357
+ return;
358
+ }
359
+ const record = cursor.value;
360
+ const deletion = cursor.delete();
361
+ deletion.onerror = () => reject(deletion.error ?? /* @__PURE__ */ new Error("IndexedDB event eviction failed"));
362
+ usage.eventCount--;
363
+ usage.totalBytes -= record.bytes;
364
+ if (record.batchId === void 0) {
365
+ cursor.continue();
366
+ return;
367
+ }
368
+ const batchRequest = batches.get(record.batchId);
369
+ batchRequest.onerror = () => reject(batchRequest.error ?? /* @__PURE__ */ new Error("IndexedDB batch eviction failed"));
370
+ batchRequest.onsuccess = () => {
371
+ const batch = batchRequest.result;
372
+ if (batch === void 0) {
373
+ cursor.continue();
374
+ return;
375
+ }
376
+ const eventIds = batch.eventIds.filter((eventId) => eventId !== record.eventId);
377
+ const update = eventIds.length === 0 ? batches.delete(batch.batchId) : batches.put({
378
+ ...batch,
379
+ eventIds
380
+ });
381
+ update.onerror = () => reject(update.error ?? /* @__PURE__ */ new Error("IndexedDB batch eviction update failed"));
382
+ update.onsuccess = () => cursor.continue();
383
+ };
384
+ };
385
+ });
386
+ }
387
+ }
388
+ async assignBatch(database, batchId, createdAt, items) {
389
+ const transaction = database.transaction([EVENTS_STORE, BATCHES_STORE], "readwrite");
390
+ const done = transactionDone(transaction);
391
+ try {
392
+ const events = transaction.objectStore(EVENTS_STORE);
393
+ const batches = transaction.objectStore(BATCHES_STORE);
394
+ const existing = await requestValue(batches.get(batchId));
395
+ if (existing !== void 0) {
396
+ const submittedIds = new Set(items.map((item) => itemIdentity(item).id));
397
+ if (!existing.eventIds.every((eventId) => submittedIds.has(eventId))) throw new BrowserObservationError(BROWSER_OBSERVATION_ERROR_CODES.unavailable, "duplicate browser observation batchId has different events");
398
+ await done;
399
+ return;
400
+ }
401
+ const records = [];
402
+ for (const item of items) {
403
+ const identity = itemIdentity(item);
404
+ const record = await requestValue(events.index("eventId").get(identity.id));
405
+ if (record === void 0) {
406
+ if (identity.priority === "critical") throw new BrowserObservationError(BROWSER_OBSERVATION_ERROR_CODES.unavailable, "critical browser observation event was not durably staged");
407
+ continue;
408
+ }
409
+ records.push(record);
410
+ }
411
+ if (records.length === 0) {
412
+ await done;
413
+ return;
414
+ }
415
+ const record = {
416
+ batchId,
417
+ createdAt,
418
+ eventIds: records.map((event) => event.eventId),
419
+ order: Math.min(...records.map((event) => event.order))
420
+ };
421
+ batches.add(record);
422
+ for (const event of records) events.put({
423
+ ...event,
424
+ batchId
425
+ });
426
+ await done;
427
+ } catch (error) {
428
+ try {
429
+ transaction.abort();
430
+ } catch {}
431
+ await done.catch(() => void 0);
432
+ throw mapDatabaseError(error, "browser observation batch commit failed");
433
+ }
434
+ }
435
+ };
436
+
437
+ //#endregion
438
+ //#region src/lifecycle/browser.ts
439
+ /**
440
+ * Opt into best-effort browser lifecycle flushing. Page lifecycle events do not
441
+ * provide a durability extension, so callers must inspect queue recovery later.
442
+ */
443
+ function installBrowserObservabilityLifecycle(observation, options = {}) {
444
+ if (typeof observation?.flush !== "function") throw new TypeError("browser lifecycle requires observability.flush");
445
+ const documentTarget = options.document ?? globalThis.document;
446
+ const pageTarget = options.page ?? globalThis.window;
447
+ if (documentTarget === void 0 || pageTarget === void 0) throw new TypeError("browser lifecycle requires document and window event targets");
448
+ let disposed = false;
449
+ let pending;
450
+ const flush = () => {
451
+ if (disposed || pending !== void 0) return;
452
+ try {
453
+ pending = observation.flush();
454
+ pending.catch((error) => {
455
+ try {
456
+ options.onFlushFailure?.(error);
457
+ } catch {}
458
+ }).finally(() => {
459
+ pending = void 0;
460
+ });
461
+ } catch (error) {
462
+ try {
463
+ options.onFlushFailure?.(error);
464
+ } catch {}
465
+ }
466
+ };
467
+ const visibility = () => {
468
+ if (documentTarget.visibilityState === "hidden") flush();
469
+ };
470
+ documentTarget.addEventListener("visibilitychange", visibility);
471
+ pageTarget.addEventListener("pagehide", flush);
472
+ return () => {
473
+ if (disposed) return;
474
+ disposed = true;
475
+ documentTarget.removeEventListener("visibilitychange", visibility);
476
+ pageTarget.removeEventListener("pagehide", flush);
477
+ };
478
+ }
479
+
480
+ //#endregion
481
+ //#region src/runtime/indexeddb-plugin.ts
482
+ var RuntimeIndexedDbExporter = class extends IndexedDbObservationExporter {
483
+ stageRuntime(item) {
484
+ return this.stageItem(item);
485
+ }
486
+ async exportRuntime(batch, signal) {
487
+ const items = [...batch.events, ...batch.runRecords];
488
+ const createdAt = batch.events[0]?.occurredAt ?? batch.runRecords[0]?.endedAt ?? (/* @__PURE__ */ new Date()).toISOString();
489
+ await this.exportItems(batch.id, createdAt, items, signal);
490
+ return Object.freeze({
491
+ batchId: batch.id,
492
+ acceptedEventIds: batch.events.map((event) => event.eventId),
493
+ acceptedRunIds: batch.runRecords.map((record) => record.runId)
494
+ });
495
+ }
496
+ };
497
+ function captureOptions(value) {
498
+ if (value !== void 0 && (typeof value !== "object" || value === null)) throw new TypeError("IndexedDB observation exporter options must be an object");
499
+ const options = value ?? {};
500
+ return Object.freeze({
501
+ ...options.id === void 0 ? {} : { id: options.id },
502
+ ...options.databaseName === void 0 ? {} : { databaseName: options.databaseName },
503
+ ...options.indexedDB === void 0 ? {} : { indexedDB: options.indexedDB },
504
+ ...options.maxEvents === void 0 ? {} : { maxEvents: options.maxEvents },
505
+ ...options.maxBytes === void 0 ? {} : { maxBytes: options.maxBytes },
506
+ ...options.openTimeoutMs === void 0 ? {} : { openTimeoutMs: options.openTimeoutMs }
507
+ });
508
+ }
509
+ function abortable(pending, signal) {
510
+ if (signal.aborted) return Promise.reject(signal.reason ?? new DOMException("browser observation aborted", "AbortError"));
511
+ return new Promise((resolve, reject) => {
512
+ const abort = () => {
513
+ cleanup();
514
+ reject(signal.reason ?? new DOMException("browser observation aborted", "AbortError"));
515
+ };
516
+ const cleanup = () => signal.removeEventListener("abort", abort);
517
+ signal.addEventListener("abort", abort, { once: true });
518
+ pending.then((value) => {
519
+ cleanup();
520
+ resolve(value);
521
+ }, (error) => {
522
+ cleanup();
523
+ reject(error);
524
+ });
525
+ });
526
+ }
527
+ /** Inert runtime adapter; IndexedDB acquisition starts only in explicit readiness. */
528
+ function indexedDbObservationExporter(value) {
529
+ const options = captureOptions(value);
530
+ const id = options.id ?? "indexeddb";
531
+ let exporter;
532
+ let readiness;
533
+ const required = () => {
534
+ if (exporter === void 0) throw new Error("IndexedDB runtime observation exporter is not ready");
535
+ return exporter;
536
+ };
537
+ return defineObservationExporter({
538
+ id,
539
+ supportedBoundaries: ["local-durable"],
540
+ async ready(signal) {
541
+ signal.throwIfAborted();
542
+ exporter ??= new RuntimeIndexedDbExporter(options);
543
+ readiness ??= exporter.ready();
544
+ await abortable(readiness, signal);
545
+ },
546
+ stage(item) {
547
+ return required().stageRuntime(item);
548
+ },
549
+ export(batch, signal) {
550
+ return required().exportRuntime(batch, signal);
551
+ },
552
+ async shutdown(signal) {
553
+ if (exporter !== void 0) await exporter.shutdown(signal);
554
+ }
555
+ });
556
+ }
557
+
558
+ //#endregion
559
+ export { BROWSER_OBSERVATION_ERROR_CODES, BrowserObservationError, IndexedDbObservationExporter, indexedDbObservationExporter, installBrowserObservabilityLifecycle };
560
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/storage/indexeddb-exporter.ts","../src/lifecycle/browser.ts","../src/runtime/indexeddb-plugin.ts"],"sourcesContent":["import { deepFreeze, type ObservationBoundary, type ObservationEvent } from '@alvin0/ai-agent-sdk-core'\nimport type {\n ExportAck,\n ObservationBatch,\n ObservationExporter,\n ObservationExportItem,\n} from '@alvin0/ai-agent-sdk-core/observability'\n\nconst DATABASE_VERSION = 1\nconst EVENTS_STORE = 'events'\nconst BATCHES_STORE = 'batches'\nconst META_STORE = 'meta'\nconst USAGE_KEY = 'usage'\nconst DEFAULT_DATABASE_NAME = 'ai-agent-sdk-observability'\nconst DEFAULT_MAX_EVENTS = 50_000\nconst DEFAULT_MAX_BYTES = 64 * 1024 * 1024\nconst DEFAULT_OPEN_TIMEOUT_MS = 10_000\n\nexport const BROWSER_OBSERVATION_ERROR_CODES = Object.freeze({\n quota: 'OBSERVABILITY_BROWSER_QUOTA',\n unavailable: 'OBSERVABILITY_EXPORT_FAILED',\n} as const)\n\nexport type BrowserObservationErrorCode = typeof BROWSER_OBSERVATION_ERROR_CODES[keyof typeof BROWSER_OBSERVATION_ERROR_CODES]\n\nexport class BrowserObservationError extends Error {\n override readonly name = 'BrowserObservationError'\n readonly code: BrowserObservationErrorCode\n\n constructor(code: BrowserObservationErrorCode, message: string, options?: ErrorOptions) {\n super(message, options)\n this.code = code\n }\n}\n\nexport interface IndexedDbObservationExporterOptions {\n readonly id?: string\n readonly databaseName?: string\n readonly indexedDB?: IDBFactory\n readonly maxEvents?: number\n readonly maxBytes?: number\n readonly openTimeoutMs?: number\n}\n\nexport interface BrowserQueueStats {\n readonly eventCount: number\n readonly totalBytes: number\n readonly batchCount: number\n}\n\ninterface UsageRecord {\n readonly key: typeof USAGE_KEY\n eventCount: number\n totalBytes: number\n nextOrder: number\n}\n\ninterface EventRecord {\n readonly itemKind: 'event' | 'run-record'\n readonly runId: string\n readonly sequence: number\n readonly eventId: string\n readonly priority: ObservationEvent['priority']\n readonly order: number\n readonly bytes: number\n readonly payloadJson: string\n batchId?: string\n}\n\ninterface BatchRecord {\n readonly batchId: string\n readonly createdAt: string\n readonly eventIds: readonly string[]\n readonly order: number\n}\n\ninterface PendingStage {\n readonly payloadJson: string\n readonly promise: Promise<void>\n}\n\nfunction positiveSafeInteger(value: number, field: string): number {\n if (!Number.isSafeInteger(value) || value < 1) throw new RangeError(`${field} must be a positive safe integer`)\n return value\n}\n\nfunction safeDatabaseName(value: string): string {\n const normalized = value.trim()\n if (normalized.length < 1 || normalized.length > 128) {\n throw new TypeError('IndexedDB observation databaseName must contain 1-128 characters')\n }\n return normalized\n}\n\nfunction requestValue<T>(request: IDBRequest<T>): Promise<T> {\n return new Promise<T>((resolve, reject) => {\n request.onsuccess = () => resolve(request.result)\n request.onerror = () => reject(request.error ?? new Error('IndexedDB request failed'))\n })\n}\n\nfunction transactionDone(transaction: IDBTransaction): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n transaction.oncomplete = () => resolve()\n transaction.onabort = () => reject(transaction.error ?? new DOMException('IndexedDB transaction aborted', 'AbortError'))\n transaction.onerror = () => { /* abort carries the terminal error */ }\n })\n}\n\nfunction mapDatabaseError(error: unknown, message: string): BrowserObservationError {\n const name = typeof error === 'object' && error !== null ? Reflect.get(error, 'name') : undefined\n if (name === 'QuotaExceededError') {\n return new BrowserObservationError(BROWSER_OBSERVATION_ERROR_CODES.quota, 'browser observation quota exceeded')\n }\n if (error instanceof BrowserObservationError) return error\n return new BrowserObservationError(BROWSER_OBSERVATION_ERROR_CODES.unavailable, message)\n}\n\nfunction raceAbort<T>(pending: Promise<T>, signal: AbortSignal): Promise<T> {\n if (signal.aborted) return Promise.reject(signal.reason ?? new DOMException('browser observation aborted', 'AbortError'))\n return new Promise<T>((resolve, reject) => {\n const abort = () => { cleanup(); reject(signal.reason ?? new DOMException('browser observation aborted', 'AbortError')) }\n const cleanup = () => signal.removeEventListener('abort', abort)\n signal.addEventListener('abort', abort, { once: true })\n void pending.then(\n value => { cleanup(); resolve(value) },\n error => { cleanup(); reject(error) },\n )\n })\n}\n\nfunction hasSchema(database: IDBDatabase): boolean {\n return database.objectStoreNames.contains(EVENTS_STORE)\n && database.objectStoreNames.contains(BATCHES_STORE)\n && database.objectStoreNames.contains(META_STORE)\n}\n\nfunction createSchema(database: IDBDatabase): void {\n const events = database.createObjectStore(EVENTS_STORE, { keyPath: ['runId', 'sequence'] })\n events.createIndex('eventId', 'eventId', { unique: true })\n events.createIndex('priorityOrder', ['priority', 'order'])\n events.createIndex('order', 'order', { unique: true })\n events.createIndex('batchId', 'batchId')\n const batches = database.createObjectStore(BATCHES_STORE, { keyPath: 'batchId' })\n batches.createIndex('order', 'order', { unique: true })\n database.createObjectStore(META_STORE, { keyPath: 'key' })\n}\n\nfunction openDatabase(factory: IDBFactory, name: string, timeoutMs: number): Promise<IDBDatabase> {\n return new Promise<IDBDatabase>((resolve, reject) => {\n let settled = false\n let blocked = false\n const request = factory.open(name, DATABASE_VERSION)\n const timer = setTimeout(() => finishError(new BrowserObservationError(\n BROWSER_OBSERVATION_ERROR_CODES.unavailable, 'browser observation database open timed out',\n )), timeoutMs)\n const finishError = (error: unknown) => {\n if (settled) return\n settled = true\n clearTimeout(timer)\n reject(mapDatabaseError(error, blocked\n ? 'browser observation database upgrade is blocked'\n : 'browser observation database is unavailable'))\n }\n request.onblocked = () => {\n blocked = true\n finishError(new BrowserObservationError(\n BROWSER_OBSERVATION_ERROR_CODES.unavailable, 'browser observation database upgrade is blocked',\n ))\n }\n request.onerror = () => finishError(request.error)\n request.onupgradeneeded = event => {\n try {\n if ((event as IDBVersionChangeEvent).oldVersion === 0) createSchema(request.result)\n } catch (error) {\n try { request.transaction?.abort() } catch { /* already aborted */ }\n finishError(error)\n }\n }\n request.onsuccess = () => {\n const database = request.result\n if (settled) {\n database.close()\n return\n }\n if (!hasSchema(database)) {\n database.close()\n finishError(new BrowserObservationError(\n BROWSER_OBSERVATION_ERROR_CODES.unavailable, 'browser observation database schema is invalid',\n ))\n return\n }\n settled = true\n clearTimeout(timer)\n database.onversionchange = () => database.close()\n resolve(database)\n }\n })\n}\n\nfunction defaultUsage(): UsageRecord {\n return { key: USAGE_KEY, eventCount: 0, totalBytes: 0, nextOrder: 1 }\n}\n\nfunction serializedBytes(value: string): number {\n return new TextEncoder().encode(value).byteLength\n}\n\ninterface ItemIdentity {\n readonly itemKind: EventRecord['itemKind']\n readonly runId: string\n readonly sequence: number\n readonly id: string\n readonly priority: ObservationEvent['priority']\n}\n\nfunction itemIdentity(item: ObservationExportItem): ItemIdentity {\n if ('eventId' in item) return {\n itemKind: 'event', runId: item.correlation.runId, sequence: item.sequence,\n id: item.eventId, priority: item.priority,\n }\n return { itemKind: 'run-record', runId: item.runId, sequence: -1,\n id: `run:${item.runId}`, priority: 'critical' }\n}\n\n/** Browser-local durable exporter backed by IndexedDB schema version 1. */\nexport class IndexedDbObservationExporter implements ObservationExporter {\n readonly id: string\n readonly supportedBoundaries: readonly ObservationBoundary[] = Object.freeze(['local-durable'])\n private readonly maxEvents: number\n private readonly maxBytes: number\n private readonly databasePromise: Promise<IDBDatabase>\n private database: IDBDatabase | undefined\n private readonly pendingStages = new Map<string, PendingStage>()\n private closing = false\n\n constructor(options: IndexedDbObservationExporterOptions = {}) {\n this.id = options.id ?? 'indexeddb'\n if (!/^[A-Za-z0-9](?:[A-Za-z0-9._-]{0,63})$/.test(this.id)) {\n throw new TypeError('IndexedDB observation exporter id must be a safe 1-64 character identifier')\n }\n this.maxEvents = positiveSafeInteger(options.maxEvents ?? DEFAULT_MAX_EVENTS, 'maxEvents')\n this.maxBytes = positiveSafeInteger(options.maxBytes ?? DEFAULT_MAX_BYTES, 'maxBytes')\n const factory = options.indexedDB ?? globalThis.indexedDB\n if (factory === undefined || typeof factory.open !== 'function') {\n throw new TypeError('IndexedDB observation exporter requires indexedDB')\n }\n const databaseName = safeDatabaseName(options.databaseName ?? DEFAULT_DATABASE_NAME)\n const timeoutMs = positiveSafeInteger(options.openTimeoutMs ?? DEFAULT_OPEN_TIMEOUT_MS, 'openTimeoutMs')\n this.databasePromise = openDatabase(factory, databaseName, timeoutMs).then(database => {\n if (this.closing) {\n database.close()\n throw new BrowserObservationError(\n BROWSER_OBSERVATION_ERROR_CODES.unavailable, 'browser observation exporter is closed',\n )\n }\n this.database = database\n return database\n })\n void this.databasePromise.catch(() => undefined)\n }\n\n async ready(): Promise<void> {\n await this.databasePromise\n }\n\n stage(event: ObservationEvent): Promise<void> {\n return this.stageItem(event)\n }\n\n protected stageItem(item: ObservationExportItem): Promise<void> {\n if (this.closing) throw new BrowserObservationError(\n BROWSER_OBSERVATION_ERROR_CODES.unavailable, 'browser observation exporter is closed',\n )\n const identity = itemIdentity(item)\n const payloadJson = JSON.stringify(item)\n const existing = this.pendingStages.get(identity.id)\n if (existing !== undefined) {\n if (existing.payloadJson !== payloadJson) throw new BrowserObservationError(\n BROWSER_OBSERVATION_ERROR_CODES.unavailable, 'duplicate browser observation identity has different data',\n )\n return existing.promise\n }\n const start = (database: IDBDatabase) => this.writeItem(database, item, identity, payloadJson)\n const promise = this.database === undefined ? this.databasePromise.then(start) : start(this.database)\n this.pendingStages.set(identity.id, { payloadJson, promise })\n void promise.catch(() => undefined)\n return promise\n }\n\n async export(batch: ObservationBatch, signal: AbortSignal): Promise<ExportAck> {\n await this.exportItems(batch.batchId, batch.createdAt, batch.events, signal)\n return deepFreeze({ batchId: batch.batchId, accepted: true, retryable: false })\n }\n\n protected async exportItems(\n batchId: string,\n createdAt: string,\n items: readonly ObservationExportItem[],\n signal: AbortSignal,\n ): Promise<void> {\n if (signal.aborted) throw signal.reason ?? new DOMException('browser observation export aborted', 'AbortError')\n await raceAbort(Promise.all(items.map(item => this.stageItem(item))).then(() => undefined), signal)\n const database = await raceAbort(this.databasePromise, signal)\n await raceAbort(this.assignBatch(database, batchId, createdAt, items), signal)\n for (const item of items) this.pendingStages.delete(itemIdentity(item).id)\n }\n\n async recoverEvents(): Promise<readonly ObservationEvent[]> {\n const database = await this.databasePromise\n const transaction = database.transaction(EVENTS_STORE, 'readonly')\n const done = transactionDone(transaction)\n const records = await requestValue<EventRecord[]>(transaction.objectStore(EVENTS_STORE).index('order').getAll())\n await done\n return deepFreeze(records\n .filter(record => record.itemKind === undefined || record.itemKind === 'event')\n .map(record => JSON.parse(record.payloadJson) as ObservationEvent))\n }\n\n async acknowledgeBatch(batchId: string): Promise<number> {\n if (typeof batchId !== 'string' || batchId.length === 0) throw new TypeError('batchId must be non-empty')\n const database = await this.databasePromise\n const transaction = database.transaction([EVENTS_STORE, BATCHES_STORE, META_STORE], 'readwrite')\n const done = transactionDone(transaction)\n try {\n const events = transaction.objectStore(EVENTS_STORE)\n const batches = transaction.objectStore(BATCHES_STORE)\n const meta = transaction.objectStore(META_STORE)\n const batch = await requestValue<BatchRecord | undefined>(batches.get(batchId))\n if (batch === undefined) {\n await done\n return 0\n }\n const usage = (await requestValue<UsageRecord | undefined>(meta.get(USAGE_KEY))) ?? defaultUsage()\n let removed = 0\n for (const eventId of batch.eventIds) {\n const record = await requestValue<EventRecord | undefined>(events.index('eventId').get(eventId))\n if (record === undefined || record.batchId !== batchId) continue\n events.delete([record.runId, record.sequence])\n usage.eventCount--\n usage.totalBytes -= record.bytes\n removed++\n }\n batches.delete(batchId)\n meta.put(usage)\n await done\n return removed\n } catch (error) {\n try { transaction.abort() } catch { /* already completed */ }\n await done.catch(() => undefined)\n throw mapDatabaseError(error, 'browser observation acknowledgment failed')\n }\n }\n\n async stats(): Promise<BrowserQueueStats> {\n const database = await this.databasePromise\n const transaction = database.transaction([META_STORE, BATCHES_STORE], 'readonly')\n const done = transactionDone(transaction)\n const usage = (await requestValue<UsageRecord | undefined>(transaction.objectStore(META_STORE).get(USAGE_KEY)))\n ?? defaultUsage()\n const batchCount = await requestValue(transaction.objectStore(BATCHES_STORE).count())\n await done\n return deepFreeze({ eventCount: usage.eventCount, totalBytes: usage.totalBytes, batchCount })\n }\n\n async pendingBatchIds(): Promise<readonly string[]> {\n const database = await this.databasePromise\n const transaction = database.transaction(BATCHES_STORE, 'readonly')\n const done = transactionDone(transaction)\n const records = await requestValue<BatchRecord[]>(transaction.objectStore(BATCHES_STORE).index('order').getAll())\n await done\n return Object.freeze(records.map(record => record.batchId))\n }\n\n async shutdown(_signal: AbortSignal): Promise<void> {\n if (this.closing) return\n this.closing = true\n await Promise.allSettled([...this.pendingStages.values()].map(stage => stage.promise))\n const database = await this.databasePromise.catch(() => undefined)\n database?.close()\n this.database = undefined\n }\n\n private async writeItem(\n database: IDBDatabase,\n _item: ObservationExportItem,\n identity: ItemIdentity,\n payloadJson: string,\n ): Promise<void> {\n const transaction = database.transaction([EVENTS_STORE, BATCHES_STORE, META_STORE], 'readwrite')\n const done = transactionDone(transaction)\n try {\n const events = transaction.objectStore(EVENTS_STORE)\n const meta = transaction.objectStore(META_STORE)\n const [sameId, sameKey, storedUsage] = await Promise.all([\n requestValue<EventRecord | undefined>(events.index('eventId').get(identity.id)),\n requestValue<EventRecord | undefined>(events.get([identity.runId, identity.sequence])),\n requestValue<UsageRecord | undefined>(meta.get(USAGE_KEY)),\n ])\n const duplicate = sameId ?? sameKey\n if (duplicate !== undefined) {\n if (duplicate.eventId !== identity.id || duplicate.payloadJson !== payloadJson\n || duplicate.runId !== identity.runId || duplicate.sequence !== identity.sequence) {\n throw new BrowserObservationError(\n BROWSER_OBSERVATION_ERROR_CODES.unavailable,\n 'duplicate browser observation identity has different data',\n )\n }\n await done\n return\n }\n const usage = storedUsage ?? defaultUsage()\n const bytes = serializedBytes(payloadJson)\n await this.evictForCapacity(transaction, usage, bytes)\n if (usage.eventCount + 1 > this.maxEvents || usage.totalBytes + bytes > this.maxBytes) {\n throw new BrowserObservationError(\n BROWSER_OBSERVATION_ERROR_CODES.quota,\n 'browser observation capacity contains only protected critical events',\n )\n }\n const record: EventRecord = {\n itemKind: identity.itemKind,\n runId: identity.runId,\n sequence: identity.sequence,\n eventId: identity.id,\n priority: identity.priority,\n order: usage.nextOrder,\n bytes,\n payloadJson,\n }\n events.add(record)\n usage.eventCount++\n usage.totalBytes += bytes\n usage.nextOrder++\n meta.put(usage)\n await done\n } catch (error) {\n try { transaction.abort() } catch { /* already completed */ }\n await done.catch(() => undefined)\n throw mapDatabaseError(error, 'browser observation staging failed')\n }\n }\n\n private async evictForCapacity(\n transaction: IDBTransaction,\n usage: UsageRecord,\n incomingBytes: number,\n ): Promise<void> {\n for (const priority of ['verbose', 'normal'] as const) {\n if (usage.eventCount + 1 <= this.maxEvents && usage.totalBytes + incomingBytes <= this.maxBytes) return\n await new Promise<void>((resolve, reject) => {\n const events = transaction.objectStore(EVENTS_STORE)\n const batches = transaction.objectStore(BATCHES_STORE)\n const range = IDBKeyRange.bound([priority, 0], [priority, Number.MAX_SAFE_INTEGER])\n const request = events.index('priorityOrder').openCursor(range)\n request.onerror = () => reject(request.error ?? new Error('IndexedDB eviction cursor failed'))\n request.onsuccess = () => {\n const cursor = request.result\n if (cursor === null || (usage.eventCount + 1 <= this.maxEvents\n && usage.totalBytes + incomingBytes <= this.maxBytes)) {\n resolve()\n return\n }\n const record = cursor.value as EventRecord\n const deletion = cursor.delete()\n deletion.onerror = () => reject(deletion.error ?? new Error('IndexedDB event eviction failed'))\n usage.eventCount--\n usage.totalBytes -= record.bytes\n if (record.batchId === undefined) {\n cursor.continue()\n return\n }\n const batchRequest = batches.get(record.batchId) as IDBRequest<BatchRecord | undefined>\n batchRequest.onerror = () => reject(batchRequest.error ?? new Error('IndexedDB batch eviction failed'))\n batchRequest.onsuccess = () => {\n const batch = batchRequest.result\n if (batch === undefined) {\n cursor.continue()\n return\n }\n const eventIds = batch.eventIds.filter(eventId => eventId !== record.eventId)\n const update = eventIds.length === 0\n ? batches.delete(batch.batchId)\n : batches.put({ ...batch, eventIds })\n update.onerror = () => reject(update.error ?? new Error('IndexedDB batch eviction update failed'))\n update.onsuccess = () => cursor.continue()\n }\n }\n })\n }\n }\n\n private async assignBatch(\n database: IDBDatabase,\n batchId: string,\n createdAt: string,\n items: readonly ObservationExportItem[],\n ): Promise<void> {\n const transaction = database.transaction([EVENTS_STORE, BATCHES_STORE], 'readwrite')\n const done = transactionDone(transaction)\n try {\n const events = transaction.objectStore(EVENTS_STORE)\n const batches = transaction.objectStore(BATCHES_STORE)\n const existing = await requestValue<BatchRecord | undefined>(batches.get(batchId))\n if (existing !== undefined) {\n const submittedIds = new Set(items.map(item => itemIdentity(item).id))\n if (!existing.eventIds.every(eventId => submittedIds.has(eventId))) {\n throw new BrowserObservationError(\n BROWSER_OBSERVATION_ERROR_CODES.unavailable, 'duplicate browser observation batchId has different events',\n )\n }\n await done\n return\n }\n const records: EventRecord[] = []\n for (const item of items) {\n const identity = itemIdentity(item)\n const record = await requestValue<EventRecord | undefined>(events.index('eventId').get(identity.id))\n if (record === undefined) {\n if (identity.priority === 'critical') throw new BrowserObservationError(\n BROWSER_OBSERVATION_ERROR_CODES.unavailable, 'critical browser observation event was not durably staged',\n )\n continue\n }\n records.push(record)\n }\n if (records.length === 0) {\n await done\n return\n }\n const record: BatchRecord = {\n batchId,\n createdAt,\n eventIds: records.map(event => event.eventId),\n order: Math.min(...records.map(event => event.order)),\n }\n batches.add(record)\n for (const event of records) events.put({ ...event, batchId })\n await done\n } catch (error) {\n try { transaction.abort() } catch { /* already completed */ }\n await done.catch(() => undefined)\n throw mapDatabaseError(error, 'browser observation batch commit failed')\n }\n }\n}\n","import type { Observability } from '@alvin0/ai-agent-sdk-core/observability'\n\ninterface LifecycleTarget {\n readonly visibilityState?: string\n addEventListener(type: string, listener: EventListener): void\n removeEventListener(type: string, listener: EventListener): void\n}\n\nexport interface BrowserLifecycleOptions {\n readonly document?: LifecycleTarget\n readonly page?: LifecycleTarget\n readonly onFlushFailure?: (error: unknown) => void\n}\n\n/**\n * Opt into best-effort browser lifecycle flushing. Page lifecycle events do not\n * provide a durability extension, so callers must inspect queue recovery later.\n */\nexport function installBrowserObservabilityLifecycle(\n observation: Pick<Observability, 'flush'>,\n options: BrowserLifecycleOptions = {},\n): () => void {\n if (typeof observation?.flush !== 'function') throw new TypeError('browser lifecycle requires observability.flush')\n const documentTarget = options.document ?? globalThis.document\n const pageTarget = options.page ?? globalThis.window\n if (documentTarget === undefined || pageTarget === undefined) {\n throw new TypeError('browser lifecycle requires document and window event targets')\n }\n let disposed = false\n let pending: Promise<unknown> | undefined\n const flush = () => {\n if (disposed || pending !== undefined) return\n try {\n pending = observation.flush()\n void pending.catch(error => {\n try { options.onFlushFailure?.(error) } catch { /* user callback is contained */ }\n }).finally(() => { pending = undefined })\n } catch (error) {\n try { options.onFlushFailure?.(error) } catch { /* user callback is contained */ }\n }\n }\n const visibility = () => {\n if (documentTarget.visibilityState === 'hidden') flush()\n }\n documentTarget.addEventListener('visibilitychange', visibility)\n pageTarget.addEventListener('pagehide', flush)\n return () => {\n if (disposed) return\n disposed = true\n documentTarget.removeEventListener('visibilitychange', visibility)\n pageTarget.removeEventListener('pagehide', flush)\n }\n}\n","import {\n defineObservationExporter,\n type ObservationDeliveryBatch,\n type ObservationExportItem,\n type ObservationExporterPlugin,\n} from '@alvin0/ai-agent-sdk-core/observability'\nimport {\n IndexedDbObservationExporter,\n type IndexedDbObservationExporterOptions,\n} from '../storage/indexeddb-exporter.ts'\n\nclass RuntimeIndexedDbExporter extends IndexedDbObservationExporter {\n stageRuntime(item: ObservationExportItem): Promise<void> {\n return this.stageItem(item)\n }\n\n async exportRuntime(batch: ObservationDeliveryBatch, signal: AbortSignal) {\n const items: readonly ObservationExportItem[] = [...batch.events, ...batch.runRecords]\n const createdAt = batch.events[0]?.occurredAt ?? batch.runRecords[0]?.endedAt ?? new Date().toISOString()\n await this.exportItems(batch.id, createdAt, items, signal)\n return Object.freeze({\n batchId: batch.id,\n acceptedEventIds: batch.events.map(event => event.eventId),\n acceptedRunIds: batch.runRecords.map(record => record.runId),\n })\n }\n}\n\nfunction captureOptions(\n value: IndexedDbObservationExporterOptions | undefined,\n): IndexedDbObservationExporterOptions {\n if (value !== undefined && (typeof value !== 'object' || value === null)) {\n throw new TypeError('IndexedDB observation exporter options must be an object')\n }\n const options = value ?? {}\n return Object.freeze({\n ...(options.id === undefined ? {} : { id: options.id }),\n ...(options.databaseName === undefined ? {} : { databaseName: options.databaseName }),\n ...(options.indexedDB === undefined ? {} : { indexedDB: options.indexedDB }),\n ...(options.maxEvents === undefined ? {} : { maxEvents: options.maxEvents }),\n ...(options.maxBytes === undefined ? {} : { maxBytes: options.maxBytes }),\n ...(options.openTimeoutMs === undefined ? {} : { openTimeoutMs: options.openTimeoutMs }),\n })\n}\n\nfunction abortable<T>(pending: Promise<T>, signal: AbortSignal): Promise<T> {\n if (signal.aborted) return Promise.reject(signal.reason ?? new DOMException('browser observation aborted', 'AbortError'))\n return new Promise<T>((resolve, reject) => {\n const abort = () => { cleanup(); reject(signal.reason ?? new DOMException('browser observation aborted', 'AbortError')) }\n const cleanup = () => signal.removeEventListener('abort', abort)\n signal.addEventListener('abort', abort, { once: true })\n void pending.then(value => { cleanup(); resolve(value) }, error => { cleanup(); reject(error) })\n })\n}\n\n/** Inert runtime adapter; IndexedDB acquisition starts only in explicit readiness. */\nexport function indexedDbObservationExporter(\n value?: IndexedDbObservationExporterOptions,\n): ObservationExporterPlugin {\n const options = captureOptions(value)\n const id = options.id ?? 'indexeddb'\n let exporter: RuntimeIndexedDbExporter | undefined\n let readiness: Promise<void> | undefined\n const required = (): RuntimeIndexedDbExporter => {\n if (exporter === undefined) throw new Error('IndexedDB runtime observation exporter is not ready')\n return exporter\n }\n return defineObservationExporter({\n id,\n supportedBoundaries: ['local-durable'],\n async ready(signal) {\n signal.throwIfAborted()\n exporter ??= new RuntimeIndexedDbExporter(options)\n readiness ??= exporter.ready()\n await abortable(readiness, signal)\n },\n stage(item) { return required().stageRuntime(item) },\n export(batch, signal) { return required().exportRuntime(batch, signal) },\n async shutdown(signal) {\n if (exporter !== undefined) await exporter.shutdown(signal)\n },\n })\n}\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "@alvin0/ai-agent-sdk-observability-browser",
3
+ "author": {
4
+ "name": "alvin0 - chaulamdinhai",
5
+ "email": "chaulamdinhai@gmail.com"
6
+ },
7
+ "version": "0.1.0",
8
+ "description": "IndexedDB durable observation queue and opt-in browser lifecycle integration",
9
+ "license": "MIT",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/alvin0/ai-agent-sdk.git",
13
+ "directory": "packages/observability-browser"
14
+ },
15
+ "homepage": "https://github.com/alvin0/ai-agent-sdk/tree/main/packages/observability-browser#readme",
16
+ "bugs": {
17
+ "url": "https://github.com/alvin0/ai-agent-sdk/issues"
18
+ },
19
+ "type": "module",
20
+ "sideEffects": false,
21
+ "files": [
22
+ "dist",
23
+ "README.md",
24
+ "LICENSE"
25
+ ],
26
+ "main": "./dist/index.js",
27
+ "types": "./dist/index.d.ts",
28
+ "exports": {
29
+ ".": {
30
+ "types": "./dist/index.d.ts",
31
+ "import": "./dist/index.js",
32
+ "default": "./dist/index.js"
33
+ },
34
+ "./package.json": "./package.json"
35
+ },
36
+ "publishConfig": {
37
+ "access": "public",
38
+ "provenance": true
39
+ },
40
+ "peerDependencies": {
41
+ "@alvin0/ai-agent-sdk-core": "^0.1.0"
42
+ },
43
+ "devDependencies": {
44
+ "@alvin0/ai-agent-sdk-core": "^0.1.0",
45
+ "@arethetypeswrong/cli": "0.18.5",
46
+ "playwright": "1.62.1",
47
+ "publint": "0.3.24",
48
+ "tsdown": "0.22.14",
49
+ "typescript": "7.0.2"
50
+ },
51
+ "aiAgentSdk": {
52
+ "runtime": "browser",
53
+ "coreApi": 1,
54
+ "roles": [
55
+ "observation-exporter"
56
+ ]
57
+ },
58
+ "scripts": {
59
+ "build": "tsdown",
60
+ "clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true});require('node:fs').rmSync('artifacts',{recursive:true,force:true})\"",
61
+ "typecheck": "tsc --noEmit",
62
+ "test": "pnpm build && node scripts/test-packed.mts",
63
+ "pack": "pnpm pack --pack-destination artifacts",
64
+ "test:pack": "node scripts/test-packed.mts",
65
+ "check:publint": "publint",
66
+ "check:types": "attw --profile esm-only --pack ."
67
+ }
68
+ }