@noego/testing 0.1.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/dist/index.cjs +721 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +262 -0
- package/dist/index.d.ts +262 -0
- package/dist/index.js +676 -0
- package/dist/index.js.map +1 -0
- package/package.json +45 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,721 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
LeakDetector: () => LeakDetector,
|
|
24
|
+
ManualClock: () => ManualClock,
|
|
25
|
+
ManualScheduler: () => ManualScheduler,
|
|
26
|
+
MemoryKeyValueStore: () => MemoryKeyValueStore,
|
|
27
|
+
MemoryObjectStore: () => MemoryObjectStore,
|
|
28
|
+
NoopLogSink: () => NoopLogSink,
|
|
29
|
+
RecordingEventBus: () => RecordingEventBus,
|
|
30
|
+
RecordingLogSink: () => RecordingLogSink,
|
|
31
|
+
RecordingTelemetrySink: () => RecordingTelemetrySink,
|
|
32
|
+
RecordingTraceSink: () => RecordingTraceSink,
|
|
33
|
+
ScriptedFetchClient: () => ScriptedFetchClient,
|
|
34
|
+
ScriptedProcessRunner: () => ScriptedProcessRunner,
|
|
35
|
+
SeededRandomSource: () => SeededRandomSource,
|
|
36
|
+
SequenceIdGenerator: () => SequenceIdGenerator,
|
|
37
|
+
runClockContract: () => runClockContract,
|
|
38
|
+
runEventBusContract: () => runEventBusContract,
|
|
39
|
+
runKeyValueStoreContract: () => runKeyValueStoreContract,
|
|
40
|
+
runObjectStoreContract: () => runObjectStoreContract,
|
|
41
|
+
testEvent: () => testEvent
|
|
42
|
+
});
|
|
43
|
+
module.exports = __toCommonJS(index_exports);
|
|
44
|
+
|
|
45
|
+
// src/time/manual_clock.ts
|
|
46
|
+
var import_runtime = require("@noego/runtime");
|
|
47
|
+
var ManualClock = class _ManualClock extends import_runtime.Clock {
|
|
48
|
+
static defaultStart = import_runtime.Instant.ofEpochMilliseconds(Date.UTC(2020, 0, 1));
|
|
49
|
+
current;
|
|
50
|
+
constructor(start = _ManualClock.defaultStart) {
|
|
51
|
+
super();
|
|
52
|
+
this.current = start;
|
|
53
|
+
}
|
|
54
|
+
now() {
|
|
55
|
+
return this.current;
|
|
56
|
+
}
|
|
57
|
+
set(instant) {
|
|
58
|
+
if (instant.isBefore(this.current)) {
|
|
59
|
+
throw new RangeError(`ManualClock cannot move backwards: ${this.current.toString()} -> ${instant.toString()}`);
|
|
60
|
+
}
|
|
61
|
+
this.current = instant;
|
|
62
|
+
}
|
|
63
|
+
advanceBy(duration) {
|
|
64
|
+
this.current = this.current.plus(duration);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// src/time/manual_scheduler.ts
|
|
69
|
+
var import_runtime2 = require("@noego/runtime");
|
|
70
|
+
var ManualScheduler = class extends import_runtime2.Scheduler {
|
|
71
|
+
constructor(clock) {
|
|
72
|
+
super();
|
|
73
|
+
this.clock = clock;
|
|
74
|
+
}
|
|
75
|
+
clock;
|
|
76
|
+
tasks = [];
|
|
77
|
+
nextSequence = 0;
|
|
78
|
+
nextId = 0;
|
|
79
|
+
schedule(input) {
|
|
80
|
+
const id = (0, import_runtime2.brandId)(`task-${++this.nextId}`, "ScheduledTask");
|
|
81
|
+
const internal = {
|
|
82
|
+
id,
|
|
83
|
+
label: input.label,
|
|
84
|
+
deadline: this.clock.now().plus(input.delay),
|
|
85
|
+
sequence: this.nextSequence++,
|
|
86
|
+
task: input.task,
|
|
87
|
+
cancelled: false
|
|
88
|
+
};
|
|
89
|
+
this.tasks.push(internal);
|
|
90
|
+
return {
|
|
91
|
+
id,
|
|
92
|
+
cancel: () => {
|
|
93
|
+
internal.cancelled = true;
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
pending() {
|
|
98
|
+
return this.live().sort(compareTasks).map(({ id, label, deadline }) => ({ id, label, deadline }));
|
|
99
|
+
}
|
|
100
|
+
/** Advance the clock, running every task due inside the window in order. */
|
|
101
|
+
async advanceBy(duration) {
|
|
102
|
+
const target = this.clock.now().plus(duration);
|
|
103
|
+
for (; ; ) {
|
|
104
|
+
const next = this.nextDue(target);
|
|
105
|
+
if (next === null) break;
|
|
106
|
+
if (next.deadline.isAfter(this.clock.now())) {
|
|
107
|
+
this.clock.set(next.deadline);
|
|
108
|
+
}
|
|
109
|
+
await this.execute(next);
|
|
110
|
+
}
|
|
111
|
+
if (target.isAfter(this.clock.now())) {
|
|
112
|
+
this.clock.set(target);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/** Advance to the earliest pending deadline and run exactly that task. */
|
|
116
|
+
async runNext() {
|
|
117
|
+
const next = this.live().sort(compareTasks)[0];
|
|
118
|
+
if (next === void 0) {
|
|
119
|
+
throw new Error("ManualScheduler.runNext: no pending tasks");
|
|
120
|
+
}
|
|
121
|
+
if (next.deadline.isAfter(this.clock.now())) {
|
|
122
|
+
this.clock.set(next.deadline);
|
|
123
|
+
}
|
|
124
|
+
await this.execute(next);
|
|
125
|
+
}
|
|
126
|
+
/** Run tasks (advancing time as needed) until none remain. */
|
|
127
|
+
async runUntilIdle(options) {
|
|
128
|
+
const maxTasks = options?.maxTasks ?? 1e4;
|
|
129
|
+
let executed = 0;
|
|
130
|
+
while (this.live().length > 0) {
|
|
131
|
+
if (executed >= maxTasks) {
|
|
132
|
+
const labels = this.pending().slice(0, 5).map((t) => t.label).join(", ");
|
|
133
|
+
throw new Error(
|
|
134
|
+
`ManualScheduler.runUntilIdle: still not idle after ${maxTasks} tasks \u2014 probable infinite reschedule loop (next: ${labels})`
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
await this.runNext();
|
|
138
|
+
executed += 1;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/** Teardown assertion: fails when pending tasks remain, unless allowed. */
|
|
142
|
+
assertNoPendingTasks(options) {
|
|
143
|
+
if (options?.allow === true) return;
|
|
144
|
+
const remaining = this.pending();
|
|
145
|
+
if (remaining.length > 0) {
|
|
146
|
+
const labels = remaining.map((t) => `${t.label} @ ${t.deadline.toString()}`).join("; ");
|
|
147
|
+
throw new Error(`ManualScheduler: ${remaining.length} pending task(s) at teardown: ${labels}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
live() {
|
|
151
|
+
return this.tasks.filter((t) => !t.cancelled);
|
|
152
|
+
}
|
|
153
|
+
nextDue(target) {
|
|
154
|
+
const candidates = this.live().filter((t) => !t.deadline.isAfter(target)).sort(compareTasks);
|
|
155
|
+
return candidates[0] ?? null;
|
|
156
|
+
}
|
|
157
|
+
async execute(task) {
|
|
158
|
+
this.remove(task);
|
|
159
|
+
await task.task();
|
|
160
|
+
}
|
|
161
|
+
remove(task) {
|
|
162
|
+
const index = this.tasks.indexOf(task);
|
|
163
|
+
this.tasks.splice(index, 1);
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
function compareTasks(a, b) {
|
|
167
|
+
const byDeadline = a.deadline.compareTo(b.deadline);
|
|
168
|
+
return byDeadline !== 0 ? byDeadline : a.sequence - b.sequence;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// src/identity/sequence_id_generator.ts
|
|
172
|
+
var import_runtime3 = require("@noego/runtime");
|
|
173
|
+
var SequenceIdGenerator = class extends import_runtime3.IdGenerator {
|
|
174
|
+
counters = /* @__PURE__ */ new Map();
|
|
175
|
+
next(brand) {
|
|
176
|
+
const current = (this.counters.get(brand) ?? 0) + 1;
|
|
177
|
+
this.counters.set(brand, current);
|
|
178
|
+
return (0, import_runtime3.brandId)(`${brand.toLowerCase()}-${current}`, brand);
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// src/identity/seeded_random_source.ts
|
|
183
|
+
var import_runtime4 = require("@noego/runtime");
|
|
184
|
+
var SeededRandomSource = class extends import_runtime4.RandomSource {
|
|
185
|
+
state;
|
|
186
|
+
constructor(seed = 1) {
|
|
187
|
+
super();
|
|
188
|
+
if (!Number.isInteger(seed)) {
|
|
189
|
+
throw new RangeError(`SeededRandomSource seed must be an integer, got ${seed}`);
|
|
190
|
+
}
|
|
191
|
+
this.state = seed >>> 0;
|
|
192
|
+
}
|
|
193
|
+
nextUint32() {
|
|
194
|
+
this.state = this.state + 1831565813 >>> 0;
|
|
195
|
+
let t = this.state;
|
|
196
|
+
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
197
|
+
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
|
|
198
|
+
return (t ^ t >>> 14) >>> 0;
|
|
199
|
+
}
|
|
200
|
+
bytes(length) {
|
|
201
|
+
if (!Number.isInteger(length) || length < 0) {
|
|
202
|
+
throw new RangeError(`bytes length must be a non-negative integer, got ${length}`);
|
|
203
|
+
}
|
|
204
|
+
const out = new Uint8Array(length);
|
|
205
|
+
for (let i = 0; i < length; i += 1) {
|
|
206
|
+
out[i] = this.nextUint32() & 255;
|
|
207
|
+
}
|
|
208
|
+
return out;
|
|
209
|
+
}
|
|
210
|
+
integer(minInclusive, maxExclusive) {
|
|
211
|
+
if (maxExclusive <= minInclusive) {
|
|
212
|
+
throw new RangeError(`integer range is empty: [${minInclusive}, ${maxExclusive})`);
|
|
213
|
+
}
|
|
214
|
+
const span = maxExclusive - minInclusive;
|
|
215
|
+
return minInclusive + this.nextUint32() % span;
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
// src/network/scripted_fetch_client.ts
|
|
220
|
+
var import_runtime5 = require("@noego/runtime");
|
|
221
|
+
var ScriptedFetchClient = class extends import_runtime5.FetchClient {
|
|
222
|
+
script;
|
|
223
|
+
cursor = 0;
|
|
224
|
+
executed = [];
|
|
225
|
+
constructor(script) {
|
|
226
|
+
super();
|
|
227
|
+
this.script = [...script];
|
|
228
|
+
}
|
|
229
|
+
execute(request) {
|
|
230
|
+
const entry = this.script[this.cursor];
|
|
231
|
+
if (entry === void 0) {
|
|
232
|
+
return Promise.reject(
|
|
233
|
+
new Error(`ScriptedFetchClient: unexpected call #${this.cursor + 1} (${request.method} ${request.url}); script is exhausted`)
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
if (!entry.matches(request)) {
|
|
237
|
+
return Promise.reject(
|
|
238
|
+
new Error(`ScriptedFetchClient: call #${this.cursor + 1} (${request.method} ${request.url}) does not match script entry "${entry.describe}"`)
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
this.cursor += 1;
|
|
242
|
+
this.executed.push(request);
|
|
243
|
+
const outcome = entry.respond(request);
|
|
244
|
+
return outcome instanceof import_runtime5.FetchError ? Promise.reject(outcome) : Promise.resolve(outcome);
|
|
245
|
+
}
|
|
246
|
+
/** Teardown assertion: every scripted call must have been consumed. */
|
|
247
|
+
assertScriptConsumed() {
|
|
248
|
+
if (this.cursor < this.script.length) {
|
|
249
|
+
const remaining = this.script.slice(this.cursor).map((e) => e.describe).join(", ");
|
|
250
|
+
throw new Error(`ScriptedFetchClient: ${this.script.length - this.cursor} unconsumed script entr(ies): ${remaining}`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
// src/process/scripted_process_runner.ts
|
|
256
|
+
var import_runtime6 = require("@noego/runtime");
|
|
257
|
+
var ScriptedProcessRunner = class extends import_runtime6.ProcessRunner {
|
|
258
|
+
script;
|
|
259
|
+
cursor = 0;
|
|
260
|
+
openHandles = /* @__PURE__ */ new Set();
|
|
261
|
+
constructor(script) {
|
|
262
|
+
super();
|
|
263
|
+
this.script = [...script];
|
|
264
|
+
}
|
|
265
|
+
consume(command, kind) {
|
|
266
|
+
const entry = this.script[this.cursor];
|
|
267
|
+
if (entry === void 0) {
|
|
268
|
+
throw new Error(`ScriptedProcessRunner: unexpected ${kind} of "${command.executable}"; script is exhausted`);
|
|
269
|
+
}
|
|
270
|
+
if (!entry.matches(command)) {
|
|
271
|
+
throw new Error(`ScriptedProcessRunner: ${kind} of "${command.executable}" does not match script entry "${entry.describe}"`);
|
|
272
|
+
}
|
|
273
|
+
this.cursor += 1;
|
|
274
|
+
return entry;
|
|
275
|
+
}
|
|
276
|
+
run(command) {
|
|
277
|
+
try {
|
|
278
|
+
const entry = this.consume(command, "run");
|
|
279
|
+
return Promise.resolve(foldEvents(entry.events));
|
|
280
|
+
} catch (error) {
|
|
281
|
+
return Promise.reject(error);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
spawn(command) {
|
|
285
|
+
const entry = this.consume(command, "spawn");
|
|
286
|
+
const handle = new ScriptedProcessHandle(command.label, entry.events, () => {
|
|
287
|
+
this.openHandles.delete(handle);
|
|
288
|
+
});
|
|
289
|
+
this.openHandles.add(handle);
|
|
290
|
+
return handle;
|
|
291
|
+
}
|
|
292
|
+
/** Teardown assertion: no spawned process may remain open. */
|
|
293
|
+
assertNoOpenHandles() {
|
|
294
|
+
if (this.openHandles.size > 0) {
|
|
295
|
+
const labels = [...this.openHandles].map((h) => h.label).join(", ");
|
|
296
|
+
throw new Error(`ScriptedProcessRunner: ${this.openHandles.size} open process handle(s) at teardown: ${labels}`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
var ScriptedProcessHandle = class {
|
|
301
|
+
constructor(label, events, onClosed) {
|
|
302
|
+
this.label = label;
|
|
303
|
+
this.events = events;
|
|
304
|
+
this.onClosed = onClosed;
|
|
305
|
+
this.resultPromise = new Promise((resolve) => {
|
|
306
|
+
this.resolveResult = resolve;
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
label;
|
|
310
|
+
events;
|
|
311
|
+
onClosed;
|
|
312
|
+
listeners = [];
|
|
313
|
+
resultPromise;
|
|
314
|
+
resolveResult;
|
|
315
|
+
flushed = false;
|
|
316
|
+
killed = false;
|
|
317
|
+
onEvent(listener) {
|
|
318
|
+
this.listeners.push(listener);
|
|
319
|
+
}
|
|
320
|
+
/** Deliver all scripted events now (deterministic, test-controlled). */
|
|
321
|
+
flush() {
|
|
322
|
+
if (this.flushed || this.killed) return;
|
|
323
|
+
this.flushed = true;
|
|
324
|
+
for (const event of this.events) {
|
|
325
|
+
for (const listener of this.listeners) listener(event);
|
|
326
|
+
if (event.kind === "exit") {
|
|
327
|
+
this.resolveResult(foldEvents(this.events));
|
|
328
|
+
this.onClosed();
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
kill(signal = "SIGTERM") {
|
|
333
|
+
if (this.flushed || this.killed) return;
|
|
334
|
+
this.killed = true;
|
|
335
|
+
const exit = { kind: "exit", exitCode: null, signal };
|
|
336
|
+
for (const listener of this.listeners) listener(exit);
|
|
337
|
+
this.resolveResult({ exitCode: null, signal, stdout: "", stderr: "" });
|
|
338
|
+
this.onClosed();
|
|
339
|
+
}
|
|
340
|
+
wait() {
|
|
341
|
+
return this.resultPromise;
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
function foldEvents(events) {
|
|
345
|
+
let stdout = "";
|
|
346
|
+
let stderr = "";
|
|
347
|
+
let exitCode = null;
|
|
348
|
+
let signal = null;
|
|
349
|
+
for (const event of events) {
|
|
350
|
+
if (event.kind === "stdout") stdout += event.chunk;
|
|
351
|
+
else if (event.kind === "stderr") stderr += event.chunk;
|
|
352
|
+
else {
|
|
353
|
+
exitCode = event.exitCode;
|
|
354
|
+
signal = event.signal;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return { exitCode, signal, stdout, stderr };
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// src/storage/memory_key_value_store.ts
|
|
361
|
+
var import_runtime7 = require("@noego/runtime");
|
|
362
|
+
var MemoryKeyValueStore = class extends import_runtime7.KeyValueStore {
|
|
363
|
+
constructor(clock) {
|
|
364
|
+
super();
|
|
365
|
+
this.clock = clock;
|
|
366
|
+
}
|
|
367
|
+
clock;
|
|
368
|
+
entries = /* @__PURE__ */ new Map();
|
|
369
|
+
get(key, codec) {
|
|
370
|
+
const entry = this.entries.get(key);
|
|
371
|
+
if (entry === void 0) return Promise.resolve(null);
|
|
372
|
+
if (entry.expiresAt !== null && !this.clock.now().isBefore(entry.expiresAt)) {
|
|
373
|
+
this.entries.delete(key);
|
|
374
|
+
return Promise.resolve(null);
|
|
375
|
+
}
|
|
376
|
+
return Promise.resolve(codec.decode(entry.encoded));
|
|
377
|
+
}
|
|
378
|
+
put(key, value, codec, options) {
|
|
379
|
+
const expiresAt = options?.timeToLive !== void 0 ? this.clock.now().plus(options.timeToLive) : null;
|
|
380
|
+
this.entries.set(key, { encoded: codec.encode(value), expiresAt });
|
|
381
|
+
return Promise.resolve();
|
|
382
|
+
}
|
|
383
|
+
delete(key) {
|
|
384
|
+
this.entries.delete(key);
|
|
385
|
+
return Promise.resolve();
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
// src/storage/memory_object_store.ts
|
|
390
|
+
var import_runtime8 = require("@noego/runtime");
|
|
391
|
+
var MemoryObjectStore = class extends import_runtime8.ObjectStore {
|
|
392
|
+
objects = /* @__PURE__ */ new Map();
|
|
393
|
+
nextVersion = 0;
|
|
394
|
+
read(key) {
|
|
395
|
+
return Promise.resolve(this.objects.get(key) ?? null);
|
|
396
|
+
}
|
|
397
|
+
write(key, object) {
|
|
398
|
+
const version = (0, import_runtime8.brandId)(`v${++this.nextVersion}`, "ObjectVersion");
|
|
399
|
+
this.objects.set(key, {
|
|
400
|
+
bytes: object.bytes.slice(),
|
|
401
|
+
contentType: object.contentType,
|
|
402
|
+
metadata: object.metadata !== void 0 ? new Map(object.metadata) : void 0,
|
|
403
|
+
version
|
|
404
|
+
});
|
|
405
|
+
return Promise.resolve(version);
|
|
406
|
+
}
|
|
407
|
+
delete(key) {
|
|
408
|
+
this.objects.delete(key);
|
|
409
|
+
return Promise.resolve();
|
|
410
|
+
}
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
// src/events/recording_event_bus.ts
|
|
414
|
+
var import_runtime9 = require("@noego/runtime");
|
|
415
|
+
var RecordingEventBus = class extends import_runtime9.EventBus {
|
|
416
|
+
constructor(mode = "immediate") {
|
|
417
|
+
super();
|
|
418
|
+
this.mode = mode;
|
|
419
|
+
}
|
|
420
|
+
mode;
|
|
421
|
+
published = [];
|
|
422
|
+
handlers = /* @__PURE__ */ new Set();
|
|
423
|
+
queue = [];
|
|
424
|
+
async publish(event) {
|
|
425
|
+
this.published.push(event);
|
|
426
|
+
if (this.mode === "queued") {
|
|
427
|
+
this.queue.push(event);
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
await this.deliver(event);
|
|
431
|
+
}
|
|
432
|
+
subscribe(handler) {
|
|
433
|
+
this.handlers.add(handler);
|
|
434
|
+
const bus = this;
|
|
435
|
+
const subscription = {
|
|
436
|
+
active: true,
|
|
437
|
+
unsubscribe() {
|
|
438
|
+
if (!subscription.active) return;
|
|
439
|
+
subscription.active = false;
|
|
440
|
+
bus.handlers.delete(handler);
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
return subscription;
|
|
444
|
+
}
|
|
445
|
+
/** Deliver buffered events (queued mode) in publication order. */
|
|
446
|
+
async deliverQueued() {
|
|
447
|
+
while (this.queue.length > 0) {
|
|
448
|
+
const event = this.queue.shift();
|
|
449
|
+
await this.deliver(event);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
get activeSubscriptionCount() {
|
|
453
|
+
return this.handlers.size;
|
|
454
|
+
}
|
|
455
|
+
assertNoActiveSubscriptions() {
|
|
456
|
+
if (this.handlers.size > 0) {
|
|
457
|
+
throw new Error(`RecordingEventBus: ${this.handlers.size} active subscription(s) at teardown`);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
async deliver(event) {
|
|
461
|
+
for (const handler of [...this.handlers]) {
|
|
462
|
+
await handler(event);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
// src/observability/recording_sinks.ts
|
|
468
|
+
var import_runtime10 = require("@noego/runtime");
|
|
469
|
+
var RecordingLogSink = class extends import_runtime10.LogSink {
|
|
470
|
+
envelopes = [];
|
|
471
|
+
emit(envelope) {
|
|
472
|
+
this.envelopes.push(envelope);
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
var RecordingTraceSink = class extends import_runtime10.TraceSink {
|
|
476
|
+
envelopes = [];
|
|
477
|
+
emit(envelope) {
|
|
478
|
+
this.envelopes.push(envelope);
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
var RecordingTelemetrySink = class extends import_runtime10.TelemetrySink {
|
|
482
|
+
envelopes = [];
|
|
483
|
+
emit(envelope) {
|
|
484
|
+
this.envelopes.push(envelope);
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
var NoopLogSink = class extends import_runtime10.LogSink {
|
|
488
|
+
emit(_envelope) {
|
|
489
|
+
}
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
// src/contracts/clock.contract.ts
|
|
493
|
+
function runClockContract(name, setup) {
|
|
494
|
+
describe(`Clock contract: ${name}`, () => {
|
|
495
|
+
it("returns a monotonically non-decreasing instant across consecutive reads", () => {
|
|
496
|
+
const { clock } = setup();
|
|
497
|
+
const first = clock.now();
|
|
498
|
+
const second = clock.now();
|
|
499
|
+
expect(second.isBefore(first)).toBe(false);
|
|
500
|
+
});
|
|
501
|
+
it("nowMs agrees with now()", () => {
|
|
502
|
+
const { clock } = setup();
|
|
503
|
+
const instant = clock.now();
|
|
504
|
+
const ms = clock.nowMs();
|
|
505
|
+
expect(ms).toBeGreaterThanOrEqual(instant.epochMilliseconds);
|
|
506
|
+
});
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// src/contracts/key_value_store.contract.ts
|
|
511
|
+
var import_runtime11 = require("@noego/runtime");
|
|
512
|
+
function runKeyValueStoreContract(name, setup) {
|
|
513
|
+
const codec = (0, import_runtime11.jsonCodec)();
|
|
514
|
+
describe(`KeyValueStore contract: ${name}`, () => {
|
|
515
|
+
it("returns null for a missing key", async () => {
|
|
516
|
+
const { store } = setup();
|
|
517
|
+
await expect(store.get((0, import_runtime11.storageKey)("missing"), codec)).resolves.toBeNull();
|
|
518
|
+
});
|
|
519
|
+
it("round-trips a stored value", async () => {
|
|
520
|
+
const { store } = setup();
|
|
521
|
+
const key = (0, import_runtime11.storageKey)("k1");
|
|
522
|
+
await store.put(key, { n: 42 }, codec);
|
|
523
|
+
await expect(store.get(key, codec)).resolves.toEqual({ n: 42 });
|
|
524
|
+
});
|
|
525
|
+
it("overwrites an existing value", async () => {
|
|
526
|
+
const { store } = setup();
|
|
527
|
+
const key = (0, import_runtime11.storageKey)("k1");
|
|
528
|
+
await store.put(key, { n: 1 }, codec);
|
|
529
|
+
await store.put(key, { n: 2 }, codec);
|
|
530
|
+
await expect(store.get(key, codec)).resolves.toEqual({ n: 2 });
|
|
531
|
+
});
|
|
532
|
+
it("deletes a value and tolerates deleting a missing key", async () => {
|
|
533
|
+
const { store } = setup();
|
|
534
|
+
const key = (0, import_runtime11.storageKey)("k1");
|
|
535
|
+
await store.put(key, { n: 1 }, codec);
|
|
536
|
+
await store.delete(key);
|
|
537
|
+
await expect(store.get(key, codec)).resolves.toBeNull();
|
|
538
|
+
await expect(store.delete((0, import_runtime11.storageKey)("missing"))).resolves.toBeUndefined();
|
|
539
|
+
});
|
|
540
|
+
it("expires values after their TTL when TTL is supported", async () => {
|
|
541
|
+
const context = setup();
|
|
542
|
+
if (!context.supportsTtl) return;
|
|
543
|
+
const key = (0, import_runtime11.storageKey)("expiring");
|
|
544
|
+
await context.store.put(key, { n: 7 }, codec, { timeToLive: import_runtime11.Duration.ofSeconds(30) });
|
|
545
|
+
await expect(context.store.get(key, codec)).resolves.toEqual({ n: 7 });
|
|
546
|
+
await context.advanceBy(import_runtime11.Duration.ofSeconds(31));
|
|
547
|
+
await expect(context.store.get(key, codec)).resolves.toBeNull();
|
|
548
|
+
});
|
|
549
|
+
it("keeps values without a TTL alive as time passes", async () => {
|
|
550
|
+
const context = setup();
|
|
551
|
+
const key = (0, import_runtime11.storageKey)("durable");
|
|
552
|
+
await context.store.put(key, { n: 9 }, codec);
|
|
553
|
+
await context.advanceBy(import_runtime11.Duration.ofMinutes(60));
|
|
554
|
+
await expect(context.store.get(key, codec)).resolves.toEqual({ n: 9 });
|
|
555
|
+
});
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// src/contracts/object_store.contract.ts
|
|
560
|
+
var import_runtime12 = require("@noego/runtime");
|
|
561
|
+
function runObjectStoreContract(name, setup) {
|
|
562
|
+
const bytes = (...values) => new Uint8Array(values);
|
|
563
|
+
describe(`ObjectStore contract: ${name}`, () => {
|
|
564
|
+
it("returns null for a missing object", async () => {
|
|
565
|
+
const { store } = setup();
|
|
566
|
+
await expect(store.read((0, import_runtime12.objectKey)("missing"))).resolves.toBeNull();
|
|
567
|
+
});
|
|
568
|
+
it("round-trips bytes, content type, and metadata", async () => {
|
|
569
|
+
const { store } = setup();
|
|
570
|
+
const key = (0, import_runtime12.objectKey)("docs/a.bin");
|
|
571
|
+
await store.write(key, {
|
|
572
|
+
bytes: bytes(1, 2, 3),
|
|
573
|
+
contentType: "application/octet-stream",
|
|
574
|
+
metadata: /* @__PURE__ */ new Map([["owner", "alice"]])
|
|
575
|
+
});
|
|
576
|
+
const stored = await store.read(key);
|
|
577
|
+
expect(stored).not.toBeNull();
|
|
578
|
+
expect([...stored.bytes]).toEqual([1, 2, 3]);
|
|
579
|
+
expect(stored.contentType).toBe("application/octet-stream");
|
|
580
|
+
expect(stored.metadata?.get("owner")).toBe("alice");
|
|
581
|
+
});
|
|
582
|
+
it("issues a new version on every write", async () => {
|
|
583
|
+
const { store } = setup();
|
|
584
|
+
const key = (0, import_runtime12.objectKey)("docs/a.bin");
|
|
585
|
+
const v1 = await store.write(key, { bytes: bytes(1), contentType: "text/plain" });
|
|
586
|
+
const v2 = await store.write(key, { bytes: bytes(2), contentType: "text/plain" });
|
|
587
|
+
expect(v1).not.toBe(v2);
|
|
588
|
+
const stored = await store.read(key);
|
|
589
|
+
expect(stored.version).toBe(v2);
|
|
590
|
+
expect([...stored.bytes]).toEqual([2]);
|
|
591
|
+
});
|
|
592
|
+
it("deletes an object and tolerates deleting a missing key", async () => {
|
|
593
|
+
const { store } = setup();
|
|
594
|
+
const key = (0, import_runtime12.objectKey)("docs/a.bin");
|
|
595
|
+
await store.write(key, { bytes: bytes(1), contentType: "text/plain" });
|
|
596
|
+
await store.delete(key);
|
|
597
|
+
await expect(store.read(key)).resolves.toBeNull();
|
|
598
|
+
await expect(store.delete((0, import_runtime12.objectKey)("missing"))).resolves.toBeUndefined();
|
|
599
|
+
});
|
|
600
|
+
it("does not expose caller mutations of written bytes", async () => {
|
|
601
|
+
const { store } = setup();
|
|
602
|
+
const key = (0, import_runtime12.objectKey)("docs/mutable.bin");
|
|
603
|
+
const input = bytes(9);
|
|
604
|
+
await store.write(key, { bytes: input, contentType: "text/plain" });
|
|
605
|
+
input[0] = 0;
|
|
606
|
+
const stored = await store.read(key);
|
|
607
|
+
expect([...stored.bytes]).toEqual([9]);
|
|
608
|
+
});
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// src/contracts/event_bus.contract.ts
|
|
613
|
+
var testEvent = (value) => ({ type: "test-event", value });
|
|
614
|
+
function runEventBusContract(name, setup) {
|
|
615
|
+
describe(`EventBus contract: ${name}`, () => {
|
|
616
|
+
it("delivers published events to subscribers in order", async () => {
|
|
617
|
+
const { bus, deliver } = setup();
|
|
618
|
+
const seen = [];
|
|
619
|
+
const subscription = bus.subscribe((event) => {
|
|
620
|
+
seen.push(event.value);
|
|
621
|
+
});
|
|
622
|
+
await bus.publish(testEvent(1));
|
|
623
|
+
await bus.publish(testEvent(2));
|
|
624
|
+
await deliver();
|
|
625
|
+
expect(seen).toEqual([1, 2]);
|
|
626
|
+
subscription.unsubscribe();
|
|
627
|
+
});
|
|
628
|
+
it("stops delivering after unsubscribe", async () => {
|
|
629
|
+
const { bus, deliver } = setup();
|
|
630
|
+
const seen = [];
|
|
631
|
+
const subscription = bus.subscribe((event) => {
|
|
632
|
+
seen.push(event.value);
|
|
633
|
+
});
|
|
634
|
+
await bus.publish(testEvent(1));
|
|
635
|
+
await deliver();
|
|
636
|
+
subscription.unsubscribe();
|
|
637
|
+
await bus.publish(testEvent(2));
|
|
638
|
+
await deliver();
|
|
639
|
+
expect(seen).toEqual([1]);
|
|
640
|
+
});
|
|
641
|
+
it("supports multiple subscribers", async () => {
|
|
642
|
+
const { bus, deliver } = setup();
|
|
643
|
+
const a = [];
|
|
644
|
+
const b = [];
|
|
645
|
+
const sa = bus.subscribe((event) => {
|
|
646
|
+
a.push(event.value);
|
|
647
|
+
});
|
|
648
|
+
const sb = bus.subscribe((event) => {
|
|
649
|
+
b.push(event.value);
|
|
650
|
+
});
|
|
651
|
+
await bus.publish(testEvent(5));
|
|
652
|
+
await deliver();
|
|
653
|
+
expect(a).toEqual([5]);
|
|
654
|
+
expect(b).toEqual([5]);
|
|
655
|
+
sa.unsubscribe();
|
|
656
|
+
sb.unsubscribe();
|
|
657
|
+
});
|
|
658
|
+
it("marks subscriptions inactive after unsubscribe and tolerates double unsubscribe", async () => {
|
|
659
|
+
const { bus, deliver } = setup();
|
|
660
|
+
const seen = [];
|
|
661
|
+
const subscription = bus.subscribe((event) => {
|
|
662
|
+
seen.push(event.value);
|
|
663
|
+
});
|
|
664
|
+
await bus.publish(testEvent(1));
|
|
665
|
+
await deliver();
|
|
666
|
+
expect(seen).toEqual([1]);
|
|
667
|
+
expect(subscription.active).toBe(true);
|
|
668
|
+
subscription.unsubscribe();
|
|
669
|
+
expect(subscription.active).toBe(false);
|
|
670
|
+
subscription.unsubscribe();
|
|
671
|
+
expect(subscription.active).toBe(false);
|
|
672
|
+
});
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// src/leak/leak_detector.ts
|
|
677
|
+
var LeakDetector = class {
|
|
678
|
+
checks = [];
|
|
679
|
+
register(check) {
|
|
680
|
+
this.checks.push(check);
|
|
681
|
+
}
|
|
682
|
+
findLeaks() {
|
|
683
|
+
const leaks = [];
|
|
684
|
+
for (const { name, check } of this.checks) {
|
|
685
|
+
for (const description of check()) {
|
|
686
|
+
leaks.push(`[${name}] ${description}`);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
return leaks;
|
|
690
|
+
}
|
|
691
|
+
assertNoLeaks() {
|
|
692
|
+
const leaks = this.findLeaks();
|
|
693
|
+
if (leaks.length > 0) {
|
|
694
|
+
throw new Error(`LeakDetector: ${leaks.length} leaked resource(s):
|
|
695
|
+
${leaks.join("\n")}`);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
};
|
|
699
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
700
|
+
0 && (module.exports = {
|
|
701
|
+
LeakDetector,
|
|
702
|
+
ManualClock,
|
|
703
|
+
ManualScheduler,
|
|
704
|
+
MemoryKeyValueStore,
|
|
705
|
+
MemoryObjectStore,
|
|
706
|
+
NoopLogSink,
|
|
707
|
+
RecordingEventBus,
|
|
708
|
+
RecordingLogSink,
|
|
709
|
+
RecordingTelemetrySink,
|
|
710
|
+
RecordingTraceSink,
|
|
711
|
+
ScriptedFetchClient,
|
|
712
|
+
ScriptedProcessRunner,
|
|
713
|
+
SeededRandomSource,
|
|
714
|
+
SequenceIdGenerator,
|
|
715
|
+
runClockContract,
|
|
716
|
+
runEventBusContract,
|
|
717
|
+
runKeyValueStoreContract,
|
|
718
|
+
runObjectStoreContract,
|
|
719
|
+
testEvent
|
|
720
|
+
});
|
|
721
|
+
//# sourceMappingURL=index.cjs.map
|