@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.
@@ -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
@@ -21,6 +21,12 @@ export class MemoryHarbor {
21
21
  readonly partitions = new Map<string, Record<string, unknown>>(); // seed -> memory. the harbor keeps it and reads nothing
22
22
  readonly wards = new Map<string, Booted>(); // seed -> pointers
23
23
  readonly objects = new WeakMap<object, BeingLike>(); // cells -> being object. what instantiate constructed. a hand for tests, never the ward's; it follows the cells out when she is unbooted
24
+ // Where the wards this harbor boots draw their entropy. The device's own
25
+ // by default. A harbor may hand a fixed stream instead, which is what pins
26
+ // bytes: every key a ward mints and every nonce it seals under comes from
27
+ // here, so a ward on a known seed with a known stream answers the same
28
+ // bytes every run, and a corpus of them is a corpus and not a sample.
29
+ random: Ground['random'] = entropy;
24
30
 
25
31
  // the directory: its own doors, else a peer it is linked to. Quo says nothing about how.
26
32
  async route(farPk: string, bytes: Uint8Array): Promise<Uint8Array | undefined> {
@@ -66,7 +72,7 @@ export class MemoryHarbor {
66
72
  const back = await this.route(farPk, new Uint8Array(bytes));
67
73
  return back === undefined ? undefined : new Uint8Array(back);
68
74
  },
69
- random: entropy,
75
+ random: (n) => this.random(n),
70
76
  };
71
77
  const w = await Ward(ground);
72
78
  const booted: Booted = { ...w, pk: await learnPk(w) };
@@ -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);
@@ -85,18 +85,83 @@ const key32 = (value: Uint8Array, what: string): Uint8Array => {
85
85
  return value;
86
86
  };
87
87
  const pkcs8 = (prefix: Uint8Array, value: Uint8Array, what: string) => concat([prefix, key32(value, what)]);
88
- const secretKey = (alg: { name: string }, prefix: Uint8Array, value: Uint8Array, what: string, uses: KeyUsage[]) =>
89
- subtle().importKey('pkcs8', pkcs8(prefix, value, what) as BufferSource, alg, true, uses);
90
- const publicKey = (alg: { name: string }, value: Uint8Array, what: string, uses: KeyUsage[]) =>
91
- subtle().importKey('raw', key32(value, what) as BufferSource, alg, true, uses);
92
-
93
- // Subtle exports the public half of a private key only through a JWK, where `x` is the 32 raw bytes in base64url.
94
- async function rawPublic(secret: CryptoKey): Promise<Uint8Array> {
95
- const jwk = await subtle().exportKey('jwk', secret);
96
- const binary = atob(jwk.x!.replaceAll('-', '+').replaceAll('_', '/'));
97
- const out = new Uint8Array(binary.length);
98
- for (let at = 0; at < binary.length; at += 1) out[at] = binary.charCodeAt(at);
99
- return out;
88
+
89
+ // Importing a key is the most expensive thing on the path of an ask, and most
90
+ // of the imports are the same key again: a ward signs every reply with the one
91
+ // key, opens every ask with the one padlock, and verifies a relation under the
92
+ // key it verified it under last time. Measured over a round trip, fifteen of
93
+ // the twenty-five imports were bytes already imported once.
94
+ //
95
+ // So an imported key is kept, by the bytes it was imported from. A CryptoKey
96
+ // cannot be changed once it exists, so handing the same one out twice is
97
+ // handing out what a second import would have built. Nothing here is a
98
+ // decision a peer can see: two wards that cache differently, or not at all,
99
+ // speak the same bytes.
100
+ //
101
+ // It is bounded, and that is not a detail. A relation mints a fresh key on
102
+ // every ask, so a ward that talked all day would otherwise hold a key for
103
+ // every ask it ever made. Past the bound the least recently used goes, which
104
+ // is the key of a relation that has fallen quiet, and importing it again
105
+ // costs what it cost the first time.
106
+ //
107
+ // The secret keys in here are the ones the partition already holds in this
108
+ // process, as seeds. The cache is another shape of what the ward is already
109
+ // standing on, and never a second place a secret comes from.
110
+ const KEYS = 512;
111
+ const imported = new Map<string, Promise<CryptoKey>>();
112
+ const keep = (id: string, make: () => Promise<CryptoKey>): Promise<CryptoKey> => {
113
+ const had = imported.get(id);
114
+ if (had !== undefined) {
115
+ imported.delete(id); // and set again below: the most recently used goes last
116
+ imported.set(id, had);
117
+ return had;
118
+ }
119
+ const made = make();
120
+ // A key that would not import is not kept: the next call asks subtle again
121
+ // and hears the same refusal, rather than reading one this cache remembered.
122
+ // Node takes any thirty-two bytes as a public key and finds out at verify,
123
+ // so nothing here reaches this line; a terrain that checks the point at the
124
+ // import does, and a refusal it remembered would be a relation killed for
125
+ // good by one bad arrival.
126
+ made.catch(() => imported.delete(id));
127
+ imported.set(id, made);
128
+ // One in, at most one out: a map keeps what was put in the order it was put,
129
+ // so the first key it names is the one used longest ago.
130
+ if (imported.size > KEYS) imported.delete(imported.keys().next().value!);
131
+ return made;
132
+ };
133
+
134
+ // How many imported keys are held, and the bound they are held under. Nothing
135
+ // in the ward reads either: they are here to be looked at, and for the suite
136
+ // that holds the bound to what it says.
137
+ export const heldKeys = (): { held: number; bound: number } => ({ held: imported.size, bound: KEYS });
138
+
139
+ const secretKey = (alg: { name: string }, prefix: Uint8Array, value: Uint8Array, what: string, uses: KeyUsage[]) => {
140
+ const bytes = pkcs8(prefix, value, what);
141
+ return keep(`${alg.name}|${uses.join('+')}|${hex(bytes)}`, () => subtle().importKey('pkcs8', bytes as BufferSource, alg, true, uses));
142
+ };
143
+ const publicKey = (alg: { name: string }, value: Uint8Array, what: string, uses: KeyUsage[]) => {
144
+ const bytes = key32(value, what);
145
+ return keep(`${alg.name}|${uses.join('+')}|pk|${hex(bytes)}`, () => subtle().importKey('raw', bytes as BufferSource, alg, true, uses));
146
+ };
147
+
148
+ // Subtle exports the public half of a private key only through a JWK, where
149
+ // `x` is the 32 raw bytes in base64url. The answer is a fact about the key and
150
+ // never changes, so it is kept beside the key it was read from and goes when
151
+ // the key does.
152
+ const publics = new WeakMap<CryptoKey, Promise<Uint8Array>>();
153
+ function rawPublic(secret: CryptoKey): Promise<Uint8Array> {
154
+ const had = publics.get(secret);
155
+ if (had !== undefined) return had;
156
+ const read = (async () => {
157
+ const jwk = await subtle().exportKey('jwk', secret);
158
+ const binary = atob(jwk.x!.replaceAll('-', '+').replaceAll('_', '/'));
159
+ const out = new Uint8Array(binary.length);
160
+ for (let at = 0; at < binary.length; at += 1) out[at] = binary.charCodeAt(at);
161
+ return out;
162
+ })();
163
+ publics.set(secret, read);
164
+ return read;
100
165
  }
101
166
 
102
167
  export async function sha256(...parts: Uint8Array[]): Promise<Uint8Array> {
@@ -104,13 +169,17 @@ export async function sha256(...parts: Uint8Array[]): Promise<Uint8Array> {
104
169
  }
105
170
 
106
171
  export type Pair = { secret: Uint8Array; pk: Uint8Array };
172
+ // Both halves are copies. The seed and the public key are kept behind the two
173
+ // caches above, and a pair is handed to whoever asked for it: what she does
174
+ // with the bytes in her hand is hers, and must not reach what the next caller
175
+ // is given.
107
176
  export async function signingPair(seed: Uint8Array): Promise<Pair> {
108
177
  const secret = await secretKey(ED, ED_SECRET, seed, 'seed', ['sign']);
109
- return { secret: Uint8Array.from(seed), pk: await rawPublic(secret) };
178
+ return { secret: Uint8Array.from(seed), pk: Uint8Array.from(await rawPublic(secret)) };
110
179
  }
111
180
  export async function sealingPair(seed: Uint8Array): Promise<Pair> {
112
181
  const secret = await secretKey(X, X_SECRET, seed, 'seed', ['deriveBits']);
113
- return { secret: Uint8Array.from(seed), pk: await rawPublic(secret) };
182
+ return { secret: Uint8Array.from(seed), pk: Uint8Array.from(await rawPublic(secret)) };
114
183
  }
115
184
 
116
185
  export async function sign(message: Uint8Array, secret: Uint8Array): Promise<Uint8Array> {
@@ -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/seal.ts CHANGED
@@ -54,7 +54,7 @@ export const SIZE = 1024 * 1024;
54
54
  // designs and no separation at all: one secret would be doing two jobs with
55
55
  // nothing said about it, and a second kit would have to reproduce a
56
56
  // construction nobody named. HKDF-SHA-256 under a label is the separation
57
- // said out loud, and it is what `vectors/framing.json` pins.
57
+ // said out loud, and it is what `protocol/vectors/framing.json` pins.
58
58
  //
59
59
  // Bytes are key material and text is not. A seed handed in as bytes of the
60
60
  // key length is taken as it stands, which is what a harbor mints; anything