@demlik/tea 0.16.0 → 0.18.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/README.md +3 -2
- package/dist/agent/index.d.ts +182 -35
- package/dist/agent/index.js +11 -11
- package/dist/{chunk-5XU6ZJO2.js → chunk-AHW2CLPT.js} +80 -27
- package/dist/chunk-AHW2CLPT.js.map +1 -0
- package/dist/{chunk-CH5KTOEZ.js → chunk-AIIXV4XB.js} +65 -13
- package/dist/chunk-AIIXV4XB.js.map +1 -0
- package/dist/{chunk-BCTEOVOU.js → chunk-BRX3243I.js} +3 -3
- package/dist/{chunk-BCTEOVOU.js.map → chunk-BRX3243I.js.map} +1 -1
- package/dist/{chunk-IVJP3GKJ.js → chunk-C3G2QFEZ.js} +4 -4
- package/dist/{chunk-IVJP3GKJ.js.map → chunk-C3G2QFEZ.js.map} +1 -1
- package/dist/{chunk-GOX2CYIM.js → chunk-FNGZMVIT.js} +3 -3
- package/dist/chunk-FNGZMVIT.js.map +1 -0
- package/dist/{chunk-WT5PBPLB.js → chunk-H2ELKHI6.js} +16 -6
- package/dist/chunk-H2ELKHI6.js.map +1 -0
- package/dist/chunk-H3MWNH7A.js +119 -0
- package/dist/chunk-H3MWNH7A.js.map +1 -0
- package/dist/chunk-I7E7AD2S.js +213 -0
- package/dist/chunk-I7E7AD2S.js.map +1 -0
- package/dist/{chunk-NSIAORRY.js → chunk-IHUNKHDX.js} +3 -3
- package/dist/{chunk-NSIAORRY.js.map → chunk-IHUNKHDX.js.map} +1 -1
- package/dist/chunk-JVE2LBFJ.js +21 -0
- package/dist/chunk-JVE2LBFJ.js.map +1 -0
- package/dist/chunk-JVKU2UQC.js +34 -0
- package/dist/chunk-JVKU2UQC.js.map +1 -0
- package/dist/{chunk-MYNLY3HP.js → chunk-JYNYG5SF.js} +4 -4
- package/dist/{chunk-MYNLY3HP.js.map → chunk-JYNYG5SF.js.map} +1 -1
- package/dist/{chunk-EHUDXAKC.js → chunk-PCJA2HTZ.js} +437 -162
- package/dist/chunk-PCJA2HTZ.js.map +1 -0
- package/dist/{chunk-MVZS5FDM.js → chunk-PQSDUX6B.js} +3 -3
- package/dist/{chunk-MVZS5FDM.js.map → chunk-PQSDUX6B.js.map} +1 -1
- package/dist/{chunk-6NG4V5SB.js → chunk-WTSOW2MD.js} +5 -5
- package/dist/{chunk-6NG4V5SB.js.map → chunk-WTSOW2MD.js.map} +1 -1
- package/dist/{chunk-7MGXQZPW.js → chunk-XVIKSRCK.js} +4 -4
- package/dist/{chunk-7MGXQZPW.js.map → chunk-XVIKSRCK.js.map} +1 -1
- package/dist/{chunk-CIHSHLQX.js → chunk-YIMOFXST.js} +3 -3
- package/dist/{chunk-CIHSHLQX.js.map → chunk-YIMOFXST.js.map} +1 -1
- package/dist/{core-BbgtiXdg.d.ts → core-Ce9ncpuv.d.ts} +10 -5
- package/dist/do/index.d.ts +140 -64
- package/dist/do/index.js +17 -18
- package/dist/do/index.js.map +1 -1
- package/dist/drive-loop-CsUsW3Wo.d.ts +127 -0
- package/dist/{durable-effects-7MTK_jnj.d.ts → durable-effects-BmUrRA-h.d.ts} +1 -1
- package/dist/effect/index.d.ts +6 -150
- package/dist/effect/index.js +4 -96
- package/dist/effect/index.js.map +1 -1
- package/dist/extension/index.d.ts +7 -5
- package/dist/extension/index.js +2 -3
- package/dist/extension/index.js.map +1 -1
- package/dist/flow/index.d.ts +13 -13
- package/dist/flow/index.js +13 -12
- package/dist/flow/index.js.map +1 -1
- package/dist/idempotency/index.d.ts +3 -3
- package/dist/idempotency/index.js +4 -4
- package/dist/{index-Cou86-GX.d.ts → index-2kNxy6az.d.ts} +2 -2
- package/dist/{index-DU6JpQ8G.d.ts → index-B1Wzarkl.d.ts} +1 -1
- package/dist/{index-_dOR3xqA.d.ts → index-Bf9_bNql.d.ts} +2 -2
- package/dist/index-BnsAqn0-.d.ts +286 -0
- package/dist/{index-CkFYA9Y6.d.ts → index-Ce5GYAuI.d.ts} +2 -2
- package/dist/{index-CjFDUBeq.d.ts → index-D8sYVzCI.d.ts} +1 -1
- package/dist/{index-7vV8xmsD.d.ts → index-DWdg7R1l.d.ts} +1 -1
- package/dist/{index-Bz5Rh9Ga.d.ts → index-Dy3379ve.d.ts} +5 -5
- package/dist/{index-Xufl55W8.d.ts → index-e36uEyl4.d.ts} +3 -3
- package/dist/index.d.ts +109 -36
- package/dist/index.js +2 -2
- package/dist/jev/index.d.ts +8 -8
- package/dist/jev/index.js +6 -7
- package/dist/jev/index.js.map +1 -1
- package/dist/{machine-BQzTV8On.d.ts → machine-CuojfZup.d.ts} +355 -59
- package/dist/machine-viz/index.d.ts +1 -1
- package/dist/machine-viz/index.js +2 -2
- package/dist/mem/index.d.ts +6 -6
- package/dist/mem/index.js +6 -2
- package/dist/mem/index.js.map +1 -1
- package/dist/node/index.d.ts +16 -10
- package/dist/node/index.js +22 -3
- package/dist/node/index.js.map +1 -1
- package/dist/otel/index.d.ts +161 -0
- package/dist/otel/index.js +403 -0
- package/dist/otel/index.js.map +1 -0
- package/dist/paginate/index.d.ts +4 -4
- package/dist/paginate/index.js +5 -5
- package/dist/parity/index.d.ts +3 -3
- package/dist/parity/index.js +3 -3
- package/dist/pbt/index.d.ts +166 -53
- package/dist/pbt/index.js +2 -2
- package/dist/pbt/index.js.map +1 -1
- package/dist/persistence/index.d.ts +9 -10
- package/dist/persistence/index.js +5 -5
- package/dist/promise/index.d.ts +2 -2
- package/dist/promise/index.js +4 -3
- package/dist/react/index.d.ts +2 -2
- package/dist/resilience/index.d.ts +13 -9
- package/dist/resilience/index.js +5 -5
- package/dist/resilience/index.js.map +1 -1
- package/dist/{runtime-types-CiVpC0HN.d.ts → runtime-types-BKg2CGMx.d.ts} +89 -15
- package/dist/testing/effect/index.d.ts +65 -0
- package/dist/testing/effect/index.js +148 -0
- package/dist/testing/effect/index.js.map +1 -0
- package/dist/testing/index.d.ts +25 -257
- package/dist/testing/index.js +25 -301
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/promise/index.d.ts +41 -0
- package/dist/testing/promise/index.js +37 -0
- package/dist/testing/promise/index.js.map +1 -0
- package/dist/timing/index.d.ts +4 -4
- package/dist/timing/index.js +4 -4
- package/dist/{types-LxZxGYda.d.ts → types-SzNSHKlV.d.ts} +1 -1
- package/dist/work-queue/index.d.ts +4 -4
- package/dist/work-queue/index.js +4 -3
- package/dist/work-queue/index.js.map +1 -1
- package/package.json +27 -15
- package/dist/chunk-5XU6ZJO2.js.map +0 -1
- package/dist/chunk-CH5KTOEZ.js.map +0 -1
- package/dist/chunk-EHUDXAKC.js.map +0 -1
- package/dist/chunk-G5E74IJB.js +0 -8
- package/dist/chunk-G5E74IJB.js.map +0 -1
- package/dist/chunk-GOX2CYIM.js.map +0 -1
- package/dist/chunk-WT5PBPLB.js.map +0 -1
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { CellFailure, builtinRunners, cellResult, settleCellExit } from '../../chunk-I7E7AD2S.js';
|
|
2
|
+
import { driveLoop, DEFAULT_MAX_ROUNDS, driveContractOf, DriveRoundsExceededError, DriveNoHandlerError, annotateTrace, driveTraceOf, desiredSubs } from '../../chunk-H3MWNH7A.js';
|
|
3
|
+
export { DEFAULT_MAX_ROUNDS, DriveNoHandlerError, DriveRoundsExceededError, driveTraceOf } from '../../chunk-H3MWNH7A.js';
|
|
4
|
+
import '../../chunk-JVKU2UQC.js';
|
|
5
|
+
import '../../chunk-AHW2CLPT.js';
|
|
6
|
+
import '../../chunk-JVE2LBFJ.js';
|
|
7
|
+
import '../../chunk-FNGZMVIT.js';
|
|
8
|
+
import '../../chunk-AIIXV4XB.js';
|
|
9
|
+
import { Effect, Stream, Exit, Cause, Fiber } from 'effect';
|
|
10
|
+
|
|
11
|
+
var DriveInterrupted = class {
|
|
12
|
+
};
|
|
13
|
+
function drive(machine, initial, msg, interpret, ...[opts]) {
|
|
14
|
+
const options = opts ?? {};
|
|
15
|
+
return Effect.gen(function* () {
|
|
16
|
+
const services = yield* Effect.context();
|
|
17
|
+
const runEffect = Effect.runPromiseExitWith(services);
|
|
18
|
+
const fork = Effect.runForkWith(services);
|
|
19
|
+
return yield* Effect.callback((resume, signal) => {
|
|
20
|
+
const cells = interpret;
|
|
21
|
+
const defined = new Set((machine.cmds ?? []).map((def) => def.cmdType));
|
|
22
|
+
const cellFor = (type) => {
|
|
23
|
+
const cell = cells[type];
|
|
24
|
+
if (cell === void 0) return void 0;
|
|
25
|
+
return async (cmd) => {
|
|
26
|
+
const exit = await runEffect(cellResult(cell(cmd)), { signal });
|
|
27
|
+
if (signal.aborted) throw new DriveInterrupted();
|
|
28
|
+
return settleCellExit(exit, defined.has(type));
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
driveLoop(
|
|
32
|
+
machine,
|
|
33
|
+
initial,
|
|
34
|
+
msg,
|
|
35
|
+
options.ctx,
|
|
36
|
+
options.maxRounds ?? DEFAULT_MAX_ROUNDS,
|
|
37
|
+
{
|
|
38
|
+
contract: driveContractOf(machine, options.clock ?? Date.now),
|
|
39
|
+
cellFor,
|
|
40
|
+
subs: effectSubs(
|
|
41
|
+
machine,
|
|
42
|
+
options.subscribe ?? {},
|
|
43
|
+
fork,
|
|
44
|
+
signal
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
).then(
|
|
48
|
+
(result) => resume(Effect.succeed(result)),
|
|
49
|
+
(error) => resume(failureOf(error))
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
function failureOf(error) {
|
|
55
|
+
if (error instanceof DriveRoundsExceededError || error instanceof DriveNoHandlerError) {
|
|
56
|
+
return Effect.fail(error);
|
|
57
|
+
}
|
|
58
|
+
if (error instanceof CellFailure) {
|
|
59
|
+
annotateTrace(error.failure, driveTraceOf(error) ?? []);
|
|
60
|
+
return Effect.fail(error.failure);
|
|
61
|
+
}
|
|
62
|
+
if (error instanceof DriveInterrupted) return Effect.interrupt;
|
|
63
|
+
return Effect.die(error);
|
|
64
|
+
}
|
|
65
|
+
function effectSubs(machine, subscribe, fork, signal) {
|
|
66
|
+
const builtins = builtinRunners;
|
|
67
|
+
const started = /* @__PURE__ */ new Map();
|
|
68
|
+
let buffered = [];
|
|
69
|
+
let failed;
|
|
70
|
+
let wake;
|
|
71
|
+
const notify = () => {
|
|
72
|
+
const woken = wake;
|
|
73
|
+
wake = void 0;
|
|
74
|
+
woken?.();
|
|
75
|
+
};
|
|
76
|
+
signal.addEventListener("abort", notify, { once: true });
|
|
77
|
+
const stop = (sub) => {
|
|
78
|
+
sub.stopped = true;
|
|
79
|
+
return sub.ended || sub.fiber === void 0 ? Promise.resolve() : Effect.runPromise(Fiber.interrupt(sub.fiber));
|
|
80
|
+
};
|
|
81
|
+
const anyRunning = () => [...started.values()].some((sub) => !sub.ended);
|
|
82
|
+
return {
|
|
83
|
+
reconcile(state) {
|
|
84
|
+
const desired = desiredSubs(machine, state);
|
|
85
|
+
for (const [id, sub] of started) {
|
|
86
|
+
if (desired.has(id)) continue;
|
|
87
|
+
started.delete(id);
|
|
88
|
+
void stop(sub);
|
|
89
|
+
}
|
|
90
|
+
for (const [id, sub] of desired) {
|
|
91
|
+
if (started.has(id)) continue;
|
|
92
|
+
const make = subscribe[sub.type] ?? builtins[sub.type];
|
|
93
|
+
if (make === void 0) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
`@demlik/tea: no subscribe runner for Sub type "${sub.type}". Pass one to drive in \`subscribe\`.`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
const entry = { stopped: false, ended: false };
|
|
99
|
+
started.set(id, entry);
|
|
100
|
+
entry.fiber = fork(
|
|
101
|
+
Stream.runForEach(
|
|
102
|
+
make(sub),
|
|
103
|
+
(next) => Effect.sync(() => {
|
|
104
|
+
if (entry.stopped) return;
|
|
105
|
+
buffered.push(next);
|
|
106
|
+
notify();
|
|
107
|
+
})
|
|
108
|
+
).pipe(
|
|
109
|
+
Effect.onExit(
|
|
110
|
+
(exit) => Effect.sync(() => {
|
|
111
|
+
entry.ended = true;
|
|
112
|
+
if (!entry.stopped && Exit.isFailure(exit) && !Cause.hasInterruptsOnly(exit.cause)) {
|
|
113
|
+
failed ??= exit.cause;
|
|
114
|
+
}
|
|
115
|
+
notify();
|
|
116
|
+
})
|
|
117
|
+
)
|
|
118
|
+
)
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
async next() {
|
|
123
|
+
for (; ; ) {
|
|
124
|
+
if (signal.aborted) throw new DriveInterrupted();
|
|
125
|
+
if (buffered.length > 0) {
|
|
126
|
+
const out = buffered;
|
|
127
|
+
buffered = [];
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
if (failed !== void 0) throw Cause.squash(failed);
|
|
131
|
+
if (!anyRunning()) return void 0;
|
|
132
|
+
await new Promise((resolve) => {
|
|
133
|
+
wake = resolve;
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
async close() {
|
|
138
|
+
signal.removeEventListener("abort", notify);
|
|
139
|
+
const stopping = [...started.values()].map(stop);
|
|
140
|
+
started.clear();
|
|
141
|
+
await Promise.all(stopping);
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export { drive };
|
|
147
|
+
//# sourceMappingURL=index.js.map
|
|
148
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/testing/effect/drive.ts"],"names":[],"mappings":";;;;;;;;;;AAwDA,IAAM,mBAAN,MAAuB;AAAC,CAAA;AAuDjB,SAAS,MAUd,OAAA,EACA,OAAA,EACA,KACA,SAAA,EAAA,GACG,CAAC,IAAI,CAAA,EAOR;AACA,EAAA,MAAM,OAAA,GAAW,QAAQ,EAAC;AAM1B,EAAA,OAAO,MAAA,CAAO,IAAI,aAAa;AAC7B,IAAA,MAAM,QAAA,GAAW,OAAO,MAAA,CAAO,OAAA,EAE7B;AACF,IAAA,MAAM,SAAA,GAAY,MAAA,CAAO,kBAAA,CAAmB,QAAQ,CAAA;AACpD,IAAA,MAAM,IAAA,GAAO,MAAA,CAAO,WAAA,CAAY,QAAQ,CAAA;AAExC,IAAA,OAAO,OAAO,MAAA,CAAO,QAAA,CAGnB,CAAC,QAAQ,MAAA,KAAW;AACpB,MAAA,MAAM,KAAA,GAAQ,SAAA;AAGd,MAAA,MAAM,OAAA,GAAU,IAAI,GAAA,CAAA,CAAK,OAAA,CAAQ,IAAA,IAAQ,EAAC,EAAG,GAAA,CAAI,CAAC,GAAA,KAAQ,GAAA,CAAI,OAAO,CAAC,CAAA;AACtE,MAAA,MAAM,OAAA,GAAU,CAAC,IAAA,KAA8C;AAC7D,QAAA,MAAM,IAAA,GAAO,MAAM,IAAI,CAAA;AACvB,QAAA,IAAI,IAAA,KAAS,QAAW,OAAO,MAAA;AAC/B,QAAA,OAAO,OAAO,GAAA,KAAQ;AACpB,UAAA,MAAM,IAAA,GAAO,MAAM,SAAA,CAAU,UAAA,CAAW,IAAA,CAAK,GAAG,CAAC,CAAA,EAAG,EAAE,MAAA,EAAQ,CAAA;AAC9D,UAAA,IAAI,MAAA,CAAO,OAAA,EAAS,MAAM,IAAI,gBAAA,EAAiB;AAC/C,UAAA,OAAO,cAAA,CAAe,IAAA,EAAM,OAAA,CAAQ,GAAA,CAAI,IAAI,CAAC,CAAA;AAAA,QAC/C,CAAA;AAAA,MACF,CAAA;AAEA,MAAA,SAAA;AAAA,QACE,OAAA;AAAA,QACA,OAAA;AAAA,QACA,GAAA;AAAA,QACA,OAAA,CAAQ,GAAA;AAAA,QACR,QAAQ,SAAA,IAAa,kBAAA;AAAA,QACrB;AAAA,UACE,UAAU,eAAA,CAAgB,OAAA,EAAS,OAAA,CAAQ,KAAA,IAAS,KAAK,GAAG,CAAA;AAAA,UAC5D,OAAA;AAAA,UACA,IAAA,EAAM,UAAA;AAAA,YACJ,OAAA;AAAA,YACA,OAAA,CAAQ,aAAa,EAAC;AAAA,YACtB,IAAA;AAAA,YACA;AAAA;AACF;AACF,OACF,CAAE,IAAA;AAAA,QACA,CAAC,MAAA,KAAW,MAAA,CAAO,MAAA,CAAO,OAAA,CAAQ,MAAM,CAAC,CAAA;AAAA,QACzC,CAAC,KAAA,KAAmB,MAAA,CAAO,SAAA,CAAmB,KAAK,CAAC;AAAA,OACtD;AAAA,IACF,CAAC,CAAA;AAAA,EACH,CAAC,CAAA;AACH;AAMA,SAAS,UACP,KAAA,EACiD;AACjD,EAAA,IACE,KAAA,YAAiB,wBAAA,IACjB,KAAA,YAAiB,mBAAA,EACjB;AACA,IAAA,OAAO,MAAA,CAAO,KAAK,KAAkC,CAAA;AAAA,EACvD;AACA,EAAA,IAAI,iBAAiB,WAAA,EAAa;AAChC,IAAA,aAAA,CAAc,MAAM,OAAA,EAAS,YAAA,CAAa,KAAK,CAAA,IAAK,EAAE,CAAA;AACtD,IAAA,OAAO,MAAA,CAAO,IAAA,CAAK,KAAA,CAAM,OAAoC,CAAA;AAAA,EAC/D;AACA,EAAA,IAAI,KAAA,YAAiB,gBAAA,EAAkB,OAAO,MAAA,CAAO,SAAA;AACrD,EAAA,OAAO,MAAA,CAAO,IAAI,KAAK,CAAA;AACzB;AAmBA,SAAS,UAAA,CACP,OAAA,EACA,SAAA,EACA,IAAA,EACA,MAAA,EACiB;AACjB,EAAA,MAAM,QAAA,GAAW,cAAA;AAGjB,EAAA,MAAM,OAAA,uBAAc,GAAA,EAAwB;AAC5C,EAAA,IAAI,WAAgB,EAAC;AACrB,EAAA,IAAI,MAAA;AACJ,EAAA,IAAI,IAAA;AACJ,EAAA,MAAM,SAAS,MAAY;AACzB,IAAA,MAAM,KAAA,GAAQ,IAAA;AACd,IAAA,IAAA,GAAO,MAAA;AACP,IAAA,KAAA,IAAQ;AAAA,EACV,CAAA;AACA,EAAA,MAAA,CAAO,iBAAiB,OAAA,EAAS,MAAA,EAAQ,EAAE,IAAA,EAAM,MAAM,CAAA;AAEvD,EAAA,MAAM,IAAA,GAAO,CAAC,GAAA,KAAmC;AAC/C,IAAA,GAAA,CAAI,OAAA,GAAU,IAAA;AACd,IAAA,OAAO,GAAA,CAAI,KAAA,IAAS,GAAA,CAAI,KAAA,KAAU,SAC9B,OAAA,CAAQ,OAAA,EAAQ,GAChB,MAAA,CAAO,UAAA,CAAW,KAAA,CAAM,SAAA,CAAU,GAAA,CAAI,KAAK,CAAC,CAAA;AAAA,EAClD,CAAA;AACA,EAAA,MAAM,UAAA,GAAa,MACjB,CAAC,GAAG,OAAA,CAAQ,MAAA,EAAQ,CAAA,CAAE,IAAA,CAAK,CAAC,GAAA,KAAQ,CAAC,IAAI,KAAK,CAAA;AAEhD,EAAA,OAAO;AAAA,IACL,UAAU,KAAA,EAAO;AACf,MAAA,MAAM,OAAA,GAAU,WAAA,CAAY,OAAA,EAAS,KAAK,CAAA;AAC1C,MAAA,KAAA,MAAW,CAAC,EAAA,EAAI,GAAG,CAAA,IAAK,OAAA,EAAS;AAC/B,QAAA,IAAI,OAAA,CAAQ,GAAA,CAAI,EAAE,CAAA,EAAG;AACrB,QAAA,OAAA,CAAQ,OAAO,EAAE,CAAA;AACjB,QAAA,KAAK,KAAK,GAAG,CAAA;AAAA,MACf;AACA,MAAA,KAAA,MAAW,CAAC,EAAA,EAAI,GAAG,CAAA,IAAK,OAAA,EAAS;AAC/B,QAAA,IAAI,OAAA,CAAQ,GAAA,CAAI,EAAE,CAAA,EAAG;AACrB,QAAA,MAAM,OAAO,SAAA,CAAU,GAAA,CAAI,IAAI,CAAA,IAAK,QAAA,CAAS,IAAI,IAAI,CAAA;AACrD,QAAA,IAAI,SAAS,MAAA,EAAW;AACtB,UAAA,MAAM,IAAI,KAAA;AAAA,YACR,CAAA,+CAAA,EAAkD,IAAI,IAAI,CAAA,sCAAA;AAAA,WAE5D;AAAA,QACF;AACA,QAAA,MAAM,KAAA,GAAoB,EAAE,OAAA,EAAS,KAAA,EAAO,OAAO,KAAA,EAAM;AACzD,QAAA,OAAA,CAAQ,GAAA,CAAI,IAAI,KAAK,CAAA;AACrB,QAAA,KAAA,CAAM,KAAA,GAAQ,IAAA;AAAA,UACZ,MAAA,CAAO,UAAA;AAAA,YAAW,KAAK,GAAG,CAAA;AAAA,YAAG,CAAC,IAAA,KAC5B,MAAA,CAAO,IAAA,CAAK,MAAM;AAChB,cAAA,IAAI,MAAM,OAAA,EAAS;AACnB,cAAA,QAAA,CAAS,KAAK,IAAS,CAAA;AACvB,cAAA,MAAA,EAAO;AAAA,YACT,CAAC;AAAA,WACH,CAAE,IAAA;AAAA,YACA,MAAA,CAAO,MAAA;AAAA,cAAO,CAAC,IAAA,KACb,MAAA,CAAO,IAAA,CAAK,MAAM;AAChB,gBAAA,KAAA,CAAM,KAAA,GAAQ,IAAA;AACd,gBAAA,IACE,CAAC,KAAA,CAAM,OAAA,IACP,IAAA,CAAK,SAAA,CAAU,IAAI,CAAA,IACnB,CAAC,KAAA,CAAM,iBAAA,CAAkB,IAAA,CAAK,KAAK,CAAA,EACnC;AACA,kBAAA,MAAA,KAAW,IAAA,CAAK,KAAA;AAAA,gBAClB;AACA,gBAAA,MAAA,EAAO;AAAA,cACT,CAAC;AAAA;AACH;AACF,SACF;AAAA,MACF;AAAA,IACF,CAAA;AAAA,IACA,MAAM,IAAA,GAAO;AACX,MAAA,WAAS;AACP,QAAA,IAAI,MAAA,CAAO,OAAA,EAAS,MAAM,IAAI,gBAAA,EAAiB;AAC/C,QAAA,IAAI,QAAA,CAAS,SAAS,CAAA,EAAG;AACvB,UAAA,MAAM,GAAA,GAAM,QAAA;AACZ,UAAA,QAAA,GAAW,EAAC;AACZ,UAAA,OAAO,GAAA;AAAA,QACT;AACA,QAAA,IAAI,MAAA,KAAW,MAAA,EAAW,MAAM,KAAA,CAAM,OAAO,MAAM,CAAA;AACnD,QAAA,IAAI,CAAC,UAAA,EAAW,EAAG,OAAO,MAAA;AAC1B,QAAA,MAAM,IAAI,OAAA,CAAc,CAAC,OAAA,KAAY;AACnC,UAAA,IAAA,GAAO,OAAA;AAAA,QACT,CAAC,CAAA;AAAA,MACH;AAAA,IACF,CAAA;AAAA,IACA,MAAM,KAAA,GAAQ;AACZ,MAAA,MAAA,CAAO,mBAAA,CAAoB,SAAS,MAAM,CAAA;AAC1C,MAAA,MAAM,QAAA,GAAW,CAAC,GAAG,OAAA,CAAQ,QAAQ,CAAA,CAAE,IAAI,IAAI,CAAA;AAC/C,MAAA,OAAA,CAAQ,KAAA,EAAM;AACd,MAAA,MAAM,OAAA,CAAQ,IAAI,QAAQ,CAAA;AAAA,IAC5B;AAAA,GACF;AACF","file":"index.js","sourcesContent":["// ---------------------------------------------------------------------------\n// `drive(machine, initial, msg, interpret, opts)` for the Effect engine: the\n// Promise `drive`'s rounds, trace and bound, over the Effect engine's handlers\n// and Subs (#321).\n//\n// There is no second loop. Each Effect cell is adapted into the shared loop's\n// cell with the settle rule the engine itself uses (`settleCellExit` in\n// `src/effect/run.ts`), run with the services the caller provided, so a cell\n// settles here exactly as it does under `run`. The Subs the machine wants are\n// started and stopped after every fold, as the engine reconciles them, and\n// their Msgs are folded once the Cmds in flight have settled.\n// ---------------------------------------------------------------------------\n\nimport { Cause, Effect, Exit, Fiber, Stream } from \"effect\";\nimport { CellFailure } from \"../../effect/failures\";\nimport {\n builtinRunners,\n type CellErrors,\n cellResult,\n type EffectInterpret,\n type EffectSubscribe,\n type InterpretServices,\n type SubscribeOption,\n type SubscribeServices,\n settleCellExit,\n} from \"../../effect/run\";\nimport type { Cmd, Machine, Sub } from \"../../pure/core\";\nimport {\n annotateTrace,\n DEFAULT_MAX_ROUNDS,\n type DriveCell,\n DriveNoHandlerError,\n type DriveOptions,\n type DriveResult,\n DriveRoundsExceededError,\n type DriveSubs,\n desiredSubs,\n driveContractOf,\n driveLoop,\n driveTraceOf,\n} from \"../drive-loop\";\n\n/**\n * The Effect `drive`'s options: the Promise `drive`'s, plus `subscribe` — the\n * runners the engine's `run` takes, required exactly when `run` requires them.\n */\nexport type EffectDriveOptions<U extends Sub, Ctx, B> = DriveOptions<Ctx> &\n SubscribeOption<U, B>;\n\n/** The failures a drive ends with: the loop's own, and a hand-written cell's. */\nexport type EffectDriveError<M, C extends Cmd, I> =\n | DriveRoundsExceededError<M, C>\n | DriveNoHandlerError<M, C>\n | CellErrors<C, I>;\n\n/** The drive was interrupted; nothing is left to run. */\nclass DriveInterrupted {}\n\ntype AnyCell = (cmd: unknown) => Effect.Effect<unknown, unknown>;\ntype AnyRunner = (sub: Sub) => Stream.Stream<unknown, unknown>;\ntype RunEffect = (\n effect: Effect.Effect<unknown, unknown>,\n options: { readonly signal: AbortSignal },\n) => Promise<Exit.Exit<unknown, unknown>>;\ntype ForkEffect = (\n effect: Effect.Effect<unknown, unknown>,\n) => Fiber.Fiber<unknown, unknown>;\n\n/**\n * Drive `machine` from `initial` through `msg` against the REAL Effect\n * `interpret` map — the one a host hands the Effect engine's `run` — and the\n * Subs the machine wants, feeding every Msg back until the machine goes quiet.\n * Yields the settled state together with the whole history, `{ state, trace }`.\n *\n * The rounds, the trace, `maxRounds` and `clock` are the Promise `drive`'s\n * (`@demlik/tea/testing/promise`). What is the Effect engine's:\n *\n * - a cell returns an Effect. A `Cmd.define`d Cmd's success mints `_ok` and\n * its declared failure `_err`; a hand-written Cmd's success is its\n * follow-up — a Msg, a list of them folded in order, or nothing (#324) —\n * and its failure fails the drive, typed. A defect dies. A cell takes no\n * `dispatch`, so a defined Cmd's handler cannot send its own `_ok` /\n * `_err`: the engine's rule holds by construction;\n * - the services the cells and runners read are this Effect's requirements,\n * so a test provides them the way a host does: `Effect.provide(layer)`;\n * - the Subs the machine wants run, through `opts.subscribe` or the engine's\n * built-in runner, started and stopped after every fold. Their Msgs are\n * folded once no Cmd's follow-up is pending. The drive settles once no Msg\n * is pending and no Sub is still running, so hand it a runner whose Stream\n * ends — a Sub that never ends keeps it waiting. A Stream that fails with\n * an error it did not map to a Msg dies, as it stops the engine's run.\n *\n * Interrupting the drive interrupts every cell and Sub still running.\n *\n * ```ts\n * const { state, trace } = await Effect.runPromise(\n * drive(machine, initial, { type: \"go\" }, {\n * fetch: (cmd) =>\n * Effect.gen(function* () {\n * const api = yield* Api;\n * return yield* api.get(cmd.id);\n * }),\n * }).pipe(Effect.provide(ApiTest)),\n * );\n * ```\n *\n * Fails with {@link DriveRoundsExceededError} when the machine is still\n * emitting work after `opts.maxRounds` rounds, {@link DriveNoHandlerError}\n * for a Cmd no cell answers, or a hand-written cell's own failure — each with\n * the partial trace, readable via `driveTraceOf`.\n */\nexport function drive<\n S,\n M extends { type: string },\n C extends Cmd,\n U extends Sub,\n Ctx,\n // The defaults read no services, so an omitted map adds none to `R`.\n I extends EffectInterpret<M, C> = EffectInterpret<M, C, never>,\n B extends EffectSubscribe<M, U> = EffectSubscribe<M, U, never>,\n>(\n machine: Machine<S, M, C, U, Ctx>,\n initial: S,\n msg: M,\n interpret: I,\n ...[opts]: Record<never, never> extends EffectDriveOptions<U, Ctx, B>\n ? [opts?: EffectDriveOptions<U, Ctx, B>]\n : [opts: EffectDriveOptions<U, Ctx, B>]\n): Effect.Effect<\n DriveResult<S, M, C>,\n EffectDriveError<M, C, I>,\n InterpretServices<I> | SubscribeServices<B>\n> {\n const options = (opts ?? {}) as {\n ctx?: Ctx;\n maxRounds?: number;\n clock?: () => number;\n subscribe?: Readonly<Record<string, AnyRunner | undefined>>;\n };\n return Effect.gen(function* () {\n const services = yield* Effect.context<\n InterpretServices<I> | SubscribeServices<B>\n >();\n const runEffect = Effect.runPromiseExitWith(services) as RunEffect;\n const fork = Effect.runForkWith(services) as ForkEffect;\n\n return yield* Effect.callback<\n DriveResult<S, M, C>,\n EffectDriveError<M, C, I>\n >((resume, signal) => {\n const cells = interpret as unknown as Readonly<\n Record<string, AnyCell | undefined>\n >;\n const defined = new Set((machine.cmds ?? []).map((def) => def.cmdType));\n const cellFor = (type: string): DriveCell<M, C> | undefined => {\n const cell = cells[type];\n if (cell === undefined) return undefined;\n return async (cmd) => {\n const exit = await runEffect(cellResult(cell(cmd)), { signal });\n if (signal.aborted) throw new DriveInterrupted();\n return settleCellExit(exit, defined.has(type));\n };\n };\n\n driveLoop(\n machine,\n initial,\n msg,\n options.ctx as Ctx,\n options.maxRounds ?? DEFAULT_MAX_ROUNDS,\n {\n contract: driveContractOf(machine, options.clock ?? Date.now),\n cellFor,\n subs: effectSubs<S, M>(\n machine,\n options.subscribe ?? {},\n fork,\n signal,\n ),\n },\n ).then(\n (result) => resume(Effect.succeed(result)),\n (error: unknown) => resume(failureOf<M, C, I>(error)),\n );\n });\n });\n}\n\n/**\n * Sort what left the loop: its own failures and a hand-written cell's failure\n * are typed, everything else — a defect, a contract breach, a failed Sub — dies.\n */\nfunction failureOf<M, C extends Cmd, I>(\n error: unknown,\n): Effect.Effect<never, EffectDriveError<M, C, I>> {\n if (\n error instanceof DriveRoundsExceededError ||\n error instanceof DriveNoHandlerError\n ) {\n return Effect.fail(error as EffectDriveError<M, C, I>);\n }\n if (error instanceof CellFailure) {\n annotateTrace(error.failure, driveTraceOf(error) ?? []);\n return Effect.fail(error.failure as EffectDriveError<M, C, I>);\n }\n if (error instanceof DriveInterrupted) return Effect.interrupt;\n return Effect.die(error);\n}\n\n/**\n * One started Sub. It stays registered while the machine wants it, even after\n * its Stream ends, so a Sub that finished is not started again — the engine\n * keeps it the same way. `stopped` drops any Msg that arrives after a fold\n * turned it off; `ended` says its Stream no longer runs.\n */\ninterface StartedSub {\n stopped: boolean;\n ended: boolean;\n fiber?: Fiber.Fiber<unknown, unknown>;\n}\n\n/**\n * The Effect engine's Subs as the shared loop runs them: each wanted Sub's\n * Stream is drained on its own fiber into a buffer the loop reads between\n * rounds.\n */\nfunction effectSubs<S, M>(\n machine: { readonly subs?: ReadonlyArray<unknown> },\n subscribe: Readonly<Record<string, AnyRunner | undefined>>,\n fork: ForkEffect,\n signal: AbortSignal,\n): DriveSubs<S, M> {\n const builtins = builtinRunners as unknown as Readonly<\n Record<string, AnyRunner | undefined>\n >;\n const started = new Map<string, StartedSub>();\n let buffered: M[] = [];\n let failed: Cause.Cause<unknown> | undefined;\n let wake: (() => void) | undefined;\n const notify = (): void => {\n const woken = wake;\n wake = undefined;\n woken?.();\n };\n signal.addEventListener(\"abort\", notify, { once: true });\n\n const stop = (sub: StartedSub): Promise<void> => {\n sub.stopped = true;\n return sub.ended || sub.fiber === undefined\n ? Promise.resolve()\n : Effect.runPromise(Fiber.interrupt(sub.fiber));\n };\n const anyRunning = (): boolean =>\n [...started.values()].some((sub) => !sub.ended);\n\n return {\n reconcile(state) {\n const desired = desiredSubs(machine, state);\n for (const [id, sub] of started) {\n if (desired.has(id)) continue;\n started.delete(id);\n void stop(sub);\n }\n for (const [id, sub] of desired) {\n if (started.has(id)) continue;\n const make = subscribe[sub.type] ?? builtins[sub.type];\n if (make === undefined) {\n throw new Error(\n `@demlik/tea: no subscribe runner for Sub type \"${sub.type}\". ` +\n \"Pass one to drive in `subscribe`.\",\n );\n }\n const entry: StartedSub = { stopped: false, ended: false };\n started.set(id, entry);\n entry.fiber = fork(\n Stream.runForEach(make(sub), (next) =>\n Effect.sync(() => {\n if (entry.stopped) return;\n buffered.push(next as M);\n notify();\n }),\n ).pipe(\n Effect.onExit((exit) =>\n Effect.sync(() => {\n entry.ended = true;\n if (\n !entry.stopped &&\n Exit.isFailure(exit) &&\n !Cause.hasInterruptsOnly(exit.cause)\n ) {\n failed ??= exit.cause;\n }\n notify();\n }),\n ),\n ),\n );\n }\n },\n async next() {\n for (;;) {\n if (signal.aborted) throw new DriveInterrupted();\n if (buffered.length > 0) {\n const out = buffered;\n buffered = [];\n return out;\n }\n if (failed !== undefined) throw Cause.squash(failed);\n if (!anyRunning()) return undefined;\n await new Promise<void>((resolve) => {\n wake = resolve;\n });\n }\n },\n async close() {\n signal.removeEventListener(\"abort\", notify);\n const stopping = [...started.values()].map(stop);\n started.clear();\n await Promise.all(stopping);\n },\n };\n}\n"]}
|
package/dist/testing/index.d.ts
CHANGED
|
@@ -1,112 +1,7 @@
|
|
|
1
|
-
import { C as Cmd,
|
|
2
|
-
import {
|
|
1
|
+
import { C as Cmd, c as Sub, M as Machine, B as BuiltinSub } from '../core-Ce9ncpuv.js';
|
|
2
|
+
import { R as Runtime } from '../runtime-types-BKg2CGMx.js';
|
|
3
3
|
import '@standard-schema/spec';
|
|
4
4
|
|
|
5
|
-
/**
|
|
6
|
-
* The composed Model shape every `withX` wrapper produces: the base machine's
|
|
7
|
-
* state nested under `base`, and the wrapper's own NAMED, serializable slice
|
|
8
|
-
* under a single `$`-prefixed key. This helper does not care WHICH `$`-key the
|
|
9
|
-
* wrapper uses — it discovers it structurally (the one non-`base` field).
|
|
10
|
-
*/
|
|
11
|
-
type WrapperModel<S> = {
|
|
12
|
-
readonly base: S;
|
|
13
|
-
} & {
|
|
14
|
-
readonly [K: `$${string}`]: unknown;
|
|
15
|
-
};
|
|
16
|
-
/** Options for the conformance replay. Mirrors `replay`'s second argument. */
|
|
17
|
-
interface AssertWrapperFaithfulOpts<S, WM, BaseM, BaseCtx, WrapperCtx> {
|
|
18
|
-
/** The Msg sequence to fold through BOTH machines. Use the base's Msgs. */
|
|
19
|
-
readonly msgs: readonly BaseM[];
|
|
20
|
-
/** Ctx for the WRAPPED machine (base ctx + wrapper ports). */
|
|
21
|
-
readonly ctx: WrapperCtx;
|
|
22
|
-
/** Ctx for the BARE base machine. Defaults to `ctx` when omitted. */
|
|
23
|
-
readonly baseCtx?: BaseCtx;
|
|
24
|
-
/** Optional pre-`init` snapshot of the COMPOSED Model (wrapped rehydrate). */
|
|
25
|
-
readonly loaded?: WM | null;
|
|
26
|
-
/**
|
|
27
|
-
* Optional pre-`init` snapshot of the BARE base state. Supply when the
|
|
28
|
-
* wrapped `loaded` is non-null so the base-equivalence check rehydrates the
|
|
29
|
-
* base machine from the matching nested slice. Defaults to `loaded?.base`.
|
|
30
|
-
*/
|
|
31
|
-
readonly baseLoaded?: S | null;
|
|
32
|
-
/**
|
|
33
|
-
* Opt-in relaxation for an INTERCEPTING wrapper (e.g. a retry wrapper).
|
|
34
|
-
*
|
|
35
|
-
* DEFAULT (`undefined` / `false`) — the OBSERVE-ONLY contract that a
|
|
36
|
-
* deadline wrapper satisfies: every bare base Cmd must appear
|
|
37
|
-
* UNCHANGED, in order, as a subsequence of the wrapped Cmd stream (properties
|
|
38
|
-
* 1 and 3b). The wrapper may APPEND its own Cmds but may NOT alter, drop, or
|
|
39
|
-
* retime a base Cmd. This is the strict default and it is NOT weakened.
|
|
40
|
-
*
|
|
41
|
-
* WHEN SET — for a wrapper whose whole job is to INTERCEPT a chosen base Cmd
|
|
42
|
-
* (retag it into a wrapper-owned carrier so the concern — cache / breaker /
|
|
43
|
-
* rate-limit / retry — can gate it), a target base Cmd LEGITIMATELY does not
|
|
44
|
-
* appear raw at the top level of the wrapped stream. The contract is still
|
|
45
|
-
* **retag-and-re-emit, never swallow** (rule 2): the Cmd must be RECORDED, not
|
|
46
|
-
* vanished. So when a bare base Cmd is ABSENT from the top-level wrapped
|
|
47
|
-
* subsequence, this option accepts it IFF, in that SAME transition window, it
|
|
48
|
-
* is recorded by the wrapper as one of:
|
|
49
|
-
*
|
|
50
|
-
* - the inner payload of a `$x:*` "run" carrier Cmd (`carrierCmdType` +
|
|
51
|
-
* `innerField`) — the retag path; OR
|
|
52
|
-
* - any other `$x:*` divergence Cmd whose `recordsField` deep-equals the
|
|
53
|
-
* base Cmd — the gate/defer path (e.g. a "fast-failed" or "deferred"
|
|
54
|
-
* record Cmd that carries the base Cmd it diverged on).
|
|
55
|
-
*
|
|
56
|
-
* A target base Cmd that is NEITHER present raw NOR recorded by any `$x:*`
|
|
57
|
-
* Cmd in its window is a SWALLOW betrayal and still fails the gate.
|
|
58
|
-
*
|
|
59
|
-
* Provide the field names the carrier uses so the gate can unwrap it
|
|
60
|
-
* structurally without hard-coding any one wrapper's vocabulary.
|
|
61
|
-
*/
|
|
62
|
-
readonly intercepting?: InterceptingOpt;
|
|
63
|
-
}
|
|
64
|
-
/**
|
|
65
|
-
* Configures the intercepting relaxation (see
|
|
66
|
-
* `AssertWrapperFaithfulOpts.intercepting`). All fields name the carrier /
|
|
67
|
-
* divergence shape so the gate can structurally find a retagged base Cmd
|
|
68
|
-
* inside a `$x:*` Cmd. The gate only EVER inspects `$x:*`-prefixed wrapped
|
|
69
|
-
* Cmds for a recorded base Cmd — it never relaxes the check for a base Cmd
|
|
70
|
-
* that appears under any non-wrapper Cmd type.
|
|
71
|
-
*/
|
|
72
|
-
interface InterceptingOpt {
|
|
73
|
-
/**
|
|
74
|
-
* The `Cmd.type` of the wrapper's run-carrier (e.g. `"$resilience:run"`).
|
|
75
|
-
* A bare base Cmd that is retagged appears as `carrier[innerField]`.
|
|
76
|
-
*/
|
|
77
|
-
readonly carrierCmdType: string;
|
|
78
|
-
/** The field on the carrier Cmd that holds the original base Cmd. */
|
|
79
|
-
readonly innerField: string;
|
|
80
|
-
/**
|
|
81
|
-
* OPTIONAL field on ANY OTHER `$x:*` Cmd that records a base Cmd the wrapper
|
|
82
|
-
* gated/deferred without running it (e.g. a fast-fail record). When present
|
|
83
|
-
* on a `$x:*` Cmd and deep-equal to the missing base Cmd, it counts as a
|
|
84
|
-
* recorded divergence. Omit when the wrapper only ever retags via the
|
|
85
|
-
* carrier.
|
|
86
|
-
*/
|
|
87
|
-
readonly recordsField?: string;
|
|
88
|
-
}
|
|
89
|
-
/**
|
|
90
|
-
* Assert a `withX` wrapper is TEA-faithful. Replays the wrapped machine and the
|
|
91
|
-
* bare base machine over the same Msgs and checks the four conformance
|
|
92
|
-
* properties (see file header).
|
|
93
|
-
*
|
|
94
|
-
* @param base the bare base `Machine`.
|
|
95
|
-
* @param makeWrapped a thunk that wraps `base` and returns the composed machine
|
|
96
|
-
* (e.g. `() => withTimeout(wired, cfg).machine`, where `wired.machine` is
|
|
97
|
-
* `base`). A thunk — not the wrapped value — so a wrapper that registers
|
|
98
|
-
* process-scoped ports (`definePort`) is constructed inside the assertion's
|
|
99
|
-
* control, and so the same betrayal-detector reads as
|
|
100
|
-
* `assertWrapperFaithful(wired.machine, () => withX(wired, cfg).machine, ...)`
|
|
101
|
-
* at every wrapper's test site.
|
|
102
|
-
* @param opts the conformance replay options.
|
|
103
|
-
*/
|
|
104
|
-
declare function assertWrapperFaithful<S, BaseM extends {
|
|
105
|
-
type: string;
|
|
106
|
-
}, BaseC extends Cmd, BaseU extends Sub, BaseCtx, WM extends WrapperModel<S>, WrapperM extends {
|
|
107
|
-
type: string;
|
|
108
|
-
}, WrapperC extends Cmd, WrapperU extends Sub, WrapperCtx>(base: Machine<S, BaseM, BaseC, BaseU, BaseCtx>, makeWrapped: () => Machine<WM, WrapperM, WrapperC, WrapperU, WrapperCtx>, opts: AssertWrapperFaithfulOpts<S, WM, BaseM, BaseCtx, WrapperCtx>): void;
|
|
109
|
-
|
|
110
5
|
/**
|
|
111
6
|
* Shared options shape for every test assertion below. Mirrors the
|
|
112
7
|
* second argument of `replay(machine, opts)` — `msgs` is the sequence to
|
|
@@ -266,154 +161,23 @@ declare function bindMachine<S, M extends {
|
|
|
266
161
|
}, C extends Cmd, U extends Sub, Ctx>(machine: Machine<S, M, C, U, Ctx>, ctx: Ctx): BoundMachine<S, M, C, U, Ctx>;
|
|
267
162
|
|
|
268
163
|
/**
|
|
269
|
-
*
|
|
164
|
+
* Assert that replaying `opts.msgs` is a pure function of the Msg log: the
|
|
165
|
+
* final state and every emitted Cmd come out the same under two different
|
|
166
|
+
* global wall-clocks and RNG seeds.
|
|
270
167
|
*
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
*
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
/**
|
|
278
|
-
* One entry of a driven run's history, in dispatch order.
|
|
168
|
+
* `init` and `update` never receive a clock, so a cell that reads
|
|
169
|
+
* `Date.now()` or `Math.random()` reaches for the global one, and a replay of
|
|
170
|
+
* the same log on another day or another host lands somewhere else. Two
|
|
171
|
+
* back-to-back replays under one clock would hide that; this helper stubs
|
|
172
|
+
* two distinct ambients and compares. Time belongs in the Msg (an `at`
|
|
173
|
+
* field), where it is identical across replays.
|
|
279
174
|
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
* returned (or fired through its injected `dispatch`).
|
|
283
|
-
* - `cmd` — a Cmd handed to an interpret handler. It is recorded BEFORE the
|
|
284
|
-
* handler is awaited, so a trace recovered from a rejected handler names the
|
|
285
|
-
* Cmd that rejected as its last entry.
|
|
175
|
+
* Subs are not compared: they are derived from the final state, so a state
|
|
176
|
+
* that matches yields the same Subs.
|
|
286
177
|
*/
|
|
287
|
-
|
|
288
|
-
readonly kind: "msg";
|
|
289
|
-
readonly msg: M;
|
|
290
|
-
} | {
|
|
291
|
-
readonly kind: "cmd";
|
|
292
|
-
readonly cmd: C;
|
|
293
|
-
};
|
|
294
|
-
/** What a settled {@link drive} hands back. */
|
|
295
|
-
interface DriveResult<S, M, C> {
|
|
296
|
-
/** The state after the machine went quiet — no Msg pending, no Cmd unperformed. */
|
|
297
|
-
readonly state: S;
|
|
298
|
-
/**
|
|
299
|
-
* Every Msg folded and every Cmd dispatched, in order. Filtering it to its
|
|
300
|
-
* `msg` entries and replaying those through `replay` from the same initial
|
|
301
|
-
* state reproduces {@link DriveResult.state}.
|
|
302
|
-
*/
|
|
303
|
-
readonly trace: readonly DriveTraceEntry<M, C>[];
|
|
304
|
-
}
|
|
305
|
-
/**
|
|
306
|
-
* {@link drive}'s `ctx` field. `CtxArg` widened by exactly one case: a machine
|
|
307
|
-
* declaring `ctx: undefined` in its `types` block — the pure shape every
|
|
308
|
-
* how-to on this package writes — may omit it too. `CtxArg` alone keeps `ctx`
|
|
309
|
-
* REQUIRED there, because `{}` is not assignable to `undefined`, which would
|
|
310
|
-
* make the common pure call site spell `{ ctx: undefined }` to say nothing.
|
|
311
|
-
* A machine whose `Ctx` carries a field a handler reads still requires it.
|
|
312
|
-
*/
|
|
313
|
-
type DriveCtxArg<Ctx> = [undefined] extends [Ctx] ? {
|
|
314
|
-
readonly ctx?: Ctx;
|
|
315
|
-
} : CtxArg<Ctx>;
|
|
316
|
-
/**
|
|
317
|
-
* {@link drive}'s options. `ctx` is conditionally optional (see
|
|
318
|
-
* {@link DriveCtxArg}).
|
|
319
|
-
*/
|
|
320
|
-
type DriveOptions<Ctx> = DriveCtxArg<Ctx> & {
|
|
321
|
-
/**
|
|
322
|
-
* The round bound. Exceeding it throws {@link DriveRoundsExceededError};
|
|
323
|
-
* `drive` never returns a half-driven state. Defaults to
|
|
324
|
-
* {@link DEFAULT_MAX_ROUNDS} (100).
|
|
325
|
-
*/
|
|
326
|
-
readonly maxRounds?: number;
|
|
327
|
-
/**
|
|
328
|
-
* The clock that stamps `at` on a `Cmd.define`d Cmd's minted `_ok` / `_err`
|
|
329
|
-
* Msg — the one `run` takes. Defaults to `Date.now`; pin it for a test that
|
|
330
|
-
* asserts on `at`.
|
|
331
|
-
*/
|
|
332
|
-
readonly clock?: () => number;
|
|
333
|
-
};
|
|
334
|
-
/**
|
|
335
|
-
* Raised when a driven machine is still emitting work after `maxRounds` rounds.
|
|
336
|
-
* Follows the tea error dialect (`override readonly name`) and carries a `_tag`
|
|
337
|
-
* discriminant, so a test branches on it by `instanceof` or on the tag.
|
|
338
|
-
*
|
|
339
|
-
* It carries the round count it stopped at and the PARTIAL trace up to that
|
|
340
|
-
* point, which is what a test needs to see the cycle — the bound alone says
|
|
341
|
-
* only that one exists.
|
|
342
|
-
*/
|
|
343
|
-
declare class DriveRoundsExceededError<M, C> extends Error {
|
|
344
|
-
/** The bound that was exceeded. */
|
|
345
|
-
readonly maxRounds: number;
|
|
346
|
-
/** The round the driver stopped at — always `maxRounds + 1`. */
|
|
347
|
-
readonly rounds: number;
|
|
348
|
-
/** Every Msg folded and Cmd dispatched before the driver gave up. */
|
|
349
|
-
readonly trace: readonly DriveTraceEntry<M, C>[];
|
|
350
|
-
readonly name = "DriveRoundsExceededError";
|
|
351
|
-
readonly _tag: "DriveRoundsExceededError";
|
|
352
|
-
constructor(
|
|
353
|
-
/** The bound that was exceeded. */
|
|
354
|
-
maxRounds: number,
|
|
355
|
-
/** The round the driver stopped at — always `maxRounds + 1`. */
|
|
356
|
-
rounds: number,
|
|
357
|
-
/** Every Msg folded and Cmd dispatched before the driver gave up. */
|
|
358
|
-
trace: readonly DriveTraceEntry<M, C>[]);
|
|
359
|
-
}
|
|
360
|
-
/**
|
|
361
|
-
* Raised when a Cmd reaches {@link drive} with no handler for its `type` in the
|
|
362
|
-
* `handlers` record. `Interpret`'s mapped type makes that a compile error at a
|
|
363
|
-
* well-typed call site; this is the runtime arm for a handler record assembled
|
|
364
|
-
* dynamically or cast.
|
|
365
|
-
*/
|
|
366
|
-
declare class DriveNoHandlerError<M, C> extends Error {
|
|
367
|
-
/** The Cmd `type` nothing in the handler record answered. */
|
|
368
|
-
readonly cmdType: string;
|
|
369
|
-
/** Every Msg folded and Cmd dispatched before the miss. */
|
|
370
|
-
readonly trace: readonly DriveTraceEntry<M, C>[];
|
|
371
|
-
readonly name = "DriveNoHandlerError";
|
|
372
|
-
readonly _tag: "DriveNoHandlerError";
|
|
373
|
-
constructor(
|
|
374
|
-
/** The Cmd `type` nothing in the handler record answered. */
|
|
375
|
-
cmdType: string,
|
|
376
|
-
/** Every Msg folded and Cmd dispatched before the miss. */
|
|
377
|
-
trace: readonly DriveTraceEntry<M, C>[]);
|
|
378
|
-
}
|
|
379
|
-
/**
|
|
380
|
-
* Read back the partial trace `drive` attached to an error a handler rejected
|
|
381
|
-
* with. Returns `undefined` for anything `drive` did not annotate.
|
|
382
|
-
*
|
|
383
|
-
* A rejecting handler's error propagates out of `drive` UNSWALLOWED — same
|
|
384
|
-
* value, same `instanceof`, same `_tag` — so a test that already branches on
|
|
385
|
-
* the handler's own error type keeps doing so. The trace rides along on a
|
|
386
|
-
* non-enumerable property so it is recoverable without changing how the error
|
|
387
|
-
* prints or serializes.
|
|
388
|
-
*/
|
|
389
|
-
declare function driveTraceOf<M, C>(err: unknown): readonly DriveTraceEntry<M, C>[] | undefined;
|
|
390
|
-
/**
|
|
391
|
-
* Drive `machine` from `initial` through `msg` against the REAL interpret
|
|
392
|
-
* `handlers`, feeding every settle Msg back until the machine goes quiet, and
|
|
393
|
-
* hand back the settled state together with the whole history.
|
|
394
|
-
*
|
|
395
|
-
* `handlers` is the same table a host hands `run` as its `interpret` (e.g.
|
|
396
|
-
* a jev knob's `resilient_run` handler), so the test drives the real interpreter and
|
|
397
|
-
* mocks only the port beneath it.
|
|
398
|
-
*
|
|
399
|
-
* One round is: fold every pending Msg, then await a handler for every Cmd
|
|
400
|
-
* those folds emitted. A Msg a handler returns — or fires through its injected
|
|
401
|
-
* `dispatch` — is pending for the next round. A machine that emits no Cmd for
|
|
402
|
-
* its seed Msg therefore settles in ONE round and never awaits a handler at
|
|
403
|
-
* all.
|
|
404
|
-
*
|
|
405
|
-
* The `ctx` handed to each handler is `opts.ctx` augmented with a NO-OP `emit`,
|
|
406
|
-
* so a handler that emits to a `Port` runs without a live runtime; nothing here
|
|
407
|
-
* observes ports. Supply your own `emit` on `ctx` to capture them.
|
|
408
|
-
*
|
|
409
|
-
* @throws {DriveRoundsExceededError} when the machine is still emitting work
|
|
410
|
-
* after `opts.maxRounds` rounds. It never returns a half-driven state.
|
|
411
|
-
* @throws whatever a handler rejects with, unchanged — annotated with the
|
|
412
|
-
* partial trace, readable via {@link driveTraceOf}.
|
|
413
|
-
*/
|
|
414
|
-
declare function drive<S, M extends {
|
|
178
|
+
declare function expectReplayDeterministic<S, M extends {
|
|
415
179
|
type: string;
|
|
416
|
-
}, C extends Cmd, U extends Sub, Ctx>(machine: Machine<S, M, C, U, Ctx>,
|
|
180
|
+
}, C extends Cmd, U extends Sub, Ctx>(machine: Machine<S, M, C, U, Ctx>, opts: NoInfer<ReplayOpts<S, M, Ctx>>): void;
|
|
417
181
|
|
|
418
182
|
/**
|
|
419
183
|
* Construct an inert `Runtime<S, M>` value. Use at ctx sites that require
|
|
@@ -464,14 +228,18 @@ type StateFactoryAPI<S extends {
|
|
|
464
228
|
* overrides (undefineds stripped), then stamps the `type` discriminator.
|
|
465
229
|
*
|
|
466
230
|
* @example
|
|
467
|
-
*
|
|
468
|
-
*
|
|
469
|
-
*
|
|
470
|
-
*
|
|
471
|
-
*
|
|
231
|
+
* ```ts
|
|
232
|
+
* import { stateFactory } from "@demlik/tea/testing";
|
|
233
|
+
*
|
|
234
|
+
* type S = { type: "a"; n: number } | { type: "b"; s: string };
|
|
235
|
+
* const f = stateFactory<S>({ a: { n: 0 }, b: { s: "" } });
|
|
236
|
+
* f.a(); // { type: "a", n: 0 }
|
|
237
|
+
* f.a({ n: 42 }); // { type: "a", n: 42 }
|
|
238
|
+
* f.b({ s: "hi" }); // { type: "b", s: "hi" }
|
|
239
|
+
* ```
|
|
472
240
|
*/
|
|
473
241
|
declare function stateFactory<S extends {
|
|
474
242
|
type: string;
|
|
475
243
|
}>(defaults: StateFactoryDefaults<S>): StateFactoryAPI<S>;
|
|
476
244
|
|
|
477
|
-
export { type
|
|
245
|
+
export { type BoundMachine, type ReplayOpts, type StateFactoryAPI, type StateFactoryDefaults, bindMachine, expectActiveSubs, expectCmdEmitted, expectCmdSequence, expectFinalState, expectReplayDeterministic, noopRuntime, stateFactory, step };
|