@quo-systems/quo 0.2.15 → 0.2.17

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.
@@ -0,0 +1,128 @@
1
+ # Getting started
2
+
3
+ Quo lets an object ask another object and get an answer, without knowing
4
+ whether that other object is in the same process, on the same device, or
5
+ on another planet. Three words: a harbor boots wards, a ward keeps beings
6
+ and judges its door, and a being is one ordinary object with one voice.
7
+ This is the shortest road from nothing to each of the three, for a
8
+ stranger with a terminal. `SPEC.md` is the truth behind every sentence
9
+ here and assumes nothing; `quo-dock.md` is the dock, the part a box runs.
10
+
11
+ Two packages, and you start with the one that fits what you have:
12
+
13
+ - `@quo-systems/quo`, the library. A harbor, a ward and a being in one
14
+ process, no wire, no files. For a program that wants Quo inside it.
15
+ - `@quo-systems/dock`, the dock. A daemon and one command, `quo`, that
16
+ stand a box up: a person's world with a page, a model's side, an api, a
17
+ door on the wire. For a box that receives people and models.
18
+
19
+ ## A being, in one process
20
+
21
+ ```bash
22
+ npm install @quo-systems/quo
23
+ ```
24
+
25
+ A being is a class with `asks`, the methods anyone may reach, each with
26
+ the JSON schema of its input. Everything else on the class is hers alone.
27
+
28
+ ```js
29
+ import { Being } from '@quo-systems/quo';
30
+ import { Ward } from '@quo-systems/quo/ward';
31
+ import { MemoryHarbor } from '@quo-systems/quo/harbor';
32
+
33
+ class Shop extends Being {
34
+ static asks = { price: { input: { type: 'object', properties: { item: { type: 'string' } } } } };
35
+ price({ item }) {
36
+ return { item, eur: 12 };
37
+ }
38
+ }
39
+ class Customer extends Being {
40
+ static asks = {};
41
+ }
42
+
43
+ const harbor = new MemoryHarbor();
44
+ const ward = await harbor.boot('acme', Ward, { Shop, Customer });
45
+ await ward.ask('boot', { key: 'shop', class: 'Shop' });
46
+ await ward.ask('boot', { key: 'ana', class: 'Customer' });
47
+ ```
48
+
49
+ The harbor booted a ward named `acme` with two classes it may make beings
50
+ of, and the ward's owner, the process itself, booted one of each by key.
51
+ The ward's own asks are seven, `boot`, `public`, `invite`, `knock`,
52
+ `remove`, `unboot` and `ask`, and `ward.ask()` with no method is her
53
+ describe: those asks and her `notes`, the ward's `pk` and her beings.
54
+
55
+ Nobody reaches a being she has not invited. An invitation is minted on a
56
+ being for one id, the shop's for `ana`, and the customer knocks with it;
57
+ from then she holds a standing at the shop, under the name she took, and
58
+ the shop holds her as an occupant.
59
+
60
+ ```js
61
+ const invitation = await ward.ask('invite', { being: 'shop', id: 'ana' });
62
+ const ana = harbor.objects.get(harbor.partitions.get('acme').beings.ana);
63
+ await ana.knock(invitation);
64
+ await ana.take('shop', invitation);
65
+ console.log(await ana.standings.shop.ask('price', { item: 'bread' }));
66
+ // { item: 'bread', eur: 12 }
67
+ ```
68
+
69
+ That is the whole protocol: a standing on one side, an occupant on the
70
+ other, an ask that rides the relation and an answer that rides it back.
71
+ The two beings here share a process; the same lines hold when the shop is
72
+ on a box across the sea, because a standing is an address and a key, and
73
+ the harbor owns the wire. `harbor.objects` is the memory harbor's hand for
74
+ a test and a first program; a being on a real box is reached through her
75
+ ward, never held.
76
+
77
+ ## A box
78
+
79
+ ```bash
80
+ npm install @quo-systems/dock
81
+ npx quo init --dir ~/.quo --ward acme --user razvan --domain acme.com --default --show
82
+ npx quo serve --dir ~/.quo --http 8787
83
+ ```
84
+
85
+ `init` mints the ward `acme`, boots razvan's user being, her doorbell and
86
+ the desk in it, marks it the default ward and shows it at the web route,
87
+ and writes `routes.json`, the four routes of a box, `mcp.`, `web.`, `quo.`
88
+ and `api.` under the domain, for a proxy to map onto the one loopback
89
+ port. Without `--domain`, write it yourself; on a Mac the four are paths
90
+ at one loopback address:
91
+
92
+ ```json
93
+ { "mcp": "http://127.0.0.1:8787/mcp", "web": "http://127.0.0.1:8787/web", "quo": "http://127.0.0.1:8787/quo", "api": "http://127.0.0.1:8787/api" }
94
+ ```
95
+
96
+ `serve` is the daemon, the one process over that folder, and every other
97
+ command is its client. From a second terminal, a phone:
98
+
99
+ ```bash
100
+ npx quo invite --dir ~/.quo '{"being":"razvan","id":"phone"}'
101
+ ```
102
+
103
+ The answer carries `link`, the ward's page with the invitation in its
104
+ fragment. Opened on the phone, the tab boots a harbor of its own, joins as
105
+ that device, and is in: no account, nothing typed. What a model gets is
106
+ the same world as tools at `mcp.`, what a program gets is the same asks as
107
+ JSON at `api.`, and what another box gets is the door at `quo.`; a being
108
+ answers each the same, because each is an ask at her door.
109
+
110
+ The box's own doings are rows on the faculties of its dock ward, placed by
111
+ the root with `quo ask --ward dock`: a socket held to another box, an
112
+ agent run for a ward, a schedule on the clock. A second box owns this one
113
+ across the wire with an invitation on the ward's own pk, knocked with
114
+ from there, and every owner command with `--via` from then on. Each of
115
+ those, the edge, and what an estate stands beyond the init, is the
116
+ "Getting started" chapter of `quo-dock.md`, proven cold on a Mac by
117
+ somebody with nothing else to read.
118
+
119
+ ## Your own beings on a box
120
+
121
+ A box holds the dock's classes and yours. `classes/index.ts` beside the
122
+ wards exports each of yours by name, and `quo boot '{"key":"shop",
123
+ "class":"Shop"}'` boots one; `quo init --class Shop` makes the ward's home
124
+ being one of yours, an organisation's ward with the org as its being. A
125
+ being's asks may name who may reach them, `for`, over the record of the
126
+ occupant asking, and her `cells` are what she keeps between boots: the
127
+ whole of what a being is, in `SPEC.md`, and the words above the spec, a
128
+ world, a home, a membership, in `WORLDS.md` and `GLOSSARY.md`.
package/README.md CHANGED
@@ -28,18 +28,19 @@ no dependencies. The package ships JavaScript with declarations, emitted by
28
28
  types nowhere under `node_modules`.
29
29
 
30
30
  ```
31
+ protocol/ the shelf a kit in any language reads: SPEC.md and the vectors, no code
32
+ protocol/vectors/ fixed inputs and outputs, so another language proves its bytes
31
33
  src/being/ the Being side: what a being author imports, if anything
32
34
  src/ward/ the ward: the Ground contract, door, seal, arithmetic, heirs, stance, allowance
33
35
  src/harbor/ MemoryHarbor, and the harbor core with its store, reach and dialer
34
36
  src/conformance/ behaviours any ward must show, written against the truth
35
37
  test/ the suites
36
- vectors/ fixed inputs and outputs, so another language proves its bytes
37
38
  ```
38
39
 
39
40
  ## Requirements
40
41
 
41
- Node 22.18 or later for `npm run check`. The terrain gate,
42
- `npm run check:terrain`, runs the same conformance suite out of a bundle in a
42
+ Node 22.18 or later for `npm run check`. The deep gate,
43
+ `npm run deep:quo`, runs the same conformance suite out of a bundle in a
43
44
  real Chromium, in workerd, in Deno and in Bun. All four binaries come with
44
45
  `npm install`, except the browser itself: run `npx playwright install
45
46
  chromium` once. A terrain whose binary is missing skips and says so.
@@ -53,12 +54,13 @@ and runs from there:
53
54
  npm run check
54
55
  ```
55
56
 
56
- Build, typecheck, lint, then the suites of every package. The pieces run
57
- alone there too: `npm run typecheck`, `npm run lint` (`lint:ts` is oxlint
58
- with type-aware rules, `lint:md` is markdownlint), `npm run lint:fix`, and
59
- `npm test`. Inside this package three scripts exist and no more: `npm test`
60
- runs the suites under `test/`, `npm run build` emits `dist/`, and
61
- `npm run check:terrain` runs the terrains.
57
+ Build, typecheck, lint, then one `check:<layer>` for every layer of that
58
+ repository. The pieces run alone there too: `npm run typecheck`,
59
+ `npm run lint` (`lint:ts` is oxlint with type-aware rules, `lint:md` is
60
+ markdownlint), `npm run lint:fix`, and `npm run check:quo`, which is this
61
+ package. Inside this package three scripts exist and no more:
62
+ `npm run check` runs the suites under `test/`, `npm run build` emits
63
+ `dist/`, and `npm run deep` runs the terrains.
62
64
 
63
65
  The toolchain is TypeScript 7, the native compiler, and oxlint, which is the
64
66
  linter built for it. Both are in Rust or Go rather than JavaScript, so the
@@ -77,28 +79,38 @@ import { conform } from '@quo-systems/quo/conformance';
77
79
 
78
80
  ## Another language
79
81
 
80
- The hand to a kit in another language is two things of two kinds. The
81
- vectors are the byte-level hand, everything a stranger can observe:
82
- `vectors/arithmetic.json`, the primitives the seal rests on, SHA-256,
83
- Ed25519, X25519, HKDF and AES-256-GCM; `vectors/framing.json`, Quo's own,
84
- the ward pk, the digest, the signed ask body, the sealed shapes, the
85
- invitation and the knock; `vectors/wire.json`, the frames on a socket and
86
- the one request a door takes. They import by name,
87
- `@quo-systems/quo/vectors/framing.json`, so a kit's own suite can read them
88
- from the package. A kit reproduces them or it is not this
89
- protocol. `src/conformance/` is the behavioural hand, and it is a checklist
90
- a kit ports, not a harness it runs: the beings it is shown with run only in
91
- a TypeScript ward. `quo-kit.md` "The tree" says the whole of it.
82
+ The hand to a kit in another language is one folder, `protocol/`, and it
83
+ holds two things of two kinds. `SPEC.md` is the protocol, and it assumes
84
+ nothing: a kit is written against it and against nothing else here. The
85
+ vectors beside it are the byte-level hand, everything a stranger can
86
+ observe: `protocol/vectors/arithmetic.json`, the primitives the seal rests
87
+ on, SHA-256, Ed25519, X25519, HKDF and AES-256-GCM;
88
+ `protocol/vectors/framing.json`, Quo's own, the ward pk, the digest, the
89
+ signed ask body, the sealed shapes, the invitation and the knock;
90
+ `protocol/vectors/wire.json`, the frames on a socket and the one request a
91
+ door takes; and `protocol/vectors/door.json`, the door's thirteen cases,
92
+ each one an arrival a ward will not answer, with the bytes that arrive, the
93
+ bytes that leave and the partition's digest on both sides of the judgement.
94
+ They import by name,
95
+ `@quo-systems/quo/protocol/vectors/framing.json`, so a kit's own suite can
96
+ read them from the package. A kit reproduces them or it is not this
97
+ protocol.
98
+
99
+ Nothing outside that folder is the protocol. `src/` is this kit's
100
+ interpretation, and `src/conformance/` is a checklist a kit ports rather
101
+ than a harness it runs: it imports the base class, the silence spelling and
102
+ this kit's store and reach, so the beings it is shown with run only in a
103
+ TypeScript ward. `quo-kit.md` "The three shelves" says which is which.
92
104
 
93
105
  ## Publishing
94
106
 
95
107
  Published from 0.1.0, the first version under this name, with no
96
108
  compatibility promise before 1.0.0: the words may still move. `npm pack
97
- --dry-run` shows what ships: the emitted `dist/`, the source, the vectors,
98
- the spec, this file, the licence and the notice, and no tests and no
99
- configs. The spec ships because it is the truth the source and the vectors
100
- are read against. Publishing is gated from the root of the repository:
101
- `npm run release:quo` there runs both gates, `check` and `check:terrain`,
109
+ --dry-run` shows what ships: the emitted `dist/`, the source, the protocol
110
+ shelf with the spec inside it, this file, the licence and the notice, and no
111
+ tests and no configs. The spec ships because it is the truth the source and
112
+ the vectors are read against. Publishing is gated from the root of the repository:
113
+ `npm run release:quo` there runs both gates, `check` and `deep`,
102
114
  and publishes on green; `npm publish` inside this package refuses and says
103
115
  so. This package carries the version of its own work and is bound to no
104
116
  other's: a number equal to another package's is a coincidence.
@@ -6,7 +6,15 @@
6
6
  // what it put and expects it back, as the harbor will.
7
7
  import { assert } from './assert.js';
8
8
  const seed = (b) => new Uint8Array(32).fill(b);
9
- const kept = (b, extra = {}) => ({ seed: seed(b), partition: { beings: {}, ...extra }, record: { pk: '', code: 'classes/index.ts', user: 'me' } });
9
+ const kept = (b, extra = {}) => ({ seed: seed(b), partition: { beings: {}, bind: {}, ...extra }, record: { pk: '', code: 'classes/index.ts', user: 'me' } });
10
+ // A partition as a ward writes one: beings under their keys, a bind table
11
+ // beside each, and whatever else the head carries.
12
+ const ward = (beings, extra = {}) => ({
13
+ beings,
14
+ bind: Object.fromEntries(Object.keys(beings).map((k) => [k, { standings: {} }])),
15
+ ...extra,
16
+ });
17
+ const ALL = (p) => ['', ...Object.keys(p.beings)];
10
18
  export function conformStore(label, make, { test }) {
11
19
  const t = (name, fn) => test(`[${label}] ${name}`, {}, fn);
12
20
  t('a fresh store keeps nothing', async () => {
@@ -23,7 +31,7 @@ export function conformStore(label, make, { test }) {
23
31
  const back = await s.load('main');
24
32
  assert.ok(back);
25
33
  assert.deepEqual(back.seed, seed(7));
26
- assert.deepEqual(back.partition, { beings: {}, n: 1, deep: { list: [1, 'two', null], flag: true } });
34
+ assert.deepEqual(back.partition, { beings: {}, bind: {}, n: 1, deep: { list: [1, 'two', null], flag: true } });
27
35
  assert.deepEqual(back.record, { pk: '', code: 'classes/index.ts', user: 'me' });
28
36
  });
29
37
  t('a name already kept is refused, and what was kept stands', async () => {
@@ -39,26 +47,44 @@ export function conformStore(label, make, { test }) {
39
47
  assert.ok(refused, 'a second put under one name must throw');
40
48
  assert.deepEqual((await s.load('main')).seed, seed(1));
41
49
  });
42
- t('save replaces the partition and touches nothing else', async () => {
50
+ t('save writes every row it is given, and touches nothing else', async () => {
43
51
  const s = await make();
44
52
  await s.put('main', kept(3));
45
- await s.save('main', { beings: { a: { x: 1 } }, bind: {} });
53
+ const p = ward({ a: { x: 1 } });
54
+ await s.save('main', p, ALL(p));
46
55
  const back = (await s.load('main'));
47
- assert.deepEqual(back.partition, { beings: { a: { x: 1 } }, bind: {} });
56
+ assert.deepEqual(back.partition, p);
48
57
  assert.deepEqual(back.seed, seed(3));
49
58
  assert.deepEqual(back.record, kept(3).record);
50
59
  });
60
+ // What a store may do with the rows it is told about is its own: writing
61
+ // the whole partition every time is correct. What it may never do is write
62
+ // less than it was told, or keep a being the rows say is gone.
63
+ t('save writes the rows it names, and a being the rows drop is gone', async () => {
64
+ const s = await make();
65
+ const p = ward({ a: { x: 1 }, b: { y: 1 } });
66
+ await s.put('main', { ...kept(3), partition: p });
67
+ // one being moves, and only her row is named
68
+ p.beings.a.x = 2;
69
+ await s.save('main', p, ['a']);
70
+ assert.deepEqual((await s.load('main')).partition, p, 'the row that was named is written');
71
+ // she leaves: her row is named and her key is gone from the partition
72
+ delete p.beings.a;
73
+ delete p.bind.a;
74
+ await s.save('main', p, ['a']);
75
+ assert.deepEqual((await s.load('main')).partition, p, 'a row whose being is gone is dropped');
76
+ });
51
77
  t('record replaces the record and touches nothing else', async () => {
52
78
  const s = await make();
53
79
  await s.put('main', kept(4, { n: 9 }));
54
80
  await s.record('main', { pk: 'ab'.repeat(64), code: 'elsewhere.ts', user: 'her' });
55
81
  const back = (await s.load('main'));
56
82
  assert.deepEqual(back.record, { pk: 'ab'.repeat(64), code: 'elsewhere.ts', user: 'her' });
57
- assert.deepEqual(back.partition, { beings: {}, n: 9 });
83
+ assert.deepEqual(back.partition, { beings: {}, bind: {}, n: 9 });
58
84
  });
59
85
  t('save and record on a name not kept are nothing', async () => {
60
86
  const s = await make();
61
- await s.save('ghost', { beings: {} });
87
+ await s.save('ghost', { beings: {}, bind: {} }, ['']);
62
88
  await s.record('ghost', { pk: '', code: '', user: '' });
63
89
  assert.deepEqual(await s.list(), []);
64
90
  });
@@ -67,10 +93,10 @@ export function conformStore(label, make, { test }) {
67
93
  const k = kept(5, { n: 1 });
68
94
  await s.put('main', k);
69
95
  k.partition.n = 2;
70
- assert.deepEqual((await s.load('main')).partition, { beings: {}, n: 1 });
96
+ assert.deepEqual((await s.load('main')).partition, { beings: {}, bind: {}, n: 1 });
71
97
  const a = (await s.load('main'));
72
98
  a.partition.n = 3;
73
- assert.deepEqual((await s.load('main')).partition, { beings: {}, n: 1 });
99
+ assert.deepEqual((await s.load('main')).partition, { beings: {}, bind: {}, n: 1 });
74
100
  });
75
101
  t('take hands the ward out and forgets it; the name is free again', async () => {
76
102
  const s = await make();
@@ -79,7 +105,7 @@ export function conformStore(label, make, { test }) {
79
105
  const out = await s.take('main');
80
106
  assert.ok(out);
81
107
  assert.deepEqual(out.seed, seed(6));
82
- assert.deepEqual(out.partition, { beings: {}, n: 6 });
108
+ assert.deepEqual(out.partition, { beings: {}, bind: {}, n: 6 });
83
109
  assert.equal(await s.load('main'), undefined);
84
110
  assert.deepEqual(await s.list(), ['other']);
85
111
  await s.put('main', kept(9));
@@ -119,13 +119,13 @@ export class Harbor {
119
119
  return this.#saving.get(name)?.memory;
120
120
  }
121
121
  // ---- saving
122
- // The ward wrote. Its partition is saved once the line is free, and once
123
- // for every burst of writes, in the order the writes came.
124
- #wrote(name) {
122
+ // The ward wrote one row. Its partition is saved once the line is free, and
123
+ // once for every burst of writes, with every row named in that burst.
124
+ #wrote(name, row) {
125
125
  const s = this.#saving.get(name);
126
126
  if (!s || s.gone)
127
127
  return;
128
- s.dirty = true;
128
+ s.dirty.add(row);
129
129
  if (s.timer === undefined)
130
130
  s.timer = setTimeout(() => void this.#flush(name), 0);
131
131
  }
@@ -140,10 +140,13 @@ export class Harbor {
140
140
  if (s.running)
141
141
  return s.running;
142
142
  s.running = (async () => {
143
- while (s.dirty && !s.gone) {
144
- s.dirty = false;
143
+ while (s.dirty.size > 0 && !s.gone) {
144
+ // Taken before the write, so a row named while this one is in flight
145
+ // is written by the next turn of the loop and not lost with it.
146
+ const rows = [...s.dirty];
147
+ s.dirty.clear();
145
148
  try {
146
- await this.store.save(name, s.memory);
149
+ await this.store.save(name, s.memory, rows);
147
150
  }
148
151
  catch {
149
152
  this.faults.set(name, (this.faults.get(name) ?? 0) + 1);
@@ -332,12 +335,12 @@ export class Harbor {
332
335
  instantiate: maker(objects, classes, this.classes),
333
336
  carry: (pk, bytes) => this.carry(pk, new Uint8Array(bytes)),
334
337
  random,
335
- wrote: () => this.#wrote(name),
338
+ wrote: (row) => this.#wrote(name, row),
336
339
  };
337
340
  const lend = this.lendFor(name, kept.record);
338
341
  if (lend)
339
342
  ground.lend = lend;
340
- const saving = { memory, dirty: false, running: null, timer: undefined, gone: false };
343
+ const saving = { memory, dirty: new Set(), running: null, timer: undefined, gone: false };
341
344
  this.#saving.set(name, saving);
342
345
  // A ward that will not be born leaves nothing behind: a partition of a
343
346
  // shape this kit cannot read throws here, and a name the harbor does not
@@ -1,7 +1,8 @@
1
1
  export { MemoryHarbor, type Booted, type WardFactory } from './memory.ts';
2
2
  export { Harbor, DEFAULT_CODE, type Hosted, type Bound, type Loader } from './core.ts';
3
3
  export { dial, type Dialer } from './dial.ts';
4
- export { MemoryStore, values, type Store, type Kept, type WardRecord } from './store.ts';
4
+ export { MemoryStore, values, rowsOf, type Store, type Kept, type WardRecord } from './store.ts';
5
+ export { HEAD, fromRows, rowsIn } from '../ward/partition.ts';
5
6
  export { request, Socket, SUITE, REFUSED, type Reach, type Carry, type Line, type Announce } from './reach.ts';
6
7
  export type { Ground, WardPointers, Lend } from '../ward/ground.ts';
7
8
  export { maker, entropy, learnPk } from '../ward/ground.ts';
@@ -6,7 +6,10 @@
6
6
  export { MemoryHarbor } from './memory.js';
7
7
  export { Harbor, DEFAULT_CODE } from './core.js';
8
8
  export { dial } from './dial.js';
9
- export { MemoryStore, values } from './store.js';
9
+ export { MemoryStore, values, rowsOf } from './store.js';
10
+ // What a store keeping a ward in rows needs: the name of the row that is not
11
+ // a being, and the partition back from the rows it was cut into.
12
+ export { HEAD, fromRows, rowsIn } from '../ward/partition.js';
10
13
  export { request, Socket, SUITE, REFUSED } from './reach.js';
11
14
  // What a harbor builds a ground out of. Convenience, never contract: a kit
12
15
  // writing its own harbor may write these three again.
@@ -17,6 +17,7 @@ export declare class MemoryHarbor {
17
17
  readonly partitions: Map<string, Record<string, unknown>>;
18
18
  readonly wards: Map<string, Booted>;
19
19
  readonly objects: WeakMap<object, BeingLike>;
20
+ random: Ground['random'];
20
21
  route(farPk: string, bytes: Uint8Array): Promise<Uint8Array | undefined>;
21
22
  link(harbor: MemoryHarbor): void;
22
23
  boot(seed: string, Ward: WardFactory, classes: Record<string, BeingClass>, lend?: Lend): Promise<Booted>;
@@ -10,6 +10,12 @@ export class MemoryHarbor {
10
10
  partitions = new Map(); // seed -> memory. the harbor keeps it and reads nothing
11
11
  wards = new Map(); // seed -> pointers
12
12
  objects = new WeakMap(); // cells -> being object. what instantiate constructed. a hand for tests, never the ward's; it follows the cells out when she is unbooted
13
+ // Where the wards this harbor boots draw their entropy. The device's own
14
+ // by default. A harbor may hand a fixed stream instead, which is what pins
15
+ // bytes: every key a ward mints and every nonce it seals under comes from
16
+ // here, so a ward on a known seed with a known stream answers the same
17
+ // bytes every run, and a corpus of them is a corpus and not a sample.
18
+ random = entropy;
13
19
  // the directory: its own doors, else a peer it is linked to. Quo says nothing about how.
14
20
  async route(farPk, bytes) {
15
21
  if (this.down.has(farPk))
@@ -55,7 +61,7 @@ export class MemoryHarbor {
55
61
  const back = await this.route(farPk, new Uint8Array(bytes));
56
62
  return back === undefined ? undefined : new Uint8Array(back);
57
63
  },
58
- random: entropy,
64
+ random: (n) => this.random(n),
59
65
  };
60
66
  const w = await Ward(ground);
61
67
  const booted = { ...w, pk: await learnPk(w) };
@@ -12,22 +12,29 @@ export type Store = {
12
12
  list(): Promise<string[]>;
13
13
  load(name: string): Promise<Kept | undefined>;
14
14
  put(name: string, kept: Kept): Promise<void>;
15
- save(name: string, partition: Record<string, unknown>): Promise<void>;
15
+ save(name: string, partition: Record<string, unknown>, rows: readonly string[]): Promise<void>;
16
16
  record(name: string, record: WardRecord): Promise<void>;
17
17
  take(name: string): Promise<Kept | undefined>;
18
18
  hints(): Promise<Record<string, string>>;
19
19
  hint(pk: string, url: string): Promise<void>;
20
20
  };
21
21
  export declare const values: (p: Record<string, unknown>) => Record<string, unknown>;
22
+ export declare const rowsOf: (partition: Record<string, unknown>, rows: readonly string[]) => [string, Record<string, unknown> | undefined][];
23
+ type Row = {
24
+ seed: Uint8Array;
25
+ parts: Map<string, Record<string, unknown>>;
26
+ record: WardRecord;
27
+ };
22
28
  export declare class MemoryStore implements Store {
23
- readonly rows: Map<string, Kept>;
29
+ readonly rows: Map<string, Row>;
24
30
  readonly reach: Map<string, string>;
25
31
  list(): Promise<string[]>;
26
32
  load(name: string): Promise<Kept | undefined>;
27
33
  put(name: string, kept: Kept): Promise<void>;
28
- save(name: string, partition: Record<string, unknown>): Promise<void>;
34
+ save(name: string, partition: Record<string, unknown>, rows: readonly string[]): Promise<void>;
29
35
  record(name: string, record: WardRecord): Promise<void>;
30
36
  take(name: string): Promise<Kept | undefined>;
31
37
  hints(): Promise<Record<string, string>>;
32
38
  hint(pk: string, url: string): Promise<void>;
33
39
  }
40
+ export {};
@@ -1,7 +1,34 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The store: keeping wards by name. A ward is three things and the store
3
+ // keeps all three under one name: the seed, the partition and the ward
4
+ // record, which says where the class bodies come from and which being is
5
+ // the user's. Plus the directory's hints, which are the harbor's and
6
+ // survive a restart. The store reads nothing it keeps; the partition is
7
+ // values the ward wrote, and the seed is bytes only the ward derives from.
8
+ // Every terrain has one: files on a disk, IndexedDB in a tab, an object's
9
+ // storage on the edge; the memory store below is the library's own, for a
10
+ // harbor core with no device under it. `src/conformance/store.ts` is what
11
+ // every one of them passes.
12
+ import { fromRows, rowOf, rowsIn } from '../ward/partition.js';
1
13
  // The partition is values only, and the ward hands it out through a guard,
2
14
  // so a store takes a copy of the values the way a file would: through JSON.
3
15
  export const values = (p) => JSON.parse(JSON.stringify(p));
4
- // The store as a map, one process, nothing kept past it.
16
+ // The rows a store was told about, as values, ready to be written down. A row
17
+ // whose key names nobody is a being who left, and comes back undefined, which
18
+ // is the store dropping what it holds for her.
19
+ export const rowsOf = (partition, rows) => rows.map((row) => {
20
+ const value = rowOf(partition, row);
21
+ return [row, value === undefined ? undefined : values(value)];
22
+ });
23
+ // One store's copy, brought up to date with the rows it was told about.
24
+ const keep = (row, partition, rows) => {
25
+ for (const [id, value] of rowsOf(partition, rows)) {
26
+ if (value === undefined)
27
+ row.parts.delete(id);
28
+ else
29
+ row.parts.set(id, value);
30
+ }
31
+ };
5
32
  export class MemoryStore {
6
33
  rows = new Map();
7
34
  reach = new Map();
@@ -10,17 +37,19 @@ export class MemoryStore {
10
37
  }
11
38
  async load(name) {
12
39
  const row = this.rows.get(name);
13
- return row && { seed: new Uint8Array(row.seed), partition: values(row.partition), record: { ...row.record } };
40
+ return row && { seed: new Uint8Array(row.seed), partition: fromRows(Object.fromEntries(row.parts)), record: { ...row.record } };
14
41
  }
15
42
  async put(name, kept) {
16
43
  if (this.rows.has(name))
17
44
  throw new Error(`ward ${name} already exists here`);
18
- this.rows.set(name, { seed: new Uint8Array(kept.seed), partition: values(kept.partition), record: { ...kept.record } });
45
+ const row = { seed: new Uint8Array(kept.seed), parts: new Map(), record: { ...kept.record } };
46
+ this.rows.set(name, row);
47
+ keep(row, kept.partition, rowsIn(kept.partition));
19
48
  }
20
- async save(name, partition) {
49
+ async save(name, partition, rows) {
21
50
  const row = this.rows.get(name);
22
51
  if (row)
23
- row.partition = values(partition);
52
+ keep(row, partition, rows);
24
53
  }
25
54
  async record(name, record) {
26
55
  const row = this.rows.get(name);
@@ -7,6 +7,10 @@ export declare function unhex(text: string): Uint8Array;
7
7
  export declare function concat(parts: Uint8Array[]): Uint8Array;
8
8
  export declare function sameBytes(a: Uint8Array, b: Uint8Array): boolean;
9
9
  export declare const smallOrder: (pk: Uint8Array) => boolean;
10
+ export declare const heldKeys: () => {
11
+ held: number;
12
+ bound: number;
13
+ };
10
14
  export declare function sha256(...parts: Uint8Array[]): Promise<Uint8Array>;
11
15
  export type Pair = {
12
16
  secret: Uint8Array;