@listen-fire/module-runtime 0.12.1 → 0.12.3
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.
|
@@ -46,6 +46,13 @@ export declare const secretsReadOnlySlotCases: SlotConformanceCase[];
|
|
|
46
46
|
* list, a read of nothing, writes by compare-and-swap on the version, the
|
|
47
47
|
* path rule. `client` reaches a module that can change its files. */
|
|
48
48
|
export declare const movementsSlotCases: SlotConformanceCase[];
|
|
49
|
+
/** A removed file is reported `removed`. The wire has no delete, so the
|
|
50
|
+
* harness supplies the removal (the test module's `remove`, a file deleted
|
|
51
|
+
* from the folder). */
|
|
52
|
+
export declare const movementsWatchRemovalCase: (remove: (path: string) => Promise<void>) => SlotConformanceCase;
|
|
53
|
+
/** A module honours at most the cap: with nothing changing, a request for a
|
|
54
|
+
* minute is answered within the cap (plus slack). It takes about 30 s. */
|
|
55
|
+
export declare const movementsWatchCapCase: SlotConformanceCase;
|
|
49
56
|
/** What a module that keeps the project's movements where lf cannot change
|
|
50
57
|
* them answers: `client` reaches one. A write is refused by name with a
|
|
51
58
|
* reason; listing and reading still work. */
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// `loop_guard/contract.ts`) driven over this same wire; the browser to
|
|
11
11
|
// `services/browser/contract.ts` through its wire client.
|
|
12
12
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
-
exports.movementsReadOnlySlotCases = exports.movementsSlotCases = exports.secretsReadOnlySlotCases = exports.secretsSlotCases = exports.wakeSlotCases = exports.ocrSlotCases = exports.filesSlotCases = exports.judgeSlotCases = exports.modelsSlotCases = void 0;
|
|
13
|
+
exports.movementsReadOnlySlotCases = exports.movementsWatchCapCase = exports.movementsWatchRemovalCase = exports.movementsSlotCases = exports.secretsReadOnlySlotCases = exports.secretsSlotCases = exports.wakeSlotCases = exports.ocrSlotCases = exports.filesSlotCases = exports.judgeSlotCases = exports.modelsSlotCases = void 0;
|
|
14
14
|
exports.commonSlotCases = commonSlotCases;
|
|
15
15
|
const node_assert_1 = require("node:assert");
|
|
16
16
|
const node_crypto_1 = require("node:crypto");
|
|
@@ -463,6 +463,40 @@ async function createdMovement(client, path, text) {
|
|
|
463
463
|
node_assert_1.strict.equal(made.kind, 'written', `creating ${path} answered ${JSON.stringify(made)}`);
|
|
464
464
|
return made.kind === 'written' ? made.version : '';
|
|
465
465
|
}
|
|
466
|
+
const watchMovements = (client, params) => client.read('movements.watch', params, slots_1.MovementsWatchAnswer);
|
|
467
|
+
/** The module's current cursor (a watch without one answers it, no events). */
|
|
468
|
+
async function currentCursor(client) {
|
|
469
|
+
const answer = await watchMovements(client, { waitMs: 0 });
|
|
470
|
+
node_assert_1.strict.ok('cursor' in answer, `a watch without a cursor answered ${JSON.stringify(answer)}`);
|
|
471
|
+
return 'cursor' in answer ? answer.cursor : '';
|
|
472
|
+
}
|
|
473
|
+
const WATCH_DEADLINE_MS = 10_000;
|
|
474
|
+
/** Re-asks from `cursor` until an event satisfies `wanted` or the deadline
|
|
475
|
+
* passes, so a module that answers at once (and one that holds) both pass.
|
|
476
|
+
* Returns the cursor after the answer that carried it and every event seen. */
|
|
477
|
+
async function watchedUntil(client, cursor, wanted) {
|
|
478
|
+
const deadline = Date.now() + WATCH_DEADLINE_MS;
|
|
479
|
+
const seen = [];
|
|
480
|
+
let from = cursor;
|
|
481
|
+
while (Date.now() < deadline) {
|
|
482
|
+
const asked = Date.now();
|
|
483
|
+
const answer = await watchMovements(client, { cursor: from, waitMs: 1_000 });
|
|
484
|
+
node_assert_1.strict.ok('cursor' in answer, `a watch from a cursor the module gave answered ${JSON.stringify(answer)}`);
|
|
485
|
+
if (!('cursor' in answer))
|
|
486
|
+
break;
|
|
487
|
+
from = answer.cursor;
|
|
488
|
+
seen.push(...answer.events);
|
|
489
|
+
if (answer.events.some(wanted))
|
|
490
|
+
return { cursor: from, seen };
|
|
491
|
+
if (Date.now() - asked < 500)
|
|
492
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
493
|
+
}
|
|
494
|
+
node_assert_1.strict.fail(`no wanted event within ${WATCH_DEADLINE_MS} ms; saw ${JSON.stringify(seen)}`);
|
|
495
|
+
}
|
|
496
|
+
const eventAt = (path, kind) => (event) => event.path === path && event.kind === kind;
|
|
497
|
+
async function readVersion(client, path) {
|
|
498
|
+
return (await client.read('movements.read', { path }, slots_1.MovementsReadAnswer))?.version;
|
|
499
|
+
}
|
|
466
500
|
/** The movements slot's own cases (docs/contracts/movements-slot.md): the
|
|
467
501
|
* list, a read of nothing, writes by compare-and-swap on the version, the
|
|
468
502
|
* path rule. `client` reaches a module that can change its files. */
|
|
@@ -554,7 +588,98 @@ exports.movementsSlotCases = [
|
|
|
554
588
|
node_assert_1.strict.equal(await client.call('movements.read', { path }), null);
|
|
555
589
|
},
|
|
556
590
|
},
|
|
591
|
+
{
|
|
592
|
+
name: 'movements: a watch without a cursor answers a cursor and no events',
|
|
593
|
+
async run(client) {
|
|
594
|
+
const answer = await watchMovements(client, { waitMs: 0 });
|
|
595
|
+
node_assert_1.strict.ok('cursor' in answer && answer.events.length === 0, `answered ${JSON.stringify(answer)}`);
|
|
596
|
+
},
|
|
597
|
+
},
|
|
598
|
+
{
|
|
599
|
+
name: 'movements: a file created after a watch is reported added with the version read answers; a replace is changed with the new version',
|
|
600
|
+
async run(client) {
|
|
601
|
+
const cursor = await currentCursor(client);
|
|
602
|
+
const path = NEW_MOVEMENT();
|
|
603
|
+
const first = await createdMovement(client, path, 'one');
|
|
604
|
+
const added = await watchedUntil(client, cursor, eventAt(path, 'added'));
|
|
605
|
+
const event = added.seen.find(eventAt(path, 'added'));
|
|
606
|
+
node_assert_1.strict.equal(event && event.kind !== 'removed' ? event.version : undefined, first, 'added carries the version read answers');
|
|
607
|
+
const replaced = await writeMovement(client, path, 'two', first);
|
|
608
|
+
node_assert_1.strict.equal(replaced.kind, 'written');
|
|
609
|
+
const changed = await watchedUntil(client, added.cursor, eventAt(path, 'changed'));
|
|
610
|
+
const edit = changed.seen.find(eventAt(path, 'changed'));
|
|
611
|
+
node_assert_1.strict.equal(edit && edit.kind !== 'removed' ? edit.version : undefined, await readVersion(client, path), 'changed carries the new version');
|
|
612
|
+
},
|
|
613
|
+
},
|
|
614
|
+
{
|
|
615
|
+
name: 'movements: an event answered once is not answered again from the next cursor, and a write between two calls appears in the second',
|
|
616
|
+
async run(client) {
|
|
617
|
+
const first = NEW_MOVEMENT();
|
|
618
|
+
await createdMovement(client, first, 'one');
|
|
619
|
+
const cursor = await currentCursor(client);
|
|
620
|
+
const second = NEW_MOVEMENT();
|
|
621
|
+
await createdMovement(client, second, 'two');
|
|
622
|
+
const afterSecond = await watchedUntil(client, cursor, eventAt(second, 'added'));
|
|
623
|
+
node_assert_1.strict.equal(afterSecond.seen.filter(eventAt(first, 'added')).length, 0, 'a file written before the cursor is not an event after it');
|
|
624
|
+
const third = NEW_MOVEMENT();
|
|
625
|
+
await createdMovement(client, third, 'three');
|
|
626
|
+
const afterThird = await watchedUntil(client, afterSecond.cursor, eventAt(third, 'added'));
|
|
627
|
+
node_assert_1.strict.equal(afterThird.seen.filter(eventAt(second, 'added')).length, 0, 'an answered event is not answered again');
|
|
628
|
+
},
|
|
629
|
+
},
|
|
630
|
+
{
|
|
631
|
+
name: 'movements: two watchers from one cursor both see the change',
|
|
632
|
+
async run(client) {
|
|
633
|
+
const cursor = await currentCursor(client);
|
|
634
|
+
const path = NEW_MOVEMENT();
|
|
635
|
+
await createdMovement(client, path, 'one');
|
|
636
|
+
await watchedUntil(client, cursor, eventAt(path, 'added'));
|
|
637
|
+
await watchedUntil(client, cursor, eventAt(path, 'added'));
|
|
638
|
+
},
|
|
639
|
+
},
|
|
640
|
+
{
|
|
641
|
+
name: 'movements: a cursor the module never gave answers resync',
|
|
642
|
+
async run(client) {
|
|
643
|
+
node_assert_1.strict.deepEqual(await watchMovements(client, { cursor: 'conformance-unknown', waitMs: 0 }), { resync: true });
|
|
644
|
+
},
|
|
645
|
+
},
|
|
646
|
+
{
|
|
647
|
+
name: 'movements: a watch without waitMs, with a negative waitMs or with a cursor that is not a string is refused as bad_request',
|
|
648
|
+
async run(client) {
|
|
649
|
+
await refusedWith('bad_request', client.call('movements.watch', {}));
|
|
650
|
+
await refusedWith('bad_request', client.call('movements.watch', { cursor: 'x' }));
|
|
651
|
+
await refusedWith('bad_request', client.call('movements.watch', { waitMs: -1 }));
|
|
652
|
+
await refusedWith('bad_request', client.call('movements.watch', { waitMs: 0, cursor: 5 }));
|
|
653
|
+
},
|
|
654
|
+
},
|
|
557
655
|
];
|
|
656
|
+
/** A removed file is reported `removed`. The wire has no delete, so the
|
|
657
|
+
* harness supplies the removal (the test module's `remove`, a file deleted
|
|
658
|
+
* from the folder). */
|
|
659
|
+
const movementsWatchRemovalCase = (remove) => ({
|
|
660
|
+
name: 'movements: a removed file is reported removed',
|
|
661
|
+
async run(client) {
|
|
662
|
+
const path = NEW_MOVEMENT();
|
|
663
|
+
await createdMovement(client, path, 'one');
|
|
664
|
+
const cursor = await currentCursor(client);
|
|
665
|
+
await remove(path);
|
|
666
|
+
await watchedUntil(client, cursor, eventAt(path, 'removed'));
|
|
667
|
+
},
|
|
668
|
+
});
|
|
669
|
+
exports.movementsWatchRemovalCase = movementsWatchRemovalCase;
|
|
670
|
+
/** A module honours at most the cap: with nothing changing, a request for a
|
|
671
|
+
* minute is answered within the cap (plus slack). It takes about 30 s. */
|
|
672
|
+
exports.movementsWatchCapCase = {
|
|
673
|
+
name: 'movements: a watch for longer than the cap is answered within the cap',
|
|
674
|
+
async run(client) {
|
|
675
|
+
const cursor = await currentCursor(client);
|
|
676
|
+
const asked = Date.now();
|
|
677
|
+
const answer = await watchMovements(client, { cursor, waitMs: 60_000 });
|
|
678
|
+
node_assert_1.strict.ok(!('resync' in answer), 'a cursor the module gave is not resync');
|
|
679
|
+
const took = Date.now() - asked;
|
|
680
|
+
node_assert_1.strict.ok(took < slots_1.WATCH_WAIT_CAP_MS + 2_000, `a watch for 60000 ms took ${took} ms`);
|
|
681
|
+
},
|
|
682
|
+
};
|
|
558
683
|
/** What a module that keeps the project's movements where lf cannot change
|
|
559
684
|
* them answers: `client` reaches one. A write is refused by name with a
|
|
560
685
|
* reason; listing and reading still work. */
|
|
@@ -576,4 +701,11 @@ exports.movementsReadOnlySlotCases = [
|
|
|
576
701
|
node_assert_1.strict.equal(await client.call('movements.read', { path: NEW_MOVEMENT() }), null);
|
|
577
702
|
},
|
|
578
703
|
},
|
|
704
|
+
{
|
|
705
|
+
name: 'movements: a read-only module still watches: a cursor without one, resync on one it never gave',
|
|
706
|
+
async run(client) {
|
|
707
|
+
await currentCursor(client);
|
|
708
|
+
node_assert_1.strict.deepEqual(await watchMovements(client, { cursor: 'conformance-unknown', waitMs: 0 }), { resync: true });
|
|
709
|
+
},
|
|
710
|
+
},
|
|
579
711
|
];
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@listen-fire/module-runtime",
|
|
3
3
|
"license": "Apache-2.0",
|
|
4
|
-
"version": "0.12.
|
|
4
|
+
"version": "0.12.3",
|
|
5
5
|
"description": "What a module and the host that loads it share at run time on Node, beside the runtime-neutral contracts (@listen-fire/engine-contracts): a live adapter's file bytes as a stream, the files slot, the secrets family with the generic OAuth 2 refresh and connect, the in-memory vault, the browser slot and a bounded concurrency queue. The engine and the module kit both depend on it, so the kit never depends on the engine",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=22.23.4 <23"
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"type": "commonjs",
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"zod": "4.1.12",
|
|
25
|
-
"@listen-fire/engine-contracts": "^0.12.
|
|
25
|
+
"@listen-fire/engine-contracts": "^0.12.3"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@types/node": "20.16.5",
|