@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.
- package/GETTING_STARTED.md +128 -0
- package/README.md +14 -12
- package/dist/conformance/store.js +36 -10
- package/dist/harbor/core.js +12 -9
- package/dist/harbor/index.d.ts +2 -1
- package/dist/harbor/index.js +4 -1
- package/dist/harbor/store.d.ts +10 -3
- package/dist/harbor/store.js +34 -5
- package/dist/ward/ground.d.ts +1 -1
- package/dist/ward/partition.d.ts +4 -0
- package/dist/ward/partition.js +53 -0
- package/dist/ward/ward.js +29 -19
- package/package.json +5 -4
- package/quo-kit.md +15 -4
- package/src/conformance/store.ts +37 -10
- package/src/harbor/core.ts +17 -10
- package/src/harbor/index.ts +4 -1
- package/src/harbor/store.ts +39 -7
- package/src/ward/ground.ts +11 -4
- package/src/ward/partition.ts +53 -0
- package/src/ward/ward.ts +29 -19
|
@@ -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
|
|
17
|
-
any language must do for its bytes to be Quo. It is
|
|
18
|
-
assumes nothing from any other document, this README
|
|
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
|
|
43
|
-
`npm run
|
|
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
|
|
58
|
-
alone there too: `npm run typecheck`,
|
|
59
|
-
with type-aware rules, `lint:md` is
|
|
60
|
-
`npm
|
|
61
|
-
|
|
62
|
-
`npm run check
|
|
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 `
|
|
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
|
|
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
|
-
|
|
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,
|
|
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));
|
package/dist/harbor/core.js
CHANGED
|
@@ -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
|
|
123
|
-
// for every burst of writes,
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
package/dist/harbor/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/harbor/index.js
CHANGED
|
@@ -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.
|
package/dist/harbor/store.d.ts
CHANGED
|
@@ -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
|
|
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,
|
|
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
|
|
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 {};
|
package/dist/harbor/store.js
CHANGED
|
@@ -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
|
|
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:
|
|
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
|
-
|
|
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
|
|
52
|
+
keep(row, partition, rows);
|
|
24
53
|
}
|
|
25
54
|
async record(name, record) {
|
|
26
55
|
const row = this.rows.get(name);
|
package/dist/ward/ground.d.ts
CHANGED
|
@@ -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;
|
package/dist/ward/partition.d.ts
CHANGED
|
@@ -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>;
|
package/dist/ward/partition.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
102
|
-
//
|
|
103
|
-
|
|
104
|
-
|
|
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.
|
|
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
|
-
"
|
|
44
|
-
"
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
package/src/conformance/store.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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,
|
|
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));
|
package/src/harbor/core.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
159
|
-
// for every burst of writes,
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
package/src/harbor/index.ts
CHANGED
|
@@ -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
|
package/src/harbor/store.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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,
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
|
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);
|
package/src/ward/ground.ts
CHANGED
|
@@ -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
|
|
35
|
-
//
|
|
36
|
-
//
|
|
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
|
-
|
|
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
|
package/src/ward/partition.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
118
|
-
//
|
|
119
|
-
|
|
120
|
-
|
|
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
|
|
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
|
}
|