nervur 0.21.1 → 0.21.2
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 +7 -0
- package/dist/cli/command.js +11 -4
- package/dist/harbor/dock.d.ts +5 -1
- package/dist/harbor/dock.js +57 -8
- package/dist/harbor/harbor.d.ts +3 -1
- package/dist/harbor/harbor.js +78 -12
- package/dist/harbor/index.d.ts +1 -0
- package/dist/harbor/index.js +1 -0
- package/dist/harbor/package.d.ts +2 -1
- package/dist/harbor/package.js +12 -6
- package/dist/harbor/registry.js +6 -1
- package/dist/harbor/store.d.ts +13 -0
- package/dist/harbor/store.js +53 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/proof/index.d.ts +1 -1
- package/dist/proof/index.js +4 -2
- package/dist/proof/law.d.ts +40 -2
- package/dist/proof/law.js +250 -4
- package/dist/ward/ward.js +18 -12
- package/package.json +1 -1
- package/src/cli/command.ts +11 -4
- package/src/harbor/dock.ts +60 -11
- package/src/harbor/harbor.ts +78 -14
- package/src/harbor/index.ts +1 -0
- package/src/harbor/package.ts +16 -6
- package/src/harbor/registry.ts +7 -1
- package/src/harbor/store.ts +69 -0
- package/src/index.ts +2 -0
- package/src/proof/index.ts +4 -2
- package/src/proof/law.ts +243 -4
- package/src/ward/ward.ts +15 -10
package/dist/proof/law.js
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
// It names no runtime, so it bundles.
|
|
8
8
|
import { Being, Faculty } from '../being/index.js';
|
|
9
9
|
import { Memory } from '../contract/index.js';
|
|
10
|
-
import { copyPackage, rootLine } from '../harbor/index.js';
|
|
11
|
-
import { CryptoEntropy, HeldLoader, World } from '../pointer/index.js';
|
|
10
|
+
import { copyPackage, GROUND_STORE, rootLine, Store } from '../harbor/index.js';
|
|
11
|
+
import { CryptoEntropy, HeldLoader, VolatileMemory, World } from '../pointer/index.js';
|
|
12
12
|
class Echoes extends Faculty {
|
|
13
13
|
static kind = 'org.example.echoes';
|
|
14
14
|
}
|
|
@@ -52,9 +52,53 @@ class Customer extends Being {
|
|
|
52
52
|
return { said: ((await this.stance.standings.get('echo')?.ask('echo', args)) ?? null) };
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
|
+
// A being whose fields are her cells, and nothing else.
|
|
56
|
+
class Profile extends Being {
|
|
57
|
+
static kind = 'org.example.profile';
|
|
58
|
+
static cells = { name: '', age: 0 };
|
|
59
|
+
static asks = { set: {}, get: {} };
|
|
60
|
+
set(args) {
|
|
61
|
+
if (typeof args.name === 'string')
|
|
62
|
+
this.cells.name = args.name;
|
|
63
|
+
if (typeof args.age === 'number')
|
|
64
|
+
this.cells.age = args.age;
|
|
65
|
+
return this.get();
|
|
66
|
+
}
|
|
67
|
+
get() {
|
|
68
|
+
return { name: this.cells.name ?? null, age: this.cells.age ?? null };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// A store a module brings: places on a shelf of this process, by the
|
|
72
|
+
// faculty's key, which outlive a harbor as a database outlives its client.
|
|
73
|
+
// A scene empties every shelf before it stands.
|
|
74
|
+
class Shelf extends Store {
|
|
75
|
+
static kind = 'org.example.shelf';
|
|
76
|
+
static #shelves = new Map();
|
|
77
|
+
static shelf(key) {
|
|
78
|
+
let shelf = Shelf.#shelves.get(key);
|
|
79
|
+
if (!shelf)
|
|
80
|
+
Shelf.#shelves.set(key, (shelf = new Watched(new VolatileMemory())));
|
|
81
|
+
return shelf;
|
|
82
|
+
}
|
|
83
|
+
static reset() {
|
|
84
|
+
Shelf.#shelves.clear();
|
|
85
|
+
}
|
|
86
|
+
read(place) {
|
|
87
|
+
return Shelf.shelf(this.stance.key).read(place);
|
|
88
|
+
}
|
|
89
|
+
write(place, entries) {
|
|
90
|
+
return Shelf.shelf(this.stance.key).write(place, entries);
|
|
91
|
+
}
|
|
92
|
+
places() {
|
|
93
|
+
return Shelf.shelf(this.stance.key).places();
|
|
94
|
+
}
|
|
95
|
+
forget(place) {
|
|
96
|
+
return Shelf.shelf(this.stance.key).forget(place);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
55
99
|
// One set of classes for every scene in a bundle, since a bundler renames
|
|
56
100
|
// a second class of the same name and a row resolves a class by its name.
|
|
57
|
-
export const classes = [Echo, Shop, Customer];
|
|
101
|
+
export const classes = [Echo, Shop, Customer, Profile, Shelf];
|
|
58
102
|
export const law = { module: 'org.example.law', version: '1', classes };
|
|
59
103
|
// A harbor of the world whose catalogue runs the law's module.
|
|
60
104
|
export const standOne = async (world, name, parts = {}) => {
|
|
@@ -64,10 +108,12 @@ export const standOne = async (world, name, parts = {}) => {
|
|
|
64
108
|
};
|
|
65
109
|
const CATALOGUE = { catalogue: { class: 'org.nervur.catalogue', public: false, absent: false } };
|
|
66
110
|
const NONE = { wards: {}, routes: {} };
|
|
67
|
-
// Every byte a memory is handed, seen from outside it
|
|
111
|
+
// Every byte a memory is handed, seen from outside it, and a keep it
|
|
112
|
+
// refuses where `refuse` says.
|
|
68
113
|
class Watched extends Memory {
|
|
69
114
|
inner;
|
|
70
115
|
seen = [];
|
|
116
|
+
refuse = () => false;
|
|
71
117
|
constructor(inner) {
|
|
72
118
|
super();
|
|
73
119
|
this.inner = inner;
|
|
@@ -76,6 +122,8 @@ class Watched extends Memory {
|
|
|
76
122
|
return this.inner.read(place);
|
|
77
123
|
}
|
|
78
124
|
write(place, entries) {
|
|
125
|
+
if (this.refuse(place))
|
|
126
|
+
return Promise.reject(new Error('refused'));
|
|
79
127
|
for (const [name, bytes] of entries)
|
|
80
128
|
this.seen.push({ place, name, bytes: bytes && bytes.slice() });
|
|
81
129
|
return this.inner.write(place, entries);
|
|
@@ -343,3 +391,201 @@ export const lawScenes = (t, expect) => [
|
|
|
343
391
|
},
|
|
344
392
|
],
|
|
345
393
|
];
|
|
394
|
+
const KEEPERS = [{ name: 'the root memory' }, { name: 'a ground store', store: 'vault', kind: GROUND_STORE }, { name: 'a store of a module', store: 'shelf', kind: Shelf.kind }];
|
|
395
|
+
const onKeeper = (k) => (k.store === undefined ? {} : { store: k.store });
|
|
396
|
+
// The keeper opened in the box ward, where it is a store.
|
|
397
|
+
const openKeeper = async (harbor, k, expect) => {
|
|
398
|
+
if (k.store !== undefined)
|
|
399
|
+
expect.same(await harbor.root('open', { key: k.store, class: k.kind }), { opened: k.store });
|
|
400
|
+
};
|
|
401
|
+
const profile = async (harbor, method, args = {}) => (await harbor.ward('ana').root('ask', { being: 'me', method, args })).answer;
|
|
402
|
+
// Acme's shop kept by `k`, home's alice on the root memory, joined, and
|
|
403
|
+
// every box that reaches the shop's door seen.
|
|
404
|
+
const shopOn = async (world, t, k, expect) => {
|
|
405
|
+
const bodies = { acme: t.make(), home: t.make() };
|
|
406
|
+
const acme = await standOne(world, 'acme', bodies.acme);
|
|
407
|
+
const home = await standOne(world, 'home', bodies.home);
|
|
408
|
+
await openKeeper(acme, k, expect);
|
|
409
|
+
expect.same(await acme.root('host', { ward: 'shop', ...onKeeper(k) }), { hosted: 'shop' });
|
|
410
|
+
expect.same(await acme.ward('shop').root('boot', { key: 'shop', class: 'org.example.shop' }), { booted: 'shop' });
|
|
411
|
+
expect.same(await home.root('host', { ward: 'alice' }), { hosted: 'alice' });
|
|
412
|
+
expect.same(await home.ward('alice').root('boot', { key: 'me', class: 'org.example.customer' }), { booted: 'me' });
|
|
413
|
+
const invitation = (await acme.ward('shop').root('invite', { being: 'shop', id: 'alice' }));
|
|
414
|
+
const shop = acme.ward('shop').pk;
|
|
415
|
+
const boxes = [];
|
|
416
|
+
const carry = world.carry.bind(world);
|
|
417
|
+
world.carry = (pk, bytes) => {
|
|
418
|
+
if (pk === shop)
|
|
419
|
+
boxes.push(bytes.slice());
|
|
420
|
+
return carry(pk, bytes);
|
|
421
|
+
};
|
|
422
|
+
const me = async (method, args = {}) => (await world.get('home').ward('alice').root('ask', { being: 'me', method, args })).answer;
|
|
423
|
+
expect.same(await me('join', { invitation }), { joined: 'shop' });
|
|
424
|
+
return { acme: bodies.acme, boxes, me };
|
|
425
|
+
};
|
|
426
|
+
const sold = (harbor) => harbor.ward('shop').partition.being('shop')?.cells.sold;
|
|
427
|
+
// A cell is a being's field made durable, whoever keeps it: the law's
|
|
428
|
+
// scenes of a being's fields, run over every keeper a hosted ward's place
|
|
429
|
+
// may live with.
|
|
430
|
+
export const keeperScenes = (t, expect) => [
|
|
431
|
+
...KEEPERS.flatMap((k) => [
|
|
432
|
+
[
|
|
433
|
+
`law on ${t.name}: a being's fields kept by ${k.name} come back at wake, sealed, where that keeper alone holds them`,
|
|
434
|
+
async () => {
|
|
435
|
+
Shelf.reset();
|
|
436
|
+
const bodies = t.make();
|
|
437
|
+
const watched = new Watched(bodies.memory);
|
|
438
|
+
const world = new World();
|
|
439
|
+
const harbor = await standOne(world, 'h', { custody: bodies.custody, memory: watched });
|
|
440
|
+
await openKeeper(harbor, k, expect);
|
|
441
|
+
expect.same(await harbor.root('host', { ward: 'ana', ...onKeeper(k) }), { hosted: 'ana' });
|
|
442
|
+
expect.same(await harbor.ward('ana').root('boot', { key: 'me', class: Profile.kind }), { booted: 'me' });
|
|
443
|
+
expect.same(await profile(harbor, 'set', { name: 'Anastasia', age: 31 }), { name: 'Anastasia', age: 31 });
|
|
444
|
+
const again = await world.restart('h', t.again(bodies));
|
|
445
|
+
expect.same(await profile(again, 'get'), { name: 'Anastasia', age: 31 });
|
|
446
|
+
expect.same((await census(again)).failed, NONE);
|
|
447
|
+
const root = await bodies.memory.places();
|
|
448
|
+
const shelf = await Shelf.shelf('shelf').places();
|
|
449
|
+
if (k.store === undefined)
|
|
450
|
+
expect.same([root.length, shelf.length], [2, 0], 'the box ward and ana, on the root memory');
|
|
451
|
+
else if (k.kind === GROUND_STORE)
|
|
452
|
+
expect.same([root.length, root.filter((p) => p.length > 32).length, shelf.length], [2, 1, 0], 'ana behind the ground store key');
|
|
453
|
+
else
|
|
454
|
+
expect.same([root.length, shelf.length], [1, 1], 'the box ward on the root memory, ana on the shelf');
|
|
455
|
+
const seen = [...watched.seen, ...Shelf.shelf('shelf').seen];
|
|
456
|
+
for (const { bytes } of seen) {
|
|
457
|
+
if (bytes === null)
|
|
458
|
+
continue;
|
|
459
|
+
for (const word of ['Anastasia', 'org.example', 'vault'].map(utf8))
|
|
460
|
+
expect.ok(!holds(bytes, word), 'no field, kind or key in the clear');
|
|
461
|
+
}
|
|
462
|
+
},
|
|
463
|
+
],
|
|
464
|
+
[
|
|
465
|
+
`law on ${t.name}: an answer whose keep ${k.name} refuses is silence, and the being keeps what she had`,
|
|
466
|
+
async () => {
|
|
467
|
+
Shelf.reset();
|
|
468
|
+
const bodies = t.make();
|
|
469
|
+
const watched = new Watched(bodies.memory);
|
|
470
|
+
const world = new World();
|
|
471
|
+
const harbor = await standOne(world, 'h', { custody: bodies.custody, memory: watched });
|
|
472
|
+
const box = watched.seen[0].place;
|
|
473
|
+
await openKeeper(harbor, k, expect);
|
|
474
|
+
expect.same(await harbor.root('host', { ward: 'ana', ...onKeeper(k) }), { hosted: 'ana' });
|
|
475
|
+
expect.same(await harbor.ward('ana').root('boot', { key: 'me', class: Profile.kind }), { booted: 'me' });
|
|
476
|
+
expect.same(await profile(harbor, 'set', { age: 31 }), { name: '', age: 31 });
|
|
477
|
+
// Only the keeper named refuses, so a keep that went elsewhere
|
|
478
|
+
// would answer.
|
|
479
|
+
if (k.store === undefined)
|
|
480
|
+
watched.refuse = (place) => place !== box;
|
|
481
|
+
else if (k.kind === GROUND_STORE)
|
|
482
|
+
watched.refuse = (place) => place.length > box.length;
|
|
483
|
+
else
|
|
484
|
+
Shelf.shelf('shelf').refuse = () => true;
|
|
485
|
+
expect.ok(JSON.stringify(await profile(harbor, 'set', { age: 32 })) !== JSON.stringify({ name: '', age: 32 }), 'no answer leaves an unkept keep');
|
|
486
|
+
expect.equal(harbor.ward('ana').partition.being('me')?.cells.age, 31, 'what she wrote is put back');
|
|
487
|
+
watched.refuse = () => false;
|
|
488
|
+
Shelf.shelf('shelf').refuse = () => false;
|
|
489
|
+
const again = await world.restart('h', t.again(bodies));
|
|
490
|
+
expect.same(await profile(again, 'get'), { name: '', age: 31 });
|
|
491
|
+
expect.same(await profile(again, 'set', { age: 32 }), { name: '', age: 32 });
|
|
492
|
+
},
|
|
493
|
+
],
|
|
494
|
+
[
|
|
495
|
+
`law on ${t.name}: the door's keys and the being's cells are one keep with ${k.name}, so a box replayed after a wake is answered once`,
|
|
496
|
+
async () => {
|
|
497
|
+
Shelf.reset();
|
|
498
|
+
const world = new World();
|
|
499
|
+
const { acme, boxes, me } = await shopOn(world, t, k, expect);
|
|
500
|
+
const from = boxes.length;
|
|
501
|
+
expect.same(await me('buy', { item: 'tea' }), { said: { bought: 'tea' } });
|
|
502
|
+
const replayed = boxes.slice(from);
|
|
503
|
+
expect.ok(replayed.length > 0, 'the buy crossed the door');
|
|
504
|
+
const again = await world.restart('acme', t.again(acme));
|
|
505
|
+
expect.same(sold(again), ['tea to alice']);
|
|
506
|
+
for (const bytes of replayed)
|
|
507
|
+
await again.arrive(again.ward('shop').pk, bytes);
|
|
508
|
+
expect.same(sold(again), ['tea to alice'], 'a replayed box does not buy again');
|
|
509
|
+
expect.same(await me('buy', { item: 'cake' }), { said: { bought: 'cake' } });
|
|
510
|
+
expect.same(sold(world.get('acme')), ['tea to alice', 'cake to alice']);
|
|
511
|
+
},
|
|
512
|
+
],
|
|
513
|
+
]),
|
|
514
|
+
[
|
|
515
|
+
`law on ${t.name}: a ward whose store does not stand at wake fails alone, and stands whole once it returns`,
|
|
516
|
+
async () => {
|
|
517
|
+
Shelf.reset();
|
|
518
|
+
const bodies = t.make();
|
|
519
|
+
const world = new World();
|
|
520
|
+
const harbor = await standOne(world, 'h', bodies);
|
|
521
|
+
await openKeeper(harbor, KEEPERS[2], expect);
|
|
522
|
+
expect.same(await harbor.root('host', { ward: 'ana', store: 'shelf' }), { hosted: 'ana' });
|
|
523
|
+
expect.same(await harbor.root('host', { ward: 'bea' }), { hosted: 'bea' });
|
|
524
|
+
expect.same(await harbor.ward('ana').root('boot', { key: 'me', class: Profile.kind }), { booted: 'me' });
|
|
525
|
+
expect.same(await profile(harbor, 'set', { age: 31 }), { name: '', age: 31 });
|
|
526
|
+
const lost = await world.restart('h', { ...t.again(bodies), loader: new HeldLoader([]) });
|
|
527
|
+
expect.same(lost.names, ['bea'], 'the root and its own ward stand');
|
|
528
|
+
expect.same((await census(lost)).failed, { wards: { ana: 'the store shelf does not stand' }, routes: {} });
|
|
529
|
+
const back = await world.restart('h', { ...t.again(bodies), loader: new HeldLoader([law]) });
|
|
530
|
+
expect.same(back.names, ['ana', 'bea']);
|
|
531
|
+
expect.same(await profile(back, 'get'), { name: '', age: 31 });
|
|
532
|
+
},
|
|
533
|
+
],
|
|
534
|
+
[
|
|
535
|
+
`law on ${t.name}: a ward carried between keepers keeps its fields, its door and its key, and leaves nothing where it was`,
|
|
536
|
+
async () => {
|
|
537
|
+
Shelf.reset();
|
|
538
|
+
const world = new World();
|
|
539
|
+
const { acme, me } = await shopOn(world, t, KEEPERS[0], expect);
|
|
540
|
+
const harbor = world.get('acme');
|
|
541
|
+
for (const k of KEEPERS.slice(1))
|
|
542
|
+
await openKeeper(harbor, k, expect);
|
|
543
|
+
const pk = harbor.ward('shop').pk;
|
|
544
|
+
const items = ['tea', 'cake', 'bread', 'milk'];
|
|
545
|
+
const vault = harbor.box.being('vault');
|
|
546
|
+
const counts = async () => [(await acme.memory.places()).filter((p) => p.length === 32).length, (await vault.places()).length, (await Shelf.shelf('shelf').places()).length];
|
|
547
|
+
expect.same(await me('buy', { item: items[0] }), { said: { bought: items[0] } });
|
|
548
|
+
const road = [
|
|
549
|
+
[KEEPERS[1], [1, 1, 0]],
|
|
550
|
+
[KEEPERS[2], [1, 0, 1]],
|
|
551
|
+
[KEEPERS[0], [2, 0, 0]],
|
|
552
|
+
];
|
|
553
|
+
for (const [i, [k, want]] of road.entries()) {
|
|
554
|
+
expect.same(await world.get('acme').root('move', { ward: 'shop', store: k.store ?? null }), { moved: 'shop', store: k.store ?? null });
|
|
555
|
+
expect.same(await counts(), want, `only ${k.name} holds the shop`);
|
|
556
|
+
const again = await world.restart('acme', t.again(acme));
|
|
557
|
+
expect.equal(again.ward('shop').pk, pk, 'the key moves with the ward');
|
|
558
|
+
expect.same(await me('buy', { item: items[i + 1] }), { said: { bought: items[i + 1] } });
|
|
559
|
+
expect.same(sold(again), items.slice(0, i + 2).map((item) => `${item} to alice`));
|
|
560
|
+
}
|
|
561
|
+
expect.same(await world.get('acme').root('move', { ward: 'shop', store: null }), { error: 'shop is kept there already' });
|
|
562
|
+
expect.same(await world.get('acme').root('move', { ward: 'shop', store: 'echo' }), { error: 'no such store' });
|
|
563
|
+
},
|
|
564
|
+
],
|
|
565
|
+
[
|
|
566
|
+
`law on ${t.name}: a store keeping a ward does not go, and the root sweeps what no keeper is named for`,
|
|
567
|
+
async () => {
|
|
568
|
+
Shelf.reset();
|
|
569
|
+
const bodies = t.make();
|
|
570
|
+
const world = new World();
|
|
571
|
+
const harbor = await standOne(world, 'h', bodies);
|
|
572
|
+
for (const k of KEEPERS.slice(1))
|
|
573
|
+
await openKeeper(harbor, k, expect);
|
|
574
|
+
expect.same(await harbor.root('host', { ward: 'ana', store: 'vault' }), { hosted: 'ana' });
|
|
575
|
+
expect.same(await harbor.root('host', { ward: 'bea', store: 'shelf' }), { hosted: 'bea' });
|
|
576
|
+
expect.same(await harbor.root('host', { ward: 'cy', store: 'nothing' }), { error: 'no such store' });
|
|
577
|
+
expect.same(await harbor.root('unboot', { being: 'vault' }), { error: 'a ward is kept by this store' });
|
|
578
|
+
const vault = harbor.box.being('vault');
|
|
579
|
+
const before = [(await bodies.memory.places()).sort(), await vault.places(), await Shelf.shelf('shelf').places()];
|
|
580
|
+
const stray = 'ee'.repeat(16);
|
|
581
|
+
await vault.write(stray, new Map([['ab', new Uint8Array([1])]]));
|
|
582
|
+
await Shelf.shelf('shelf').write(stray, new Map([['ab', new Uint8Array([2])]]));
|
|
583
|
+
expect.same(await harbor.root('sweep'), { swept: [stray, stray] });
|
|
584
|
+
expect.same([(await bodies.memory.places()).sort(), await vault.places(), await Shelf.shelf('shelf').places()], before, 'what the dock names stays');
|
|
585
|
+
expect.same(await harbor.root('move', { ward: 'bea', store: null }), { moved: 'bea', store: null });
|
|
586
|
+
expect.same(await harbor.root('unboot', { being: 'shelf' }), { unbooted: 'shelf' });
|
|
587
|
+
const again = await world.restart('h', t.again(bodies));
|
|
588
|
+
expect.same(again.names, ['ana', 'bea']);
|
|
589
|
+
},
|
|
590
|
+
],
|
|
591
|
+
];
|
package/dist/ward/ward.js
CHANGED
|
@@ -98,23 +98,29 @@ export class Ward {
|
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
100
|
// The unsealed ask: the ward-being, asked by whoever holds this ward.
|
|
101
|
+
// It reaches every being of the ward through the ward-being, so every
|
|
102
|
+
// row is put back where the memory refuses.
|
|
101
103
|
async root(method, args = {}) {
|
|
102
|
-
return this.#kept(
|
|
104
|
+
return this.#kept(Object.keys(this.partition.beings), () => this.#answer(WARD, { id: OWNER }, method, args));
|
|
103
105
|
}
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
+
// An answer, kept. Where the memory refuses, what the beings wrote is put
|
|
107
|
+
// back and the answer is silence. What their relations stand on is not
|
|
106
108
|
// put back: a relation's keys move only as far as its standing follows.
|
|
107
|
-
async #kept(
|
|
108
|
-
const
|
|
109
|
-
|
|
109
|
+
async #kept(keys, work) {
|
|
110
|
+
const before = keys.flatMap((key) => {
|
|
111
|
+
const row = this.partition.being(key);
|
|
112
|
+
return row ? [[key, row.cells, JSON.stringify(row.cells)]] : [];
|
|
113
|
+
});
|
|
110
114
|
const out = await work();
|
|
111
115
|
if (await this.partition.kept())
|
|
112
116
|
return out;
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
for (const
|
|
117
|
-
|
|
117
|
+
for (const [key, cells, was] of before) {
|
|
118
|
+
if (JSON.stringify(cells) === was)
|
|
119
|
+
continue;
|
|
120
|
+
for (const k of Object.keys(cells))
|
|
121
|
+
delete cells[k];
|
|
122
|
+
for (const [k, v] of Object.entries(JSON.parse(was)))
|
|
123
|
+
Object.defineProperty(cells, k, { value: v, enumerable: true, writable: true, configurable: true });
|
|
118
124
|
this.partition.told(key);
|
|
119
125
|
}
|
|
120
126
|
return silence;
|
|
@@ -132,7 +138,7 @@ export class Ward {
|
|
|
132
138
|
const args = p.args === undefined ? {} : readValue(p.args, DEPTH + 1);
|
|
133
139
|
if (key === null || key === undefined || typeof args !== 'object' || args === null || Array.isArray(args))
|
|
134
140
|
return { silence: true };
|
|
135
|
-
const out = await this.#kept(key, () => this.#answer(key, asker, p.method, args));
|
|
141
|
+
const out = await this.#kept([key], () => this.#answer(key, asker, p.method, args));
|
|
136
142
|
const object = isSilence(out) || isWord(out) ? undefined : writeValue(out, DEPTH);
|
|
137
143
|
if (object === undefined)
|
|
138
144
|
return { silence: true };
|
package/package.json
CHANGED
package/src/cli/command.ts
CHANGED
|
@@ -28,7 +28,10 @@
|
|
|
28
28
|
// dev, from this folder, at
|
|
29
29
|
// http://127.0.0.1:P/quo
|
|
30
30
|
// nervur open <key> <kind> a faculty stands in the box ward
|
|
31
|
-
// nervur host <ward> [--seed S]
|
|
31
|
+
// nervur host <ward> [--seed S] [--store K] a hosted ward stands, kept by the
|
|
32
|
+
// root memory or the store K
|
|
33
|
+
// nervur move <ward> <store | root> a hosted ward carried to a store,
|
|
34
|
+
// or back to the root memory
|
|
32
35
|
// nervur route <ward pk> <address... | none> where a ward is reached, in order
|
|
33
36
|
// nervur reach <address...> where this harbor is reached, as
|
|
34
37
|
// every invitation it gives says
|
|
@@ -63,8 +66,8 @@ import { carry, isServed, land, open, pack, request, serve } from './harbor.ts';
|
|
|
63
66
|
// Where `edge dev` answers unless --port names another, wrangler's own.
|
|
64
67
|
const EDGE_PORT = 8787;
|
|
65
68
|
|
|
66
|
-
const USAGE = 'usage: nervur <init|serve|module|modules|census|sweep|open|host|route|reach|boot|unboot|public|invite|ask|own|disown|pilot|export|import|edge> [args] [--dir D] [--ward W] [--via V] [--seed S] [--with-seed] [--host H] [--port P]';
|
|
67
|
-
const FLAGS = new Set(['dir', 'ward', 'via', 'seed', 'host', 'port']);
|
|
69
|
+
const USAGE = 'usage: nervur <init|serve|module|modules|census|sweep|open|host|move|route|reach|boot|unboot|public|invite|ask|own|disown|pilot|export|import|edge> [args] [--dir D] [--ward W] [--via V] [--seed S] [--store K] [--with-seed] [--host H] [--port P]';
|
|
70
|
+
const FLAGS = new Set(['dir', 'ward', 'via', 'seed', 'store', 'host', 'port']);
|
|
68
71
|
// The one flag that stands alone, since what it names is a yes.
|
|
69
72
|
const ALONE = 'with-seed';
|
|
70
73
|
|
|
@@ -120,7 +123,11 @@ export const requestOf = (command: string, words: string[], flags: Record<string
|
|
|
120
123
|
}
|
|
121
124
|
case 'host': {
|
|
122
125
|
const [ward] = need(1);
|
|
123
|
-
return { method: 'host', args: { ward: ward!, ...(flags.seed === undefined ? {} : { seed: flags.seed }) } };
|
|
126
|
+
return { method: 'host', args: { ward: ward!, ...(flags.seed === undefined ? {} : { seed: flags.seed }), ...(flags.store === undefined ? {} : { store: flags.store }) } };
|
|
127
|
+
}
|
|
128
|
+
case 'move': {
|
|
129
|
+
const [ward, store] = need(2);
|
|
130
|
+
return { method: 'move', args: { ward: ward!, store: store === 'root' ? null : store! } };
|
|
124
131
|
}
|
|
125
132
|
case 'route': {
|
|
126
133
|
const [ward, ...where] = words;
|
package/src/harbor/dock.ts
CHANGED
|
@@ -5,9 +5,14 @@
|
|
|
5
5
|
//
|
|
6
6
|
// open { key, class } a faculty stands in the box ward, and the
|
|
7
7
|
// dock holds a standing on her under her key
|
|
8
|
-
// host { ward, seed
|
|
9
|
-
//
|
|
10
|
-
// dock; the dock keeps its name
|
|
8
|
+
// host { ward, seed?, a hosted ward stands under the seed named,
|
|
9
|
+
// store? } or a drawn one, holding a standing on the
|
|
10
|
+
// dock; the dock keeps its name, its seed and
|
|
11
|
+
// the store faculty that keeps its place, or
|
|
12
|
+
// none for the root memory
|
|
13
|
+
// move { ward, store } a hosted ward's place carried whole to
|
|
14
|
+
// another keeper, a store's key or null for
|
|
15
|
+
// the root memory, and forgotten where it was
|
|
11
16
|
// lend { contract } asked by a hosted ward: an offer from a
|
|
12
17
|
// faculty that fulfils the contract
|
|
13
18
|
// retract { contract, heir } an offer that was not taken
|
|
@@ -22,13 +27,19 @@
|
|
|
22
27
|
// route, where the root set none.
|
|
23
28
|
import { Faculty, KIT, type Asker, type AskSpec, type Blueprint, type Invitation, type JsonObject } from '../being/index.ts';
|
|
24
29
|
import { CATALOGUE } from './catalogue.ts';
|
|
30
|
+
import { STORE } from './store.ts';
|
|
25
31
|
import { readAt, readInvitation } from '../quo/index.ts';
|
|
26
32
|
import { ownerAsk, pilots, rootAsk, WardBeing, type Lender, type WardBeingStance } from '../ward/index.ts';
|
|
27
33
|
|
|
28
34
|
// What the dock asks of her harbor: a ward standing, and the seed, as hex,
|
|
29
35
|
// it stands under.
|
|
30
36
|
export interface Hosting {
|
|
31
|
-
host(name: string, invitation: Invitation, seed?: string): Promise<string | null>;
|
|
37
|
+
host(name: string, invitation: Invitation, seed?: string, store?: string): Promise<string | null>;
|
|
38
|
+
// A standing ward's place, carried to another keeper and standing from
|
|
39
|
+
// there: null, or why not. Where it was stays until `release`.
|
|
40
|
+
move(name: string, seed: string, from: string | undefined, to: string | undefined): Promise<string | null>;
|
|
41
|
+
// A ward's place, forgotten by the keeper named.
|
|
42
|
+
release(name: string, store: string | undefined): Promise<void>;
|
|
32
43
|
// Whether the harbor's carrier took the route.
|
|
33
44
|
route(pk: string, at: readonly string[] | null): boolean;
|
|
34
45
|
// Whether the box ward's partition was kept.
|
|
@@ -45,8 +56,9 @@ export interface Hosting {
|
|
|
45
56
|
lull(key: string): Promise<void>;
|
|
46
57
|
}
|
|
47
58
|
|
|
48
|
-
// One hosted ward, as the dock's cells keep it
|
|
49
|
-
|
|
59
|
+
// One hosted ward, as the dock's cells keep it, with the key of the store
|
|
60
|
+
// that keeps its place where the root memory does not.
|
|
61
|
+
export type Hosted = { ward: string; seed: string; store?: string };
|
|
50
62
|
|
|
51
63
|
// Each hosted ward that did not unpack and each route the carrier did not
|
|
52
64
|
// take, with the reason, by name and by ward pk.
|
|
@@ -64,7 +76,8 @@ export class Dock extends WardBeing implements Lender {
|
|
|
64
76
|
static override asks: Record<string, AskSpec> = {
|
|
65
77
|
...WardBeing.asks,
|
|
66
78
|
open: ownerAsk('a faculty of this harbor', { required: ['key', 'class'] }),
|
|
67
|
-
host: ownerAsk('a ward this harbor hosts', { required: ['ward'] }),
|
|
79
|
+
host: ownerAsk('a ward this harbor hosts, kept by the root memory or by the store named', { required: ['ward'] }),
|
|
80
|
+
move: ownerAsk('a hosted ward carried to a store, or to the root memory with null', { required: ['ward', 'store'] }),
|
|
68
81
|
route: ownerAsk('where a ward is reached, addresses in order, or null', { required: ['ward', 'at'] }),
|
|
69
82
|
reach: ownerAsk('where this harbor is reached, the addresses every invitation carries', { required: ['at'] }),
|
|
70
83
|
sweep: rootAsk('every place of the memory this harbor does not name, gone'),
|
|
@@ -87,7 +100,12 @@ export class Dock extends WardBeing implements Lender {
|
|
|
87
100
|
}
|
|
88
101
|
|
|
89
102
|
get hosted(): Hosted[] {
|
|
90
|
-
return (this.cells.hosted as Hosted[]).map(({ ward, seed }) => ({ ward, seed }));
|
|
103
|
+
return (this.cells.hosted as Hosted[]).map(({ ward, seed, store }) => ({ ward, seed, ...(store === undefined ? {} : { store }) }));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Whether a key names a store faculty the dock opened.
|
|
107
|
+
#store(key: unknown): key is string {
|
|
108
|
+
return text(key) && this.#fulfilling(STORE).includes(key);
|
|
91
109
|
}
|
|
92
110
|
|
|
93
111
|
get routes(): Record<string, string[]> {
|
|
@@ -130,8 +148,10 @@ export class Dock extends WardBeing implements Lender {
|
|
|
130
148
|
if ((await this.stance.boot(kind, CATALOGUE, CATALOGUE)) === null || !(await this.#hosting.keep())) throw new Error('the catalogue did not stand');
|
|
131
149
|
}
|
|
132
150
|
|
|
133
|
-
// A faculty that goes stops listening first
|
|
151
|
+
// A faculty that goes stops listening first, and a store keeping a
|
|
152
|
+
// ward's place does not go.
|
|
134
153
|
override async unboot(args: JsonObject): Promise<JsonObject> {
|
|
154
|
+
if (this.hosted.some((h) => h.store !== undefined && h.store === args.being)) return { error: 'a ward is kept by this store' };
|
|
135
155
|
if (typeof args.being === 'string') await this.#hosting.lull(args.being);
|
|
136
156
|
return super.unboot(args);
|
|
137
157
|
}
|
|
@@ -162,17 +182,46 @@ export class Dock extends WardBeing implements Lender {
|
|
|
162
182
|
if (name === BOX) return { error: 'box is the box ward' };
|
|
163
183
|
if (this.hosted.some((h) => h.ward === name)) return { error: `${name} is hosted already` };
|
|
164
184
|
if (args.seed !== undefined && !text(args.seed)) return { error: 'seed is text' };
|
|
185
|
+
const store = args.store ?? undefined;
|
|
186
|
+
if (store !== undefined && !this.#store(store)) return { error: 'no such store' };
|
|
165
187
|
const invitation = await this.stance.occupants.invite(name, { hosted: true });
|
|
166
188
|
if (invitation === null) return { error: 'not hosted' };
|
|
167
|
-
const seed = await this.#hosting.host(name, invitation, args.seed);
|
|
189
|
+
const seed = await this.#hosting.host(name, invitation, args.seed, store);
|
|
168
190
|
if (seed === null) {
|
|
169
191
|
this.stance.occupants.remove(name);
|
|
170
192
|
return { error: 'not hosted' };
|
|
171
193
|
}
|
|
172
|
-
(this.cells.hosted as Hosted[]).push({ ward: name, seed });
|
|
194
|
+
(this.cells.hosted as Hosted[]).push({ ward: name, seed, ...(store === undefined ? {} : { store }) });
|
|
173
195
|
return { hosted: name };
|
|
174
196
|
}
|
|
175
197
|
|
|
198
|
+
// The place is written where it goes before the dock's cells say so, and
|
|
199
|
+
// forgotten where it was only once they are kept, so a cut at any step
|
|
200
|
+
// leaves the ward whole under the keeper the cells name.
|
|
201
|
+
async move(args: JsonObject): Promise<JsonObject> {
|
|
202
|
+
const rows = this.cells.hosted as Hosted[];
|
|
203
|
+
const row = rows.find((h) => h.ward === args.ward);
|
|
204
|
+
if (!row) return { error: 'no such ward' };
|
|
205
|
+
const to = args.store ?? undefined;
|
|
206
|
+
if (to !== undefined && !this.#store(to)) return { error: 'no such store' };
|
|
207
|
+
const from = row.store;
|
|
208
|
+
if (from === to) return { error: `${row.ward} is kept there already` };
|
|
209
|
+
const refused = await this.#hosting.move(row.ward, row.seed, from, to);
|
|
210
|
+
if (refused !== null) return { error: refused };
|
|
211
|
+
const set = (store: string | undefined): void => {
|
|
212
|
+
if (store === undefined) delete row.store;
|
|
213
|
+
else row.store = store;
|
|
214
|
+
};
|
|
215
|
+
set(to);
|
|
216
|
+
if (!(await this.#hosting.keep())) {
|
|
217
|
+
set(from);
|
|
218
|
+
await this.#hosting.move(row.ward, row.seed, to, from);
|
|
219
|
+
return { error: 'not kept' };
|
|
220
|
+
}
|
|
221
|
+
await this.#hosting.release(row.ward, from);
|
|
222
|
+
return { moved: row.ward, store: to ?? null };
|
|
223
|
+
}
|
|
224
|
+
|
|
176
225
|
async lend(args: JsonObject): Promise<JsonObject> {
|
|
177
226
|
if (!text(args.contract)) return { error: 'contract is text' };
|
|
178
227
|
const invitation = await this.offer(args.contract);
|