@semiont/core 0.5.8 → 0.5.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config/node-config-loader.d.ts +0 -1
- package/dist/config/node-config-loader.js +1 -2
- package/dist/config/node-config-loader.js.map +1 -1
- package/dist/index.d.ts +465 -295
- package/dist/index.js +267 -138
- package/dist/index.js.map +1 -1
- package/dist/testing.d.ts +96 -0
- package/dist/testing.js +176 -0
- package/dist/testing.js.map +1 -0
- package/package.json +15 -2
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { Observable } from 'rxjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Marker for the state-unit pattern: a stateful, lifecycled object with an
|
|
5
|
+
* RxJS-shaped public surface, constructed by a factory function
|
|
6
|
+
* (`createFooStateUnit`), with internal state held in a closure.
|
|
7
|
+
*
|
|
8
|
+
* The structural contract is `dispose()` — the rest of the pattern
|
|
9
|
+
* (closure-based identity, Observable public surface, internal Subjects
|
|
10
|
+
* exposed as `.asObservable()` views, no leaked subscriptions, composition
|
|
11
|
+
* by parameter rather than ownership) is convention, made executable by the
|
|
12
|
+
* axiom harness in `@semiont/core/testing` (`assertStateUnitAxioms`).
|
|
13
|
+
*
|
|
14
|
+
* Lives in `@semiont/core` (not sdk) so every layer can share one definition
|
|
15
|
+
* without dependency cycles — `http-transport`'s `ActorStateUnit` sits below
|
|
16
|
+
* sdk and would otherwise have to re-declare it. See
|
|
17
|
+
* `packages/sdk/docs/STATE-UNITS.md` for the full pattern and rationale, and
|
|
18
|
+
* `.plans/STATE-UNIT-AXIOMS.md` for the axiom ledger.
|
|
19
|
+
*/
|
|
20
|
+
interface StateUnit {
|
|
21
|
+
/**
|
|
22
|
+
* Idempotent, total teardown. Completes every Subject the unit owns,
|
|
23
|
+
* unsubscribes every internal subscription, releases timers / abort
|
|
24
|
+
* controllers / network handles. Safe to call multiple times — the
|
|
25
|
+
* second call is a no-op.
|
|
26
|
+
*/
|
|
27
|
+
dispose(): void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Executable enforcement of the StateUnit pattern — the runtime twin of
|
|
32
|
+
* `packages/sdk/docs/STATE-UNITS.md` and the ledger in
|
|
33
|
+
* `.plans/STATE-UNIT-AXIOMS.md`. The `StateUnit` interface's own comment notes
|
|
34
|
+
* the pattern is convention; this file makes it executable.
|
|
35
|
+
*
|
|
36
|
+
* `assertStateUnitAxioms(spec)` runs every applicable axiom against a factory in
|
|
37
|
+
* one shot, throwing a labeled Error on the first violation (the axiom id is in
|
|
38
|
+
* the message). It is framework-agnostic on purpose — only `rxjs` + `fast-check`,
|
|
39
|
+
* no `vitest` — so it ships through `@semiont/core/testing` and any package's test
|
|
40
|
+
* runner can invoke it from a single `it(...)` per state unit. It lives in core
|
|
41
|
+
* (not sdk) so even packages below sdk (e.g. `http-transport`) can use it without
|
|
42
|
+
* a dependency cycle.
|
|
43
|
+
*
|
|
44
|
+
* Axioms (random-input dimension; fast-check):
|
|
45
|
+
* A5 dispose() is idempotent and total (n ∈ [1,20] calls never throw)
|
|
46
|
+
* A5b post-dispose inertness — every public method is a no-op after dispose
|
|
47
|
+
* A6 every pre-dispose subscriber (k ∈ [1,10]) sees `complete` on dispose
|
|
48
|
+
* X3-runtime instance isolation — driving one instance never moves another's surfaces
|
|
49
|
+
* Structural assertions (single-shot):
|
|
50
|
+
* A1 plain-object identity (no class instance)
|
|
51
|
+
* X1 no raw Subject on the public surface
|
|
52
|
+
* A7-passed disposing the unit must NOT dispose an injected dependency
|
|
53
|
+
* A7-owned disposing the unit MUST dispose its internally-constructed children
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* A disposable stand-in for an injected dependency. Pass one as a unit's
|
|
58
|
+
* constructor arg, then list it in `setup().passedIn` so A7-passed can assert
|
|
59
|
+
* the unit never disposed it. Counts calls so A7-passed also holds under the
|
|
60
|
+
* repeated-dispose stress of A5.
|
|
61
|
+
*/
|
|
62
|
+
interface DisposeProbe extends StateUnit {
|
|
63
|
+
readonly disposeCount: number;
|
|
64
|
+
}
|
|
65
|
+
declare function disposeProbe(): DisposeProbe;
|
|
66
|
+
type SetupResult<T extends StateUnit> = T | {
|
|
67
|
+
unit: T;
|
|
68
|
+
passedIn?: readonly DisposeProbe[];
|
|
69
|
+
teardown?: () => void;
|
|
70
|
+
};
|
|
71
|
+
interface StateUnitAxiomSpec<T extends StateUnit> {
|
|
72
|
+
/**
|
|
73
|
+
* Build a FRESH unit. Called many times (fast-check re-runs), so it must
|
|
74
|
+
* return an independent instance each call. Return the bare unit, or an object
|
|
75
|
+
* carrying the injected `passedIn` probes (A7-passed) and a `teardown` to
|
|
76
|
+
* release per-instance resources (e.g. a mock bus).
|
|
77
|
+
*/
|
|
78
|
+
setup: () => SetupResult<T>;
|
|
79
|
+
/** Owned public Observables — Subjects the unit completes on dispose (A6, X3, post-dispose inertness). */
|
|
80
|
+
surfaces?: (unit: T) => readonly Observable<unknown>[];
|
|
81
|
+
/** Public input methods as zero-arg callers (A5b post-dispose, X3 drive). */
|
|
82
|
+
invocations?: (unit: T) => readonly (() => unknown)[];
|
|
83
|
+
/** Surfaces of internally-constructed children — must complete when the outer disposes (A7-owned). */
|
|
84
|
+
ownedChildSurfaces?: (unit: T) => readonly Observable<unknown>[];
|
|
85
|
+
/** fast-check run budget per property (default 30). */
|
|
86
|
+
numRuns?: number;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Run every applicable axiom against `spec`. Throws a labeled Error on the first
|
|
90
|
+
* violation. Axioms whose accessors are omitted are skipped (e.g. A7-passed
|
|
91
|
+
* runs only when `setup` returns `passedIn`; A6 only when `surfaces` is given).
|
|
92
|
+
*/
|
|
93
|
+
declare function assertStateUnitAxioms<T extends StateUnit>(spec: StateUnitAxiomSpec<T>): void;
|
|
94
|
+
|
|
95
|
+
export { assertStateUnitAxioms, disposeProbe };
|
|
96
|
+
export type { DisposeProbe, StateUnitAxiomSpec };
|
package/dist/testing.js
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import * as fc from 'fast-check';
|
|
2
|
+
import { Subject } 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
|
+
fc.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", fc.property(fc.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", fc.property(fc.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", fc.property(fc.array(fc.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", fc.property(fc.array(fc.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
|
+
export { assertStateUnitAxioms, disposeProbe };
|
|
175
|
+
//# sourceMappingURL=testing.js.map
|
|
176
|
+
//# sourceMappingURL=testing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/state-unit-axioms.ts"],"names":[],"mappings":"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","file":"testing.js","sourcesContent":["/**\n * Executable enforcement of the StateUnit pattern — the runtime twin of\n * `packages/sdk/docs/STATE-UNITS.md` and the ledger in\n * `.plans/STATE-UNIT-AXIOMS.md`. The `StateUnit` interface's own comment notes\n * the pattern is convention; this file makes it executable.\n *\n * `assertStateUnitAxioms(spec)` runs every applicable axiom against a factory in\n * one shot, throwing a labeled Error on the first violation (the axiom id is in\n * the message). It is framework-agnostic on purpose — only `rxjs` + `fast-check`,\n * no `vitest` — so it ships through `@semiont/core/testing` and any package's test\n * runner can invoke it from a single `it(...)` per state unit. It lives in core\n * (not sdk) so even packages below sdk (e.g. `http-transport`) can use it without\n * a dependency cycle.\n *\n * Axioms (random-input dimension; fast-check):\n * A5 dispose() is idempotent and total (n ∈ [1,20] calls never throw)\n * A5b post-dispose inertness — every public method is a no-op after dispose\n * A6 every pre-dispose subscriber (k ∈ [1,10]) sees `complete` on dispose\n * X3-runtime instance isolation — driving one instance never moves another's surfaces\n * Structural assertions (single-shot):\n * A1 plain-object identity (no class instance)\n * X1 no raw Subject on the public surface\n * A7-passed disposing the unit must NOT dispose an injected dependency\n * A7-owned disposing the unit MUST dispose its internally-constructed children\n */\n\nimport * as fc from 'fast-check';\nimport { Subject, type Observable } from 'rxjs';\nimport type { StateUnit } from './state-unit';\n\n/**\n * A disposable stand-in for an injected dependency. Pass one as a unit's\n * constructor arg, then list it in `setup().passedIn` so A7-passed can assert\n * the unit never disposed it. Counts calls so A7-passed also holds under the\n * repeated-dispose stress of A5.\n */\nexport interface DisposeProbe extends StateUnit {\n readonly disposeCount: number;\n}\n\nexport function disposeProbe(): DisposeProbe {\n let n = 0;\n return {\n dispose() { n += 1; },\n get disposeCount() { return n; },\n };\n}\n\ntype SetupResult<T extends StateUnit> =\n | T\n | { unit: T; passedIn?: readonly DisposeProbe[]; teardown?: () => void };\n\nexport interface StateUnitAxiomSpec<T extends StateUnit> {\n /**\n * Build a FRESH unit. Called many times (fast-check re-runs), so it must\n * return an independent instance each call. Return the bare unit, or an object\n * carrying the injected `passedIn` probes (A7-passed) and a `teardown` to\n * release per-instance resources (e.g. a mock bus).\n */\n setup: () => SetupResult<T>;\n /** Owned public Observables — Subjects the unit completes on dispose (A6, X3, post-dispose inertness). */\n surfaces?: (unit: T) => readonly Observable<unknown>[];\n /** Public input methods as zero-arg callers (A5b post-dispose, X3 drive). */\n invocations?: (unit: T) => readonly (() => unknown)[];\n /** Surfaces of internally-constructed children — must complete when the outer disposes (A7-owned). */\n ownedChildSurfaces?: (unit: T) => readonly Observable<unknown>[];\n /** fast-check run budget per property (default 30). */\n numRuns?: number;\n}\n\ninterface Normalized<T extends StateUnit> {\n unit: T;\n passedIn: readonly DisposeProbe[];\n teardown: () => void;\n}\n\nfunction normalize<T extends StateUnit>(r: SetupResult<T>): Normalized<T> {\n if (r && typeof r === 'object' && 'unit' in r) {\n return { unit: r.unit, passedIn: r.passedIn ?? [], teardown: r.teardown ?? (() => {}) };\n }\n return { unit: r as T, passedIn: [], teardown: () => {} };\n}\n\nfunction swallowAsync(value: unknown): void {\n if (value && typeof value === 'object' && typeof (value as { then?: unknown }).then === 'function') {\n (value as Promise<unknown>).then(undefined, () => {});\n }\n}\n\n/**\n * Run every applicable axiom against `spec`. Throws a labeled Error on the first\n * violation. Axioms whose accessors are omitted are skipped (e.g. A7-passed\n * runs only when `setup` returns `passedIn`; A6 only when `surfaces` is given).\n */\nexport function assertStateUnitAxioms<T extends StateUnit>(spec: StateUnitAxiomSpec<T>): void {\n const numRuns = spec.numRuns ?? 30;\n const fresh = (): Normalized<T> => normalize(spec.setup());\n\n // fast-check's default falsification message is just \"Property failed after N\n // tests {seed}\" — it drops the thrown axiom id. Prepend the axiom so a real\n // failure names itself; the seed/shrink detail is preserved.\n const run = (axiom: string, prop: Parameters<typeof fc.assert>[0]): void => {\n try {\n fc.assert(prop, { numRuns });\n } catch (e) {\n throw new Error(`${axiom}: ${e instanceof Error ? e.message : String(e)}`);\n }\n };\n\n // A1 — plain-object identity (no class instance).\n {\n const { unit, teardown } = fresh();\n const proto = Object.getPrototypeOf(unit);\n if (proto !== Object.prototype && proto !== null) {\n throw new Error('A1: state unit is not a plain object (has a class prototype)');\n }\n unit.dispose();\n teardown();\n }\n\n // X1 — no raw *origin* Subject on the public surface (expose `.asObservable()`).\n // Flag only origin Subjects — `new Subject` / `new BehaviorSubject`, the unit's own\n // state, where `.source` is undefined. Derived `AnonymousSubject`s from `Subject.lift`\n // (`shareReplay().pipe(...)`) carry a `.source` and are inert sinks (`.next()` does\n // nothing useful) — idiomatic, not the forgotten-internal-Subject smell — so exclude them.\n {\n const { unit, teardown } = fresh();\n for (const [key, value] of Object.entries(unit)) {\n if (value instanceof Subject && (value as { source?: unknown }).source === undefined) {\n throw new Error(`X1: public field \"${key}\" is a raw Subject — expose it via .asObservable()`);\n }\n }\n unit.dispose();\n teardown();\n }\n\n // A7-passed — disposing the unit must NOT dispose an injected dependency.\n {\n const { unit, passedIn, teardown } = fresh();\n unit.dispose();\n passedIn.forEach((probe, i) => {\n if (probe.disposeCount > 0) {\n throw new Error(`A7-passed: the unit disposed injected dependency #${i} (it doesn't own it)`);\n }\n });\n teardown();\n }\n\n // A5 — dispose() idempotent & total: n calls never throw; injected deps stay untouched.\n run('A5', fc.property(fc.integer({ min: 1, max: 20 }), (n) => {\n const { unit, passedIn, teardown } = fresh();\n for (let i = 0; i < n; i++) unit.dispose();\n passedIn.forEach((probe, i) => {\n if (probe.disposeCount > 0) {\n throw new Error(`injected dependency #${i} disposed under ${n} dispose() calls`);\n }\n });\n teardown();\n }));\n\n // A6 — k pre-dispose subscribers on each owned surface all see `complete` on dispose.\n if (spec.surfaces) {\n const surfaces = spec.surfaces;\n run('A6', fc.property(fc.integer({ min: 1, max: 10 }), (k) => {\n const { unit, teardown } = fresh();\n const completed: boolean[] = [];\n const subs = surfaces(unit).flatMap((o) =>\n Array.from({ length: k }, () => {\n const idx = completed.push(false) - 1;\n return o.subscribe({ complete: () => { completed[idx] = true; } });\n }),\n );\n unit.dispose();\n completed.forEach((seen, idx) => {\n if (!seen) throw new Error(`a subscriber did not see complete on dispose (k=${k}, sub #${idx})`);\n });\n subs.forEach((s) => s.unsubscribe());\n teardown();\n }));\n }\n\n // A5b — post-dispose inertness: every public method is a no-op (no throw) after dispose.\n if (spec.invocations) {\n const invocations = spec.invocations;\n run('A5b', fc.property(fc.array(fc.nat(), { maxLength: 12 }), (seq) => {\n const { unit, teardown } = fresh();\n const callers = invocations(unit);\n unit.dispose();\n for (const raw of seq) {\n if (callers.length === 0) break;\n swallowAsync(callers[raw % callers.length]());\n }\n teardown();\n }));\n }\n\n // X3-runtime — instance isolation: driving instance A never moves instance B's surfaces.\n if (spec.surfaces && spec.invocations) {\n const surfaces = spec.surfaces;\n const invocations = spec.invocations;\n run('X3-runtime', fc.property(fc.array(fc.nat(), { minLength: 1, maxLength: 8 }), (seq) => {\n const a = fresh();\n const b = fresh();\n const bSurfaces = surfaces(b.unit);\n const bCounts = bSurfaces.map(() => 0);\n const bSubs = bSurfaces.map((o, i) => o.subscribe(() => { bCounts[i] += 1; }));\n const baseline = bCounts.slice(); // initial replay captured synchronously above\n const aCallers = invocations(a.unit);\n for (const raw of seq) {\n if (aCallers.length === 0) break;\n swallowAsync(aCallers[raw % aCallers.length]());\n }\n bCounts.forEach((count, i) => {\n if (count !== baseline[i]) {\n throw new Error(`driving instance A perturbed instance B's surface #${i}`);\n }\n });\n bSubs.forEach((s) => s.unsubscribe());\n a.unit.dispose();\n b.unit.dispose();\n a.teardown();\n b.teardown();\n }));\n }\n\n // A7-owned — internally-constructed children are disposed (their surfaces complete) on outer dispose.\n if (spec.ownedChildSurfaces) {\n const { unit, teardown } = fresh();\n const childSurfaces = spec.ownedChildSurfaces(unit);\n const completed = childSurfaces.map(() => false);\n const subs = childSurfaces.map((o, i) => o.subscribe({ complete: () => { completed[i] = true; } }));\n unit.dispose();\n completed.forEach((seen, i) => {\n if (!seen) throw new Error(`A7-owned: owned child surface #${i} did not complete on outer dispose`);\n });\n subs.forEach((s) => s.unsubscribe());\n teardown();\n }\n\n // Post-dispose surface inertness: a NEW subscription after dispose completes with no `next`.\n if (spec.surfaces) {\n const { unit, teardown } = fresh();\n unit.dispose();\n spec.surfaces(unit).forEach((o, i) => {\n let nexts = 0;\n let done = false;\n o.subscribe({ next: () => { nexts += 1; }, complete: () => { done = true; } }).unsubscribe();\n if (nexts > 0) throw new Error(`A5b/inert: owned surface #${i} emitted a value after dispose`);\n if (!done) throw new Error(`A5b/inert: owned surface #${i} did not complete after dispose`);\n });\n teardown();\n }\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@semiont/core",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.10",
|
|
4
4
|
"description": "Core types and domain logic for Semiont - Resource, Annotation, and Graph models",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -15,6 +15,11 @@
|
|
|
15
15
|
"types": "./dist/config/node-config-loader.d.ts",
|
|
16
16
|
"import": "./dist/config/node-config-loader.js",
|
|
17
17
|
"default": "./dist/config/node-config-loader.js"
|
|
18
|
+
},
|
|
19
|
+
"./testing": {
|
|
20
|
+
"types": "./dist/testing.d.ts",
|
|
21
|
+
"import": "./dist/testing.js",
|
|
22
|
+
"default": "./dist/testing.js"
|
|
18
23
|
}
|
|
19
24
|
},
|
|
20
25
|
"engines": {
|
|
@@ -74,6 +79,14 @@
|
|
|
74
79
|
"ajv": "^8.20.0",
|
|
75
80
|
"ajv-formats": "^3.0.1",
|
|
76
81
|
"rxjs": "^7.8.1",
|
|
77
|
-
"smol-toml": "^1.
|
|
82
|
+
"smol-toml": "^1.7.0"
|
|
83
|
+
},
|
|
84
|
+
"peerDependencies": {
|
|
85
|
+
"fast-check": "^4.8.0"
|
|
86
|
+
},
|
|
87
|
+
"peerDependenciesMeta": {
|
|
88
|
+
"fast-check": {
|
|
89
|
+
"optional": true
|
|
90
|
+
}
|
|
78
91
|
}
|
|
79
92
|
}
|