@semiont/core 0.5.9 → 0.5.11

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,745 @@
1
+ import * as fc2 from 'fast-check';
2
+ import { Subject, BehaviorSubject } from 'rxjs';
3
+
4
+ // src/state-unit-axioms.ts
5
+ function disposeProbe() {
6
+ let n = 0;
7
+ return {
8
+ dispose() {
9
+ n += 1;
10
+ },
11
+ get disposeCount() {
12
+ return n;
13
+ }
14
+ };
15
+ }
16
+ function normalize(r) {
17
+ if (r && typeof r === "object" && "unit" in r) {
18
+ return { unit: r.unit, passedIn: r.passedIn ?? [], teardown: r.teardown ?? (() => {
19
+ }) };
20
+ }
21
+ return { unit: r, passedIn: [], teardown: () => {
22
+ } };
23
+ }
24
+ function swallowAsync(value) {
25
+ if (value && typeof value === "object" && typeof value.then === "function") {
26
+ value.then(void 0, () => {
27
+ });
28
+ }
29
+ }
30
+ function assertStateUnitAxioms(spec) {
31
+ const numRuns = spec.numRuns ?? 30;
32
+ const fresh = () => normalize(spec.setup());
33
+ const run = (axiom, prop) => {
34
+ try {
35
+ fc2.assert(prop, { numRuns });
36
+ } catch (e) {
37
+ throw new Error(`${axiom}: ${e instanceof Error ? e.message : String(e)}`);
38
+ }
39
+ };
40
+ {
41
+ const { unit, teardown } = fresh();
42
+ const proto = Object.getPrototypeOf(unit);
43
+ if (proto !== Object.prototype && proto !== null) {
44
+ throw new Error("A1: state unit is not a plain object (has a class prototype)");
45
+ }
46
+ unit.dispose();
47
+ teardown();
48
+ }
49
+ {
50
+ const { unit, teardown } = fresh();
51
+ for (const [key, value] of Object.entries(unit)) {
52
+ if (value instanceof Subject && value.source === void 0) {
53
+ throw new Error(`X1: public field "${key}" is a raw Subject \u2014 expose it via .asObservable()`);
54
+ }
55
+ }
56
+ unit.dispose();
57
+ teardown();
58
+ }
59
+ {
60
+ const { unit, passedIn, teardown } = fresh();
61
+ unit.dispose();
62
+ passedIn.forEach((probe, i) => {
63
+ if (probe.disposeCount > 0) {
64
+ throw new Error(`A7-passed: the unit disposed injected dependency #${i} (it doesn't own it)`);
65
+ }
66
+ });
67
+ teardown();
68
+ }
69
+ run("A5", fc2.property(fc2.integer({ min: 1, max: 20 }), (n) => {
70
+ const { unit, passedIn, teardown } = fresh();
71
+ for (let i = 0; i < n; i++) unit.dispose();
72
+ passedIn.forEach((probe, i) => {
73
+ if (probe.disposeCount > 0) {
74
+ throw new Error(`injected dependency #${i} disposed under ${n} dispose() calls`);
75
+ }
76
+ });
77
+ teardown();
78
+ }));
79
+ if (spec.surfaces) {
80
+ const surfaces = spec.surfaces;
81
+ run("A6", fc2.property(fc2.integer({ min: 1, max: 10 }), (k) => {
82
+ const { unit, teardown } = fresh();
83
+ const completed = [];
84
+ const subs = surfaces(unit).flatMap(
85
+ (o) => Array.from({ length: k }, () => {
86
+ const idx = completed.push(false) - 1;
87
+ return o.subscribe({ complete: () => {
88
+ completed[idx] = true;
89
+ } });
90
+ })
91
+ );
92
+ unit.dispose();
93
+ completed.forEach((seen, idx) => {
94
+ if (!seen) throw new Error(`a subscriber did not see complete on dispose (k=${k}, sub #${idx})`);
95
+ });
96
+ subs.forEach((s) => s.unsubscribe());
97
+ teardown();
98
+ }));
99
+ }
100
+ if (spec.invocations) {
101
+ const invocations = spec.invocations;
102
+ run("A5b", fc2.property(fc2.array(fc2.nat(), { maxLength: 12 }), (seq) => {
103
+ const { unit, teardown } = fresh();
104
+ const callers = invocations(unit);
105
+ unit.dispose();
106
+ for (const raw of seq) {
107
+ if (callers.length === 0) break;
108
+ swallowAsync(callers[raw % callers.length]());
109
+ }
110
+ teardown();
111
+ }));
112
+ }
113
+ if (spec.surfaces && spec.invocations) {
114
+ const surfaces = spec.surfaces;
115
+ const invocations = spec.invocations;
116
+ run("X3-runtime", fc2.property(fc2.array(fc2.nat(), { minLength: 1, maxLength: 8 }), (seq) => {
117
+ const a = fresh();
118
+ const b = fresh();
119
+ const bSurfaces = surfaces(b.unit);
120
+ const bCounts = bSurfaces.map(() => 0);
121
+ const bSubs = bSurfaces.map((o, i) => o.subscribe(() => {
122
+ bCounts[i] += 1;
123
+ }));
124
+ const baseline = bCounts.slice();
125
+ const aCallers = invocations(a.unit);
126
+ for (const raw of seq) {
127
+ if (aCallers.length === 0) break;
128
+ swallowAsync(aCallers[raw % aCallers.length]());
129
+ }
130
+ bCounts.forEach((count, i) => {
131
+ if (count !== baseline[i]) {
132
+ throw new Error(`driving instance A perturbed instance B's surface #${i}`);
133
+ }
134
+ });
135
+ bSubs.forEach((s) => s.unsubscribe());
136
+ a.unit.dispose();
137
+ b.unit.dispose();
138
+ a.teardown();
139
+ b.teardown();
140
+ }));
141
+ }
142
+ if (spec.ownedChildSurfaces) {
143
+ const { unit, teardown } = fresh();
144
+ const childSurfaces = spec.ownedChildSurfaces(unit);
145
+ const completed = childSurfaces.map(() => false);
146
+ const subs = childSurfaces.map((o, i) => o.subscribe({ complete: () => {
147
+ completed[i] = true;
148
+ } }));
149
+ unit.dispose();
150
+ completed.forEach((seen, i) => {
151
+ if (!seen) throw new Error(`A7-owned: owned child surface #${i} did not complete on outer dispose`);
152
+ });
153
+ subs.forEach((s) => s.unsubscribe());
154
+ teardown();
155
+ }
156
+ if (spec.surfaces) {
157
+ const { unit, teardown } = fresh();
158
+ unit.dispose();
159
+ spec.surfaces(unit).forEach((o, i) => {
160
+ let nexts = 0;
161
+ let done = false;
162
+ o.subscribe({ next: () => {
163
+ nexts += 1;
164
+ }, complete: () => {
165
+ done = true;
166
+ } }).unsubscribe();
167
+ if (nexts > 0) throw new Error(`A5b/inert: owned surface #${i} emitted a value after dispose`);
168
+ if (!done) throw new Error(`A5b/inert: owned surface #${i} did not complete after dispose`);
169
+ });
170
+ teardown();
171
+ }
172
+ }
173
+
174
+ // src/branded-types.ts
175
+ function baseUrl(value) {
176
+ return value;
177
+ }
178
+
179
+ // src/bus-operations.ts
180
+ var BUS_OPERATIONS = {
181
+ // ── BIND ────────────────────────────────────────────────────────
182
+ "bind:update-body": { result: "bind:body-updated", failure: "bind:body-update-failed" },
183
+ // ── BROWSE (reads) ──────────────────────────────────────────────
184
+ "browse:resource-requested": { result: "browse:resource-result", failure: "browse:resource-failed" },
185
+ "browse:resources-requested": { result: "browse:resources-result", failure: "browse:resources-failed" },
186
+ "browse:annotation-requested": { result: "browse:annotation-result", failure: "browse:annotation-failed" },
187
+ "browse:annotations-requested": { result: "browse:annotations-result", failure: "browse:annotations-failed" },
188
+ "browse:annotation-history-requested": { result: "browse:annotation-history-result", failure: "browse:annotation-history-failed" },
189
+ "browse:events-requested": { result: "browse:events-result", failure: "browse:events-failed" },
190
+ "browse:referenced-by-requested": { result: "browse:referenced-by-result", failure: "browse:referenced-by-failed" },
191
+ "browse:entity-types-requested": { result: "browse:entity-types-result", failure: "browse:entity-types-failed" },
192
+ "browse:tag-schemas-requested": { result: "browse:tag-schemas-result", failure: "browse:tag-schemas-failed" },
193
+ "browse:agents-requested": { result: "browse:agents-result", failure: "browse:agents-failed" },
194
+ "browse:directory-requested": { result: "browse:directory-result", failure: "browse:directory-failed" },
195
+ // dormant — backend handler complete, no client caller yet (annotation-detail capability)
196
+ "browse:annotation-context-requested": { result: "browse:annotation-context-result", failure: "browse:annotation-context-failed" },
197
+ // ── FRAME (KB schema writes) ────────────────────────────────────
198
+ "frame:add-entity-type": { result: "frame:entity-type-add-ok", failure: "frame:entity-type-add-failed" },
199
+ "frame:add-tag-schema": { result: "frame:tag-schema-add-ok", failure: "frame:tag-schema-add-failed" },
200
+ // ── GATHER ──────────────────────────────────────────────────────
201
+ // streaming: take-1 result + failure plus an intermediate progress channel
202
+ "gather:requested": { result: "gather:complete", failure: "gather:failed", progress: "gather:annotation-progress" },
203
+ "gather:resource-requested": { result: "gather:resource-complete", failure: "gather:resource-failed" },
204
+ // dormant — backend handler complete, no client caller yet (annotation summary)
205
+ "gather:summary-requested": { result: "gather:summary-result", failure: "gather:summary-failed" },
206
+ // ── JOB ─────────────────────────────────────────────────────────
207
+ "job:create": { result: "job:created", failure: "job:create-failed" },
208
+ "job:status-requested": { result: "job:status-result", failure: "job:status-failed" },
209
+ "job:cancel-requested": { result: "job:cancel-ok", failure: "job:cancel-failed" },
210
+ // worker-side: the worker claims a queued job (not an SDK call)
211
+ "job:claim": { result: "job:claimed", failure: "job:claim-failed" },
212
+ // ── MARK ────────────────────────────────────────────────────────
213
+ "mark:create-request": { result: "mark:create-ok", failure: "mark:create-failed" },
214
+ "mark:delete": { result: "mark:delete-ok", failure: "mark:delete-failed" },
215
+ "mark:archive": { result: "mark:archive-ok", failure: "mark:archive-failed" },
216
+ "mark:unarchive": { result: "mark:unarchive-ok", failure: "mark:unarchive-failed" },
217
+ "mark:update-entity-types": { result: "mark:update-entity-types-ok", failure: "mark:update-entity-types-failed" },
218
+ // ── MATCH ───────────────────────────────────────────────────────
219
+ // take-1 dressed as an Observable in the SDK; no progress channel
220
+ "match:search-requested": { result: "match:search-results", failure: "match:search-failed" },
221
+ // ── YIELD ───────────────────────────────────────────────────────
222
+ // live in-process (resource-operations.ts emits + awaits via race()); the
223
+ // client also .on()-subscribes -ok for cache invalidation
224
+ "yield:create": { result: "yield:create-ok", failure: "yield:create-failed" },
225
+ // dormant — handler in stower exists, no request emitter; client pre-subscribes -ok
226
+ "yield:update": { result: "yield:update-ok", failure: "yield:update-failed" },
227
+ "yield:clone-create": { result: "yield:clone-created", failure: "yield:clone-create-failed" },
228
+ "yield:clone-resource-requested": { result: "yield:clone-resource-result", failure: "yield:clone-resource-failed" },
229
+ "yield:clone-token-requested": { result: "yield:clone-token-generated", failure: "yield:clone-token-failed" }
230
+ };
231
+
232
+ // src/bridged-channels.ts
233
+ var BRIDGED_BROADCASTS = [
234
+ "job:report-progress",
235
+ "job:complete",
236
+ "job:fail",
237
+ "frame:entity-type-added",
238
+ "frame:tag-schema-added",
239
+ "beckon:focus",
240
+ "beckon:sparkle",
241
+ "bus:resume-gap"
242
+ ];
243
+ var REGISTRY_REPLIES = Object.keys(BUS_OPERATIONS).flatMap(
244
+ (key) => {
245
+ const op = BUS_OPERATIONS[key];
246
+ return "progress" in op ? [op.result, op.failure, op.progress] : [op.result, op.failure];
247
+ }
248
+ );
249
+ var BRIDGED_CHANNELS = [
250
+ ...REGISTRY_REPLIES,
251
+ ...BRIDGED_BROADCASTS
252
+ ];
253
+
254
+ // src/bus-log.ts
255
+ var NODE_BUS_LOG = typeof process !== "undefined" && !!process.env?.SEMIONT_BUS_LOG;
256
+ var IS_NODE = typeof process !== "undefined" && !!process.versions?.node;
257
+ function busLogEnabled() {
258
+ const g = globalThis;
259
+ if (g.__SEMIONT_BUS_LOG__) return true;
260
+ return NODE_BUS_LOG;
261
+ }
262
+ function busLog(op, channel, payload, scope) {
263
+ if (!busLogEnabled()) return;
264
+ const cidRaw = payload?.correlationId;
265
+ const cid = typeof cidRaw === "string" ? cidRaw.slice(0, 8) : void 0;
266
+ const tag = `[bus ${op}] ${channel}` + ("") + (cid ? ` cid=${cid}` : "") + ("");
267
+ console.debug(tag, payload);
268
+ }
269
+ function warnUnobservedRepliesEnabled() {
270
+ return IS_NODE;
271
+ }
272
+ var unobservedReplyWarned = /* @__PURE__ */ new Set();
273
+ function warnIfUnobservedReply(channel, payload, observerCount) {
274
+ if (observerCount > 0) return;
275
+ const cidRaw = payload?.correlationId;
276
+ if (typeof cidRaw !== "string" || cidRaw.length === 0) return;
277
+ if (BRIDGED_CHANNELS.includes(channel)) return;
278
+ if (unobservedReplyWarned.has(channel)) return;
279
+ unobservedReplyWarned.add(channel);
280
+ console.warn(
281
+ `[bus DROP] ${channel} cid=${cidRaw.slice(0, 8)} emitted with 0 subscribers and not in BRIDGED_CHANNELS \u2014 a correlation reply with no forwarder is dropped, so the awaiting client times out (no error). Bridge it by declaring its operation in BUS_OPERATIONS (packages/core/src/bus-operations.ts) \u2014 or, if it is a non-reply broadcast, add it to BRIDGED_BROADCASTS (packages/core/src/bridged-channels.ts) \u2014 so transports subscribe to it.`
282
+ );
283
+ }
284
+
285
+ // src/event-bus.ts
286
+ var EventBus = class {
287
+ subjects;
288
+ isDestroyed;
289
+ constructor() {
290
+ this.subjects = /* @__PURE__ */ new Map();
291
+ this.isDestroyed = false;
292
+ }
293
+ /**
294
+ * Get the RxJS Subject for an event
295
+ *
296
+ * Returns a typed Subject that can be used with all RxJS operators.
297
+ * Subjects are created lazily on first access.
298
+ *
299
+ * @param eventName - The event name
300
+ * @returns The RxJS Subject for this event
301
+ *
302
+ * @example
303
+ * ```typescript
304
+ * // Emit
305
+ * eventBus.get('beckon:hover').next({ annotationId: 'ann-1' });
306
+ *
307
+ * // Subscribe
308
+ * const sub = eventBus.get('beckon:hover').subscribe(handleHover);
309
+ *
310
+ * // With operators
311
+ * eventBus.get('beckon:hover')
312
+ * .pipe(debounceTime(100), distinctUntilChanged())
313
+ * .subscribe(handleHover);
314
+ * ```
315
+ */
316
+ get(eventName) {
317
+ if (this.isDestroyed) {
318
+ throw new Error(`Cannot access event '${String(eventName)}' on destroyed bus`);
319
+ }
320
+ if (!this.subjects.has(eventName)) {
321
+ const subject = new Subject();
322
+ const wantBusLog = busLogEnabled();
323
+ const wantDropCheck = warnUnobservedRepliesEnabled();
324
+ if (wantBusLog || wantDropCheck) {
325
+ const wrapped = subject;
326
+ const originalNext = subject.next.bind(subject);
327
+ subject.next = (value) => {
328
+ if (wantBusLog) busLog("EMIT", String(eventName), value);
329
+ if (wantDropCheck) warnIfUnobservedReply(String(eventName), value, wrapped.observers.length);
330
+ originalNext(value);
331
+ };
332
+ }
333
+ this.subjects.set(eventName, subject);
334
+ }
335
+ return this.subjects.get(eventName);
336
+ }
337
+ /**
338
+ * Get the RxJS Subject for a domain event type (PersistedEventType).
339
+ *
340
+ * Domain event channels carry `StoredEvent`. This method avoids the need
341
+ * for `as keyof EventMap` casts when subscribing to domain event channels
342
+ * using runtime `PersistedEventType` strings.
343
+ */
344
+ getDomainEvent(eventType) {
345
+ return this.get(eventType);
346
+ }
347
+ /**
348
+ * Destroy the event bus and complete all subjects
349
+ *
350
+ * After calling destroy(), no new events can be emitted or subscribed to.
351
+ * All active subscriptions will be completed.
352
+ */
353
+ destroy() {
354
+ if (this.isDestroyed) {
355
+ return;
356
+ }
357
+ for (const subject of this.subjects.values()) {
358
+ subject.complete();
359
+ }
360
+ this.subjects.clear();
361
+ this.isDestroyed = true;
362
+ }
363
+ /**
364
+ * Check if the event bus has been destroyed
365
+ */
366
+ get destroyed() {
367
+ return this.isDestroyed;
368
+ }
369
+ /**
370
+ * Create a resource-scoped event bus
371
+ *
372
+ * Events emitted or subscribed through the scoped bus are isolated to that resource.
373
+ * Internally, events are namespaced but the API remains identical to the parent bus.
374
+ *
375
+ * @param resourceId - Resource identifier to scope events to
376
+ * @returns A scoped event bus for this resource
377
+ *
378
+ * @example
379
+ * ```typescript
380
+ * const eventBus = new EventBus();
381
+ * const resource1 = eventBus.scope('resource-1');
382
+ * const resource2 = eventBus.scope('resource-2');
383
+ *
384
+ * // These are isolated - only resource1 subscribers will fire
385
+ * resource1.get('detection:progress').next({ status: 'started' });
386
+ * ```
387
+ */
388
+ scope(resourceId) {
389
+ return new ScopedEventBus(this, resourceId);
390
+ }
391
+ };
392
+ var ScopedEventBus = class _ScopedEventBus {
393
+ constructor(parent, scopePrefix) {
394
+ this.parent = parent;
395
+ this.scopePrefix = scopePrefix;
396
+ }
397
+ parent;
398
+ scopePrefix;
399
+ /**
400
+ * Get the RxJS Subject for a scoped event
401
+ *
402
+ * Returns the same type as the parent bus, but events are isolated to this scope.
403
+ * Internally uses namespaced keys but preserves type safety.
404
+ *
405
+ * @param event - The event name
406
+ * @returns The RxJS Subject for this scoped event
407
+ */
408
+ get(event) {
409
+ const scopedKey = `${this.scopePrefix}:${event}`;
410
+ const parentSubjects = this.parent.subjects;
411
+ if (!parentSubjects.has(scopedKey)) {
412
+ parentSubjects.set(scopedKey, new Subject());
413
+ }
414
+ return parentSubjects.get(scopedKey);
415
+ }
416
+ /** Get the RxJS Subject for a domain event type on this scoped bus. */
417
+ getDomainEvent(eventType) {
418
+ return this.get(eventType);
419
+ }
420
+ /**
421
+ * Create a nested scope
422
+ *
423
+ * Allows hierarchical scoping like `resource-1:subsystem-a`
424
+ *
425
+ * @param subScope - Additional scope level
426
+ * @returns A nested scoped event bus
427
+ */
428
+ scope(subScope) {
429
+ return new _ScopedEventBus(this.parent, `${this.scopePrefix}:${subScope}`);
430
+ }
431
+ };
432
+
433
+ // src/faulty-transport.ts
434
+ function isOperation(channel) {
435
+ return channel in BUS_OPERATIONS;
436
+ }
437
+ function retryKeyOf(channel, payload) {
438
+ const entries = Object.entries(payload).filter(([k]) => k !== "correlationId" && k !== "_trace" && k !== "_userId").sort(([a], [b]) => a < b ? -1 : 1);
439
+ return `${channel} ${JSON.stringify(entries)}`;
440
+ }
441
+ var FaultyTransport = class {
442
+ baseUrl = baseUrl("faulty://simulator");
443
+ state$ = new BehaviorSubject("open");
444
+ errorsSubject = new Subject();
445
+ errors$ = this.errorsSubject.asObservable();
446
+ /** Every request-channel emit, in order — the L2 accounting surface. */
447
+ requestLog = [];
448
+ bus = new EventBus();
449
+ schedule;
450
+ scopeModel;
451
+ makeResponse;
452
+ requestCount = 0;
453
+ activeScope = null;
454
+ scopeRefs = 0;
455
+ timers = /* @__PURE__ */ new Set();
456
+ disposed = false;
457
+ constructor(cfg = {}) {
458
+ this.schedule = cfg.schedule ?? [];
459
+ this.scopeModel = cfg.scopeModel ?? "single-slot-throw";
460
+ this.makeResponse = cfg.makeResponse ?? (() => ({}));
461
+ }
462
+ // ── Bus primitives ──────────────────────────────────────────────────────
463
+ async emit(channel, payload, resourceScope) {
464
+ if (this.disposed) return;
465
+ const name = channel;
466
+ if (!isOperation(name)) {
467
+ const target = resourceScope === void 0 ? this.bus.get(channel) : this.bus.scope(resourceScope).get(channel);
468
+ target.next(payload);
469
+ return;
470
+ }
471
+ const record = payload;
472
+ const action = this.schedule.length === 0 ? { kind: "deliver" } : this.schedule[this.requestCount % this.schedule.length];
473
+ this.requestCount += 1;
474
+ this.requestLog.push({
475
+ channel: name,
476
+ action,
477
+ correlationId: typeof record.correlationId === "string" ? record.correlationId : void 0,
478
+ retryKey: retryKeyOf(name, record)
479
+ });
480
+ if (action.kind === "reject-emit") {
481
+ throw new Error(`FaultyTransport: emit rejected by schedule (reject-emit) on ${name}`);
482
+ }
483
+ this.bus.get(channel).next(payload);
484
+ const reply = () => {
485
+ if (this.disposed) return;
486
+ const response = this.makeResponse(name, record);
487
+ const replyPayload = response === void 0 ? { correlationId: record.correlationId } : { correlationId: record.correlationId, response };
488
+ const resultChannel = BUS_OPERATIONS[name].result;
489
+ this.bus.get(resultChannel).next(replyPayload);
490
+ };
491
+ switch (action.kind) {
492
+ case "deliver":
493
+ queueMicrotask(reply);
494
+ break;
495
+ case "duplicate-reply":
496
+ queueMicrotask(reply);
497
+ queueMicrotask(reply);
498
+ break;
499
+ case "delay": {
500
+ const t = setTimeout(() => {
501
+ this.timers.delete(t);
502
+ reply();
503
+ }, action.ms);
504
+ this.timers.add(t);
505
+ break;
506
+ }
507
+ }
508
+ }
509
+ on(channel, handler) {
510
+ const sub = this.bus.get(channel).subscribe(handler);
511
+ return () => sub.unsubscribe();
512
+ }
513
+ stream(channel) {
514
+ return this.bus.get(channel);
515
+ }
516
+ subscribeToResource(rid) {
517
+ const scope = rid;
518
+ if (this.scopeModel === "single-slot-throw" && this.activeScope !== null && this.activeScope !== scope) {
519
+ throw new Error(
520
+ `FaultyTransport: scope slot busy (${this.activeScope}); unsubscribe before subscribing to ${scope} (scopeModel=single-slot-throw)`
521
+ );
522
+ }
523
+ this.activeScope = this.activeScope ?? scope;
524
+ this.scopeRefs += 1;
525
+ let released = false;
526
+ return () => {
527
+ if (released) return;
528
+ released = true;
529
+ this.scopeRefs -= 1;
530
+ if (this.scopeRefs === 0) this.activeScope = null;
531
+ };
532
+ }
533
+ bridgeInto(bus) {
534
+ for (const channel of BRIDGED_CHANNELS) {
535
+ this.bus.get(channel).subscribe((payload) => {
536
+ bus.get(channel).next(payload);
537
+ });
538
+ }
539
+ }
540
+ // ── Lifecycle ───────────────────────────────────────────────────────────
541
+ dispose() {
542
+ if (this.disposed) return;
543
+ this.disposed = true;
544
+ for (const t of this.timers) clearTimeout(t);
545
+ this.timers.clear();
546
+ this.state$.next("closed");
547
+ this.state$.complete();
548
+ this.errorsSubject.complete();
549
+ this.bus.destroy();
550
+ }
551
+ };
552
+ function arbFaultAction() {
553
+ return fc2.oneof(
554
+ fc2.constant({ kind: "deliver" }),
555
+ fc2.constant({ kind: "drop-reply" }),
556
+ fc2.integer({ min: 0, max: 5 }).map((ms) => ({ kind: "delay", ms })),
557
+ fc2.constant({ kind: "duplicate-reply" }),
558
+ fc2.constant({ kind: "reject-emit" })
559
+ );
560
+ }
561
+ function arbFaultSchedule(maxLength = 8) {
562
+ return fc2.array(arbFaultAction(), { minLength: 1, maxLength });
563
+ }
564
+ function describeSchedule(schedule) {
565
+ return schedule.map((a) => a.kind === "delay" ? `delay(${a.ms})` : a.kind).join(",");
566
+ }
567
+ function sleep(ms) {
568
+ return new Promise((resolve) => setTimeout(resolve, ms));
569
+ }
570
+ async function runLabeled(fallback, slot, assertion) {
571
+ try {
572
+ await assertion();
573
+ } catch (e) {
574
+ const detail = e instanceof Error ? e.message : String(e);
575
+ if (slot.violation) throw new Error(`${slot.violation.message}
576
+ ${detail}`);
577
+ throw new Error(`${fallback}: ${detail}`);
578
+ }
579
+ }
580
+ function capturing(slot, block) {
581
+ try {
582
+ block();
583
+ } catch (e) {
584
+ slot.violation = e instanceof Error ? e : new Error(String(e));
585
+ throw e;
586
+ }
587
+ }
588
+ async function assertLivenessAxioms(spec) {
589
+ const retryBudget = spec.retryBudget ?? 1;
590
+ const slack = spec.slackFactor ?? 4;
591
+ const scheduleArb = spec.scheduleArb ?? arbFaultSchedule();
592
+ const scopeArb = spec.scopeModel === "both" ? fc2.constantFrom("single-slot-throw", "multi") : fc2.constant(spec.scopeModel ?? "single-slot-throw");
593
+ const slot = { violation: null };
594
+ await runLabeled(
595
+ "liveness",
596
+ slot,
597
+ () => fc2.assert(
598
+ fc2.asyncProperty(scheduleArb, scopeArb, async (schedule, scopeModel) => {
599
+ const delayTotal = schedule.reduce((n, a) => n + (a.kind === "delay" ? a.ms : 0), 0);
600
+ const bound = (spec.timeoutMs * (1 + retryBudget) + delayTotal) * slack;
601
+ const transport = new FaultyTransport({
602
+ schedule,
603
+ scopeModel,
604
+ ...spec.makeResponse ? { makeResponse: spec.makeResponse } : {}
605
+ });
606
+ const scenario = await spec.setup(transport);
607
+ const outputs = scenario.outputs;
608
+ const settlements = scenario.settlements ?? [];
609
+ const notified = outputs.map(() => false);
610
+ const settled = settlements.map(() => false);
611
+ let doneResolve = () => {
612
+ };
613
+ const allDone = new Promise((resolve) => {
614
+ doneResolve = resolve;
615
+ });
616
+ const check = () => {
617
+ if (notified.every(Boolean) && settled.every(Boolean)) doneResolve();
618
+ };
619
+ const subs = outputs.map(
620
+ (o, i) => o.subscribe({
621
+ next: () => {
622
+ notified[i] = true;
623
+ check();
624
+ },
625
+ error: () => {
626
+ notified[i] = true;
627
+ check();
628
+ }
629
+ })
630
+ );
631
+ settlements.forEach((p, i) => {
632
+ p.then(
633
+ () => {
634
+ settled[i] = true;
635
+ check();
636
+ },
637
+ () => {
638
+ settled[i] = true;
639
+ check();
640
+ }
641
+ );
642
+ });
643
+ check();
644
+ await Promise.race([allDone, sleep(bound)]);
645
+ try {
646
+ capturing(slot, () => {
647
+ settled.forEach((seen, i) => {
648
+ if (!seen) {
649
+ throw new Error(
650
+ `L2: settlement #${i} did not settle within ${bound}ms under schedule \u27E8${describeSchedule(schedule)}\u27E9 \u2014 the forbidden fourth state`
651
+ );
652
+ }
653
+ });
654
+ const issues = /* @__PURE__ */ new Map();
655
+ for (const entry of transport.requestLog) {
656
+ const prior = issues.get(entry.retryKey) ?? { count: 0, lastFaulted: false };
657
+ issues.set(entry.retryKey, {
658
+ count: prior.count + 1,
659
+ lastFaulted: entry.action.kind === "drop-reply" || entry.action.kind === "reject-emit"
660
+ });
661
+ }
662
+ for (const [key, { count, lastFaulted }] of issues) {
663
+ if (count > 1 + retryBudget) {
664
+ throw new Error(
665
+ `L2: request \u27E8${key}\u27E9 issued ${count} times \u2014 exceeds the retry budget (1 + ${retryBudget}) under schedule \u27E8${describeSchedule(schedule)}\u27E9`
666
+ );
667
+ }
668
+ if (lastFaulted && count <= retryBudget && outputs.length > 0 && !notified.some(Boolean)) {
669
+ throw new Error(
670
+ `L2: faulted request \u27E8${key}\u27E9 was neither re-issued nor surfaced under schedule \u27E8${describeSchedule(schedule)}\u27E9 \u2014 rejection swallowed`
671
+ );
672
+ }
673
+ }
674
+ notified.forEach((seen, i) => {
675
+ if (!seen) {
676
+ throw new Error(
677
+ `L1: output #${i} received no next/error within ${bound}ms under schedule \u27E8${describeSchedule(schedule)}\u27E9 (scope=${scopeModel}) \u2014 silently pending`
678
+ );
679
+ }
680
+ });
681
+ });
682
+ } finally {
683
+ subs.forEach((s) => s.unsubscribe());
684
+ scenario.teardown?.();
685
+ transport.dispose();
686
+ }
687
+ }),
688
+ { numRuns: spec.numRuns ?? 25 }
689
+ )
690
+ );
691
+ }
692
+ function arbDeliveryOps(maxOps = 12) {
693
+ return fc2.array(fc2.constantFrom("write", "transition"), { minLength: 1, maxLength: maxOps }).map((ops) => ops.includes("write") ? ops : [...ops, "write"]);
694
+ }
695
+ async function assertExactlyOnceDelivery(spec) {
696
+ const opsArb = spec.opsArb ?? arbDeliveryOps(spec.maxOps ?? 12);
697
+ const slot = { violation: null };
698
+ await runLabeled(
699
+ "L3",
700
+ slot,
701
+ () => fc2.assert(
702
+ fc2.asyncProperty(opsArb, async (ops) => {
703
+ const subject = spec.setup();
704
+ const delivered = [];
705
+ const sub = subject.output$.subscribe((id) => delivered.push(id));
706
+ const written = [];
707
+ try {
708
+ for (let i = 0; i < ops.length; i++) {
709
+ if (ops[i] === "write") {
710
+ const id = `e${i}`;
711
+ written.push(id);
712
+ subject.write(id);
713
+ } else {
714
+ await subject.transition();
715
+ }
716
+ }
717
+ await (subject.settle?.() ?? sleep(0));
718
+ capturing(slot, () => {
719
+ for (const id of written) {
720
+ const n = delivered.filter((d) => d === id).length;
721
+ if (n === 0) {
722
+ throw new Error(
723
+ `L3: event \u27E8${id}\u27E9 delivered 0 times under \u27E8${ops.join(",")}\u27E9 \u2014 lost across a transition (retire by drain, never by abort)`
724
+ );
725
+ }
726
+ if (n > 1) {
727
+ throw new Error(
728
+ `L3: event \u27E8${id}\u27E9 delivered ${n} times under \u27E8${ops.join(",")}\u27E9 \u2014 duplicate delivery`
729
+ );
730
+ }
731
+ }
732
+ });
733
+ } finally {
734
+ sub.unsubscribe();
735
+ subject.teardown?.();
736
+ }
737
+ }),
738
+ { numRuns: spec.numRuns ?? 50 }
739
+ )
740
+ );
741
+ }
742
+
743
+ export { FaultyTransport, arbDeliveryOps, arbFaultAction, arbFaultSchedule, assertExactlyOnceDelivery, assertLivenessAxioms, assertStateUnitAxioms, disposeProbe, retryKeyOf };
744
+ //# sourceMappingURL=testing.js.map
745
+ //# sourceMappingURL=testing.js.map