@runtypelabs/persona 1.41.0 → 1.43.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.
@@ -0,0 +1,445 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { createEventBus } from "./events";
3
+ import type { AgentWidgetControllerEventMap } from "../types";
4
+
5
+ describe("Event Stream Controller Events", () => {
6
+ it("should emit eventStream:opened with timestamp", () => {
7
+ const eventBus = createEventBus<AgentWidgetControllerEventMap>();
8
+ const handler = vi.fn();
9
+ eventBus.on("eventStream:opened", handler);
10
+
11
+ const now = Date.now();
12
+ eventBus.emit("eventStream:opened", { timestamp: now });
13
+
14
+ expect(handler).toHaveBeenCalledOnce();
15
+ expect(handler).toHaveBeenCalledWith({ timestamp: now });
16
+ });
17
+
18
+ it("should emit eventStream:closed with timestamp", () => {
19
+ const eventBus = createEventBus<AgentWidgetControllerEventMap>();
20
+ const handler = vi.fn();
21
+ eventBus.on("eventStream:closed", handler);
22
+
23
+ const now = Date.now();
24
+ eventBus.emit("eventStream:closed", { timestamp: now });
25
+
26
+ expect(handler).toHaveBeenCalledOnce();
27
+ expect(handler).toHaveBeenCalledWith({ timestamp: now });
28
+ });
29
+
30
+ it("should allow unsubscription from eventStream events", () => {
31
+ const eventBus = createEventBus<AgentWidgetControllerEventMap>();
32
+ const handler = vi.fn();
33
+ const unsub = eventBus.on("eventStream:opened", handler);
34
+
35
+ eventBus.emit("eventStream:opened", { timestamp: Date.now() });
36
+ expect(handler).toHaveBeenCalledOnce();
37
+
38
+ unsub();
39
+ eventBus.emit("eventStream:opened", { timestamp: Date.now() });
40
+ expect(handler).toHaveBeenCalledOnce(); // still 1 - not called again
41
+ });
42
+
43
+ it("should not interfere with other controller events", () => {
44
+ const eventBus = createEventBus<AgentWidgetControllerEventMap>();
45
+ const openedHandler = vi.fn();
46
+ const closedHandler = vi.fn();
47
+ const widgetOpenedHandler = vi.fn();
48
+
49
+ eventBus.on("eventStream:opened", openedHandler);
50
+ eventBus.on("eventStream:closed", closedHandler);
51
+ eventBus.on("widget:opened", widgetOpenedHandler);
52
+
53
+ eventBus.emit("eventStream:opened", { timestamp: Date.now() });
54
+
55
+ expect(openedHandler).toHaveBeenCalledOnce();
56
+ expect(closedHandler).not.toHaveBeenCalled();
57
+ expect(widgetOpenedHandler).not.toHaveBeenCalled();
58
+ });
59
+
60
+ it("should support multiple listeners for the same event", () => {
61
+ const eventBus = createEventBus<AgentWidgetControllerEventMap>();
62
+ const handler1 = vi.fn();
63
+ const handler2 = vi.fn();
64
+ eventBus.on("eventStream:opened", handler1);
65
+ eventBus.on("eventStream:opened", handler2);
66
+
67
+ const now = Date.now();
68
+ eventBus.emit("eventStream:opened", { timestamp: now });
69
+
70
+ expect(handler1).toHaveBeenCalledWith({ timestamp: now });
71
+ expect(handler2).toHaveBeenCalledWith({ timestamp: now });
72
+ });
73
+ });
74
+
75
+ /**
76
+ * Tests for programmatic controller methods (showEventStream/hideEventStream/isEventStreamVisible).
77
+ * These simulate the controller method logic from ui.ts without requiring full widget DOM setup.
78
+ */
79
+ describe("Event Stream Controller Methods", () => {
80
+ it("showEventStream should call toggleEventStreamOn when feature is enabled", () => {
81
+ const eventBus = createEventBus<AgentWidgetControllerEventMap>();
82
+ let eventStreamVisible = false;
83
+ const showEventStreamToggle = true;
84
+ const eventStreamBuffer = { push: vi.fn(), getAll: () => [] }; // mock buffer
85
+
86
+ const toggleEventStreamOn = vi.fn(() => {
87
+ eventStreamVisible = true;
88
+ eventBus.emit("eventStream:opened", { timestamp: Date.now() });
89
+ });
90
+
91
+ // Simulates controller.showEventStream() logic
92
+ const showEventStream = () => {
93
+ if (!showEventStreamToggle || !eventStreamBuffer) return;
94
+ toggleEventStreamOn();
95
+ };
96
+
97
+ const handler = vi.fn();
98
+ eventBus.on("eventStream:opened", handler);
99
+
100
+ showEventStream();
101
+
102
+ expect(toggleEventStreamOn).toHaveBeenCalledOnce();
103
+ expect(handler).toHaveBeenCalledOnce();
104
+ expect(eventStreamVisible).toBe(true);
105
+ });
106
+
107
+ it("showEventStream should no-op when feature is disabled", () => {
108
+ const showEventStreamToggle = false;
109
+ const eventStreamBuffer = { push: vi.fn(), getAll: () => [] };
110
+ const toggleEventStreamOn = vi.fn();
111
+
112
+ const showEventStream = () => {
113
+ if (!showEventStreamToggle || !eventStreamBuffer) return;
114
+ toggleEventStreamOn();
115
+ };
116
+
117
+ showEventStream();
118
+ expect(toggleEventStreamOn).not.toHaveBeenCalled();
119
+ });
120
+
121
+ it("showEventStream should no-op when buffer is null", () => {
122
+ const showEventStreamToggle = true;
123
+ const eventStreamBuffer = null;
124
+ const toggleEventStreamOn = vi.fn();
125
+
126
+ const showEventStream = () => {
127
+ if (!showEventStreamToggle || !eventStreamBuffer) return;
128
+ toggleEventStreamOn();
129
+ };
130
+
131
+ showEventStream();
132
+ expect(toggleEventStreamOn).not.toHaveBeenCalled();
133
+ });
134
+
135
+ it("hideEventStream should call toggleEventStreamOff when visible", () => {
136
+ const eventBus = createEventBus<AgentWidgetControllerEventMap>();
137
+ let eventStreamVisible = true;
138
+
139
+ const toggleEventStreamOff = vi.fn(() => {
140
+ eventStreamVisible = false;
141
+ eventBus.emit("eventStream:closed", { timestamp: Date.now() });
142
+ });
143
+
144
+ const hideEventStream = () => {
145
+ if (!eventStreamVisible) return;
146
+ toggleEventStreamOff();
147
+ };
148
+
149
+ const handler = vi.fn();
150
+ eventBus.on("eventStream:closed", handler);
151
+
152
+ hideEventStream();
153
+
154
+ expect(toggleEventStreamOff).toHaveBeenCalledOnce();
155
+ expect(handler).toHaveBeenCalledOnce();
156
+ expect(eventStreamVisible).toBe(false);
157
+ });
158
+
159
+ it("hideEventStream should no-op when already hidden", () => {
160
+ const eventStreamVisible = false;
161
+ const toggleEventStreamOff = vi.fn();
162
+
163
+ const hideEventStream = () => {
164
+ if (!eventStreamVisible) return;
165
+ toggleEventStreamOff();
166
+ };
167
+
168
+ hideEventStream();
169
+ expect(toggleEventStreamOff).not.toHaveBeenCalled();
170
+ });
171
+
172
+ it("isEventStreamVisible should return current visibility state", () => {
173
+ let eventStreamVisible = false;
174
+
175
+ const isEventStreamVisible = () => eventStreamVisible;
176
+
177
+ expect(isEventStreamVisible()).toBe(false);
178
+
179
+ eventStreamVisible = true;
180
+ expect(isEventStreamVisible()).toBe(true);
181
+
182
+ eventStreamVisible = false;
183
+ expect(isEventStreamVisible()).toBe(false);
184
+ });
185
+
186
+ it("show then hide should fire both events in sequence", () => {
187
+ const eventBus = createEventBus<AgentWidgetControllerEventMap>();
188
+ let eventStreamVisible = false;
189
+ const showEventStreamToggle = true;
190
+ const eventStreamBuffer = { push: vi.fn() };
191
+ const events: string[] = [];
192
+
193
+ const toggleEventStreamOn = () => {
194
+ eventStreamVisible = true;
195
+ eventBus.emit("eventStream:opened", { timestamp: Date.now() });
196
+ };
197
+ const toggleEventStreamOff = () => {
198
+ if (!eventStreamVisible) return;
199
+ eventStreamVisible = false;
200
+ eventBus.emit("eventStream:closed", { timestamp: Date.now() });
201
+ };
202
+
203
+ const showEventStream = () => {
204
+ if (!showEventStreamToggle || !eventStreamBuffer) return;
205
+ toggleEventStreamOn();
206
+ };
207
+ const hideEventStream = () => {
208
+ if (!eventStreamVisible) return;
209
+ toggleEventStreamOff();
210
+ };
211
+
212
+ eventBus.on("eventStream:opened", () => events.push("opened"));
213
+ eventBus.on("eventStream:closed", () => events.push("closed"));
214
+
215
+ showEventStream();
216
+ hideEventStream();
217
+
218
+ expect(events).toEqual(["opened", "closed"]);
219
+ expect(eventStreamVisible).toBe(false);
220
+ });
221
+ });
222
+
223
+ /** Listener type compatible with EventTarget in Node test env (no DOM globals). */
224
+ type EventListenerLike = ((event: Event) => void) | { handleEvent(event: Event): void };
225
+
226
+ /**
227
+ * Minimal EventTarget-based mock for window with CustomEvent support.
228
+ * Used because the test environment is Node.js (no browser globals).
229
+ */
230
+ class MockWindow {
231
+ private target = new EventTarget();
232
+ addEventListener(type: string, listener: EventListenerLike) {
233
+ this.target.addEventListener(type, listener);
234
+ }
235
+ removeEventListener(type: string, listener: EventListenerLike) {
236
+ this.target.removeEventListener(type, listener);
237
+ }
238
+ dispatchEvent(event: Event): boolean {
239
+ return this.target.dispatchEvent(event);
240
+ }
241
+ }
242
+
243
+ /**
244
+ * Tests for instance-scoped window events (persona:showEventStream / persona:hideEventStream).
245
+ * These verify the CustomEvent dispatching and instance ID filtering logic from ui.ts.
246
+ */
247
+ describe("Event Stream Window Events", () => {
248
+ let mockWindow: MockWindow;
249
+ let cleanupFns: (() => void)[];
250
+
251
+ beforeEach(() => {
252
+ mockWindow = new MockWindow();
253
+ cleanupFns = [];
254
+ });
255
+
256
+ afterEach(() => {
257
+ cleanupFns.forEach(fn => fn());
258
+ });
259
+
260
+ /**
261
+ * Creates a mock controller with event stream window event listeners,
262
+ * mirroring the logic in ui.ts lines 3930-3950.
263
+ */
264
+ function createMockWidgetInstance(mountId: string, featureEnabled = true) {
265
+ let eventStreamVisible = false;
266
+ const eventBus = createEventBus<AgentWidgetControllerEventMap>();
267
+
268
+ const controller = {
269
+ showEventStream: vi.fn(() => {
270
+ if (!featureEnabled) return;
271
+ eventStreamVisible = true;
272
+ eventBus.emit("eventStream:opened", { timestamp: Date.now() });
273
+ }),
274
+ hideEventStream: vi.fn(() => {
275
+ if (!eventStreamVisible) return;
276
+ eventStreamVisible = false;
277
+ eventBus.emit("eventStream:closed", { timestamp: Date.now() });
278
+ }),
279
+ isEventStreamVisible: () => eventStreamVisible,
280
+ on: eventBus.on.bind(eventBus),
281
+ };
282
+
283
+ // Mirror the window event registration logic from ui.ts
284
+ if (featureEnabled) {
285
+ const instanceId = mountId || "persona-" + Math.random().toString(36).slice(2, 8);
286
+ const handleShowEvent = (e: Event) => {
287
+ const detail = (e as CustomEvent).detail;
288
+ if (!detail?.instanceId || detail.instanceId === instanceId) {
289
+ controller.showEventStream();
290
+ }
291
+ };
292
+ const handleHideEvent = (e: Event) => {
293
+ const detail = (e as CustomEvent).detail;
294
+ if (!detail?.instanceId || detail.instanceId === instanceId) {
295
+ controller.hideEventStream();
296
+ }
297
+ };
298
+ mockWindow.addEventListener("persona:showEventStream", handleShowEvent);
299
+ mockWindow.addEventListener("persona:hideEventStream", handleHideEvent);
300
+ cleanupFns.push(() => {
301
+ mockWindow.removeEventListener("persona:showEventStream", handleShowEvent);
302
+ mockWindow.removeEventListener("persona:hideEventStream", handleHideEvent);
303
+ });
304
+ }
305
+
306
+ return controller;
307
+ }
308
+
309
+ it("should open event stream via window event without instanceId", () => {
310
+ const ctrl = createMockWidgetInstance("persona-root");
311
+
312
+ mockWindow.dispatchEvent(new CustomEvent("persona:showEventStream"));
313
+
314
+ expect(ctrl.showEventStream).toHaveBeenCalledOnce();
315
+ expect(ctrl.isEventStreamVisible()).toBe(true);
316
+ });
317
+
318
+ it("should close event stream via window event without instanceId", () => {
319
+ const ctrl = createMockWidgetInstance("persona-root");
320
+
321
+ // First open it
322
+ mockWindow.dispatchEvent(new CustomEvent("persona:showEventStream"));
323
+ expect(ctrl.isEventStreamVisible()).toBe(true);
324
+
325
+ // Then close it
326
+ mockWindow.dispatchEvent(new CustomEvent("persona:hideEventStream"));
327
+ expect(ctrl.hideEventStream).toHaveBeenCalledOnce();
328
+ expect(ctrl.isEventStreamVisible()).toBe(false);
329
+ });
330
+
331
+ it("should respond to matching instanceId", () => {
332
+ const ctrl = createMockWidgetInstance("persona-root");
333
+
334
+ mockWindow.dispatchEvent(new CustomEvent("persona:showEventStream", {
335
+ detail: { instanceId: "persona-root" }
336
+ }));
337
+
338
+ expect(ctrl.showEventStream).toHaveBeenCalledOnce();
339
+ expect(ctrl.isEventStreamVisible()).toBe(true);
340
+ });
341
+
342
+ it("should NOT respond to non-matching instanceId", () => {
343
+ const ctrl = createMockWidgetInstance("persona-root");
344
+
345
+ mockWindow.dispatchEvent(new CustomEvent("persona:showEventStream", {
346
+ detail: { instanceId: "wrong-id" }
347
+ }));
348
+
349
+ expect(ctrl.showEventStream).not.toHaveBeenCalled();
350
+ expect(ctrl.isEventStreamVisible()).toBe(false);
351
+ });
352
+
353
+ it("should scope events to correct instance with multiple widgets", () => {
354
+ const ctrl1 = createMockWidgetInstance("widget-1");
355
+ const ctrl2 = createMockWidgetInstance("widget-2");
356
+
357
+ // Target only widget-1
358
+ mockWindow.dispatchEvent(new CustomEvent("persona:showEventStream", {
359
+ detail: { instanceId: "widget-1" }
360
+ }));
361
+
362
+ expect(ctrl1.showEventStream).toHaveBeenCalledOnce();
363
+ expect(ctrl2.showEventStream).not.toHaveBeenCalled();
364
+ expect(ctrl1.isEventStreamVisible()).toBe(true);
365
+ expect(ctrl2.isEventStreamVisible()).toBe(false);
366
+ });
367
+
368
+ it("should broadcast to all instances when no instanceId is provided", () => {
369
+ const ctrl1 = createMockWidgetInstance("widget-1");
370
+ const ctrl2 = createMockWidgetInstance("widget-2");
371
+
372
+ mockWindow.dispatchEvent(new CustomEvent("persona:showEventStream"));
373
+
374
+ expect(ctrl1.showEventStream).toHaveBeenCalledOnce();
375
+ expect(ctrl2.showEventStream).toHaveBeenCalledOnce();
376
+ expect(ctrl1.isEventStreamVisible()).toBe(true);
377
+ expect(ctrl2.isEventStreamVisible()).toBe(true);
378
+ });
379
+
380
+ it("should not register listeners when feature is disabled", () => {
381
+ const ctrl = createMockWidgetInstance("persona-root", false);
382
+
383
+ mockWindow.dispatchEvent(new CustomEvent("persona:showEventStream"));
384
+
385
+ expect(ctrl.showEventStream).not.toHaveBeenCalled();
386
+ expect(ctrl.isEventStreamVisible()).toBe(false);
387
+ });
388
+
389
+ it("should clean up window listeners on destroy", () => {
390
+ const ctrl = createMockWidgetInstance("persona-root");
391
+
392
+ // Verify it works before cleanup
393
+ mockWindow.dispatchEvent(new CustomEvent("persona:showEventStream"));
394
+ expect(ctrl.showEventStream).toHaveBeenCalledOnce();
395
+
396
+ // Simulate destroy by running cleanup
397
+ cleanupFns.forEach(fn => fn());
398
+ cleanupFns = [];
399
+
400
+ // Reset mock and dispatch again
401
+ ctrl.showEventStream.mockClear();
402
+ mockWindow.dispatchEvent(new CustomEvent("persona:showEventStream"));
403
+ expect(ctrl.showEventStream).not.toHaveBeenCalled();
404
+ });
405
+
406
+ it("should fire eventStream:opened event via controller.on when opened via window event", () => {
407
+ const ctrl = createMockWidgetInstance("persona-root");
408
+ const handler = vi.fn();
409
+ ctrl.on("eventStream:opened", handler);
410
+
411
+ mockWindow.dispatchEvent(new CustomEvent("persona:showEventStream"));
412
+
413
+ expect(handler).toHaveBeenCalledOnce();
414
+ expect(handler).toHaveBeenCalledWith(expect.objectContaining({ timestamp: expect.any(Number) }));
415
+ });
416
+
417
+ it("should fire eventStream:closed event via controller.on when closed via window event", () => {
418
+ const ctrl = createMockWidgetInstance("persona-root");
419
+ const closedHandler = vi.fn();
420
+ ctrl.on("eventStream:closed", closedHandler);
421
+
422
+ // Open first, then close
423
+ mockWindow.dispatchEvent(new CustomEvent("persona:showEventStream"));
424
+ mockWindow.dispatchEvent(new CustomEvent("persona:hideEventStream"));
425
+
426
+ expect(closedHandler).toHaveBeenCalledOnce();
427
+ expect(closedHandler).toHaveBeenCalledWith(expect.objectContaining({ timestamp: expect.any(Number) }));
428
+ });
429
+
430
+ it("should handle detail being null gracefully (broadcast)", () => {
431
+ const ctrl = createMockWidgetInstance("persona-root");
432
+
433
+ mockWindow.dispatchEvent(new CustomEvent("persona:showEventStream", { detail: null }));
434
+
435
+ expect(ctrl.showEventStream).toHaveBeenCalledOnce();
436
+ });
437
+
438
+ it("should handle detail with empty object gracefully (broadcast)", () => {
439
+ const ctrl = createMockWidgetInstance("persona-root");
440
+
441
+ mockWindow.dispatchEvent(new CustomEvent("persona:showEventStream", { detail: {} }));
442
+
443
+ expect(ctrl.showEventStream).toHaveBeenCalledOnce();
444
+ });
445
+ });
@@ -0,0 +1,181 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import "fake-indexeddb/auto";
3
+ import { EventStreamStore } from "./event-stream-store";
4
+ import type { SSEEventRecord } from "../types";
5
+
6
+ function makeEvent(type: string, index: number): SSEEventRecord {
7
+ return {
8
+ id: `evt-${index}`,
9
+ type,
10
+ timestamp: 1000 + index,
11
+ payload: JSON.stringify({ index })
12
+ };
13
+ }
14
+
15
+ describe("EventStreamStore", () => {
16
+ let store: EventStreamStore;
17
+
18
+ beforeEach(async () => {
19
+ store = new EventStreamStore("test-db-" + Math.random(), "events");
20
+ await store.open();
21
+ });
22
+
23
+ afterEach(async () => {
24
+ await store.destroy();
25
+ });
26
+
27
+ it("should open and close without error", () => {
28
+ expect(store).toBeDefined();
29
+ });
30
+
31
+ it("should store and retrieve events via putBatch + getAll", async () => {
32
+ const events = [makeEvent("a", 1), makeEvent("b", 2), makeEvent("c", 3)];
33
+ store.putBatch(events);
34
+ // Wait for transaction to complete
35
+ await new Promise((r) => setTimeout(r, 50));
36
+ const result = await store.getAll();
37
+ expect(result).toEqual(events);
38
+ });
39
+
40
+ it("should return events ordered by timestamp", async () => {
41
+ const events = [makeEvent("a", 3), makeEvent("b", 1), makeEvent("c", 2)];
42
+ store.putBatch(events);
43
+ await new Promise((r) => setTimeout(r, 50));
44
+ const result = await store.getAll();
45
+ expect(result.map((e) => e.timestamp)).toEqual([1001, 1002, 1003]);
46
+ });
47
+
48
+ it("should write events via put with microtask batching", async () => {
49
+ store.put(makeEvent("a", 1));
50
+ store.put(makeEvent("b", 2));
51
+ store.put(makeEvent("c", 3));
52
+ // Wait for microtask flush + transaction
53
+ await new Promise((r) => setTimeout(r, 50));
54
+ const result = await store.getAll();
55
+ expect(result).toHaveLength(3);
56
+ });
57
+
58
+ it("should return count of stored events", async () => {
59
+ store.putBatch([makeEvent("a", 1), makeEvent("b", 2)]);
60
+ await new Promise((r) => setTimeout(r, 50));
61
+ const count = await store.getCount();
62
+ expect(count).toBe(2);
63
+ });
64
+
65
+ it("should clear all events", async () => {
66
+ store.putBatch([makeEvent("a", 1), makeEvent("b", 2)]);
67
+ await new Promise((r) => setTimeout(r, 50));
68
+ await store.clear();
69
+ const result = await store.getAll();
70
+ expect(result).toEqual([]);
71
+ const count = await store.getCount();
72
+ expect(count).toBe(0);
73
+ });
74
+
75
+ it("should return empty array when no events stored", async () => {
76
+ const result = await store.getAll();
77
+ expect(result).toEqual([]);
78
+ });
79
+
80
+ it("should return 0 count when no events stored", async () => {
81
+ const count = await store.getCount();
82
+ expect(count).toBe(0);
83
+ });
84
+
85
+ it("should destroy the database", async () => {
86
+ store.putBatch([makeEvent("a", 1)]);
87
+ await new Promise((r) => setTimeout(r, 50));
88
+ await store.destroy();
89
+ // After destroy, getAll should return empty (db is null)
90
+ const result = await store.getAll();
91
+ expect(result).toEqual([]);
92
+ });
93
+
94
+ it("should handle put gracefully when db is not open", () => {
95
+ const closedStore = new EventStreamStore("closed-db", "events");
96
+ // Should not throw
97
+ closedStore.put(makeEvent("a", 1));
98
+ });
99
+
100
+ it("should handle putBatch gracefully when db is not open", () => {
101
+ const closedStore = new EventStreamStore("closed-db", "events");
102
+ // Should not throw
103
+ closedStore.putBatch([makeEvent("a", 1)]);
104
+ });
105
+
106
+ it("should clear pending writes on clear", async () => {
107
+ // Put some events that are still pending
108
+ store.put(makeEvent("a", 1));
109
+ // Clear before microtask flushes
110
+ await store.clear();
111
+ // Wait for microtask
112
+ await new Promise((r) => setTimeout(r, 50));
113
+ const result = await store.getAll();
114
+ expect(result).toEqual([]);
115
+ });
116
+
117
+ it("should handle putBatch with 100 events", async () => {
118
+ const events = Array.from({ length: 100 }, (_, i) => makeEvent("bulk", i));
119
+ store.putBatch(events);
120
+ await new Promise((r) => setTimeout(r, 50));
121
+ const result = await store.getAll();
122
+ expect(result).toHaveLength(100);
123
+ const count = await store.getCount();
124
+ expect(count).toBe(100);
125
+ });
126
+
127
+ it("should prevent new writes after destroy via isDestroyed flag", async () => {
128
+ store.put(makeEvent("a", 1));
129
+ await new Promise((r) => setTimeout(r, 50));
130
+ await store.destroy();
131
+ // After destroy, the store should not accept new writes
132
+ // Re-create a store with the same db name to verify no new data was written
133
+ const store2 = new EventStreamStore("verify-destroyed-" + Math.random(), "events");
134
+ await store2.open();
135
+ // The original store is destroyed - calling put should be a no-op
136
+ store.put(makeEvent("b", 2));
137
+ store.putBatch([makeEvent("c", 3)]);
138
+ await new Promise((r) => setTimeout(r, 50));
139
+ await store2.destroy();
140
+ });
141
+
142
+ it("should discard pending writes on destroy", async () => {
143
+ // Put events that are pending (not yet flushed)
144
+ store.put(makeEvent("a", 1));
145
+ store.put(makeEvent("b", 2));
146
+ // Destroy immediately before microtask flushes
147
+ await store.destroy();
148
+ // Pending writes should have been discarded, not flushed
149
+ // After destroy, db is null so flushWrites will no-op even if microtask runs
150
+ await new Promise((r) => setTimeout(r, 50));
151
+ });
152
+
153
+ it("should not throw when put is called after destroy", async () => {
154
+ await store.destroy();
155
+ // Should not throw
156
+ store.put(makeEvent("a", 1));
157
+ store.putBatch([makeEvent("b", 2)]);
158
+ });
159
+
160
+ it("should handle IndexedDB being unavailable", async () => {
161
+ const origIndexedDB = globalThis.indexedDB;
162
+ try {
163
+ // Simulate IndexedDB being unavailable by deleting it
164
+ // @ts-expect-error - intentionally removing indexedDB for testing
165
+ delete globalThis.indexedDB;
166
+ const unavailableStore = new EventStreamStore("unavailable-db", "events");
167
+ // open() should reject, but put/putBatch should not throw
168
+ await expect(unavailableStore.open()).rejects.toBeDefined();
169
+ // Operations on a store that never opened should be silent no-ops
170
+ unavailableStore.put(makeEvent("a", 1));
171
+ unavailableStore.putBatch([makeEvent("b", 2)]);
172
+ const result = await unavailableStore.getAll();
173
+ expect(result).toEqual([]);
174
+ const count = await unavailableStore.getCount();
175
+ expect(count).toBe(0);
176
+ await unavailableStore.clear(); // should not throw
177
+ } finally {
178
+ globalThis.indexedDB = origIndexedDB;
179
+ }
180
+ });
181
+ });