@code-yeongyu/senpi-agent-core 2026.10.27-2 → 2026.10.30-1
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/conformance/index.d.ts +42 -0
- package/dist/conformance/index.js +23 -0
- package/dist/conformance/mapping.d.ts +24 -0
- package/dist/conformance/mapping.js +62 -0
- package/dist/conformance/registry.d.ts +15 -0
- package/dist/conformance/registry.js +75 -0
- package/dist/conformance/runner.d.ts +27 -0
- package/dist/conformance/runner.js +23 -0
- package/dist/conformance/suite-hash.d.ts +12 -0
- package/dist/conformance/suite-hash.js +20 -0
- package/dist/durable/store-admission.d.ts +7 -0
- package/dist/durable/store-admission.js +11 -0
- package/dist/durable/store-memory.d.ts +15 -0
- package/dist/durable/store-memory.js +140 -0
- package/dist/durable/store.d.ts +116 -0
- package/dist/durable/store.js +14 -0
- package/dist/durable/testing/kill-store.d.ts +13 -0
- package/dist/durable/testing/kill-store.js +77 -0
- package/dist/durable/testing/kill-wake.d.ts +12 -0
- package/dist/durable/testing/kill-wake.js +123 -0
- package/dist/durable/testing/manual-timers.d.ts +8 -0
- package/dist/durable/testing/manual-timers.js +31 -0
- package/dist/durable/testing/multi-host-cases.d.ts +6 -0
- package/dist/durable/testing/multi-host-cases.js +32 -0
- package/dist/durable/testing/store-cases.d.ts +21 -0
- package/dist/durable/testing/store-cases.js +168 -0
- package/dist/durable/testing/store-harness.d.ts +23 -0
- package/dist/durable/testing/store-harness.js +64 -0
- package/dist/durable/testing/wake-cases.d.ts +6 -0
- package/dist/durable/testing/wake-cases.js +100 -0
- package/dist/durable/testing/wake-harness.d.ts +64 -0
- package/dist/durable/testing/wake-harness.js +127 -0
- package/dist/durable/testing/wake-test-loop.d.ts +13 -0
- package/dist/durable/testing/wake-test-loop.js +40 -0
- package/dist/durable/wake-host-bun-timer.d.ts +31 -0
- package/dist/durable/wake-host-bun-timer.js +66 -0
- package/dist/durable/wake-host-store-alarm.d.ts +13 -0
- package/dist/durable/wake-host-store-alarm.js +20 -0
- package/dist/durable/wake-host.d.ts +68 -0
- package/dist/durable/wake-host.js +70 -0
- package/dist/harness/utils/read-folders/index.d.ts +1 -1
- package/dist/harness/utils/read-folders/index.js +1 -1
- package/dist/harness/utils/read-folders/tree-sitter/engine.d.ts +1 -1
- package/dist/harness/utils/read-folders/tree-sitter/engine.js +38 -19
- package/dist/testing/win32-sim.d.ts +76 -0
- package/dist/testing/win32-sim.js +197 -0
- package/dist/transport/in-process-carrier.d.ts +19 -0
- package/dist/transport/in-process-carrier.js +63 -0
- package/dist/transport/local-endpoint.d.ts +13 -0
- package/dist/transport/local-endpoint.js +20 -0
- package/dist/transport/reliable-transport.d.ts +40 -0
- package/dist/transport/reliable-transport.js +232 -0
- package/dist/transport/testing/fault-cases.d.ts +6 -0
- package/dist/transport/testing/fault-cases.js +30 -0
- package/dist/transport/testing/fault-transport.d.ts +44 -0
- package/dist/transport/testing/fault-transport.js +144 -0
- package/dist/transport/testing/multi-cases.d.ts +6 -0
- package/dist/transport/testing/multi-cases.js +113 -0
- package/dist/transport/testing/transport-cases.d.ts +6 -0
- package/dist/transport/testing/transport-cases.js +60 -0
- package/dist/transport/testing/transport-harness.d.ts +53 -0
- package/dist/transport/testing/transport-harness.js +127 -0
- package/dist/transport/transport.d.ts +60 -0
- package/dist/transport/transport.js +15 -0
- package/package.json +11 -3
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { TransportClosedError, } from "./transport.js";
|
|
2
|
+
function isReferencePacket(message) {
|
|
3
|
+
if (typeof message !== "object" || message === null)
|
|
4
|
+
return false;
|
|
5
|
+
const { kind, lane, seq } = message;
|
|
6
|
+
if (typeof lane !== "string" || typeof seq !== "number" || !Number.isSafeInteger(seq) || seq < 0)
|
|
7
|
+
return false;
|
|
8
|
+
if (kind === "ack")
|
|
9
|
+
return true;
|
|
10
|
+
return kind === "data" && message.payload instanceof Uint8Array;
|
|
11
|
+
}
|
|
12
|
+
/** The decoder a conformance host uses to watch the reference transport's wire. */
|
|
13
|
+
export function decodeReferencePacket(message) {
|
|
14
|
+
return isReferencePacket(message) ? { kind: message.kind, lane: message.lane, seq: message.seq } : { kind: "other" };
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The reference transport: per-lane send windows (backpressure), round-robin transmission across lanes (fairness),
|
|
18
|
+
* cumulative ACKs sent only after the receiver's handler settles, and go-back-N from each lane's last ACK on re-attach or
|
|
19
|
+
* on that lane's own retransmit timeout. A receiver drops anything that is not the next frame of its lane, so it delivers each frame
|
|
20
|
+
* once and in order over any carrier that may drop, lose, delay or reorder messages.
|
|
21
|
+
*/
|
|
22
|
+
export function createReliableTransport(options) {
|
|
23
|
+
const window = options.window ?? 32;
|
|
24
|
+
const retransmitMs = options.retransmitMs ?? 1000;
|
|
25
|
+
const outLanes = new Map();
|
|
26
|
+
const inLanes = new Map();
|
|
27
|
+
const order = [];
|
|
28
|
+
const ackDue = new Set();
|
|
29
|
+
let next = 0;
|
|
30
|
+
let connection;
|
|
31
|
+
let writable = false;
|
|
32
|
+
let closed = false;
|
|
33
|
+
let handler;
|
|
34
|
+
const outLane = (lane) => {
|
|
35
|
+
let state = outLanes.get(lane);
|
|
36
|
+
if (state === undefined) {
|
|
37
|
+
state = { nextSeq: 1, acked: 0, transmitted: 0, outbox: new Map(), waiters: [], cancelRetransmit: undefined };
|
|
38
|
+
outLanes.set(lane, state);
|
|
39
|
+
order.push(lane);
|
|
40
|
+
}
|
|
41
|
+
return state;
|
|
42
|
+
};
|
|
43
|
+
const inLane = (lane) => {
|
|
44
|
+
let state = inLanes.get(lane);
|
|
45
|
+
if (state === undefined) {
|
|
46
|
+
state = { received: 0, delivered: 0, busy: false, inbox: [] };
|
|
47
|
+
inLanes.set(lane, state);
|
|
48
|
+
}
|
|
49
|
+
return state;
|
|
50
|
+
};
|
|
51
|
+
const admit = (state) => {
|
|
52
|
+
while (state.nextSeq - 1 - state.acked < window) {
|
|
53
|
+
const waiter = state.waiters.shift();
|
|
54
|
+
if (waiter === undefined)
|
|
55
|
+
return;
|
|
56
|
+
state.outbox.set(state.nextSeq, waiter.payload);
|
|
57
|
+
state.nextSeq += 1;
|
|
58
|
+
waiter.resolve();
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
const emit = (packet) => {
|
|
62
|
+
if (connection === undefined || !writable)
|
|
63
|
+
return false;
|
|
64
|
+
if (connection.send(packet))
|
|
65
|
+
return true;
|
|
66
|
+
writable = false;
|
|
67
|
+
return false;
|
|
68
|
+
};
|
|
69
|
+
const disarm = (state) => {
|
|
70
|
+
state.cancelRetransmit?.();
|
|
71
|
+
state.cancelRetransmit = undefined;
|
|
72
|
+
};
|
|
73
|
+
const disarmAll = () => {
|
|
74
|
+
for (const state of outLanes.values())
|
|
75
|
+
disarm(state);
|
|
76
|
+
};
|
|
77
|
+
const arm = () => {
|
|
78
|
+
if (connection === undefined)
|
|
79
|
+
return;
|
|
80
|
+
for (const state of outLanes.values()) {
|
|
81
|
+
if (state.cancelRetransmit !== undefined || state.outbox.size === 0)
|
|
82
|
+
continue;
|
|
83
|
+
state.cancelRetransmit = options.clock.setTimer(() => {
|
|
84
|
+
state.cancelRetransmit = undefined;
|
|
85
|
+
state.transmitted = state.acked;
|
|
86
|
+
pump();
|
|
87
|
+
}, retransmitMs);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
function pump() {
|
|
91
|
+
if (connection === undefined || !writable)
|
|
92
|
+
return;
|
|
93
|
+
for (const lane of [...ackDue]) {
|
|
94
|
+
if (!emit({ kind: "ack", lane, seq: inLane(lane).delivered }))
|
|
95
|
+
return;
|
|
96
|
+
ackDue.delete(lane);
|
|
97
|
+
}
|
|
98
|
+
for (let idle = 0; idle < order.length;) {
|
|
99
|
+
const lane = order[next % order.length];
|
|
100
|
+
if (lane === undefined)
|
|
101
|
+
return;
|
|
102
|
+
const state = outLane(lane);
|
|
103
|
+
const seq = state.transmitted + 1;
|
|
104
|
+
const payload = state.outbox.get(seq);
|
|
105
|
+
if (payload === undefined) {
|
|
106
|
+
next += 1;
|
|
107
|
+
idle += 1;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (!emit({ kind: "data", lane, seq, payload }))
|
|
111
|
+
return;
|
|
112
|
+
state.transmitted = seq;
|
|
113
|
+
next += 1;
|
|
114
|
+
idle = 0;
|
|
115
|
+
}
|
|
116
|
+
arm();
|
|
117
|
+
}
|
|
118
|
+
async function deliver(state, lane) {
|
|
119
|
+
if (state.busy || handler === undefined)
|
|
120
|
+
return;
|
|
121
|
+
state.busy = true;
|
|
122
|
+
try {
|
|
123
|
+
for (let frame = state.inbox.shift(); frame !== undefined && !closed; frame = state.inbox.shift()) {
|
|
124
|
+
try {
|
|
125
|
+
await handler(frame);
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
options.onHandlerError(error, frame);
|
|
129
|
+
}
|
|
130
|
+
state.delivered += 1;
|
|
131
|
+
ackDue.add(lane);
|
|
132
|
+
pump();
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
state.busy = false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const onData = (packet) => {
|
|
140
|
+
const state = inLane(packet.lane);
|
|
141
|
+
if (packet.seq === state.received + 1) {
|
|
142
|
+
state.received = packet.seq;
|
|
143
|
+
state.inbox.push({ lane: packet.lane, payload: packet.payload });
|
|
144
|
+
void deliver(state, packet.lane);
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
ackDue.add(packet.lane);
|
|
148
|
+
}
|
|
149
|
+
pump();
|
|
150
|
+
};
|
|
151
|
+
const onAck = (packet) => {
|
|
152
|
+
const state = outLanes.get(packet.lane);
|
|
153
|
+
if (state === undefined || packet.seq <= state.acked || packet.seq >= state.nextSeq)
|
|
154
|
+
return;
|
|
155
|
+
for (let seq = state.acked + 1; seq <= packet.seq; seq++)
|
|
156
|
+
state.outbox.delete(seq);
|
|
157
|
+
state.acked = packet.seq;
|
|
158
|
+
state.transmitted = Math.max(state.transmitted, state.acked);
|
|
159
|
+
disarm(state);
|
|
160
|
+
admit(state);
|
|
161
|
+
pump();
|
|
162
|
+
};
|
|
163
|
+
return {
|
|
164
|
+
send(lane, payload) {
|
|
165
|
+
if (closed)
|
|
166
|
+
return Promise.reject(new TransportClosedError());
|
|
167
|
+
const state = outLane(lane);
|
|
168
|
+
return new Promise((resolve, reject) => {
|
|
169
|
+
state.waiters.push({ payload, resolve, reject });
|
|
170
|
+
admit(state);
|
|
171
|
+
pump();
|
|
172
|
+
});
|
|
173
|
+
},
|
|
174
|
+
onFrame(frameHandler) {
|
|
175
|
+
handler = frameHandler;
|
|
176
|
+
for (const [lane, state] of inLanes)
|
|
177
|
+
void deliver(state, lane);
|
|
178
|
+
},
|
|
179
|
+
inFlight: (lane) => {
|
|
180
|
+
const state = outLanes.get(lane);
|
|
181
|
+
return state === undefined ? 0 : state.nextSeq - 1 - state.acked;
|
|
182
|
+
},
|
|
183
|
+
close() {
|
|
184
|
+
if (closed)
|
|
185
|
+
return;
|
|
186
|
+
closed = true;
|
|
187
|
+
disarmAll();
|
|
188
|
+
for (const state of outLanes.values()) {
|
|
189
|
+
for (const waiter of state.waiters.splice(0))
|
|
190
|
+
waiter.reject(new TransportClosedError());
|
|
191
|
+
}
|
|
192
|
+
const current = connection;
|
|
193
|
+
connection = undefined;
|
|
194
|
+
current?.close();
|
|
195
|
+
},
|
|
196
|
+
attached(newConnection) {
|
|
197
|
+
if (closed) {
|
|
198
|
+
newConnection.close();
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
connection = newConnection;
|
|
202
|
+
writable = true;
|
|
203
|
+
for (const state of outLanes.values())
|
|
204
|
+
state.transmitted = state.acked;
|
|
205
|
+
for (const [lane, state] of inLanes)
|
|
206
|
+
if (state.delivered > 0)
|
|
207
|
+
ackDue.add(lane);
|
|
208
|
+
disarmAll();
|
|
209
|
+
pump();
|
|
210
|
+
},
|
|
211
|
+
message(message) {
|
|
212
|
+
if (!isReferencePacket(message))
|
|
213
|
+
return;
|
|
214
|
+
if (message.kind === "data")
|
|
215
|
+
onData(message);
|
|
216
|
+
else
|
|
217
|
+
onAck(message);
|
|
218
|
+
},
|
|
219
|
+
writable() {
|
|
220
|
+
if (connection === undefined)
|
|
221
|
+
return;
|
|
222
|
+
writable = true;
|
|
223
|
+
pump();
|
|
224
|
+
},
|
|
225
|
+
detached() {
|
|
226
|
+
connection = undefined;
|
|
227
|
+
writable = false;
|
|
228
|
+
disarmAll();
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
//# sourceMappingURL=reliable-transport.js.map
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { CaseImplementation } from "../../conformance/runner.ts";
|
|
2
|
+
import { type TransportCaseAdapters } from "./transport-harness.ts";
|
|
3
|
+
type FaultCase = CaseImplementation<TransportCaseAdapters>;
|
|
4
|
+
export declare const FAULT_TRANSPORT_CASES: ReadonlyMap<string, FaultCase>;
|
|
5
|
+
export {};
|
|
6
|
+
//# sourceMappingURL=fault-cases.d.ts.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { createCarrierFault, FAULT_PROFILE_NAMES } from "./fault-transport.js";
|
|
2
|
+
import { allDelivered, expectExactDelivery, openLink, range } from "./transport-harness.js";
|
|
3
|
+
const LANES = ["a", "b", "c"];
|
|
4
|
+
const FRAMES = 30;
|
|
5
|
+
const PACE_MS = 20;
|
|
6
|
+
const applied = {
|
|
7
|
+
slow: (stats) => stats.refused > 0,
|
|
8
|
+
lossy: (stats) => stats.lost > 0,
|
|
9
|
+
laggy: (stats) => stats.reordered > 0,
|
|
10
|
+
flap: (stats) => stats.drops >= 2,
|
|
11
|
+
"drop-reattach": (stats) => stats.drops >= 2,
|
|
12
|
+
};
|
|
13
|
+
const underProfile = (name) => async (adapters) => {
|
|
14
|
+
const stats = { refused: 0, lost: 0, reordered: 0, drops: 0 };
|
|
15
|
+
const link = openLink(adapters, createCarrierFault(name, stats));
|
|
16
|
+
const counts = Object.fromEntries(LANES.map((lane) => [lane, FRAMES]));
|
|
17
|
+
const sends = [];
|
|
18
|
+
for (const n of range(1, FRAMES)) {
|
|
19
|
+
sends.push(...LANES.map((lane) => link.send(lane, n)));
|
|
20
|
+
await link.advance(PACE_MS);
|
|
21
|
+
}
|
|
22
|
+
await link.runUntil(allDelivered(link, counts), `FAULT-TRANSPORT ${name}: every frame delivered`, 600_000);
|
|
23
|
+
await link.awaitAll(sends, `FAULT-TRANSPORT ${name}`);
|
|
24
|
+
await link.advance(2000);
|
|
25
|
+
expectExactDelivery(link, counts, `FAULT-TRANSPORT ${name}`);
|
|
26
|
+
if (!applied[name](stats))
|
|
27
|
+
throw new Error(`FAULT-TRANSPORT ${name}: the profile never acted (${JSON.stringify(stats)})`);
|
|
28
|
+
};
|
|
29
|
+
export const FAULT_TRANSPORT_CASES = new Map(FAULT_PROFILE_NAMES.map((name, index) => [`FAULT-TRANSPORT-0${index + 1}`, underProfile(name)]));
|
|
30
|
+
//# sourceMappingURL=fault-cases.js.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { ManualTimers } from "../../durable/testing/manual-timers.ts";
|
|
2
|
+
import type { CarrierFault } from "./transport-harness.ts";
|
|
3
|
+
/**
|
|
4
|
+
* The five QX2 fault profiles as test parameters (not product settings). The plan fixes their shape; the numbers here are
|
|
5
|
+
* this suite's until M0 records final values in the gate's fault-profiles.json.
|
|
6
|
+
*/
|
|
7
|
+
declare const PROFILES: {
|
|
8
|
+
readonly slow: {
|
|
9
|
+
readonly perTick: 1;
|
|
10
|
+
readonly tickMs: 10;
|
|
11
|
+
};
|
|
12
|
+
readonly lossy: {
|
|
13
|
+
readonly lossRate: 0.2;
|
|
14
|
+
readonly seed: 7;
|
|
15
|
+
};
|
|
16
|
+
readonly laggy: {
|
|
17
|
+
readonly latencyMs: 40;
|
|
18
|
+
readonly jitterMs: 60;
|
|
19
|
+
readonly seed: 11;
|
|
20
|
+
};
|
|
21
|
+
readonly flap: {
|
|
22
|
+
readonly upMs: 100;
|
|
23
|
+
readonly downMs: 60;
|
|
24
|
+
readonly cycles: 8;
|
|
25
|
+
};
|
|
26
|
+
readonly "drop-reattach": {
|
|
27
|
+
readonly everyMessages: 9;
|
|
28
|
+
readonly reattachMs: 30;
|
|
29
|
+
readonly repetitions: 6;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
export type FaultProfileName = keyof typeof PROFILES;
|
|
33
|
+
export declare const FAULT_PROFILE_NAMES: readonly FaultProfileName[];
|
|
34
|
+
/** How often the profile actually acted, so a case can refuse to pass on a fault that never happened. */
|
|
35
|
+
export interface FaultStats {
|
|
36
|
+
refused: number;
|
|
37
|
+
lost: number;
|
|
38
|
+
reordered: number;
|
|
39
|
+
drops: number;
|
|
40
|
+
}
|
|
41
|
+
/** Builds one profile's fault for the link a case opens. */
|
|
42
|
+
export declare function createCarrierFault(name: FaultProfileName, stats: FaultStats): (timers: ManualTimers) => CarrierFault;
|
|
43
|
+
export {};
|
|
44
|
+
//# sourceMappingURL=fault-transport.d.ts.map
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The five QX2 fault profiles as test parameters (not product settings). The plan fixes their shape; the numbers here are
|
|
3
|
+
* this suite's until M0 records final values in the gate's fault-profiles.json.
|
|
4
|
+
*/
|
|
5
|
+
const PROFILES = {
|
|
6
|
+
slow: { perTick: 1, tickMs: 10 },
|
|
7
|
+
lossy: { lossRate: 0.2, seed: 7 },
|
|
8
|
+
laggy: { latencyMs: 40, jitterMs: 60, seed: 11 },
|
|
9
|
+
flap: { upMs: 100, downMs: 60, cycles: 8 },
|
|
10
|
+
"drop-reattach": { everyMessages: 9, reattachMs: 30, repetitions: 6 },
|
|
11
|
+
};
|
|
12
|
+
export const FAULT_PROFILE_NAMES = ["slow", "lossy", "laggy", "flap", "drop-reattach"];
|
|
13
|
+
const mulberry32 = (seed) => {
|
|
14
|
+
let state = seed >>> 0;
|
|
15
|
+
return () => {
|
|
16
|
+
state = (state + 0x6d2b79f5) >>> 0;
|
|
17
|
+
let t = state;
|
|
18
|
+
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
19
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
20
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
function wrapWith(send) {
|
|
24
|
+
return (side) => {
|
|
25
|
+
let current;
|
|
26
|
+
return {
|
|
27
|
+
attached: (connection) => {
|
|
28
|
+
current = connection;
|
|
29
|
+
const live = () => current === connection;
|
|
30
|
+
side.attached({
|
|
31
|
+
send: (message) => send(connection, side, message, live),
|
|
32
|
+
close: () => connection.close(),
|
|
33
|
+
});
|
|
34
|
+
},
|
|
35
|
+
message: (message) => side.message(message),
|
|
36
|
+
writable: () => side.writable(),
|
|
37
|
+
detached: () => {
|
|
38
|
+
current = undefined;
|
|
39
|
+
side.detached();
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function slow(timers, stats) {
|
|
45
|
+
const { perTick, tickMs } = PROFILES.slow;
|
|
46
|
+
const used = new WeakMap();
|
|
47
|
+
return (inner, side, message, live) => {
|
|
48
|
+
const tick = Math.floor(timers.now() / tickMs);
|
|
49
|
+
const state = used.get(side) ?? { tick, count: 0, waiting: false };
|
|
50
|
+
if (state.tick !== tick)
|
|
51
|
+
Object.assign(state, { tick, count: 0 });
|
|
52
|
+
used.set(side, state);
|
|
53
|
+
if (state.count < perTick) {
|
|
54
|
+
state.count += 1;
|
|
55
|
+
return inner.send(message);
|
|
56
|
+
}
|
|
57
|
+
stats.refused += 1;
|
|
58
|
+
if (!state.waiting) {
|
|
59
|
+
state.waiting = true;
|
|
60
|
+
timers.setTimer(() => {
|
|
61
|
+
state.waiting = false;
|
|
62
|
+
if (live())
|
|
63
|
+
side.writable();
|
|
64
|
+
}, (tick + 1) * tickMs - timers.now());
|
|
65
|
+
}
|
|
66
|
+
return false;
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function lossy(stats) {
|
|
70
|
+
const random = mulberry32(PROFILES.lossy.seed);
|
|
71
|
+
return (inner, _side, message) => {
|
|
72
|
+
if (random() < PROFILES.lossy.lossRate) {
|
|
73
|
+
stats.lost += 1;
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
return inner.send(message);
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function laggy(timers, stats) {
|
|
80
|
+
const { latencyMs, jitterMs, seed } = PROFILES.laggy;
|
|
81
|
+
const random = mulberry32(seed);
|
|
82
|
+
const order = new WeakMap();
|
|
83
|
+
return (inner, side, message, live) => {
|
|
84
|
+
const state = order.get(side) ?? { sent: 0, highest: 0 };
|
|
85
|
+
order.set(side, state);
|
|
86
|
+
state.sent += 1;
|
|
87
|
+
const index = state.sent;
|
|
88
|
+
timers.setTimer(() => {
|
|
89
|
+
if (!live())
|
|
90
|
+
return;
|
|
91
|
+
if (index < state.highest)
|
|
92
|
+
stats.reordered += 1;
|
|
93
|
+
state.highest = Math.max(state.highest, index);
|
|
94
|
+
inner.send(message);
|
|
95
|
+
}, latencyMs + Math.floor(random() * jitterMs));
|
|
96
|
+
return true;
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function flap(timers, stats, link) {
|
|
100
|
+
const { upMs, downMs, cycles } = PROFILES.flap;
|
|
101
|
+
for (let cycle = 0; cycle < cycles; cycle++) {
|
|
102
|
+
const start = timers.now() + cycle * (upMs + downMs);
|
|
103
|
+
timers.setTimer(() => {
|
|
104
|
+
stats.drops += 1;
|
|
105
|
+
link.drop();
|
|
106
|
+
}, start + upMs - timers.now());
|
|
107
|
+
timers.setTimer(() => link.connect(), start + upMs + downMs - timers.now());
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/** Builds one profile's fault for the link a case opens. */
|
|
111
|
+
export function createCarrierFault(name, stats) {
|
|
112
|
+
return (timers) => {
|
|
113
|
+
let link;
|
|
114
|
+
const passThrough = (inner, _side, message) => inner.send(message);
|
|
115
|
+
let sent = 0;
|
|
116
|
+
const dropReattach = (inner, _side, message) => {
|
|
117
|
+
const { everyMessages, reattachMs, repetitions } = PROFILES["drop-reattach"];
|
|
118
|
+
sent += 1;
|
|
119
|
+
if (sent % everyMessages === 0 && stats.drops < repetitions && link !== undefined) {
|
|
120
|
+
const target = link;
|
|
121
|
+
stats.drops += 1;
|
|
122
|
+
timers.setTimer(() => target.drop(), 0);
|
|
123
|
+
timers.setTimer(() => target.connect(), reattachMs);
|
|
124
|
+
}
|
|
125
|
+
return inner.send(message);
|
|
126
|
+
};
|
|
127
|
+
const sends = {
|
|
128
|
+
slow: slow(timers, stats),
|
|
129
|
+
lossy: lossy(stats),
|
|
130
|
+
laggy: laggy(timers, stats),
|
|
131
|
+
flap: passThrough,
|
|
132
|
+
"drop-reattach": dropReattach,
|
|
133
|
+
};
|
|
134
|
+
return {
|
|
135
|
+
wrap: wrapWith(sends[name]),
|
|
136
|
+
start(started) {
|
|
137
|
+
link = started;
|
|
138
|
+
if (name === "flap")
|
|
139
|
+
flap(timers, stats, started);
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
//# sourceMappingURL=fault-transport.js.map
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { CaseImplementation } from "../../conformance/runner.ts";
|
|
2
|
+
import { type TransportCaseAdapters } from "./transport-harness.ts";
|
|
3
|
+
type MultiCase = CaseImplementation<TransportCaseAdapters>;
|
|
4
|
+
export declare const MULTI_CASES: ReadonlyMap<string, MultiCase>;
|
|
5
|
+
export {};
|
|
6
|
+
//# sourceMappingURL=multi-cases.d.ts.map
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { createCarrierFault } from "./fault-transport.js";
|
|
2
|
+
import { allDelivered, expectExactDelivery, openLink, range, } from "./transport-harness.js";
|
|
3
|
+
const manyLanes = async (adapters) => {
|
|
4
|
+
const link = openLink(adapters);
|
|
5
|
+
const lanes = range(1, 8).map((n) => `scope-${n}`);
|
|
6
|
+
const counts = Object.fromEntries(lanes.map((lane) => [lane, 25]));
|
|
7
|
+
const sends = range(1, 25).flatMap((n) => lanes.map((lane) => link.send(lane, n)));
|
|
8
|
+
await link.runUntil(allDelivered(link, counts), "MULTI-LANE-01 every scope delivered");
|
|
9
|
+
await link.awaitAll(sends, "MULTI-LANE-01");
|
|
10
|
+
await link.advance(1000);
|
|
11
|
+
expectExactDelivery(link, counts, "MULTI-LANE-01");
|
|
12
|
+
const lanesInLog = link.log.map((entry) => entry.slice(0, entry.indexOf(":")));
|
|
13
|
+
for (const lane of lanes) {
|
|
14
|
+
const first = lanesInLog.indexOf(lane);
|
|
15
|
+
const lastOfOthers = Math.min(...lanes.filter((other) => other !== lane).map((other) => lanesInLog.lastIndexOf(other)));
|
|
16
|
+
if (first > lastOfOthers)
|
|
17
|
+
throw new Error(`MULTI-LANE-01: ${lane} started only after another scope had finished`);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
const stalledLaneDoesNotHoldOthers = async (adapters) => {
|
|
21
|
+
const link = openLink(adapters);
|
|
22
|
+
const release = link.gate("A");
|
|
23
|
+
const aSends = range(1, 100).map((n) => link.send("A", n));
|
|
24
|
+
const bSends = range(1, 10).map((n) => link.send("B", n));
|
|
25
|
+
await link.runUntil(allDelivered(link, { B: 10 }), "MULTI-FAIR-01 B delivered while A's receiver is stalled", 20_000);
|
|
26
|
+
await link.awaitAll(bSends, "MULTI-FAIR-01 B");
|
|
27
|
+
if (link.delivered("A").length > 0)
|
|
28
|
+
throw new Error("MULTI-FAIR-01: A delivered through a stalled receiver");
|
|
29
|
+
release();
|
|
30
|
+
await link.runUntil(allDelivered(link, { A: 100 }), "MULTI-FAIR-01 A delivered after release");
|
|
31
|
+
await link.awaitAll(aSends, "MULTI-FAIR-01 A");
|
|
32
|
+
await link.advance(1000);
|
|
33
|
+
expectExactDelivery(link, { A: 100, B: 10 }, "MULTI-FAIR-01 stalled lane");
|
|
34
|
+
};
|
|
35
|
+
const MAX_A_FRAMES_BEFORE_B = 4;
|
|
36
|
+
const longLaneDoesNotStarveAnother = async (adapters) => {
|
|
37
|
+
const stats = { refused: 0, lost: 0, reordered: 0, drops: 0 };
|
|
38
|
+
const link = openLink(adapters, createCarrierFault("slow", stats));
|
|
39
|
+
const aSends = range(1, 300).map((n) => link.send("A", n));
|
|
40
|
+
await link.runUntil(() => link.delivered("A").length >= 20, "MULTI-FAIR-01 A's long lane under way");
|
|
41
|
+
const before = link.delivered("A").length;
|
|
42
|
+
const bSend = link.send("B", 1);
|
|
43
|
+
await link.runUntil(allDelivered(link, { B: 1 }), "MULTI-FAIR-01 B delivered behind A's long lane");
|
|
44
|
+
const aheadOfB = link.delivered("A").length - before;
|
|
45
|
+
if (aheadOfB > MAX_A_FRAMES_BEFORE_B) {
|
|
46
|
+
throw new Error(`MULTI-FAIR-01: ${aheadOfB} of A's frames went ahead of B's one frame (at most ${MAX_A_FRAMES_BEFORE_B})`);
|
|
47
|
+
}
|
|
48
|
+
await link.runUntil(allDelivered(link, { A: 300 }), "MULTI-FAIR-01 A's long lane delivered", 600_000);
|
|
49
|
+
await link.awaitAll([...aSends, bSend], "MULTI-FAIR-01 long lane");
|
|
50
|
+
await link.advance(1000);
|
|
51
|
+
expectExactDelivery(link, { A: 300, B: 1 }, "MULTI-FAIR-01 long lane");
|
|
52
|
+
if (stats.refused === 0)
|
|
53
|
+
throw new Error("MULTI-FAIR-01: the slow carrier never pushed back");
|
|
54
|
+
};
|
|
55
|
+
const fairness = async (adapters) => {
|
|
56
|
+
await stalledLaneDoesNotHoldOthers(adapters);
|
|
57
|
+
await longLaneDoesNotStarveAnother(adapters);
|
|
58
|
+
};
|
|
59
|
+
/** Loses the first data message of `lane`#`seq` on the carrier, once; every other message passes. */
|
|
60
|
+
function loseOnce(decode, lane, seq, stats) {
|
|
61
|
+
return {
|
|
62
|
+
wrap: (side) => ({
|
|
63
|
+
attached: (connection) => side.attached({
|
|
64
|
+
send: (message) => {
|
|
65
|
+
const view = decode(message);
|
|
66
|
+
if (stats.lost === 0 && view.kind === "data" && view.lane === lane && view.seq === seq) {
|
|
67
|
+
stats.lost += 1;
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
return connection.send(message);
|
|
71
|
+
},
|
|
72
|
+
close: () => connection.close(),
|
|
73
|
+
}),
|
|
74
|
+
message: (message) => side.message(message),
|
|
75
|
+
writable: () => side.writable(),
|
|
76
|
+
detached: () => side.detached(),
|
|
77
|
+
}),
|
|
78
|
+
start: () => { },
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const LONG_LANE_FRAMES = 400;
|
|
82
|
+
const LONG_LANE_PACE_MS = 10;
|
|
83
|
+
/**
|
|
84
|
+
* B's only frame is lost while A's paced receiver keeps acknowledging A's long lane. B's resend must not wait for A: the
|
|
85
|
+
* loss is recovered on B's own deadline, before half of A's frames have gone ahead of it.
|
|
86
|
+
*/
|
|
87
|
+
const lossOnOneLaneIsNotHeldByAnother = async (adapters) => {
|
|
88
|
+
const stats = { refused: 0, lost: 0, reordered: 0, drops: 0 };
|
|
89
|
+
const decode = (message) => adapters.transport.decode(message);
|
|
90
|
+
const link = openLink(adapters, () => loseOnce(decode, "B", 1, stats));
|
|
91
|
+
link.pace("A", LONG_LANE_PACE_MS);
|
|
92
|
+
const aSends = range(1, LONG_LANE_FRAMES).map((n) => link.send("A", n));
|
|
93
|
+
await link.runUntil(() => link.delivered("A").length >= 10, "MULTI-FAIR-02 A's long lane under way");
|
|
94
|
+
const before = link.delivered("A").length;
|
|
95
|
+
const bSend = link.send("B", 1);
|
|
96
|
+
await link.runUntil(allDelivered(link, { B: 1 }), "MULTI-FAIR-02 B delivered after its loss", 600_000);
|
|
97
|
+
if (stats.lost !== 1)
|
|
98
|
+
throw new Error(`MULTI-FAIR-02: B#1 was not lost on the carrier (${JSON.stringify(stats)})`);
|
|
99
|
+
const aheadOfB = link.delivered("A").length - before;
|
|
100
|
+
if (aheadOfB >= LONG_LANE_FRAMES / 2) {
|
|
101
|
+
throw new Error(`MULTI-FAIR-02: B's lost frame was resent only after ${aheadOfB} of A's frames (fewer than ${LONG_LANE_FRAMES / 2})`);
|
|
102
|
+
}
|
|
103
|
+
await link.runUntil(allDelivered(link, { A: LONG_LANE_FRAMES }), "MULTI-FAIR-02 A delivered", 600_000);
|
|
104
|
+
await link.awaitAll([...aSends, bSend], "MULTI-FAIR-02");
|
|
105
|
+
await link.advance(2000);
|
|
106
|
+
expectExactDelivery(link, { A: LONG_LANE_FRAMES, B: 1 }, "MULTI-FAIR-02");
|
|
107
|
+
};
|
|
108
|
+
export const MULTI_CASES = new Map([
|
|
109
|
+
["MULTI-LANE-01", manyLanes],
|
|
110
|
+
["MULTI-FAIR-01", fairness],
|
|
111
|
+
["MULTI-FAIR-02", lossOnOneLaneIsNotHeldByAnother],
|
|
112
|
+
]);
|
|
113
|
+
//# sourceMappingURL=multi-cases.js.map
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { CaseImplementation } from "../../conformance/runner.ts";
|
|
2
|
+
import { type TransportCaseAdapters } from "./transport-harness.ts";
|
|
3
|
+
type TransportCase = CaseImplementation<TransportCaseAdapters>;
|
|
4
|
+
export declare const TRANSPORT_CASES: ReadonlyMap<string, TransportCase>;
|
|
5
|
+
export {};
|
|
6
|
+
//# sourceMappingURL=transport-cases.d.ts.map
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { expectEqual } from "../../durable/testing/store-harness.js";
|
|
2
|
+
import { allDelivered, expectExactDelivery, openLink, range } from "./transport-harness.js";
|
|
3
|
+
const order = async (adapters) => {
|
|
4
|
+
const link = openLink(adapters);
|
|
5
|
+
const counts = { a: 40, b: 40, c: 40 };
|
|
6
|
+
const sends = range(1, 40).flatMap((n) => ["a", "b", "c"].map((lane) => link.send(lane, n)));
|
|
7
|
+
await link.runUntil(allDelivered(link, counts), "TRANSPORT-01 every frame delivered");
|
|
8
|
+
await link.awaitAll(sends, "TRANSPORT-01");
|
|
9
|
+
await link.advance(1000);
|
|
10
|
+
expectExactDelivery(link, counts, "TRANSPORT-01");
|
|
11
|
+
};
|
|
12
|
+
const resume = async (adapters) => {
|
|
13
|
+
const link = openLink(adapters);
|
|
14
|
+
const counts = { a: 30, b: 30 };
|
|
15
|
+
const sends = range(1, 10).flatMap((n) => [link.send("a", n), link.send("b", n)]);
|
|
16
|
+
await link.runUntil(() => link.delivered("a").length >= 4, "TRANSPORT-02 first frames delivered");
|
|
17
|
+
link.link.drop();
|
|
18
|
+
sends.push(...range(11, 20).flatMap((n) => [link.send("a", n), link.send("b", n)]));
|
|
19
|
+
await link.advance(500);
|
|
20
|
+
if (link.sender.inFlight("a") + link.sender.inFlight("b") === 0) {
|
|
21
|
+
throw new Error("TRANSPORT-02: expected unacknowledged frames while the carrier is down");
|
|
22
|
+
}
|
|
23
|
+
link.link.connect();
|
|
24
|
+
await link.runUntil(() => link.delivered("a").length >= 15, "TRANSPORT-02 resumed after the first re-attach");
|
|
25
|
+
link.link.drop();
|
|
26
|
+
sends.push(...range(21, 30).flatMap((n) => [link.send("a", n), link.send("b", n)]));
|
|
27
|
+
link.link.connect();
|
|
28
|
+
await link.runUntil(allDelivered(link, counts), "TRANSPORT-02 every frame delivered after two re-attaches");
|
|
29
|
+
await link.awaitAll(sends, "TRANSPORT-02");
|
|
30
|
+
await link.advance(1000);
|
|
31
|
+
expectExactDelivery(link, counts, "TRANSPORT-02");
|
|
32
|
+
};
|
|
33
|
+
const backpressure = async (adapters) => {
|
|
34
|
+
const link = openLink(adapters);
|
|
35
|
+
const release = link.gate("slow");
|
|
36
|
+
const resolved = [];
|
|
37
|
+
const sends = range(1, 200).map((n) => link.send("slow", n).then(() => void resolved.push(n)));
|
|
38
|
+
await link.advance(2000);
|
|
39
|
+
const admitted = resolved.length;
|
|
40
|
+
if (admitted === 0 || admitted >= 200) {
|
|
41
|
+
throw new Error(`TRANSPORT-03: a stalled receiver must bound the senders, but ${admitted} of 200 sends resolved`);
|
|
42
|
+
}
|
|
43
|
+
await link.advance(2000);
|
|
44
|
+
expectEqual(resolved.length, admitted, "TRANSPORT-03 the bound holds while the receiver stays stalled");
|
|
45
|
+
expectEqual(link.delivered("slow"), [], "TRANSPORT-03 nothing delivered while the receiver is stalled");
|
|
46
|
+
if (link.sender.inFlight("slow") > admitted)
|
|
47
|
+
throw new Error("TRANSPORT-03: more in flight than admitted");
|
|
48
|
+
release();
|
|
49
|
+
await link.runUntil(allDelivered(link, { slow: 200 }), "TRANSPORT-03 every frame delivered after release");
|
|
50
|
+
await link.awaitAll(sends, "TRANSPORT-03");
|
|
51
|
+
expectEqual(resolved, range(1, 200), "TRANSPORT-03 sends resolve in send order");
|
|
52
|
+
await link.advance(1000);
|
|
53
|
+
expectExactDelivery(link, { slow: 200 }, "TRANSPORT-03");
|
|
54
|
+
};
|
|
55
|
+
export const TRANSPORT_CASES = new Map([
|
|
56
|
+
["TRANSPORT-01", order],
|
|
57
|
+
["TRANSPORT-02", resume],
|
|
58
|
+
["TRANSPORT-03", backpressure],
|
|
59
|
+
]);
|
|
60
|
+
//# sourceMappingURL=transport-cases.js.map
|