nervur 0.21.0 → 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 +14 -1
- package/dist/cli/command.js +11 -4
- package/dist/contract/index.js +2 -1
- package/dist/harbor/catalogue.js +11 -3
- package/dist/harbor/dock.d.ts +5 -1
- package/dist/harbor/dock.js +63 -10
- 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/node/index.js +12 -4
- 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 +251 -5
- package/dist/ward/ward.js +18 -12
- package/package.json +1 -1
- package/src/cli/command.ts +11 -4
- package/src/contract/index.ts +2 -1
- package/src/harbor/catalogue.ts +11 -4
- package/src/harbor/dock.ts +63 -12
- 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/node/index.ts +11 -4
- package/src/proof/index.ts +4 -2
- package/src/proof/law.ts +244 -5
- package/src/ward/ward.ts +15 -10
package/dist/node/index.js
CHANGED
|
@@ -53,8 +53,10 @@ export const isFile = (from) => /^(\.{1,2}[\\/]|[\\/]|[A-Za-z]:[\\/])/.test(from
|
|
|
53
53
|
// Code from a file or an installed package: a module that exports
|
|
54
54
|
// `module`, `version` and `classes`, found again by where it was found, a
|
|
55
55
|
// file by its absolute path and a package by its name, resolved from each
|
|
56
|
-
// folder of `bases` in order. What
|
|
57
|
-
//
|
|
56
|
+
// folder of `bases` in order. What is not there, or exports no module, is
|
|
57
|
+
// none. Code that is there and does not import throws Node's reason, so
|
|
58
|
+
// whoever added it learns why; an ES module read as CommonJS says which
|
|
59
|
+
// field its package.json lacks.
|
|
58
60
|
export class ImportLoader extends Loader {
|
|
59
61
|
#bases;
|
|
60
62
|
constructor(bases = [process.cwd()]) {
|
|
@@ -69,8 +71,14 @@ export class ImportLoader extends Loader {
|
|
|
69
71
|
try {
|
|
70
72
|
exported = (await import(__rewriteRelativeImportExtension(pathToFileURL(found).href)));
|
|
71
73
|
}
|
|
72
|
-
catch {
|
|
73
|
-
|
|
74
|
+
catch (e) {
|
|
75
|
+
const said = e.message;
|
|
76
|
+
// The name itself is nowhere, as against code there that needs
|
|
77
|
+
// something that is not.
|
|
78
|
+
if (e.code === 'ERR_MODULE_NOT_FOUND' && said.includes(`'${found}'`))
|
|
79
|
+
return null;
|
|
80
|
+
const commonjs = e instanceof SyntaxError && /\b(export|import)\b/.test(said);
|
|
81
|
+
throw new Error(`${found} did not load: ${said}${commonjs ? '; it is an ES module read as CommonJS, so set "type": "module" in its package.json' : ''}`, { cause: e });
|
|
74
82
|
}
|
|
75
83
|
const { module, version, classes } = exported;
|
|
76
84
|
if (typeof module !== 'string' || typeof version !== 'string' || !Array.isArray(classes))
|
package/dist/proof/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export { entropyScenes, custodyScenes, loaderScenes, memoryScenes, type Cut, type Scene } from './contracts.ts';
|
|
2
2
|
export { expect, type Expect } from './expect.ts';
|
|
3
|
-
export { law, lawScenes, type Bodies, type Terrain } from './law.ts';
|
|
3
|
+
export { keeperScenes, law, lawScenes, type Bodies, type Terrain } from './law.ts';
|
package/dist/proof/index.js
CHANGED
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
// for (const [title, scene] of memoryScenes('S3Memory', make, expect, half)) test(title, scene);
|
|
7
7
|
//
|
|
8
8
|
// `expect` throws on the first assertion that fails. The law's scenes stand
|
|
9
|
-
// harbors on a stranger's bodies and hold the unpacking law over them
|
|
9
|
+
// harbors on a stranger's bodies and hold the unpacking law over them, and
|
|
10
|
+
// the keeper's scenes hold a being's fields over the root memory and over
|
|
11
|
+
// every store.
|
|
10
12
|
export { entropyScenes, custodyScenes, loaderScenes, memoryScenes } from './contracts.js';
|
|
11
13
|
export { expect } from './expect.js';
|
|
12
|
-
export { law, lawScenes } from './law.js';
|
|
14
|
+
export { keeperScenes, law, lawScenes } from './law.js';
|
package/dist/proof/law.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Being, Faculty, type Asker, type JsonObject } from '../being/index.ts';
|
|
2
2
|
import { Memory, type Custody, type Module } from '../contract/index.ts';
|
|
3
|
-
import { type Harbor } from '../harbor/index.ts';
|
|
3
|
+
import { Store, type Harbor } from '../harbor/index.ts';
|
|
4
4
|
import { World } from '../pointer/index.ts';
|
|
5
5
|
import type { Expect } from './expect.ts';
|
|
6
6
|
export type Bodies = {
|
|
@@ -45,8 +45,46 @@ declare class Customer extends Being {
|
|
|
45
45
|
buy(args: JsonObject): Promise<JsonObject>;
|
|
46
46
|
echo(args: JsonObject): Promise<JsonObject>;
|
|
47
47
|
}
|
|
48
|
-
|
|
48
|
+
declare class Profile extends Being {
|
|
49
|
+
static kind: string;
|
|
50
|
+
static cells: {
|
|
51
|
+
name: string;
|
|
52
|
+
age: number;
|
|
53
|
+
};
|
|
54
|
+
static asks: {
|
|
55
|
+
set: {};
|
|
56
|
+
get: {};
|
|
57
|
+
};
|
|
58
|
+
set(args: JsonObject): JsonObject;
|
|
59
|
+
get(): JsonObject;
|
|
60
|
+
}
|
|
61
|
+
declare class Shelf extends Store {
|
|
62
|
+
#private;
|
|
63
|
+
static kind: string;
|
|
64
|
+
static shelf(key: string): Watched;
|
|
65
|
+
static reset(): void;
|
|
66
|
+
read(place: string): Promise<Map<string, Uint8Array>>;
|
|
67
|
+
write(place: string, entries: ReadonlyMap<string, Uint8Array | null>): Promise<void>;
|
|
68
|
+
places(): Promise<string[]>;
|
|
69
|
+
forget(place: string): Promise<void>;
|
|
70
|
+
}
|
|
71
|
+
export declare const classes: (typeof Echo | typeof Shop | typeof Customer | typeof Profile | typeof Shelf)[];
|
|
49
72
|
export declare const law: Module;
|
|
50
73
|
export declare const standOne: (world: World, name: string, parts?: Partial<Bodies>) => Promise<Harbor>;
|
|
74
|
+
declare class Watched extends Memory {
|
|
75
|
+
readonly inner: Memory;
|
|
76
|
+
readonly seen: {
|
|
77
|
+
place: string;
|
|
78
|
+
name: string;
|
|
79
|
+
bytes: Uint8Array | null;
|
|
80
|
+
}[];
|
|
81
|
+
refuse: (place: string) => boolean;
|
|
82
|
+
constructor(inner: Memory);
|
|
83
|
+
read(place: string): Promise<Map<string, Uint8Array>>;
|
|
84
|
+
write(place: string, entries: ReadonlyMap<string, Uint8Array | null>): Promise<void>;
|
|
85
|
+
places(): Promise<string[]>;
|
|
86
|
+
forget(place: string): Promise<void>;
|
|
87
|
+
}
|
|
51
88
|
export declare const lawScenes: (t: Terrain, expect: Expect) => [string, () => Promise<void>][];
|
|
89
|
+
export declare const keeperScenes: (t: Terrain, expect: Expect) => [string, () => Promise<void>][];
|
|
52
90
|
export {};
|
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);
|
|
@@ -210,7 +258,7 @@ export const lawScenes = (t, expect) => [
|
|
|
210
258
|
expect.same(again.names, ['good']);
|
|
211
259
|
expect.same((await census(again)).failed, { wards: { bad: 'an entry the package did not seal' }, routes: {} });
|
|
212
260
|
expect.same(await rootLine(again, { ward: 'bad' }), { error: 'the ward did not stand', reason: 'an entry the package did not seal' });
|
|
213
|
-
expect.same(await again.root('host', { ward: 'bad' }), { error: '
|
|
261
|
+
expect.same(await again.root('host', { ward: 'bad' }), { error: 'bad is hosted already' }, 'its name and seed stay kept');
|
|
214
262
|
await spoil(places[1]);
|
|
215
263
|
let refused = '';
|
|
216
264
|
await world.restart('h', t.again(bodies)).catch((e) => (refused = e.message));
|
|
@@ -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/contract/index.ts
CHANGED
|
@@ -62,7 +62,8 @@ export type Module = { readonly module: string; readonly version: string; readon
|
|
|
62
62
|
export type Loaded = Module & { readonly from: string };
|
|
63
63
|
|
|
64
64
|
// Code, by the name a harbor keeps for it, or null where this loader has
|
|
65
|
-
// none. Code
|
|
65
|
+
// none. Code it has that does not load throws, saying why. Code never
|
|
66
|
+
// crosses a door and never lives in a cell, so it is the
|
|
66
67
|
// one thing beside the seed a terrain hands in: a map, an import, a bundle.
|
|
67
68
|
export abstract class Loader {
|
|
68
69
|
abstract load(from: string): Promise<Loaded | null>;
|
package/src/harbor/catalogue.ts
CHANGED
|
@@ -67,10 +67,16 @@ export class Catalogue extends Faculty {
|
|
|
67
67
|
// it. The rest stay out, each with its reason.
|
|
68
68
|
async wake(): Promise<void> {
|
|
69
69
|
for (const [module, row] of Object.entries(this.#rows)) {
|
|
70
|
-
const loaded = await this.#harbor.load(row.from).catch(() =>
|
|
70
|
+
const loaded = await this.#harbor.load(row.from).catch((e: Error) => e);
|
|
71
71
|
const refused =
|
|
72
|
-
loaded
|
|
73
|
-
|
|
72
|
+
loaded instanceof Error
|
|
73
|
+
? loaded.message
|
|
74
|
+
: loaded === null
|
|
75
|
+
? `not found at ${row.from}`
|
|
76
|
+
: loaded.module !== module
|
|
77
|
+
? `${row.from} is module ${loaded.module}`
|
|
78
|
+
: this.#harbor.check([...this.#running.values(), loaded]);
|
|
79
|
+
if (refused === null) this.#running.set(module, loaded as Loaded);
|
|
74
80
|
else this.#failed.set(module, refused);
|
|
75
81
|
}
|
|
76
82
|
this.#harbor.run([...this.#running.values()]);
|
|
@@ -78,7 +84,8 @@ export class Catalogue extends Faculty {
|
|
|
78
84
|
|
|
79
85
|
async add(args: JsonObject): Promise<JsonObject> {
|
|
80
86
|
if (!text(args.from)) return { error: 'from is text' };
|
|
81
|
-
const loaded = await this.#harbor.load(args.from);
|
|
87
|
+
const loaded = await this.#harbor.load(args.from).catch((e: Error) => e);
|
|
88
|
+
if (loaded instanceof Error) return { error: loaded.message };
|
|
82
89
|
if (loaded === null) return { error: 'no such module' };
|
|
83
90
|
const running = new Map(this.#running);
|
|
84
91
|
running.set(loaded.module, loaded);
|