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.
@@ -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 cannot be imported, or exports no
57
- // module, is none.
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
- return null;
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))
@@ -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';
@@ -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';
@@ -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
- export declare const classes: (typeof Echo | typeof Shop | typeof Customer)[];
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: 'no such ward to host' }, 'its name and seed stay kept');
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(WARD, () => this.#answer(WARD, { id: OWNER }, method, args));
104
+ return this.#kept(Object.keys(this.partition.beings), () => this.#answer(WARD, { id: OWNER }, method, args));
103
105
  }
104
- // A being's answer, kept. Where the memory refuses, what she wrote is
105
- // put back and her answer is silence. What her relations stand on is not
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(key, work) {
108
- const row = this.partition.being(key);
109
- const before = row && JSON.stringify(row.cells);
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
- if (row && before !== undefined) {
114
- for (const k of Object.keys(row.cells))
115
- delete row.cells[k];
116
- for (const [k, v] of Object.entries(JSON.parse(before)))
117
- Object.defineProperty(row.cells, k, { value: v, enumerable: true, writable: true, configurable: true });
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nervur",
3
- "version": "0.21.0",
3
+ "version": "0.21.2",
4
4
  "description": "Nervur's kit of Quo: an object asks another object and gets an answer, without knowing where it is.",
5
5
  "keywords": [
6
6
  "quo",
@@ -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] a hosted ward stands
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;
@@ -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 never crosses a door and never lives in a cell, so it is the
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>;
@@ -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(() => null);
70
+ const loaded = await this.#harbor.load(row.from).catch((e: Error) => e);
71
71
  const refused =
72
- loaded === null ? `not found at ${row.from}` : loaded.module !== module ? `${row.from} is module ${loaded.module}` : this.#harbor.check([...this.#running.values(), loaded]);
73
- if (refused === null) this.#running.set(module, loaded!);
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);