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