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 CHANGED
@@ -212,13 +212,24 @@ catalogue, and the root's asks write the rest: `add` on the catalogue,
212
212
  `open`, `host`, `route`, and `boot` on a ward. `host { ward, seed? }`
213
213
  stands a ward under a seed you name, or a drawn one.
214
214
 
215
+ A ward's place is kept by the root memory unless you name a store.
216
+ `open { key: 'vault', class: 'org.nervur.ground-store' }` opens the
217
+ store every ground ships, and `host { ward, store: 'vault' }` keeps the
218
+ ward there. A class of yours extending `Store` keeps a ward in a bucket
219
+ or a database the same way, and `move { ward, store }` carries a ward
220
+ between keepers whole.
221
+
215
222
  ## What it holds
216
223
 
217
224
  - **Being, Faculty.** What you extend. A faculty's contract is its class
218
225
  and every class between it and `Faculty`.
219
226
  - **Entropy, Clock, Custody, Memory, Carrier, Loader.** The contracts a
220
227
  terrain fulfils. Each has one suite, and every body that fulfils it
221
- passes it.
228
+ passes it. `nervur/proof` hands you those suites and the unpacking
229
+ law's as scenes, a title and a function each, so a body of your own,
230
+ a store in a cloud or a secret of your platform's, runs them under
231
+ your own runner: `for (const [title, scene] of memoryScenes('Mine',
232
+ make, expect)) test(title, scene)`. `@nervur-org/s3` is one such body.
222
233
  - **Harbor, Terrain, Dock, Catalogue, Registry, WardBeing.** The onion.
223
234
  - **World, PointerTerrain** and the pointer bodies, `HeldLoader` among
224
235
  them. A whole world with this package alone.
@@ -240,6 +251,8 @@ stands a ward under a seed you name, or a drawn one.
240
251
  - **WebDialer, webFaculty**: Quo over the web, a post to `https://` or a
241
252
  held line to `wss://`, in any engine with `fetch` and WebSocket.
242
253
  `webServe` and `webGround`, from `nervur/http`, listen for it on Node.
254
+ `answerPost(harbor, listen, request)` answers one post and nothing
255
+ more, for a function that opens a harbor per request and closes it.
243
256
 
244
257
  - **The relay**, `org.nervur.relay` and `org.nervur.relay-client`, in
245
258
  every harbor: a harbor nobody can dial is reached through one that
@@ -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
@@ -59,8 +62,8 @@ import { edgeDev } from './edge.js';
59
62
  import { carry, isServed, land, open, pack, request, serve } from './harbor.js';
60
63
  // Where `edge dev` answers unless --port names another, wrangler's own.
61
64
  const EDGE_PORT = 8787;
62
- 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]';
63
- const FLAGS = new Set(['dir', 'ward', 'via', 'seed', 'host', 'port']);
65
+ 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]';
66
+ const FLAGS = new Set(['dir', 'ward', 'via', 'seed', 'store', 'host', 'port']);
64
67
  // The one flag that stands alone, since what it names is a yes.
65
68
  const ALONE = 'with-seed';
66
69
  export const parse = (argv) => {
@@ -120,7 +123,11 @@ export const requestOf = (command, words, flags) => {
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;
@@ -18,7 +18,8 @@ export class Memory {
18
18
  export class Custody {
19
19
  }
20
20
  // Code, by the name a harbor keeps for it, or null where this loader has
21
- // none. Code never crosses a door and never lives in a cell, so it is the
21
+ // none. Code it has that does not load throws, saying why. Code never
22
+ // crosses a door and never lives in a cell, so it is the
22
23
  // one thing beside the seed a terrain hands in: a map, an import, a bundle.
23
24
  export class Loader {
24
25
  }
@@ -42,8 +42,14 @@ export class Catalogue extends Faculty {
42
42
  // it. The rest stay out, each with its reason.
43
43
  async wake() {
44
44
  for (const [module, row] of Object.entries(this.#rows)) {
45
- const loaded = await this.#harbor.load(row.from).catch(() => null);
46
- const refused = loaded === null ? `not found at ${row.from}` : loaded.module !== module ? `${row.from} is module ${loaded.module}` : this.#harbor.check([...this.#running.values(), loaded]);
45
+ const loaded = await this.#harbor.load(row.from).catch((e) => e);
46
+ const refused = loaded instanceof Error
47
+ ? loaded.message
48
+ : loaded === null
49
+ ? `not found at ${row.from}`
50
+ : loaded.module !== module
51
+ ? `${row.from} is module ${loaded.module}`
52
+ : this.#harbor.check([...this.#running.values(), loaded]);
47
53
  if (refused === null)
48
54
  this.#running.set(module, loaded);
49
55
  else
@@ -54,7 +60,9 @@ export class Catalogue extends Faculty {
54
60
  async add(args) {
55
61
  if (!text(args.from))
56
62
  return { error: 'from is text' };
57
- const loaded = await this.#harbor.load(args.from);
63
+ const loaded = await this.#harbor.load(args.from).catch((e) => e);
64
+ if (loaded instanceof Error)
65
+ return { error: loaded.message };
58
66
  if (loaded === null)
59
67
  return { error: 'no such module' };
60
68
  const running = new Map(this.#running);
@@ -1,7 +1,9 @@
1
1
  import { type Asker, type AskSpec, type Blueprint, type Invitation, type JsonObject } from '../being/index.ts';
2
2
  import { WardBeing, type Lender, type WardBeingStance } from '../ward/index.ts';
3
3
  export interface Hosting {
4
- host(name: string, invitation: Invitation, seed?: string): Promise<string | null>;
4
+ host(name: string, invitation: Invitation, seed?: string, store?: string): Promise<string | null>;
5
+ move(name: string, seed: string, from: string | undefined, to: string | undefined): Promise<string | null>;
6
+ release(name: string, store: string | undefined): Promise<void>;
5
7
  route(pk: string, at: readonly string[] | null): boolean;
6
8
  keep(): Promise<boolean>;
7
9
  failures(): Failures;
@@ -13,6 +15,7 @@ export interface Hosting {
13
15
  export type Hosted = {
14
16
  ward: string;
15
17
  seed: string;
18
+ store?: string;
16
19
  };
17
20
  export type Failures = {
18
21
  wards: Record<string, string>;
@@ -41,6 +44,7 @@ export declare class Dock extends WardBeing implements Lender {
41
44
  open(args: JsonObject): Promise<JsonObject>;
42
45
  sweep(): Promise<JsonObject>;
43
46
  host(args: JsonObject): Promise<JsonObject>;
47
+ move(args: JsonObject): Promise<JsonObject>;
44
48
  lend(args: JsonObject): Promise<JsonObject>;
45
49
  retract(args: JsonObject): Promise<JsonObject>;
46
50
  offer(contract: string): Promise<Invitation | null>;
@@ -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? } a hosted ward stands under the seed named,
9
- // or a drawn one, holding a standing on the
10
- // dock; the dock keeps its name and seed
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,6 +27,7 @@
22
27
  // route, where the root set none.
23
28
  import { Faculty, KIT } from '../being/index.js';
24
29
  import { CATALOGUE } from './catalogue.js';
30
+ import { STORE } from './store.js';
25
31
  import { readAt, readInvitation } from '../quo/index.js';
26
32
  import { ownerAsk, pilots, rootAsk, WardBeing } from '../ward/index.js';
27
33
  export const BOX = 'box';
@@ -35,7 +41,8 @@ export class Dock extends WardBeing {
35
41
  static asks = {
36
42
  ...WardBeing.asks,
37
43
  open: ownerAsk('a faculty of this harbor', { required: ['key', 'class'] }),
38
- host: ownerAsk('a ward this harbor hosts', { required: ['ward'] }),
44
+ host: ownerAsk('a ward this harbor hosts, kept by the root memory or by the store named', { required: ['ward'] }),
45
+ move: ownerAsk('a hosted ward carried to a store, or to the root memory with null', { required: ['ward', 'store'] }),
39
46
  route: ownerAsk('where a ward is reached, addresses in order, or null', { required: ['ward', 'at'] }),
40
47
  reach: ownerAsk('where this harbor is reached, the addresses every invitation carries', { required: ['at'] }),
41
48
  sweep: rootAsk('every place of the memory this harbor does not name, gone'),
@@ -55,7 +62,11 @@ export class Dock extends WardBeing {
55
62
  return blueprint;
56
63
  }
57
64
  get hosted() {
58
- return this.cells.hosted.map(({ ward, seed }) => ({ ward, seed }));
65
+ return this.cells.hosted.map(({ ward, seed, store }) => ({ ward, seed, ...(store === undefined ? {} : { store }) }));
66
+ }
67
+ // Whether a key names a store faculty the dock opened.
68
+ #store(key) {
69
+ return text(key) && this.#fulfilling(STORE).includes(key);
59
70
  }
60
71
  get routes() {
61
72
  return Object.fromEntries(Object.entries(this.cells.routes).map(([pk, at]) => [pk, [...at]]));
@@ -97,8 +108,11 @@ export class Dock extends WardBeing {
97
108
  if ((await this.stance.boot(kind, CATALOGUE, CATALOGUE)) === null || !(await this.#hosting.keep()))
98
109
  throw new Error('the catalogue did not stand');
99
110
  }
100
- // A faculty that goes stops listening first.
111
+ // A faculty that goes stops listening first, and a store keeping a
112
+ // ward's place does not go.
101
113
  async unboot(args) {
114
+ if (this.hosted.some((h) => h.store !== undefined && h.store === args.being))
115
+ return { error: 'a ward is kept by this store' };
102
116
  if (typeof args.being === 'string')
103
117
  await this.#hosting.lull(args.being);
104
118
  return super.unboot(args);
@@ -126,21 +140,60 @@ export class Dock extends WardBeing {
126
140
  }
127
141
  async host(args) {
128
142
  const name = args.ward;
129
- if (!text(name) || name === BOX || this.hosted.some((h) => h.ward === name))
130
- return { error: 'no such ward to host' };
143
+ if (!text(name))
144
+ return { error: 'ward is text' };
145
+ if (name === BOX)
146
+ return { error: 'box is the box ward' };
147
+ if (this.hosted.some((h) => h.ward === name))
148
+ return { error: `${name} is hosted already` };
131
149
  if (args.seed !== undefined && !text(args.seed))
132
150
  return { error: 'seed is text' };
151
+ const store = args.store ?? undefined;
152
+ if (store !== undefined && !this.#store(store))
153
+ return { error: 'no such store' };
133
154
  const invitation = await this.stance.occupants.invite(name, { hosted: true });
134
155
  if (invitation === null)
135
156
  return { error: 'not hosted' };
136
- const seed = await this.#hosting.host(name, invitation, args.seed);
157
+ const seed = await this.#hosting.host(name, invitation, args.seed, store);
137
158
  if (seed === null) {
138
159
  this.stance.occupants.remove(name);
139
160
  return { error: 'not hosted' };
140
161
  }
141
- this.cells.hosted.push({ ward: name, seed });
162
+ this.cells.hosted.push({ ward: name, seed, ...(store === undefined ? {} : { store }) });
142
163
  return { hosted: name };
143
164
  }
165
+ // The place is written where it goes before the dock's cells say so, and
166
+ // forgotten where it was only once they are kept, so a cut at any step
167
+ // leaves the ward whole under the keeper the cells name.
168
+ async move(args) {
169
+ const rows = this.cells.hosted;
170
+ const row = rows.find((h) => h.ward === args.ward);
171
+ if (!row)
172
+ return { error: 'no such ward' };
173
+ const to = args.store ?? undefined;
174
+ if (to !== undefined && !this.#store(to))
175
+ return { error: 'no such store' };
176
+ const from = row.store;
177
+ if (from === to)
178
+ return { error: `${row.ward} is kept there already` };
179
+ const refused = await this.#hosting.move(row.ward, row.seed, from, to);
180
+ if (refused !== null)
181
+ return { error: refused };
182
+ const set = (store) => {
183
+ if (store === undefined)
184
+ delete row.store;
185
+ else
186
+ row.store = store;
187
+ };
188
+ set(to);
189
+ if (!(await this.#hosting.keep())) {
190
+ set(from);
191
+ await this.#hosting.move(row.ward, row.seed, to, from);
192
+ return { error: 'not kept' };
193
+ }
194
+ await this.#hosting.release(row.ward, from);
195
+ return { moved: row.ward, store: to ?? null };
196
+ }
144
197
  async lend(args) {
145
198
  if (!text(args.contract))
146
199
  return { error: 'contract is text' };
@@ -13,6 +13,8 @@ export declare class Harbor extends Carrier implements Hosting, Cataloguing, Res
13
13
  private constructor();
14
14
  static open(from?: Terrain | Probing): Promise<Harbor>;
15
15
  sweep(): Promise<string[]>;
16
+ move(name: string, seed: string, from: string | undefined, to: string | undefined): Promise<string | null>;
17
+ release(name: string, store: string | undefined): Promise<void>;
16
18
  failures(): Failures;
17
19
  get box(): Ward;
18
20
  get dock(): Dock;
@@ -30,7 +32,7 @@ export declare class Harbor extends Carrier implements Hosting, Cataloguing, Res
30
32
  run(modules: readonly Loaded[]): string | null;
31
33
  stands(kinds: readonly string[]): boolean;
32
34
  kindsOf(module: string): string[];
33
- host(name: string, invitation: Invitation, seed?: string): Promise<string | null>;
35
+ host(name: string, invitation: Invitation, seed?: string, store?: string): Promise<string | null>;
34
36
  keep(): Promise<boolean>;
35
37
  route(pk: string, at: readonly string[] | null): boolean;
36
38
  grounds(kind: string): boolean;
@@ -6,6 +6,7 @@ import { Catalogue, CATALOGUE, catalogueFor } from './catalogue.js';
6
6
  import { CarrierFaculty, UNDIALED } from './carrying.js';
7
7
  import { BOX, Dock } from './dock.js';
8
8
  import { Package } from './package.js';
9
+ import { groundStore, Store } from './store.js';
9
10
  import { Registry } from './registry.js';
10
11
  import { relayClient, relayFaculty } from './relay.js';
11
12
  import { probeGround } from './probe.js';
@@ -22,7 +23,7 @@ export class Harbor extends Carrier {
22
23
  super();
23
24
  this.terrain = terrain;
24
25
  this.#package = pack;
25
- const faculties = [...terrain.faculties.map((make) => make(this)), relayFaculty(terrain.clock), relayClient(this, terrain.clock)];
26
+ const faculties = [...terrain.faculties.map((make) => make(this)), relayFaculty(terrain.clock), relayClient(this, terrain.clock), groundStore(terrain.memory)];
26
27
  this.#grounds = new Set(faculties.map((C) => C.kind));
27
28
  this.#kit = [catalogueFor(this), Pilot, ...faculties];
28
29
  this.#registry = new Registry(this.#kit, []);
@@ -69,7 +70,7 @@ export class Harbor extends Carrier {
69
70
  }
70
71
  for (const hosted of harbor.dock.hosted) {
71
72
  try {
72
- await harbor.#unpack(hosted.ward, unhex(hosted.seed));
73
+ await harbor.#unpack(hosted.ward, unhex(hosted.seed), undefined, undefined, harbor.#keeper(hosted.store));
73
74
  }
74
75
  catch (e) {
75
76
  harbor.#failed.wards[hosted.ward] = e instanceof Error ? e.message : String(e);
@@ -77,16 +78,81 @@ export class Harbor extends Carrier {
77
78
  }
78
79
  return harbor;
79
80
  }
80
- // Hosting: every place of the terrain's memory the dock does not name,
81
- // gone. A crash inside `host` leaves such a place, sealed under a seed
82
- // nothing kept, and nothing else writes one.
81
+ // Hosting: every place of the root memory and of each store that the
82
+ // dock does not name there, gone. A crash inside `host` or `move` leaves
83
+ // such a place, sealed under a seed nothing kept or kept elsewhere, and
84
+ // nothing else writes one. A place a store keeps in the root memory is
85
+ // the store's.
83
86
  async sweep() {
84
- const mine = new Set(await Promise.all([BOX, ...this.dock.hosted.map((h) => h.ward)].map((name) => this.#package.place(name))));
85
- const swept = (await this.terrain.memory.places()).filter((place) => !mine.has(place));
86
- for (const place of swept)
87
+ const named = async (store) => {
88
+ const wards = this.dock.hosted.filter((h) => h.store === store).map((h) => h.ward);
89
+ return new Set(await Promise.all((store === undefined ? [BOX, ...wards] : wards).map((name) => this.#package.place(name))));
90
+ };
91
+ const stores = this.#stores();
92
+ const swept = [];
93
+ const root = await named(undefined);
94
+ for (const place of await this.terrain.memory.places()) {
95
+ if (root.has(place) || stores.some(([, s]) => s.occupies(place)))
96
+ continue;
87
97
  await this.terrain.memory.forget(place);
98
+ swept.push(place);
99
+ }
100
+ for (const [key, store] of stores) {
101
+ const mine = await named(key);
102
+ for (const place of await store.places()) {
103
+ if (mine.has(place))
104
+ continue;
105
+ await store.forget(place);
106
+ swept.push(place);
107
+ }
108
+ }
88
109
  return swept;
89
110
  }
111
+ // Hosting: a standing ward, carried whole to another keeper. The ward
112
+ // leaves the harbor's doors while it moves, so no arrival lands on the
113
+ // keeper it leaves.
114
+ async move(name, seed, from, to) {
115
+ const ward = this.#wards.get(name);
116
+ if (!ward)
117
+ return 'the ward does not stand';
118
+ const source = this.#keeper(from);
119
+ const target = this.#keeper(to);
120
+ this.#wards.delete(name);
121
+ try {
122
+ if (!(await ward.partition.kept()))
123
+ throw new Error('the ward was not kept');
124
+ const place = await this.#package.place(name);
125
+ const entries = new Map(await source.read(place));
126
+ for (const entry of (await target.read(place)).keys())
127
+ if (!entries.has(entry))
128
+ entries.set(entry, null);
129
+ await target.write(place, entries);
130
+ await this.#unpack(name, unhex(seed), undefined, undefined, target);
131
+ return null;
132
+ }
133
+ catch (e) {
134
+ this.#wards.set(name, ward);
135
+ return e instanceof Error ? e.message : String(e);
136
+ }
137
+ }
138
+ async release(name, store) {
139
+ await this.#keeper(store).forget(await this.#package.place(name));
140
+ }
141
+ // The root memory, or the store standing under the key named.
142
+ #keeper(store) {
143
+ if (store === undefined)
144
+ return this.terrain.memory;
145
+ const being = this.box.being(store);
146
+ if (being instanceof Store)
147
+ return being;
148
+ throw new Error(`the store ${store} does not stand`);
149
+ }
150
+ #stores() {
151
+ return Object.keys(this.box.partition.beings).flatMap((key) => {
152
+ const being = this.box.being(key);
153
+ return being instanceof Store ? [[key, being]] : [];
154
+ });
155
+ }
90
156
  // Hosting: what did not stand when the harbor opened, each alone.
91
157
  failures() {
92
158
  return { wards: { ...this.#failed.wards }, routes: { ...this.#failed.routes } };
@@ -177,14 +243,14 @@ export class Harbor extends Carrier {
177
243
  // Hosting: a ward the dock hosts stands now, under the seed named or a
178
244
  // drawn one, and the seed it stands under comes back. A ward whose key
179
245
  // another ward of this harbor already has does not stand.
180
- async host(name, invitation, seed) {
246
+ async host(name, invitation, seed, store) {
181
247
  if (this.#wards.has(name))
182
248
  return null;
183
249
  const bytes = seed === undefined ? this.terrain.entropy.draw(32) : isHex(seed, 32) ? unhex(seed) : null;
184
250
  if (bytes === null || this.holds((await WardKey.from(bytes)).pk))
185
251
  return null;
186
252
  try {
187
- await this.#unpack(name, bytes, invitation);
253
+ await this.#unpack(name, bytes, invitation, undefined, this.#keeper(store));
188
254
  }
189
255
  catch {
190
256
  return null;
@@ -238,11 +304,11 @@ export class Harbor extends Carrier {
238
304
  #dialable(at) {
239
305
  return at.some((address) => this.carriers.some((carrier) => carrier.accepts(address)));
240
306
  }
241
- async #unpack(name, seed, dock, root) {
307
+ async #unpack(name, seed, dock, root, keeper) {
242
308
  const { entropy, clock } = this.terrain;
243
309
  const ward = await Ward.unpack({
244
310
  seed,
245
- memory: await this.#package.rows(name),
311
+ memory: await this.#package.rows(name, keeper),
246
312
  catalogue: this,
247
313
  entropy,
248
314
  clock,
@@ -7,4 +7,5 @@ export { readRootRequest, rootLine, type RootRequest } from './root-line.ts';
7
7
  export { addGround, grounds, probeGround, type GroundProbe, type Probing } from './probe.ts';
8
8
  export { Registry } from './registry.ts';
9
9
  export { Push, PUSH, RELAY, RELAY_CLIENT, RELAY_HOLD, RELAY_REST, RELAY_WAIT, RELAYED } from './relay.ts';
10
+ export { GROUND_STORE, groundStore, Store, STORE } from './store.ts';
10
11
  export { Terrain, type Opening } from './terrain.ts';
@@ -10,4 +10,5 @@ export { readRootRequest, rootLine } from './root-line.js';
10
10
  export { addGround, grounds, probeGround } from './probe.js';
11
11
  export { Registry } from './registry.js';
12
12
  export { Push, PUSH, RELAY, RELAY_CLIENT, RELAY_HOLD, RELAY_REST, RELAY_WAIT, RELAYED } from './relay.js';
13
+ export { GROUND_STORE, groundStore, Store, STORE } from './store.js';
13
14
  export { Terrain } from './terrain.js';
@@ -8,6 +8,7 @@ export type Carried = {
8
8
  export declare const readCarried: (value: unknown) => Carried | null;
9
9
  export declare const carryPlaces: (memory: Memory) => Promise<Carried['places']>;
10
10
  export declare const landPlaces: (memory: Memory, carried: Carried) => Promise<string[]>;
11
+ export type Keeper = Pick<Memory, 'read' | 'write' | 'places' | 'forget'>;
11
12
  export declare class Package {
12
13
  #private;
13
14
  readonly memory: Memory;
@@ -16,5 +17,5 @@ export declare class Package {
16
17
  static open(memory: Memory, entropy: Entropy, seed: Uint8Array): Promise<Package>;
17
18
  derive(info: string[], length: number): Promise<Uint8Array>;
18
19
  place(name: string): Promise<string>;
19
- rows(name: string): Promise<Rows>;
20
+ rows(name: string, keeper?: Keeper): Promise<Rows>;
20
21
  }
@@ -99,12 +99,17 @@ class Ward {
99
99
  #pack;
100
100
  #name;
101
101
  #seal;
102
+ #keeper;
102
103
  place;
103
- constructor(pack, name, seal, place) {
104
+ constructor(pack, name, seal, place, keeper) {
104
105
  this.#pack = pack;
105
106
  this.#name = name;
106
107
  this.#seal = seal;
107
108
  this.place = place;
109
+ this.#keeper = keeper;
110
+ }
111
+ read() {
112
+ return this.#keeper.read(this.place);
108
113
  }
109
114
  entry(row) {
110
115
  return this.#pack.derive(['entry', this.#name, row], 16).then(hex);
@@ -123,7 +128,7 @@ class Ward {
123
128
  return JSON.parse(new TextDecoder().decode(text));
124
129
  }
125
130
  write(entries) {
126
- return this.#pack.memory.write(this.place, entries);
131
+ return this.#keeper.write(this.place, entries);
127
132
  }
128
133
  }
129
134
  export class Package {
@@ -145,13 +150,14 @@ export class Package {
145
150
  place(name) {
146
151
  return this.derive(['place', name], 16).then(hex);
147
152
  }
148
- // One ward's rows, as the package kept them.
149
- async rows(name) {
150
- const ward = new Ward(this, name, await this.derive(['seal', name], 32), hex(await this.derive(['place', name], 16)));
153
+ // One ward's rows, as its keeper kept them: the root memory unless a
154
+ // store is named.
155
+ async rows(name, keeper = this.memory) {
156
+ const ward = new Ward(this, name, await this.derive(['seal', name], 32), await this.place(name), keeper);
151
157
  const rows = {};
152
158
  const beings = {};
153
159
  let any = false;
154
- for (const [entry, bytes] of await this.memory.read(ward.place)) {
160
+ for (const [entry, bytes] of await ward.read()) {
155
161
  const { row, value } = await ward.open(entry, bytes);
156
162
  any = true;
157
163
  if (row === HEAD_ROW)
@@ -8,6 +8,7 @@
8
8
  // a class with no kind of its own, or one outside its module's domain
9
9
  // two classes of one kind, or one kind in two modules
10
10
  // a contract with no kind of its own, or one kind for two contracts
11
+ // a contract of the kit's kinds, save `org.nervur.store`
11
12
  // a kind that is a class and a contract
12
13
  //
13
14
  // A module's domain is its name's first two segments: `com.acme.shop`
@@ -15,7 +16,11 @@
15
16
  // so a faculty fulfils a contract another module declares. Two harbors hold
16
17
  // two registries, and nothing between them names a class.
17
18
  import { Faculty, isKind, KIT, kindOf } from '../being/index.js';
19
+ import { Store } from './store.js';
18
20
  const domainOf = (name) => `${name.split('.').slice(0, 2).join('.')}.`;
21
+ // The kit's contracts a module's class may fulfil: a store keeps a ward's
22
+ // place wherever its author reaches.
23
+ const OPEN = [Store];
19
24
  export class Registry {
20
25
  #classes = new Map();
21
26
  #modules = new Map();
@@ -54,7 +59,7 @@ export class Registry {
54
59
  continue;
55
60
  for (let c = Object.getPrototypeOf(C); c !== Faculty; c = Object.getPrototypeOf(c)) {
56
61
  const contract = kindOf(c);
57
- if (contract.startsWith(KIT))
62
+ if (contract.startsWith(KIT) && !OPEN.includes(c))
58
63
  throw new Error(`kind ${contract} is the kit's`);
59
64
  if (contracts.has(contract) && contracts.get(contract) !== c)
60
65
  throw new Error(`two contracts are of kind ${contract}`);
@@ -0,0 +1,13 @@
1
+ import { Faculty, type BeingClass } from '../being/index.ts';
2
+ import type { Memory } from '../contract/index.ts';
3
+ export declare const STORE = "org.nervur.store";
4
+ export declare const GROUND_STORE = "org.nervur.ground-store";
5
+ export declare abstract class Store extends Faculty {
6
+ static readonly kind: string;
7
+ abstract read(place: string): Promise<Map<string, Uint8Array>>;
8
+ abstract write(place: string, entries: ReadonlyMap<string, Uint8Array | null>): Promise<void>;
9
+ abstract places(): Promise<string[]>;
10
+ abstract forget(place: string): Promise<void>;
11
+ occupies(_place: string): boolean;
12
+ }
13
+ export declare const groundStore: (memory: Memory) => BeingClass;
@@ -0,0 +1,53 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The store: a faculty that keeps a hosted ward's place, where the root
3
+ // memory does not. It makes the same promise the Memory body makes, lent
4
+ // once the dock stands: sealed entries in places, each keep whole or not
5
+ // at all. The box ward never moves off the root memory, since the store
6
+ // itself stands from the box ward's cells.
7
+ //
8
+ // A ward is the unit a keeper holds: its head, its ward-being and every
9
+ // being of it are one place and one keep, so an answer's cells and the
10
+ // door's keys move together, whoever keeps them.
11
+ //
12
+ // The kit ships `org.nervur.ground-store` on every ground: places of the
13
+ // terrain's own memory under the faculty's key, so a store stands wherever
14
+ // a harbor does. A store of a cloud, a database or a ledger is a class
15
+ // fulfilling `org.nervur.store`, handed to the catalogue in a module.
16
+ import { Faculty } from '../being/index.js';
17
+ import { hex, utf8 } from '../crypto/index.js';
18
+ export const STORE = 'org.nervur.store';
19
+ export const GROUND_STORE = 'org.nervur.ground-store';
20
+ export class Store extends Faculty {
21
+ static kind = STORE;
22
+ // Whether a place of the root memory is this store's own, so the root's
23
+ // sweep leaves it.
24
+ occupies(_place) {
25
+ return false;
26
+ }
27
+ }
28
+ // A place of the package is thirty-two hex digits.
29
+ const PLACE = 32;
30
+ // The store over the terrain's memory, made for one harbor. Its places are
31
+ // the package's, behind the hex of the faculty's key, so two stores and the
32
+ // root never meet in one place.
33
+ export const groundStore = (memory) => class GroundStore extends Store {
34
+ static kind = GROUND_STORE;
35
+ get #prefix() {
36
+ return hex(utf8(this.stance.key));
37
+ }
38
+ occupies(place) {
39
+ return place.length === this.#prefix.length + PLACE && place.startsWith(this.#prefix);
40
+ }
41
+ read(place) {
42
+ return memory.read(this.#prefix + place);
43
+ }
44
+ write(place, entries) {
45
+ return memory.write(this.#prefix + place, entries);
46
+ }
47
+ async places() {
48
+ return (await memory.places()).filter((place) => this.occupies(place)).map((place) => place.slice(this.#prefix.length));
49
+ }
50
+ forget(place) {
51
+ return memory.forget(this.#prefix + place);
52
+ }
53
+ };
package/dist/index.d.ts CHANGED
@@ -3,6 +3,6 @@ export type { Answer, Ask, Asker, AskSpec, BeingClass, BeingLike, Blueprint, Inv
3
3
  export { readInvitation } from './quo/index.ts';
4
4
  export { Carrier, Clock, Custody, Entropy, Loader, Memory, type Loaded, type Module, type Rows } from './contract/index.ts';
5
5
  export { CEILING, DEFAULT, DEPTH, ownerAsk, Pilot, pilots, WardBeing, type Steward, type WardBeingStance } from './ward/index.ts';
6
- export { BOX, CarrierFaculty, Catalogue, copyPackage, Dock, Harbor, addGround, grounds, readRootRequest, Registry, Push, PUSH, RELAY, RELAY_CLIENT, rootLine, Terrain, UNDIALED, type Dialed, type Failures, type Ground, type GroundFaculty, type GroundProbe, type Opening, type Probing, type RootRequest, } from './harbor/index.ts';
6
+ export { BOX, CarrierFaculty, Catalogue, copyPackage, Dock, Harbor, addGround, grounds, readRootRequest, Registry, Push, PUSH, RELAY, RELAY_CLIENT, rootLine, Store, STORE, Terrain, UNDIALED, type Dialed, type Failures, type Ground, type GroundFaculty, type GroundProbe, type Opening, type Probing, type RootRequest, } from './harbor/index.ts';
7
7
  export { CryptoEntropy, HeldCustody, HeldLoader, NoCarrier, PointerCarrier, PointerTerrain, SystemClock, VolatileMemory, World, type PointerParts } from './pointer/index.ts';
8
8
  export { answerPost, NEUTRAL, WEB, WEB_OPENING, WEB_PUSH, WebDialer, webFaculty, webPush, type WebListen, type WebListener, type WebServe } from './line/index.ts';
package/dist/index.js CHANGED
@@ -12,7 +12,7 @@ export { Carrier, Clock, Custody, Entropy, Loader, Memory } from './contract/ind
12
12
  // Wards and the ward-being.
13
13
  export { CEILING, DEFAULT, DEPTH, ownerAsk, Pilot, pilots, WardBeing } from './ward/index.js';
14
14
  // The onion, and the one line its root's asks cross from any front.
15
- export { BOX, CarrierFaculty, Catalogue, copyPackage, Dock, Harbor, addGround, grounds, readRootRequest, Registry, Push, PUSH, RELAY, RELAY_CLIENT, rootLine, Terrain, UNDIALED, } from './harbor/index.js';
15
+ export { BOX, CarrierFaculty, Catalogue, copyPackage, Dock, Harbor, addGround, grounds, readRootRequest, Registry, Push, PUSH, RELAY, RELAY_CLIENT, rootLine, Store, STORE, Terrain, UNDIALED, } from './harbor/index.js';
16
16
  // The pointer dock: a whole world in one process.
17
17
  export { CryptoEntropy, HeldCustody, HeldLoader, NoCarrier, PointerCarrier, PointerTerrain, SystemClock, VolatileMemory, World } from './pointer/index.js';
18
18
  export { answerPost, NEUTRAL, WEB, WEB_OPENING, WEB_PUSH, WebDialer, webFaculty, webPush } from './line/index.js';