@quo-systems/quo 0.2.16 → 0.2.18

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
@@ -13,9 +13,10 @@ Nothing else is Quo.
13
13
  beings and judges its door.
14
14
  - **Being.** One ordinary object, one voice.
15
15
 
16
- [`SPEC.md`](SPEC.md) is the truth. It is the protocol alone: what any ward in
17
- any language must do for its bytes to be Quo. It is self-contained and
18
- assumes nothing from any other document, this README included. Read it first.
16
+ [`protocol/SPEC.md`](protocol/SPEC.md) is the truth. It is the protocol
17
+ alone: what any ward in any language must do for its bytes to be Quo. It is
18
+ self-contained and assumes nothing from any other document, this README
19
+ included. Read it first.
19
20
  [`quo-kit.md`](quo-kit.md) is this kit: what one TypeScript
20
21
  implementation chose and another kit may refuse. It assumes the spec, and
21
22
  where the two disagree the spec wins.
@@ -39,8 +40,8 @@ test/ the suites
39
40
 
40
41
  ## Requirements
41
42
 
42
- Node 22.18 or later for `npm run check`. The terrain gate,
43
- `npm run check:terrain`, runs the same conformance suite out of a bundle in a
43
+ Node 22.18 or later for `npm run check`. The deep gate,
44
+ `npm run deep:quo`, runs the same conformance suite out of a bundle in a
44
45
  real Chromium, in workerd, in Deno and in Bun. All four binaries come with
45
46
  `npm install`, except the browser itself: run `npx playwright install
46
47
  chromium` once. A terrain whose binary is missing skips and says so.
@@ -54,12 +55,13 @@ and runs from there:
54
55
  npm run check
55
56
  ```
56
57
 
57
- Build, typecheck, lint, then the suites of every package. The pieces run
58
- alone there too: `npm run typecheck`, `npm run lint` (`lint:ts` is oxlint
59
- with type-aware rules, `lint:md` is markdownlint), `npm run lint:fix`, and
60
- `npm test`. Inside this package three scripts exist and no more: `npm test`
61
- runs the suites under `test/`, `npm run build` emits `dist/`, and
62
- `npm run check:terrain` runs the terrains.
58
+ Build, typecheck, lint, then one `check:<layer>` for every layer of that
59
+ repository. The pieces run alone there too: `npm run typecheck`,
60
+ `npm run lint` (`lint:ts` is oxlint with type-aware rules, `lint:md` is
61
+ markdownlint), `npm run lint:fix`, and `npm run check:quo`, which is this
62
+ package. Inside this package three scripts exist and no more:
63
+ `npm run check` runs the suites under `test/`, `npm run build` emits
64
+ `dist/`, and `npm run deep` runs the terrains.
63
65
 
64
66
  The toolchain is TypeScript 7, the native compiler, and oxlint, which is the
65
67
  linter built for it. Both are in Rust or Go rather than JavaScript, so the
@@ -109,7 +111,7 @@ compatibility promise before 1.0.0: the words may still move. `npm pack
109
111
  shelf with the spec inside it, this file, the licence and the notice, and no
110
112
  tests and no configs. The spec ships because it is the truth the source and
111
113
  the vectors are read against. Publishing is gated from the root of the repository:
112
- `npm run release:quo` there runs both gates, `check` and `check:terrain`,
114
+ `npm run release:quo` there runs both gates, `check` and `deep`,
113
115
  and publishes on green; `npm publish` inside this package refuses and says
114
116
  so. This package carries the version of its own work and is bound to no
115
117
  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.
@@ -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);
@@ -4,7 +4,7 @@ export type Ground = {
4
4
  seed: string | Uint8Array;
5
5
  memory: Record<string, unknown>;
6
6
  instantiate(className: string, stance: Stance): BeingLike | null;
7
- wrote?: () => void;
7
+ wrote?: (row: string) => void;
8
8
  carry(pk: string, bytes: Uint8Array): Promise<Uint8Array | undefined>;
9
9
  random(n: number): Uint8Array;
10
10
  lend?: Lend;
@@ -58,3 +58,7 @@ export declare const put: <T>(rec: Record<string, T>, key: string, value: T) =>
58
58
  export declare const drop: (rec: Record<string, unknown>, key: string) => void;
59
59
  export declare const emptyCells: () => Cells;
60
60
  export declare const emptyBind: () => Bind;
61
+ export declare const HEAD = "";
62
+ export declare const rowOf: (p: Partition, row: string) => Record<string, unknown> | undefined;
63
+ export declare const rowsIn: (p: Record<string, unknown>) => string[];
64
+ export declare const fromRows: (rows: Record<string, Record<string, unknown>>) => Record<string, unknown>;
@@ -167,3 +167,56 @@ export const drop = (rec, key) => {
167
167
  };
168
168
  export const emptyCells = () => ({ standings: {}, occupants: {} });
169
169
  export const emptyBind = () => ({ standings: {}, occupants: {}, knocks: {}, answered: {}, minted: [] });
170
+ // ---- the partition in rows, for a store that keeps it
171
+ //
172
+ // A ward hands a harbor one object, so a being who writes one cell has every
173
+ // other being in her ward written down beside her. What a being holds is the
174
+ // one thing here that grows without limit, so she is a row: her cells and her
175
+ // bind table, under her key. Everything else is one head row, and every part
176
+ // of it is bounded already, one record per occupant and two lists that drop
177
+ // their oldest.
178
+ //
179
+ // The split is here, where the shape is, and not in the harbor, which keeps
180
+ // what it is given and reads none of it. A store is handed the rows that
181
+ // moved; what it does with that is its own, and one that writes every row is
182
+ // as correct as one that writes the few.
183
+ export const HEAD = ''; // the row that is not a being: no being's key is empty
184
+ // One row's value: a being, or the head, which is everything the partition
185
+ // holds that is not filed under a being. The head is read as what is left
186
+ // rather than as a list of names, so a store keeps whatever it was handed and
187
+ // hands the same thing back, and a field this file grows later needs no line
188
+ // in a store to survive a round trip.
189
+ //
190
+ // Undefined where the key names nobody, which is a row the store drops.
191
+ export const rowOf = (p, row) => {
192
+ const all = p;
193
+ if (row === HEAD)
194
+ return Object.fromEntries(Object.entries(all).filter(([k]) => k !== 'beings' && k !== 'bind'));
195
+ const cells = at(p.beings ?? {}, row);
196
+ return cells === undefined ? undefined : { cells, bind: at(p.bind ?? {}, row) ?? emptyBind() };
197
+ };
198
+ export const rowsIn = (p) => [HEAD, ...Object.keys(p.beings ?? {})];
199
+ // The partition back from its rows. A head that is absent is a partition
200
+ // nobody has written, and `open` makes an empty one of it.
201
+ export const fromRows = (rows) => {
202
+ const out = { ...(rows[HEAD] ?? {}) };
203
+ const beings = {};
204
+ const bind = {};
205
+ let any = Object.keys(out).length > 0;
206
+ for (const [row, value] of Object.entries(rows)) {
207
+ if (row === HEAD)
208
+ continue;
209
+ put(beings, row, value.cells);
210
+ put(bind, row, value.bind);
211
+ any = true;
212
+ }
213
+ // Nothing kept is nothing handed back. Rows that say only that there are no
214
+ // rows must come back as the empty memory they are, because `open` reads an
215
+ // empty one as a ward's first breath and anything else as a partition
216
+ // somebody wrote, which must then say which version wrote it.
217
+ if (!any)
218
+ return {};
219
+ out.beings = beings;
220
+ out.bind = bind;
221
+ return out;
222
+ };
package/dist/ward/ward.js CHANGED
@@ -6,7 +6,7 @@
6
6
  // leaves, opens every one that arrives.
7
7
  import { silence, isSilence, unreached, isWord, word } from '../being/silence.js';
8
8
  import { OWNER } from '../being/types.js';
9
- import { at, drop, open, put, emptyBind, emptyCells, MINTED } from './partition.js';
9
+ import { at, drop, open, put, emptyBind, emptyCells, HEAD, MINTED } from './partition.js';
10
10
  import { Heirs } from './heirs.js';
11
11
  import { makeDoor } from './door.js';
12
12
  import { buildStance } from './stance.js';
@@ -38,7 +38,13 @@ class Self {
38
38
  this.key = key;
39
39
  this.pk = key.pk;
40
40
  this.p = open(ground.memory); // the partition: the ward's own state, and every being's row
41
- this.heirs = new Heirs(this.p, () => this.#wrote());
41
+ // Opening is itself a write: a memory nobody has written comes back with
42
+ // a version stamped on it and the ward's own tables under it. A ward that
43
+ // said nothing here would have every being of it kept and the head that
44
+ // says which version they are written under kept by nobody, so the next
45
+ // boot would read a partition of no version and refuse it.
46
+ this.#wrote(HEAD);
47
+ this.heirs = new Heirs(this.p, () => this.#wrote(HEAD));
42
48
  this.door = makeDoor(key, this.heirs, this.doors, () => this.p.public, (n) => ground.random(n)); // pointer one
43
49
  this.#boot(this.pk, () => this); // the ward's ward is itself
44
50
  // a restart is silent: every being in the cells is constructed again, unasked.
@@ -93,15 +99,18 @@ class Self {
93
99
  unboot: (key) => this.#unboot(key),
94
100
  setPublic: (key) => {
95
101
  this.p.public = key;
96
- this.#wrote();
102
+ this.#wrote(HEAD);
97
103
  },
98
104
  }, asker, method, args);
99
105
  }
100
106
  // ---- ward functions. on the object. no stance reaches them.
101
- // The partition was written. The harbor is told, and nothing more: what it
102
- // does with the word is its own, and the ward never learns.
103
- #wrote() {
104
- this.g.wrote?.();
107
+ // The partition was written, and which row of it: a being by her key, or
108
+ // the head. The harbor is told, and nothing more: what it does with the
109
+ // word is its own, and the ward never learns. Every write says its row,
110
+ // because a harbor told only that something moved must keep all of it, and
111
+ // a being's row is the one part of a partition that grows without limit.
112
+ #wrote(row) {
113
+ this.g.wrote?.(row);
105
114
  }
106
115
  // The rows no door holds this run: beings whose class threw at birth or
107
116
  // is not the harbor's to give. Key to class.
@@ -139,7 +148,7 @@ class Self {
139
148
  if (!door)
140
149
  return null;
141
150
  unguarded(door.cells).class = className; // so a restart finds her. the ward's key, written behind her guard
142
- this.#wrote();
151
+ this.#wrote(key);
143
152
  return door;
144
153
  }
145
154
  // The inverse of boot, and the only way a being leaves a ward. Her
@@ -177,7 +186,8 @@ class Self {
177
186
  drop(this.p.beings, key);
178
187
  drop(this.p.bind, key);
179
188
  this.doors.delete(key);
180
- this.#wrote();
189
+ this.#wrote(key); // she is gone, and her row goes with her
190
+ this.#wrote(HEAD); // her heirs were closed, and the public being may have been her
181
191
  return [...occupants, ...standings];
182
192
  }
183
193
  // Nothing is written until there is somebody to write it for: a class the
@@ -188,17 +198,17 @@ class Self {
188
198
  // makers: the ground's `instantiate` for a being of the ward, and the ward
189
199
  // itself, which is the first being in its own map and is already made.
190
200
  #boot(key, make) {
191
- const cells = guardCells(at(this.p.beings, key) ?? emptyCells(), () => this.#wrote());
201
+ const cells = guardCells(at(this.p.beings, key) ?? emptyCells(), () => this.#wrote(key));
192
202
  const bind = at(this.p.bind, key) ?? emptyBind();
193
203
  const stance = buildStance({
194
204
  pk: this.pk,
195
205
  // This stance, not merely this key: unboot and boot again under the
196
206
  // same key makes a new being, and the old stance is not hers.
197
207
  live: () => this.doors.get(key)?.stance === stance,
198
- mintKey: (b) => this.#mintKey(b),
208
+ mintKey: (b) => this.#mintKey(b, key),
199
209
  openHeir: (heir, being, id) => this.heirs.open(heir, being, id),
200
210
  closeHeir: (heir) => this.heirs.close(heir),
201
- send: (b, keys, method, args, wanted) => this.#send(b, keys, method, args, wanted),
211
+ send: (b, keys, method, args, wanted) => this.#send(b, keys, method, args, wanted, key),
202
212
  // A throw at birth is null to her, not a throw in her method: she asked
203
213
  // for a being and got none, and her own answer is still hers to give.
204
214
  instantiate: (className, k) => {
@@ -215,7 +225,7 @@ class Self {
215
225
  // names there are, which ward may ask for one, and what becomes of one
216
226
  // she did not take, is the harbor's and no word of the ward.
217
227
  lend: async (name, take) => (await this.g.lend?.(name, take)) ?? false,
218
- wrote: () => this.#wrote(),
228
+ wrote: () => this.#wrote(key),
219
229
  }, key, cells, bind);
220
230
  const being = make(stance);
221
231
  if (!being || typeof being.answer !== 'function')
@@ -224,10 +234,10 @@ class Self {
224
234
  put(this.p.bind, key, bind);
225
235
  const door = { key, cells, bind, stance, being };
226
236
  this.doors.set(key, door);
227
- this.#wrote();
237
+ this.#wrote(key);
228
238
  return door;
229
239
  }
230
- async #mintKey(bind) {
240
+ async #mintKey(bind, row) {
231
241
  const k = await beingKey(this.g.random(32));
232
242
  // The last few she minted, and no more. A relation rotates on every ask,
233
243
  // so a list of all of them is a partition that grows for as long as she
@@ -236,7 +246,7 @@ class Self {
236
246
  bind.minted.push(k.pk);
237
247
  if (bind.minted.length > MINTED)
238
248
  bind.minted.splice(0, bind.minted.length - MINTED);
239
- this.#wrote();
249
+ this.#wrote(row);
240
250
  return k;
241
251
  }
242
252
  // One send for every door, the ward's own included. Mine: never leaves.
@@ -244,7 +254,7 @@ class Self {
244
254
  // next, seals to the far ward, opens the reply with the ephemeral secret,
245
255
  // and rotates to the announced key once the far door has answered under
246
256
  // the current one.
247
- async #send(bind, keys, method, args, wanted) {
257
+ async #send(bind, keys, method, args, wanted, row) {
248
258
  // What she asked for, held to what this ward allows. Asking for nothing is
249
259
  // the default, and asking for more than the ceiling is the ceiling: budget
250
260
  // is granted by a ward, never minted by a being.
@@ -255,7 +265,7 @@ class Self {
255
265
  // to rotate to. A standing on her signs with one key for life. Every other
256
266
  // ask announces its next: there is no send that does not.
257
267
  if (keys.next === null && keys.heir !== null)
258
- keys.next = (await this.#mintKey(bind)).seed;
268
+ keys.next = (await this.#mintKey(bind, row)).seed;
259
269
  const next = keys.next === null ? null : (await beingKey(unhex(keys.next))).pk;
260
270
  // Her count for this relation, one higher every call and never reused. The
261
271
  // far door honours each number once. One relation sends one at a time, so
@@ -307,7 +317,7 @@ class Self {
307
317
  keys.current = keys.next; // the far door holds `next` as announced. move to it.
308
318
  keys.next = null;
309
319
  }
310
- this.#wrote(); // the count moved, and the keys may have
320
+ this.#wrote(row); // the count moved, and the keys may have
311
321
  return reply;
312
322
  }
313
323
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quo-systems/quo",
3
- "version": "0.2.16",
3
+ "version": "0.2.18",
4
4
  "description": "Quo: an object asks another object and gets an answer, without knowing where it is. Being, Ward, Harbor.",
5
5
  "keywords": [
6
6
  "quo",
@@ -39,9 +39,9 @@
39
39
  "./package.json": "./package.json"
40
40
  },
41
41
  "scripts": {
42
- "build": "rm -rf dist && tsc -p tsconfig.build.json && cp ../../papers/SPEC.md protocol/SPEC.md && cp ../../papers/quo-kit.md quo-kit.md",
43
- "test": "node --test \"test/*.test.ts\"",
44
- "check:terrain": "node --test \"test/terrain/*.test.ts\"",
42
+ "build": "rm -rf dist && tsc -p tsconfig.build.json && cp ../../papers/SPEC.md protocol/SPEC.md && cp ../../papers/quo-kit.md quo-kit.md && cp ../../papers/GETTING_STARTED.md GETTING_STARTED.md",
43
+ "check": "node --test \"test/*.test.ts\"",
44
+ "deep": "node --test \"test/terrain/*.test.ts\"",
45
45
  "prepublishOnly": "test \"$QUO_GATED\" = 1 || { echo 'publish from the root, gated once: npm run release:quo' >&2; exit 1; }"
46
46
  },
47
47
  "publishConfig": {
@@ -52,6 +52,7 @@
52
52
  "src",
53
53
  "protocol",
54
54
  "quo-kit.md",
55
+ "GETTING_STARTED.md",
55
56
  "README.md",
56
57
  "LICENSE",
57
58
  "NOTICE"
package/quo-kit.md CHANGED
@@ -40,7 +40,7 @@ is read against and away from every line of TypeScript, because that folder
40
40
  is what a kit in another language is written against and nothing else; this
41
41
  paper to the package root, where it says which of what a reader sees here
42
42
  was a choice. Publishing runs both
43
- gates first, `npm run check` and `npm run check:terrain`, and refuses on a
43
+ gates first, `npm run check` and `npm run deep`, and refuses on a
44
44
  failure.
45
45
 
46
46
  ```
@@ -289,7 +289,7 @@ ground
289
289
  instantiate (class name, stance) -> object | null
290
290
  carry (ward pk, bytes) -> bytes | undefined undefined: no door was reached. a throw is read the same.
291
291
  random (n) -> n bytes of entropy
292
- wrote () -> nothing. said after every write to the partition. left out by a harbor that keeps nothing.
292
+ wrote (row) -> nothing. said after every write, naming the row: a being by her key, or HEAD. left out by a harbor that keeps nothing.
293
293
  lend lend(name, take) -> taken. left out by a harbor that lends nothing.
294
294
  ```
295
295
 
@@ -362,6 +362,17 @@ each have theirs, outside this tree, and every one of them passes
362
362
  `src/conformance/store.ts` untouched, as every reach passes
363
363
  `src/conformance/reach.ts`.
364
364
 
365
+ A store keeps a ward in rows. A being's cells are the one part of a partition
366
+ that grows without limit, so she is a row of her own, her cells and her bind
367
+ table under her key; everything else is one head row, and every part of that
368
+ is bounded already. `wrote` names the row, the harbor gathers the names into
369
+ the burst it saves, and `save` is handed them beside the partition. What a
370
+ store does with them is its own: `rowsOf` in `src/harbor/store.ts` is what
371
+ one writing the few uses, and one writing all of it every time is correct and
372
+ slower. The split itself is `src/ward/partition.ts`, where the shape is, so
373
+ that a harbor keeps what it is given and reads none of it, and a field that
374
+ file grows later needs no line in any store to survive a round trip.
375
+
365
376
  The dialer's wait doubles from a second to thirty when a line drops, and
366
377
  returns to that first second where the far side announced and was taken.
367
378
  Those are this harbor's numbers: what a far side sees is only that a dialer
@@ -552,7 +563,7 @@ passes the same suite. Those tests are the checklist, not the mock.
552
563
  nowhere else in this repository. A terrain whose binary is absent skips and
553
564
  names the command that installs it; it never passes quietly.
554
565
  `test/terrain/engine.ts` runs the bundle in another engine on this machine.
555
- Behind `npm run check:terrain`. `test/terrain/bundle.ts` is the
566
+ Behind `npm run deep:quo`. `test/terrain/bundle.ts` is the
556
567
  bundler and the reference run, `test/terrain/exercise.ts` is what every
557
568
  terrain runs, `test/terrain/floor.ts` is the floor -- each written once, so
558
569
  no terrain can be probed for less than another.
@@ -588,7 +599,7 @@ is no CI; the gate runs in seconds and is run before every commit. Nothing
588
599
  red is committed except a test marked todo, which is a claim the tree does
589
600
  not yet meet and says so.
590
601
 
591
- `npm run check` is Node alone, and stays fast. `npm run check:terrain` is
602
+ `npm run check` is Node alone, and stays fast. `npm run deep:quo` is
592
603
  every other terrain, the browser, workerd, Deno and Bun, kept apart because
593
604
  each is a binary of its own and the browser is a download and not a
594
605
  package: a fresh clone needs `npx playwright install chromium` first, and is
@@ -9,7 +9,15 @@ import type { Kept, Store } from '../harbor/store.ts';
9
9
  import type { Runner } from './index.ts';
10
10
 
11
11
  const seed = (b: number) => new Uint8Array(32).fill(b);
12
- const kept = (b: number, extra: Record<string, unknown> = {}): Kept => ({ seed: seed(b), partition: { beings: {}, ...extra }, record: { pk: '', code: 'classes/index.ts', user: 'me' } });
12
+ const kept = (b: number, extra: Record<string, unknown> = {}): Kept => ({ seed: seed(b), partition: { beings: {}, bind: {}, ...extra }, record: { pk: '', code: 'classes/index.ts', user: 'me' } });
13
+ // A partition as a ward writes one: beings under their keys, a bind table
14
+ // beside each, and whatever else the head carries.
15
+ const ward = (beings: Record<string, unknown>, extra: Record<string, unknown> = {}): Record<string, unknown> => ({
16
+ beings,
17
+ bind: Object.fromEntries(Object.keys(beings).map((k) => [k, { standings: {} }])),
18
+ ...extra,
19
+ });
20
+ const ALL = (p: Record<string, unknown>): string[] => ['', ...Object.keys(p.beings as object)];
13
21
 
14
22
  export function conformStore(label: string, make: () => Promise<Store>, { test }: { test: Runner }) {
15
23
  const t = (name: string, fn: () => Promise<void>) => test(`[${label}] ${name}`, {}, fn);
@@ -29,7 +37,7 @@ export function conformStore(label: string, make: () => Promise<Store>, { test }
29
37
  const back = await s.load('main');
30
38
  assert.ok(back);
31
39
  assert.deepEqual(back.seed, seed(7));
32
- assert.deepEqual(back.partition, { beings: {}, n: 1, deep: { list: [1, 'two', null], flag: true } });
40
+ assert.deepEqual(back.partition, { beings: {}, bind: {}, n: 1, deep: { list: [1, 'two', null], flag: true } });
33
41
  assert.deepEqual(back.record, { pk: '', code: 'classes/index.ts', user: 'me' });
34
42
  });
35
43
 
@@ -46,28 +54,47 @@ export function conformStore(label: string, make: () => Promise<Store>, { test }
46
54
  assert.deepEqual((await s.load('main'))!.seed, seed(1));
47
55
  });
48
56
 
49
- t('save replaces the partition and touches nothing else', async () => {
57
+ t('save writes every row it is given, and touches nothing else', async () => {
50
58
  const s = await make();
51
59
  await s.put('main', kept(3));
52
- await s.save('main', { beings: { a: { x: 1 } }, bind: {} });
60
+ const p = ward({ a: { x: 1 } });
61
+ await s.save('main', p, ALL(p));
53
62
  const back = (await s.load('main'))!;
54
- assert.deepEqual(back.partition, { beings: { a: { x: 1 } }, bind: {} });
63
+ assert.deepEqual(back.partition, p);
55
64
  assert.deepEqual(back.seed, seed(3));
56
65
  assert.deepEqual(back.record, kept(3).record);
57
66
  });
58
67
 
68
+ // What a store may do with the rows it is told about is its own: writing
69
+ // the whole partition every time is correct. What it may never do is write
70
+ // less than it was told, or keep a being the rows say is gone.
71
+ t('save writes the rows it names, and a being the rows drop is gone', async () => {
72
+ const s = await make();
73
+ const p = ward({ a: { x: 1 }, b: { y: 1 } });
74
+ await s.put('main', { ...kept(3), partition: p });
75
+ // one being moves, and only her row is named
76
+ (p.beings as Record<string, { x: number }>).a.x = 2;
77
+ await s.save('main', p, ['a']);
78
+ assert.deepEqual((await s.load('main'))!.partition, p, 'the row that was named is written');
79
+ // she leaves: her row is named and her key is gone from the partition
80
+ delete (p.beings as Record<string, unknown>).a;
81
+ delete (p.bind as Record<string, unknown>).a;
82
+ await s.save('main', p, ['a']);
83
+ assert.deepEqual((await s.load('main'))!.partition, p, 'a row whose being is gone is dropped');
84
+ });
85
+
59
86
  t('record replaces the record and touches nothing else', async () => {
60
87
  const s = await make();
61
88
  await s.put('main', kept(4, { n: 9 }));
62
89
  await s.record('main', { pk: 'ab'.repeat(64), code: 'elsewhere.ts', user: 'her' });
63
90
  const back = (await s.load('main'))!;
64
91
  assert.deepEqual(back.record, { pk: 'ab'.repeat(64), code: 'elsewhere.ts', user: 'her' });
65
- assert.deepEqual(back.partition, { beings: {}, n: 9 });
92
+ assert.deepEqual(back.partition, { beings: {}, bind: {}, n: 9 });
66
93
  });
67
94
 
68
95
  t('save and record on a name not kept are nothing', async () => {
69
96
  const s = await make();
70
- await s.save('ghost', { beings: {} });
97
+ await s.save('ghost', { beings: {}, bind: {} }, ['']);
71
98
  await s.record('ghost', { pk: '', code: '', user: '' });
72
99
  assert.deepEqual(await s.list(), []);
73
100
  });
@@ -77,10 +104,10 @@ export function conformStore(label: string, make: () => Promise<Store>, { test }
77
104
  const k = kept(5, { n: 1 });
78
105
  await s.put('main', k);
79
106
  (k.partition as { n: number }).n = 2;
80
- assert.deepEqual((await s.load('main'))!.partition, { beings: {}, n: 1 });
107
+ assert.deepEqual((await s.load('main'))!.partition, { beings: {}, bind: {}, n: 1 });
81
108
  const a = (await s.load('main'))!;
82
109
  (a.partition as { n: number }).n = 3;
83
- assert.deepEqual((await s.load('main'))!.partition, { beings: {}, n: 1 });
110
+ assert.deepEqual((await s.load('main'))!.partition, { beings: {}, bind: {}, n: 1 });
84
111
  });
85
112
 
86
113
  t('take hands the ward out and forgets it; the name is free again', async () => {
@@ -90,7 +117,7 @@ export function conformStore(label: string, make: () => Promise<Store>, { test }
90
117
  const out = await s.take('main');
91
118
  assert.ok(out);
92
119
  assert.deepEqual(out.seed, seed(6));
93
- assert.deepEqual(out.partition, { beings: {}, n: 6 });
120
+ assert.deepEqual(out.partition, { beings: {}, bind: {}, n: 6 });
94
121
  assert.equal(await s.load('main'), undefined);
95
122
  assert.deepEqual(await s.list(), ['other']);
96
123
  await s.put('main', kept(9));
@@ -50,7 +50,11 @@ export type Hosted = WardPointers & { pk: string; name: string; record: WardReco
50
50
  // snapshot cannot land after a newer one. A save that fails is the harbor's
51
51
  // to count and never a door's to answer: the door answered bytes, and a
52
52
  // disk that is full is not a reason the far side may hear.
53
- type Saving = { memory: Record<string, unknown>; dirty: boolean; running: Promise<void> | null; timer: ReturnType<typeof setTimeout> | undefined; gone: boolean };
53
+ // `dirty` holds the rows the ward has named since the last write, and never
54
+ // what changed in them: the harbor keeps the partition and reads none of it,
55
+ // so what a row is worth is the store's to read off the object when it
56
+ // writes. Empty is nothing to do.
57
+ type Saving = { memory: Record<string, unknown>; dirty: Set<string>; running: Promise<void> | null; timer: ReturnType<typeof setTimeout> | undefined; gone: boolean };
54
58
  // A reach in the directory, and whether this harbor holds it as a socket a
55
59
  // dialer opened: only those, and its own doors, take bytes from the wire.
56
60
  export type Bound = { reach: Reach; held: boolean };
@@ -155,12 +159,12 @@ export class Harbor {
155
159
 
156
160
  // ---- saving
157
161
 
158
- // The ward wrote. Its partition is saved once the line is free, and once
159
- // for every burst of writes, in the order the writes came.
160
- #wrote(name: string): void {
162
+ // The ward wrote one row. Its partition is saved once the line is free, and
163
+ // once for every burst of writes, with every row named in that burst.
164
+ #wrote(name: string, row: string): void {
161
165
  const s = this.#saving.get(name);
162
166
  if (!s || s.gone) return;
163
- s.dirty = true;
167
+ s.dirty.add(row);
164
168
  if (s.timer === undefined) s.timer = setTimeout(() => void this.#flush(name), 0);
165
169
  }
166
170
  async #flush(name: string): Promise<void> {
@@ -172,10 +176,13 @@ export class Harbor {
172
176
  }
173
177
  if (s.running) return s.running;
174
178
  s.running = (async () => {
175
- while (s.dirty && !s.gone) {
176
- s.dirty = false;
179
+ while (s.dirty.size > 0 && !s.gone) {
180
+ // Taken before the write, so a row named while this one is in flight
181
+ // is written by the next turn of the loop and not lost with it.
182
+ const rows = [...s.dirty];
183
+ s.dirty.clear();
177
184
  try {
178
- await this.store.save(name, s.memory);
185
+ await this.store.save(name, s.memory, rows);
179
186
  } catch {
180
187
  this.faults.set(name, (this.faults.get(name) ?? 0) + 1);
181
188
  }
@@ -352,11 +359,11 @@ export class Harbor {
352
359
  instantiate: maker(objects, classes, this.classes),
353
360
  carry: (pk, bytes) => this.carry(pk, new Uint8Array(bytes)),
354
361
  random,
355
- wrote: () => this.#wrote(name),
362
+ wrote: (row) => this.#wrote(name, row),
356
363
  };
357
364
  const lend = this.lendFor(name, kept.record);
358
365
  if (lend) ground.lend = lend;
359
- const saving: Saving = { memory, dirty: false, running: null, timer: undefined, gone: false };
366
+ const saving: Saving = { memory, dirty: new Set(), running: null, timer: undefined, gone: false };
360
367
  this.#saving.set(name, saving);
361
368
  // A ward that will not be born leaves nothing behind: a partition of a
362
369
  // shape this kit cannot read throws here, and a name the harbor does not
@@ -6,7 +6,10 @@
6
6
  export { MemoryHarbor, type Booted, type WardFactory } from './memory.ts';
7
7
  export { Harbor, DEFAULT_CODE, type Hosted, type Bound, type Loader } from './core.ts';
8
8
  export { dial, type Dialer } from './dial.ts';
9
- export { MemoryStore, values, type Store, type Kept, type WardRecord } from './store.ts';
9
+ export { MemoryStore, values, rowsOf, type Store, type Kept, type WardRecord } from './store.ts';
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.ts';
10
13
  export { request, Socket, SUITE, REFUSED, type Reach, type Carry, type Line, type Announce } from './reach.ts';
11
14
  export type { Ground, WardPointers, Lend } from '../ward/ground.ts';
12
15
  // What a harbor builds a ground out of. Convenience, never contract: a kit
@@ -9,6 +9,8 @@
9
9
  // storage on the edge; the memory store below is the library's own, for a
10
10
  // harbor core with no device under it. `src/conformance/store.ts` is what
11
11
  // every one of them passes.
12
+ import { fromRows, rowOf, rowsIn, type Partition } from '../ward/partition.ts';
13
+
12
14
  export type WardRecord = { pk: string; code: string; user: string };
13
15
  export type Kept = { seed: Uint8Array; partition: Record<string, unknown>; record: WardRecord };
14
16
 
@@ -19,7 +21,14 @@ export type Store = {
19
21
  put(name: string, kept: Kept): Promise<void>;
20
22
  // The partition back, after a call; the record back, after a boot learned
21
23
  // the pk. A name not kept is nothing, on both.
22
- save(name: string, partition: Record<string, unknown>): Promise<void>;
24
+ //
25
+ // `rows` names what the ward wrote since the last save: a being by her key,
26
+ // or `HEAD`. A store may write only those, which is what `rowsOf` is for,
27
+ // and a store that writes the whole partition every time is correct and
28
+ // slower. A being's cells are the one part of a partition that grows
29
+ // without limit, so a store that keeps her apart lets her neighbour be
30
+ // asked without her being written down again.
31
+ save(name: string, partition: Record<string, unknown>, rows: readonly string[]): Promise<void>;
23
32
  record(name: string, record: WardRecord): Promise<void>;
24
33
  // Take a ward out: the first move of a migration. What was kept comes back.
25
34
  take(name: string): Promise<Kept | undefined>;
@@ -31,9 +40,30 @@ export type Store = {
31
40
  // so a store takes a copy of the values the way a file would: through JSON.
32
41
  export const values = (p: Record<string, unknown>): Record<string, unknown> => JSON.parse(JSON.stringify(p)) as Record<string, unknown>;
33
42
 
34
- // The store as a map, one process, nothing kept past it.
43
+ // The rows a store was told about, as values, ready to be written down. A row
44
+ // whose key names nobody is a being who left, and comes back undefined, which
45
+ // is the store dropping what it holds for her.
46
+ export const rowsOf = (partition: Record<string, unknown>, rows: readonly string[]): [string, Record<string, unknown> | undefined][] =>
47
+ rows.map((row) => {
48
+ const value = rowOf(partition as unknown as Partition, row);
49
+ return [row, value === undefined ? undefined : (values(value) as Record<string, unknown>)];
50
+ });
51
+
52
+ // One store's copy, brought up to date with the rows it was told about.
53
+ const keep = (row: Row, partition: Record<string, unknown>, rows: readonly string[]): void => {
54
+ for (const [id, value] of rowsOf(partition, rows)) {
55
+ if (value === undefined) row.parts.delete(id);
56
+ else row.parts.set(id, value);
57
+ }
58
+ };
59
+
60
+ // The store as a map, one process, nothing kept past it. It keeps a ward as
61
+ // rows, the way a store on a device does, so what it costs to save is what a
62
+ // device pays and the library's own measurements mean something.
63
+ type Row = { seed: Uint8Array; parts: Map<string, Record<string, unknown>>; record: WardRecord };
64
+
35
65
  export class MemoryStore implements Store {
36
- readonly rows = new Map<string, Kept>();
66
+ readonly rows = new Map<string, Row>();
37
67
  readonly reach = new Map<string, string>();
38
68
 
39
69
  async list(): Promise<string[]> {
@@ -41,15 +71,17 @@ export class MemoryStore implements Store {
41
71
  }
42
72
  async load(name: string): Promise<Kept | undefined> {
43
73
  const row = this.rows.get(name);
44
- return row && { seed: new Uint8Array(row.seed), partition: values(row.partition), record: { ...row.record } };
74
+ return row && { seed: new Uint8Array(row.seed), partition: fromRows(Object.fromEntries(row.parts)), record: { ...row.record } };
45
75
  }
46
76
  async put(name: string, kept: Kept): Promise<void> {
47
77
  if (this.rows.has(name)) throw new Error(`ward ${name} already exists here`);
48
- this.rows.set(name, { seed: new Uint8Array(kept.seed), partition: values(kept.partition), record: { ...kept.record } });
78
+ const row: Row = { seed: new Uint8Array(kept.seed), parts: new Map(), record: { ...kept.record } };
79
+ this.rows.set(name, row);
80
+ keep(row, kept.partition, rowsIn(kept.partition));
49
81
  }
50
- async save(name: string, partition: Record<string, unknown>): Promise<void> {
82
+ async save(name: string, partition: Record<string, unknown>, rows: readonly string[]): Promise<void> {
51
83
  const row = this.rows.get(name);
52
- if (row) row.partition = values(partition);
84
+ if (row) keep(row, partition, rows);
53
85
  }
54
86
  async record(name: string, record: WardRecord): Promise<void> {
55
87
  const row = this.rows.get(name);
@@ -31,13 +31,20 @@ export type Ground = {
31
31
  seed: string | Uint8Array; // the ward derives its pk from it and nothing else
32
32
  memory: Record<string, unknown>; // the partition. the ward's files. opaque to the harbor
33
33
  instantiate(className: string, stance: Stance): BeingLike | null; // the code half
34
- // The ward wrote its partition. It says so after every write, a key
35
- // rotated, a relation taken, a cell she set, and says nothing else: it
36
- // never learns whether anything was kept. A harbor that keeps the
34
+ // The ward wrote its partition, and which row of it: a being by her key,
35
+ // or `HEAD` for everything that is not one being's. It says so after every
36
+ // write, a key rotated, a relation taken, a cell she set, and says nothing
37
+ // else: it never learns whether anything was kept. A harbor that keeps the
37
38
  // partition saves after this, when it likes and in the order it was told,
38
39
  // so a being driven in process is kept the way one reached through a door
39
40
  // is. A harbor that keeps nothing leaves it out.
40
- wrote?: () => void;
41
+ //
42
+ // The row is named because a being's cells are the one part of a partition
43
+ // that grows without limit, and a harbor told only that something moved has
44
+ // to write down every being in the ward to be sure of one. What a harbor
45
+ // does with the name is its own: writing the whole partition on every word
46
+ // is correct, and slower.
47
+ wrote?: (row: string) => void;
41
48
  // Sealed bytes to a ward pk. What comes back, or undefined.
42
49
  //
43
50
  // undefined is a promise, not a shrug: no door was reached, and nothing was
@@ -235,3 +235,56 @@ export const drop = (rec: Record<string, unknown>, key: string): void => {
235
235
 
236
236
  export const emptyCells = (): Cells => ({ standings: {}, occupants: {} });
237
237
  export const emptyBind = (): Bind => ({ standings: {}, occupants: {}, knocks: {}, answered: {}, minted: [] });
238
+
239
+ // ---- the partition in rows, for a store that keeps it
240
+ //
241
+ // A ward hands a harbor one object, so a being who writes one cell has every
242
+ // other being in her ward written down beside her. What a being holds is the
243
+ // one thing here that grows without limit, so she is a row: her cells and her
244
+ // bind table, under her key. Everything else is one head row, and every part
245
+ // of it is bounded already, one record per occupant and two lists that drop
246
+ // their oldest.
247
+ //
248
+ // The split is here, where the shape is, and not in the harbor, which keeps
249
+ // what it is given and reads none of it. A store is handed the rows that
250
+ // moved; what it does with that is its own, and one that writes every row is
251
+ // as correct as one that writes the few.
252
+ export const HEAD = ''; // the row that is not a being: no being's key is empty
253
+
254
+ // One row's value: a being, or the head, which is everything the partition
255
+ // holds that is not filed under a being. The head is read as what is left
256
+ // rather than as a list of names, so a store keeps whatever it was handed and
257
+ // hands the same thing back, and a field this file grows later needs no line
258
+ // in a store to survive a round trip.
259
+ //
260
+ // Undefined where the key names nobody, which is a row the store drops.
261
+ export const rowOf = (p: Partition, row: string): Record<string, unknown> | undefined => {
262
+ const all = p as unknown as Record<string, unknown>;
263
+ if (row === HEAD) return Object.fromEntries(Object.entries(all).filter(([k]) => k !== 'beings' && k !== 'bind'));
264
+ const cells = at(p.beings ?? {}, row);
265
+ return cells === undefined ? undefined : { cells, bind: at(p.bind ?? {}, row) ?? emptyBind() };
266
+ };
267
+ export const rowsIn = (p: Record<string, unknown>): string[] => [HEAD, ...Object.keys((p.beings as Record<string, unknown>) ?? {})];
268
+
269
+ // The partition back from its rows. A head that is absent is a partition
270
+ // nobody has written, and `open` makes an empty one of it.
271
+ export const fromRows = (rows: Record<string, Record<string, unknown>>): Record<string, unknown> => {
272
+ const out: Record<string, unknown> = { ...(rows[HEAD] ?? {}) };
273
+ const beings: Record<string, unknown> = {};
274
+ const bind: Record<string, unknown> = {};
275
+ let any = Object.keys(out).length > 0;
276
+ for (const [row, value] of Object.entries(rows)) {
277
+ if (row === HEAD) continue;
278
+ put(beings, row, value.cells);
279
+ put(bind, row, value.bind);
280
+ any = true;
281
+ }
282
+ // Nothing kept is nothing handed back. Rows that say only that there are no
283
+ // rows must come back as the empty memory they are, because `open` reads an
284
+ // empty one as a ward's first breath and anything else as a partition
285
+ // somebody wrote, which must then say which version wrote it.
286
+ if (!any) return {};
287
+ out.beings = beings;
288
+ out.bind = bind;
289
+ return out;
290
+ };
package/src/ward/ward.ts CHANGED
@@ -8,7 +8,7 @@ import { silence, isSilence, unreached, isWord, word } from '../being/silence.ts
8
8
  import { OWNER } from '../being/types.ts';
9
9
  import type { Asker, BeingLike, Cells, JsonObject, Reply, Stance, Wanted, Word } from '../being/types.ts';
10
10
  import type { Ground, WardPointers } from './ground.ts';
11
- import { at, drop, open, put, emptyBind, emptyCells, MINTED, type Bind, type Partition, type StandingKeys } from './partition.ts';
11
+ import { at, drop, open, put, emptyBind, emptyCells, HEAD, MINTED, type Bind, type Partition, type StandingKeys } from './partition.ts';
12
12
  import { Heirs } from './heirs.ts';
13
13
  import { makeDoor, type Door } from './door.ts';
14
14
  import { buildStance } from './stance.ts';
@@ -50,7 +50,13 @@ class Self implements BeingLike {
50
50
  this.key = key;
51
51
  this.pk = key.pk;
52
52
  this.p = open(ground.memory); // the partition: the ward's own state, and every being's row
53
- this.heirs = new Heirs(this.p, () => this.#wrote());
53
+ // Opening is itself a write: a memory nobody has written comes back with
54
+ // a version stamped on it and the ward's own tables under it. A ward that
55
+ // said nothing here would have every being of it kept and the head that
56
+ // says which version they are written under kept by nobody, so the next
57
+ // boot would read a partition of no version and refuse it.
58
+ this.#wrote(HEAD);
59
+ this.heirs = new Heirs(this.p, () => this.#wrote(HEAD));
54
60
  this.door = makeDoor(key, this.heirs, this.doors, () => this.p.public, (n) => ground.random(n)); // pointer one
55
61
  this.#boot(this.pk, () => this); // the ward's ward is itself
56
62
  // a restart is silent: every being in the cells is constructed again, unasked.
@@ -103,7 +109,7 @@ class Self implements BeingLike {
103
109
  unboot: (key) => this.#unboot(key),
104
110
  setPublic: (key) => {
105
111
  this.p.public = key;
106
- this.#wrote();
112
+ this.#wrote(HEAD);
107
113
  },
108
114
  },
109
115
  asker,
@@ -114,10 +120,13 @@ class Self implements BeingLike {
114
120
 
115
121
  // ---- ward functions. on the object. no stance reaches them.
116
122
 
117
- // The partition was written. The harbor is told, and nothing more: what it
118
- // does with the word is its own, and the ward never learns.
119
- #wrote(): void {
120
- this.g.wrote?.();
123
+ // The partition was written, and which row of it: a being by her key, or
124
+ // the head. The harbor is told, and nothing more: what it does with the
125
+ // word is its own, and the ward never learns. Every write says its row,
126
+ // because a harbor told only that something moved must keep all of it, and
127
+ // a being's row is the one part of a partition that grows without limit.
128
+ #wrote(row: string): void {
129
+ this.g.wrote?.(row);
121
130
  }
122
131
 
123
132
  // The rows no door holds this run: beings whose class threw at birth or
@@ -151,7 +160,7 @@ class Self implements BeingLike {
151
160
  }
152
161
  if (!door) return null;
153
162
  unguarded(door.cells).class = className; // so a restart finds her. the ward's key, written behind her guard
154
- this.#wrote();
163
+ this.#wrote(key);
155
164
  return door;
156
165
  }
157
166
 
@@ -184,7 +193,8 @@ class Self implements BeingLike {
184
193
  drop(this.p.beings, key);
185
194
  drop(this.p.bind, key);
186
195
  this.doors.delete(key);
187
- this.#wrote();
196
+ this.#wrote(key); // she is gone, and her row goes with her
197
+ this.#wrote(HEAD); // her heirs were closed, and the public being may have been her
188
198
  return [...occupants, ...standings];
189
199
  }
190
200
 
@@ -196,7 +206,7 @@ class Self implements BeingLike {
196
206
  // makers: the ground's `instantiate` for a being of the ward, and the ward
197
207
  // itself, which is the first being in its own map and is already made.
198
208
  #boot(key: string, make: (stance: Stance) => BeingLike | null): Resident | null {
199
- const cells = guardCells(at(this.p.beings, key) ?? emptyCells(), () => this.#wrote());
209
+ const cells = guardCells(at(this.p.beings, key) ?? emptyCells(), () => this.#wrote(key));
200
210
  const bind = at(this.p.bind, key) ?? emptyBind();
201
211
  const stance: Stance = buildStance(
202
212
  {
@@ -204,10 +214,10 @@ class Self implements BeingLike {
204
214
  // This stance, not merely this key: unboot and boot again under the
205
215
  // same key makes a new being, and the old stance is not hers.
206
216
  live: () => this.doors.get(key)?.stance === stance,
207
- mintKey: (b) => this.#mintKey(b),
217
+ mintKey: (b) => this.#mintKey(b, key),
208
218
  openHeir: (heir, being, id) => this.heirs.open(heir, being, id),
209
219
  closeHeir: (heir) => this.heirs.close(heir),
210
- send: (b, keys, method, args, wanted) => this.#send(b, keys, method, args, wanted),
220
+ send: (b, keys, method, args, wanted) => this.#send(b, keys, method, args, wanted, key),
211
221
  // A throw at birth is null to her, not a throw in her method: she asked
212
222
  // for a being and got none, and her own answer is still hers to give.
213
223
  instantiate: (className, k) => {
@@ -224,7 +234,7 @@ class Self implements BeingLike {
224
234
  // names there are, which ward may ask for one, and what becomes of one
225
235
  // she did not take, is the harbor's and no word of the ward.
226
236
  lend: async (name, take) => (await this.g.lend?.(name, take)) ?? false,
227
- wrote: () => this.#wrote(),
237
+ wrote: () => this.#wrote(key),
228
238
  },
229
239
  key,
230
240
  cells,
@@ -236,11 +246,11 @@ class Self implements BeingLike {
236
246
  put(this.p.bind, key, bind);
237
247
  const door: Resident = { key, cells, bind, stance, being };
238
248
  this.doors.set(key, door);
239
- this.#wrote();
249
+ this.#wrote(key);
240
250
  return door;
241
251
  }
242
252
 
243
- async #mintKey(bind: Bind): Promise<{ seed: string; pk: string }> {
253
+ async #mintKey(bind: Bind, row: string): Promise<{ seed: string; pk: string }> {
244
254
  const k = await beingKey(this.g.random(32));
245
255
  // The last few she minted, and no more. A relation rotates on every ask,
246
256
  // so a list of all of them is a partition that grows for as long as she
@@ -248,7 +258,7 @@ class Self implements BeingLike {
248
258
  // but the keys of the moment.
249
259
  bind.minted.push(k.pk);
250
260
  if (bind.minted.length > MINTED) bind.minted.splice(0, bind.minted.length - MINTED);
251
- this.#wrote();
261
+ this.#wrote(row);
252
262
  return k;
253
263
  }
254
264
 
@@ -257,7 +267,7 @@ class Self implements BeingLike {
257
267
  // next, seals to the far ward, opens the reply with the ephemeral secret,
258
268
  // and rotates to the announced key once the far door has answered under
259
269
  // the current one.
260
- async #send(bind: Bind, keys: StandingKeys, method: string | undefined, args: JsonObject, wanted?: Wanted): Promise<ReplyPayload | typeof silence | Word> {
270
+ async #send(bind: Bind, keys: StandingKeys, method: string | undefined, args: JsonObject, wanted: Wanted | undefined, row: string): Promise<ReplyPayload | typeof silence | Word> {
261
271
  // What she asked for, held to what this ward allows. Asking for nothing is
262
272
  // the default, and asking for more than the ceiling is the ceiling: budget
263
273
  // is granted by a ward, never minted by a being.
@@ -267,7 +277,7 @@ class Self implements BeingLike {
267
277
  // whoever asked: no key it vouched for, so nothing to announce and nothing
268
278
  // to rotate to. A standing on her signs with one key for life. Every other
269
279
  // ask announces its next: there is no send that does not.
270
- if (keys.next === null && keys.heir !== null) keys.next = (await this.#mintKey(bind)).seed;
280
+ if (keys.next === null && keys.heir !== null) keys.next = (await this.#mintKey(bind, row)).seed;
271
281
  const next = keys.next === null ? null : (await beingKey(unhex(keys.next))).pk;
272
282
  // Her count for this relation, one higher every call and never reused. The
273
283
  // far door honours each number once. One relation sends one at a time, so
@@ -317,7 +327,7 @@ class Self implements BeingLike {
317
327
  keys.current = keys.next; // the far door holds `next` as announced. move to it.
318
328
  keys.next = null;
319
329
  }
320
- this.#wrote(); // the count moved, and the keys may have
330
+ this.#wrote(row); // the count moved, and the keys may have
321
331
  return reply;
322
332
  }
323
333
  }