nervur 0.20.0 → 0.20.2
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/README.md +229 -20
- package/dist/being/being.d.ts +1 -0
- package/dist/being/being.js +5 -1
- package/dist/being/faculty.d.ts +1 -0
- package/dist/being/faculty.js +13 -7
- package/dist/being/index.d.ts +1 -0
- package/dist/being/index.js +1 -0
- package/dist/being/kind.d.ts +4 -0
- package/dist/being/kind.js +28 -0
- package/dist/being/types.d.ts +3 -4
- package/dist/browser/index.d.ts +35 -0
- package/dist/browser/index.js +188 -0
- package/dist/browser/worker.d.ts +12 -0
- package/dist/browser/worker.js +40 -0
- package/dist/browser.d.ts +1 -0
- package/dist/browser.js +7 -0
- package/dist/cli/command.d.ts +13 -0
- package/dist/cli/command.js +237 -0
- package/dist/cli/harbor.d.ts +19 -0
- package/dist/cli/harbor.js +99 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.js +14 -0
- package/dist/contract/index.d.ts +16 -2
- package/dist/contract/index.js +13 -3
- package/dist/edge/index.d.ts +57 -0
- package/dist/edge/index.js +306 -0
- package/dist/edge.d.ts +1 -0
- package/dist/edge.js +8 -0
- package/dist/folder/index.d.ts +9 -4
- package/dist/folder/index.js +89 -84
- package/dist/harbor/carrying.d.ts +17 -0
- package/dist/harbor/carrying.js +38 -0
- package/dist/harbor/catalogue.d.ts +27 -0
- package/dist/harbor/catalogue.js +101 -0
- package/dist/harbor/dock.d.ts +32 -3
- package/dist/harbor/dock.js +98 -12
- package/dist/harbor/harbor.d.ts +27 -8
- package/dist/harbor/harbor.js +217 -26
- package/dist/harbor/index.d.ts +8 -2
- package/dist/harbor/index.js +8 -1
- package/dist/harbor/package.d.ts +12 -0
- package/dist/harbor/package.js +130 -0
- package/dist/harbor/probe.d.ts +18 -0
- package/dist/harbor/probe.js +15 -0
- package/dist/harbor/registry.d.ts +6 -3
- package/dist/harbor/registry.js +83 -7
- package/dist/harbor/relay.d.ts +15 -0
- package/dist/harbor/relay.js +387 -0
- package/dist/harbor/root-line.d.ts +9 -0
- package/dist/harbor/root-line.js +38 -0
- package/dist/harbor/terrain.d.ts +14 -1
- package/dist/harbor/terrain.js +19 -0
- package/dist/http/index.d.ts +4 -0
- package/dist/http/index.js +156 -0
- package/dist/http/websocket.d.ts +29 -0
- package/dist/http/websocket.js +133 -0
- package/dist/index.d.ts +5 -4
- package/dist/index.js +12 -6
- package/dist/line/answer.d.ts +8 -0
- package/dist/line/answer.js +60 -0
- package/dist/line/frame.d.ts +34 -0
- package/dist/line/frame.js +81 -0
- package/dist/line/ground.d.ts +3 -0
- package/dist/line/ground.js +9 -0
- package/dist/line/index.d.ts +5 -0
- package/dist/line/index.js +9 -0
- package/dist/line/push.d.ts +7 -0
- package/dist/line/push.js +92 -0
- package/dist/line/web.d.ts +30 -0
- package/dist/line/web.js +181 -0
- package/dist/node/index.d.ts +32 -0
- package/dist/node/index.js +164 -0
- package/dist/node.d.ts +1 -0
- package/dist/node.js +7 -0
- package/dist/pointer/bodies.d.ts +11 -3
- package/dist/pointer/bodies.js +47 -18
- package/dist/pointer/index.d.ts +1 -1
- package/dist/pointer/index.js +1 -1
- package/dist/pointer/world.d.ts +12 -5
- package/dist/pointer/world.js +29 -14
- package/dist/quo/address.d.ts +19 -0
- package/dist/quo/address.js +84 -0
- package/dist/quo/door.d.ts +1 -1
- package/dist/quo/door.js +6 -4
- package/dist/quo/index.d.ts +1 -0
- package/dist/quo/index.js +1 -0
- package/dist/quo/invitation.d.ts +1 -0
- package/dist/quo/invitation.js +9 -5
- package/dist/quo/keys.js +1 -1
- package/dist/tcp/frame.d.ts +25 -0
- package/dist/tcp/frame.js +64 -0
- package/dist/tcp/index.d.ts +24 -0
- package/dist/tcp/index.js +260 -0
- package/dist/ward/index.d.ts +3 -2
- package/dist/ward/index.js +2 -1
- package/dist/ward/pilot.d.ts +8 -0
- package/dist/ward/pilot.js +50 -0
- package/dist/ward/stance.d.ts +4 -4
- package/dist/ward/stance.js +10 -7
- package/dist/ward/ward-being.d.ts +4 -1
- package/dist/ward/ward-being.js +8 -2
- package/dist/ward/ward.d.ts +11 -2
- package/dist/ward/ward.js +41 -5
- package/package.json +28 -2
- package/src/being/being.ts +5 -1
- package/src/being/faculty.ts +14 -7
- package/src/being/index.ts +1 -0
- package/src/being/kind.ts +30 -0
- package/src/being/types.ts +8 -6
- package/src/browser/index.ts +207 -0
- package/src/browser/worker.ts +66 -0
- package/src/browser.ts +9 -0
- package/src/cli/command.ts +224 -0
- package/src/cli/harbor.ts +103 -0
- package/src/cli/main.ts +13 -0
- package/src/contract/index.ts +42 -10
- package/src/edge/index.ts +359 -0
- package/src/edge.ts +10 -0
- package/src/folder/index.ts +87 -84
- package/src/harbor/carrying.ts +56 -0
- package/src/harbor/catalogue.ts +125 -0
- package/src/harbor/dock.ts +122 -15
- package/src/harbor/harbor.ts +240 -36
- package/src/harbor/index.ts +10 -3
- package/src/harbor/package.ts +147 -0
- package/src/harbor/probe.ts +34 -0
- package/src/harbor/registry.ts +73 -9
- package/src/harbor/relay.ts +399 -0
- package/src/harbor/root-line.ts +39 -0
- package/src/harbor/terrain.ts +31 -3
- package/src/http/index.ts +150 -0
- package/src/http/websocket.ts +124 -0
- package/src/index.ts +39 -6
- package/src/line/answer.ts +59 -0
- package/src/line/frame.ts +88 -0
- package/src/line/ground.ts +19 -0
- package/src/line/index.ts +9 -0
- package/src/line/push.ts +112 -0
- package/src/line/web.ts +227 -0
- package/src/node/index.ts +170 -0
- package/src/node.ts +9 -0
- package/src/pointer/bodies.ts +45 -16
- package/src/pointer/index.ts +1 -1
- package/src/pointer/world.ts +38 -19
- package/src/quo/address.ts +87 -0
- package/src/quo/door.ts +6 -4
- package/src/quo/index.ts +1 -0
- package/src/quo/invitation.ts +10 -6
- package/src/quo/keys.ts +1 -1
- package/src/stand/main.ts +1 -0
- package/src/stand/stand.ts +87 -6
- package/src/tcp/index.ts +284 -0
- package/src/ward/index.ts +3 -2
- package/src/ward/pilot.ts +50 -0
- package/src/ward/stance.ts +14 -8
- package/src/ward/ward-being.ts +9 -3
- package/src/ward/ward.ts +53 -6
package/dist/ward/stance.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// standing, one send at a time, inside her allowance.
|
|
5
5
|
import { readValue, writeValue } from '../crypto/index.js';
|
|
6
6
|
import { readInvitation, Standing as Wire, freshStanding } from '../quo/index.js';
|
|
7
|
-
import { OWNER, silence, word } from '../being/index.js';
|
|
7
|
+
import { OWNER, ownKind, silence, word } from '../being/index.js';
|
|
8
8
|
import { allow, Bell, LATE } from './allowance.js';
|
|
9
9
|
import { Cells, DEPTH } from './cells.js';
|
|
10
10
|
export { DEPTH };
|
|
@@ -65,18 +65,19 @@ export class WardStance {
|
|
|
65
65
|
get #live() {
|
|
66
66
|
return this.#inside.live(this.key, this.#row);
|
|
67
67
|
}
|
|
68
|
-
boot(
|
|
69
|
-
return this.#live ? this.#inside.boot(this.key,
|
|
68
|
+
boot(kind, key, id) {
|
|
69
|
+
return this.#live ? this.#inside.boot(this.key, kind, key, id) : Promise.resolve(null);
|
|
70
70
|
}
|
|
71
71
|
async lend(contract, id) {
|
|
72
|
-
|
|
72
|
+
const kind = ownKind(contract);
|
|
73
|
+
if (kind === undefined || !this.#free(id, this.#row.standings))
|
|
73
74
|
return null;
|
|
74
|
-
const invitation = await this.#inside.offer(
|
|
75
|
+
const invitation = await this.#inside.offer(kind);
|
|
75
76
|
if (invitation === null)
|
|
76
77
|
return null;
|
|
77
78
|
const taken = await this.#take(id, invitation);
|
|
78
79
|
if (taken === null)
|
|
79
|
-
await this.#inside.retract(
|
|
80
|
+
await this.#inside.retract(kind, invitation.heir);
|
|
80
81
|
return taken;
|
|
81
82
|
}
|
|
82
83
|
#free(id, of) {
|
|
@@ -86,7 +87,7 @@ export class WardStance {
|
|
|
86
87
|
const kept = writeValue(notes, DEPTH);
|
|
87
88
|
if (!this.#free(id, this.#row.occupants) || kept === undefined)
|
|
88
89
|
return null;
|
|
89
|
-
const { heir, invitation } = await this.#inside.door.invite();
|
|
90
|
+
const { heir, invitation } = await this.#inside.door.invite(this.#inside.reach());
|
|
90
91
|
if (!this.#free(id, this.#row.occupants)) {
|
|
91
92
|
this.#inside.door.remove(heir);
|
|
92
93
|
return null;
|
|
@@ -112,6 +113,8 @@ export class WardStance {
|
|
|
112
113
|
const invitation = readInvitation(value);
|
|
113
114
|
if (!invitation || !this.#free(id, this.#row.standings))
|
|
114
115
|
return null;
|
|
116
|
+
if (invitation.at)
|
|
117
|
+
await this.#inside.learn(invitation.ward, invitation.at);
|
|
115
118
|
const wire = new Wire(invitation, freshStanding(), this.#inside.draw);
|
|
116
119
|
const read = await this.#lanes.run(`take:${id}`, async () => {
|
|
117
120
|
// A knock that brought nothing back may have bound: the standing asks
|
|
@@ -18,7 +18,10 @@ export type WardBeingStance = Stance & {
|
|
|
18
18
|
};
|
|
19
19
|
export declare const pilots: (asker: Asker, notes: JsonObject | undefined) => boolean;
|
|
20
20
|
export declare const ownerAsk: (description: string, input?: JsonObject) => AskSpec;
|
|
21
|
+
export declare const rootAsk: (description: string, input?: JsonObject) => AskSpec;
|
|
22
|
+
export declare const PILOT = "org.nervur.pilot";
|
|
21
23
|
export declare class WardBeing extends Being {
|
|
24
|
+
static readonly kind: string;
|
|
22
25
|
static asks: Record<string, AskSpec>;
|
|
23
26
|
readonly steward: Steward;
|
|
24
27
|
constructor(stance: WardBeingStance);
|
|
@@ -26,7 +29,7 @@ export declare class WardBeing extends Being {
|
|
|
26
29
|
own(args: JsonObject): Promise<JsonObject>;
|
|
27
30
|
disown(args: JsonObject): JsonObject;
|
|
28
31
|
boot(args: JsonObject): Promise<JsonObject>;
|
|
29
|
-
unboot(args: JsonObject): JsonObject
|
|
32
|
+
unboot(args: JsonObject): JsonObject | Promise<JsonObject>;
|
|
30
33
|
public(args: JsonObject): JsonObject;
|
|
31
34
|
invite(args: JsonObject): Promise<JsonObject>;
|
|
32
35
|
ask(args: JsonObject): Promise<JsonObject>;
|
package/dist/ward/ward-being.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// The ward-being: the first being of every ward, the ward itself. Her asks
|
|
3
3
|
// are the root's, shown and answered to `OWNER` alone.
|
|
4
|
-
import { Being, isSilence, isWord, OWNER, told } from '../being/index.js';
|
|
4
|
+
import { Being, isSilence, isWord, KIT, OWNER, told } from '../being/index.js';
|
|
5
5
|
// Whoever pilots a ward: the root, holding its unsealed ask, or an owner,
|
|
6
6
|
// an occupant of the ward-being the root invited as one.
|
|
7
7
|
export const pilots = (asker, notes) => asker.id === OWNER || notes?.owner === true;
|
|
@@ -9,10 +9,14 @@ export const pilots = (asker, notes) => asker.id === OWNER || notes?.owner === t
|
|
|
9
9
|
export const ownerAsk = (description, input = {}) => ({ description, input: { type: 'object', ...input }, for: pilots });
|
|
10
10
|
// An ask the root alone may make, so a carried key pilots but never hands
|
|
11
11
|
// piloting on.
|
|
12
|
-
const rootAsk = (description, input = {}) => ({ description, input: { type: 'object', ...input }, for: (asker) => asker.id === OWNER });
|
|
12
|
+
export const rootAsk = (description, input = {}) => ({ description, input: { type: 'object', ...input }, for: (asker) => asker.id === OWNER });
|
|
13
|
+
// The one kit kind the root boots: every other kit class stands where the
|
|
14
|
+
// kit puts it.
|
|
15
|
+
export const PILOT = 'org.nervur.pilot';
|
|
13
16
|
const text = (v) => typeof v === 'string';
|
|
14
17
|
const object = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
15
18
|
export class WardBeing extends Being {
|
|
19
|
+
static kind = 'org.nervur.ward-being';
|
|
16
20
|
static asks = {
|
|
17
21
|
boot: ownerAsk('a new being of this ward', { required: ['key', 'class'] }),
|
|
18
22
|
unboot: ownerAsk('a being out, with every relation she holds', { required: ['being'] }),
|
|
@@ -45,6 +49,8 @@ export class WardBeing extends Being {
|
|
|
45
49
|
async boot(args) {
|
|
46
50
|
if (!text(args.key) || !text(args.class))
|
|
47
51
|
return { error: 'key and class are text' };
|
|
52
|
+
if (args.class.startsWith(KIT) && args.class !== PILOT)
|
|
53
|
+
return { error: 'no such class' };
|
|
48
54
|
const out = await this.steward.boot(args.key, args.class);
|
|
49
55
|
return out === 'booted' ? { booted: args.key } : { error: out };
|
|
50
56
|
}
|
package/dist/ward/ward.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { Door, WardKey, type Behind, type Choice, type Invitation, type Payload } from '../quo/index.ts';
|
|
2
2
|
import type { Carrier, Clock, Entropy, Rows } from '../contract/index.ts';
|
|
3
|
-
import { type Answer, type BeingClass, type JsonObject } from '../being/index.ts';
|
|
3
|
+
import { type Answer, type BeingClass, type BeingLike, type JsonObject } from '../being/index.ts';
|
|
4
4
|
import { Partition, type BeingRow } from './partition.ts';
|
|
5
5
|
import { type Inside } from './stance.ts';
|
|
6
6
|
import { WardBeing, type WardBeingStance } from './ward-being.ts';
|
|
7
7
|
export interface Catalogue {
|
|
8
|
-
classOf(
|
|
8
|
+
classOf(kind: string): BeingClass | undefined;
|
|
9
9
|
}
|
|
10
10
|
export type WardParts = {
|
|
11
11
|
readonly seed: Uint8Array | string;
|
|
@@ -16,7 +16,12 @@ export type WardParts = {
|
|
|
16
16
|
readonly carrier: Carrier;
|
|
17
17
|
readonly root?: (stance: WardBeingStance) => WardBeing;
|
|
18
18
|
readonly dock?: Invitation;
|
|
19
|
+
readonly routes?: Routes;
|
|
19
20
|
};
|
|
21
|
+
export interface Routes {
|
|
22
|
+
reach(): readonly string[];
|
|
23
|
+
learn(ward: string, at: readonly string[]): Promise<void>;
|
|
24
|
+
}
|
|
20
25
|
export declare const DOCK = "dock";
|
|
21
26
|
export interface Lender {
|
|
22
27
|
offer(contract: string): Promise<Invitation | null>;
|
|
@@ -33,12 +38,16 @@ export declare class Ward implements Behind, Inside {
|
|
|
33
38
|
get pk(): string;
|
|
34
39
|
get draw(): import("../quo/keys.ts").Draw;
|
|
35
40
|
get wardBeing(): WardBeing;
|
|
41
|
+
being(key: string): BeingLike | undefined;
|
|
42
|
+
bearAnew(): void;
|
|
36
43
|
root(method?: string, args?: JsonObject): Promise<Answer>;
|
|
37
44
|
get zero(): boolean;
|
|
38
45
|
answer(heir: string | null, p: Payload): Promise<Choice>;
|
|
39
46
|
send(pk: string, bytes: Uint8Array): Promise<Uint8Array | null>;
|
|
40
47
|
offer(contract: string): Promise<Invitation | null>;
|
|
41
48
|
retract(contract: string, heir: string): Promise<void>;
|
|
49
|
+
reach(): readonly string[];
|
|
50
|
+
learn(ward: string, at: readonly string[]): Promise<void>;
|
|
42
51
|
live(key: string, row: BeingRow): boolean;
|
|
43
52
|
boot(maker: string, className: string, key: string, id?: string): Promise<string | null>;
|
|
44
53
|
}
|
package/dist/ward/ward.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// what is behind its door for `quo/`.
|
|
6
6
|
import { readValue, writeValue } from '../crypto/index.js';
|
|
7
7
|
import { Door, readInvitation, WardKey } from '../quo/index.js';
|
|
8
|
-
import { digest, isSilence, isWord, OWNER, silence, WARD, word } from '../being/index.js';
|
|
8
|
+
import { digest, isSilence, isWord, kindOf, OWNER, silence, WARD, word } from '../being/index.js';
|
|
9
9
|
import { Partition, PartitionRelations } from './partition.js';
|
|
10
10
|
import { DEPTH, WardStance } from './stance.js';
|
|
11
11
|
import { WardBeing } from './ward-being.js';
|
|
@@ -47,10 +47,13 @@ export class Ward {
|
|
|
47
47
|
const own = ward.partition.being(WARD);
|
|
48
48
|
const stance = new RootStance(ward, own, ward.#steward);
|
|
49
49
|
const being = parts.root ? parts.root(stance) : new WardBeing(stance);
|
|
50
|
-
own.class = being.constructor
|
|
50
|
+
own.class = kindOf(being.constructor);
|
|
51
51
|
ward.#residents.set(WARD, { being, row: own, stance });
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
// The standing on the dock is kept at once: its heir is spent, so a
|
|
53
|
+
// restart before the ward's first keep would find no way back.
|
|
54
|
+
if (parts.dock && !Object.hasOwn(own.standings, DOCK) && (await stance.standings.take(DOCK, parts.dock)) !== null && !(await ward.partition.kept())) {
|
|
55
|
+
throw new Error('the standing on the dock was not kept');
|
|
56
|
+
}
|
|
54
57
|
for (const [key, row] of Object.entries(ward.partition.beings)) {
|
|
55
58
|
const C = key === WARD ? undefined : parts.catalogue.classOf(row.class);
|
|
56
59
|
if (C)
|
|
@@ -67,6 +70,33 @@ export class Ward {
|
|
|
67
70
|
get wardBeing() {
|
|
68
71
|
return this.#residents.get(WARD).being;
|
|
69
72
|
}
|
|
73
|
+
// The being standing under a key, for the harbor's own faculties.
|
|
74
|
+
being(key) {
|
|
75
|
+
return this.#residents.get(key)?.being;
|
|
76
|
+
}
|
|
77
|
+
// Every being her row names, of the class the catalogue now resolves for
|
|
78
|
+
// her kind: an absent being is born, and one whose class a module
|
|
79
|
+
// replaced is born again of the new class on the stance she holds, so
|
|
80
|
+
// her relations and their lanes stay hers. A new class that throws at
|
|
81
|
+
// birth leaves her as she was.
|
|
82
|
+
bearAnew() {
|
|
83
|
+
for (const [key, row] of Object.entries(this.partition.beings)) {
|
|
84
|
+
const C = key === WARD ? undefined : this.#parts.catalogue.classOf(row.class);
|
|
85
|
+
const resident = this.#residents.get(key);
|
|
86
|
+
if (!C || resident?.being.constructor === C)
|
|
87
|
+
continue;
|
|
88
|
+
if (!resident) {
|
|
89
|
+
this.#bear(key, row, C);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
resident.being = new C(resident.stance);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
70
100
|
// The unsealed ask: the ward-being, asked by whoever holds this ward.
|
|
71
101
|
async root(method, args = {}) {
|
|
72
102
|
return this.#kept(WARD, () => this.#answer(WARD, { id: OWNER }, method, args));
|
|
@@ -89,7 +119,7 @@ export class Ward {
|
|
|
89
119
|
}
|
|
90
120
|
return silence;
|
|
91
121
|
}
|
|
92
|
-
// Behind: the zero head reaches the public being,
|
|
122
|
+
// Behind: the zero head reaches the public being, an heir the being that
|
|
93
123
|
// invited it.
|
|
94
124
|
get zero() {
|
|
95
125
|
const key = this.partition.head.public;
|
|
@@ -147,6 +177,12 @@ export class Ward {
|
|
|
147
177
|
return own.being.withdraw(contract, heir);
|
|
148
178
|
await own.stance.standings.get(DOCK)?.ask('retract', { contract, heir });
|
|
149
179
|
}
|
|
180
|
+
reach() {
|
|
181
|
+
return this.#parts.routes?.reach() ?? [];
|
|
182
|
+
}
|
|
183
|
+
learn(ward, at) {
|
|
184
|
+
return this.#parts.routes?.learn(ward, at) ?? Promise.resolve();
|
|
185
|
+
}
|
|
150
186
|
live(key, row) {
|
|
151
187
|
return this.#residents.get(key)?.row === row;
|
|
152
188
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nervur",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.2",
|
|
4
4
|
"description": "Nervur's kit of Quo: an object asks another object and gets an answer, without knowing where it is.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"quo",
|
|
@@ -12,21 +12,47 @@
|
|
|
12
12
|
"license": "Apache-2.0",
|
|
13
13
|
"homepage": "https://nervur.org",
|
|
14
14
|
"type": "module",
|
|
15
|
+
"bin": {
|
|
16
|
+
"nervur": "dist/cli/main.js"
|
|
17
|
+
},
|
|
15
18
|
"exports": {
|
|
16
19
|
".": {
|
|
17
20
|
"types": "./dist/index.d.ts",
|
|
21
|
+
"workerd": "./dist/edge.js",
|
|
22
|
+
"browser": "./dist/browser.js",
|
|
23
|
+
"node": "./dist/node.js",
|
|
18
24
|
"default": "./dist/index.js"
|
|
19
25
|
},
|
|
26
|
+
"./edge": {
|
|
27
|
+
"types": "./dist/edge/index.d.ts",
|
|
28
|
+
"default": "./dist/edge/index.js"
|
|
29
|
+
},
|
|
30
|
+
"./browser": {
|
|
31
|
+
"types": "./dist/browser/index.d.ts",
|
|
32
|
+
"default": "./dist/browser/index.js"
|
|
33
|
+
},
|
|
34
|
+
"./node": {
|
|
35
|
+
"types": "./dist/node/index.d.ts",
|
|
36
|
+
"default": "./dist/node/index.js"
|
|
37
|
+
},
|
|
20
38
|
"./folder": {
|
|
21
39
|
"types": "./dist/folder/index.d.ts",
|
|
22
40
|
"default": "./dist/folder/index.js"
|
|
41
|
+
},
|
|
42
|
+
"./tcp": {
|
|
43
|
+
"types": "./dist/tcp/index.d.ts",
|
|
44
|
+
"default": "./dist/tcp/index.js"
|
|
45
|
+
},
|
|
46
|
+
"./http": {
|
|
47
|
+
"types": "./dist/http/index.d.ts",
|
|
48
|
+
"default": "./dist/http/index.js"
|
|
23
49
|
}
|
|
24
50
|
},
|
|
25
51
|
"scripts": {
|
|
26
52
|
"build": "tsc -p tsconfig.build.json",
|
|
27
53
|
"check": "tsc -p tsconfig.json && npm run build && node ../../test.mjs \"test/unit/*.test.ts\" \"test/e2e/*.test.ts\"",
|
|
28
54
|
"deep": "node ../../test.mjs \"test/terrain/*.test.ts\"",
|
|
29
|
-
"cover": "node --test --test-timeout=60000 --experimental-test-coverage --test-coverage-exclude=\"**/dist/**\" --test-coverage-exclude=\"**/test/**\" --test-coverage-exclude=\"**/src/stand/**\" --test-coverage-exclude=\"../../quo/**\" --test-coverage-lines=99 --test-coverage-branches=93 --test-coverage-functions=95 \"test/unit/*.test.ts\" \"test/e2e/*.test.ts\"",
|
|
55
|
+
"cover": "node --test --test-timeout=60000 --experimental-test-coverage --test-coverage-exclude=\"**/dist/**\" --test-coverage-exclude=\"**/test/**\" --test-coverage-exclude=\"**/src/stand/**\" --test-coverage-exclude=\"**/src/browser/**\" --test-coverage-exclude=\"**/src/edge/**\" --test-coverage-exclude=\"../../quo/**\" --test-coverage-lines=99 --test-coverage-branches=93 --test-coverage-functions=95 \"test/unit/*.test.ts\" \"test/e2e/*.test.ts\"",
|
|
30
56
|
"prepublishOnly": "test \"$NERVUR_GATED\" = 1 || { echo 'a publish is /release, from the root, on the human'\"'\"'s word' >&2; exit 1; }"
|
|
31
57
|
},
|
|
32
58
|
"engines": {
|
package/src/being/being.ts
CHANGED
|
@@ -28,8 +28,12 @@ const wrote = (self: object, name: string): boolean => {
|
|
|
28
28
|
type Method = (args: JsonObject, asker: Asker) => Reply | Promise<Reply>;
|
|
29
29
|
|
|
30
30
|
// A subclass's statics replace its parent's and never merge: her blueprint
|
|
31
|
-
// is what the class in front of you declares, in its order.
|
|
31
|
+
// is what the class in front of you declares, in its order. Her kind is
|
|
32
|
+
// never inherited: see `kind.ts`.
|
|
32
33
|
export class Being {
|
|
34
|
+
// The name her row keeps for this class. Every class that stands declares
|
|
35
|
+
// its own.
|
|
36
|
+
static readonly kind: string = 'org.nervur.being';
|
|
33
37
|
// Her cells' defaults, written at birth only where a key is missing.
|
|
34
38
|
static cells: JsonObject = {};
|
|
35
39
|
static asks: Record<string, AskSpec> = {};
|
package/src/being/faculty.ts
CHANGED
|
@@ -2,23 +2,30 @@
|
|
|
2
2
|
// A faculty: a being of the box ward that fulfils a contract. A contract is
|
|
3
3
|
// a class between `Faculty` and the class that stands, usually abstract:
|
|
4
4
|
//
|
|
5
|
-
// abstract class Timer extends Faculty {
|
|
6
|
-
//
|
|
5
|
+
// abstract class Timer extends Faculty { the contract
|
|
6
|
+
// static override kind = 'com.acme.timer' }
|
|
7
|
+
// class IntervalTimer extends Timer { one class that fulfils it
|
|
8
|
+
// static override kind = 'com.acme.interval-timer' }
|
|
7
9
|
//
|
|
8
|
-
// A being lends by the contract and never learns the class.
|
|
10
|
+
// A being lends by the contract's kind and never learns the class. Every
|
|
11
|
+
// class between the faculty and `Faculty` is a contract and declares its
|
|
12
|
+
// own kind, and a faculty fulfils each of them and her own. Two asks are the
|
|
9
13
|
// faculty's own and answered to the ward-being that opened her alone:
|
|
10
14
|
// `offer`, an invitation on herself for a ward the dock vouches for, and
|
|
11
15
|
// `retract`, an offer that was not taken.
|
|
12
16
|
import { Being } from './being.ts';
|
|
17
|
+
import { kindOf } from './kind.ts';
|
|
13
18
|
import { WARD, type Asker, type Blueprint, type JsonObject, type Reply } from './types.ts';
|
|
14
19
|
|
|
15
20
|
export class Faculty extends Being {
|
|
21
|
+
static override readonly kind: string = 'org.nervur.faculty';
|
|
22
|
+
|
|
16
23
|
// The contracts a class fulfils: itself and every class between it and
|
|
17
|
-
// `Faculty`, by
|
|
24
|
+
// `Faculty`, by kind. A class among them with no kind of its own throws.
|
|
18
25
|
static contracts(C: abstract new (...args: never[]) => unknown): string[] {
|
|
19
|
-
const
|
|
20
|
-
for (let c: unknown = C; typeof c === 'function' && c !== Faculty; c = Object.getPrototypeOf(c))
|
|
21
|
-
return
|
|
26
|
+
const kinds: string[] = [];
|
|
27
|
+
for (let c: unknown = C; typeof c === 'function' && c !== Faculty; c = Object.getPrototypeOf(c)) kinds.push(kindOf(c));
|
|
28
|
+
return kinds;
|
|
22
29
|
}
|
|
23
30
|
|
|
24
31
|
static fulfils(C: unknown): C is typeof Faculty {
|
package/src/being/index.ts
CHANGED
|
@@ -3,5 +3,6 @@
|
|
|
3
3
|
export { Being, type AskSpec } from './being.ts';
|
|
4
4
|
export { canonical, digest } from './digest.ts';
|
|
5
5
|
export { Faculty } from './faculty.ts';
|
|
6
|
+
export { isKind, KIT, kindOf, ownKind } from './kind.ts';
|
|
6
7
|
export { OWNER, WARD, type Answer, type Ask, type Asker, type BeingClass, type BeingLike, type Blueprint, type Invitation, type Json, type JsonObject, type Occupants, type Reply, type Schema, type Stance, type Standing, type Standings, type Wanted } from './types.ts';
|
|
7
8
|
export { isSilence, isWord, silence, told, word, type Silence, type Word, type WordName } from './words.ts';
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// A kind: the name a row keeps for the class a being is born of, and the
|
|
3
|
+
// name a contract is lent by. It is declared on the class, never taken from
|
|
4
|
+
// the name the language gives it, because a bundler renames classes.
|
|
5
|
+
//
|
|
6
|
+
// static kind = 'com.acme.shop'
|
|
7
|
+
//
|
|
8
|
+
// A kind is a reversed domain its author owns, then a name: at least three
|
|
9
|
+
// segments joined by `.`, each lowercase letters, digits and `-`, starting
|
|
10
|
+
// with a letter or digit. `org.nervur.` is the kit's own. A class declares
|
|
11
|
+
// its own kind: one it inherits names its parent, so it is none.
|
|
12
|
+
export const KIT = 'org.nervur.';
|
|
13
|
+
|
|
14
|
+
const KIND = /^[a-z0-9][a-z0-9-]*(?:\.[a-z0-9][a-z0-9-]*){2,}$/;
|
|
15
|
+
|
|
16
|
+
export const isKind = (text: unknown): text is string => typeof text === 'string' && KIND.test(text);
|
|
17
|
+
|
|
18
|
+
// The kind a class declares on itself, or undefined.
|
|
19
|
+
export const ownKind = (C: unknown): string | undefined => {
|
|
20
|
+
if (typeof C !== 'function' || !Object.hasOwn(C, 'kind')) return undefined;
|
|
21
|
+
const kind = (C as { kind?: unknown }).kind;
|
|
22
|
+
return isKind(kind) ? kind : undefined;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// The kind a class declares on itself, or a throw naming the class.
|
|
26
|
+
export const kindOf = (C: unknown): string => {
|
|
27
|
+
const kind = ownKind(C);
|
|
28
|
+
if (kind === undefined) throw new Error(`class ${typeof C === 'function' ? C.name : String(C)} declares no kind of its own`);
|
|
29
|
+
return kind;
|
|
30
|
+
};
|
package/src/being/types.ts
CHANGED
|
@@ -56,12 +56,13 @@ export interface Stance {
|
|
|
56
56
|
readonly cells: JsonObject;
|
|
57
57
|
readonly occupants: Occupants;
|
|
58
58
|
readonly standings: Standings;
|
|
59
|
-
// A new being of her ward, by
|
|
60
|
-
//
|
|
61
|
-
boot(
|
|
59
|
+
// A new being of her ward, by kind, under a key she chooses. With an id,
|
|
60
|
+
// the new being invites her under her key, and she takes it under id.
|
|
61
|
+
boot(kind: string, key: string, id?: string): Promise<string | null>;
|
|
62
62
|
// A standing under `id` at the faculty this harbor opened for a contract:
|
|
63
|
-
// the id, or null where none is open
|
|
64
|
-
|
|
63
|
+
// the id, or null where none is open, the contract declares no kind of
|
|
64
|
+
// its own, or the relation was refused.
|
|
65
|
+
lend(contract: abstract new (...args: never[]) => unknown, id: string): Promise<string | null>;
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
// Anything with `answer` is a being.
|
|
@@ -69,4 +70,5 @@ export interface BeingLike {
|
|
|
69
70
|
answer(asker: Asker, method: string | undefined, args: JsonObject): Reply | Promise<Reply>;
|
|
70
71
|
describe?(asker: Asker): Blueprint;
|
|
71
72
|
}
|
|
72
|
-
|
|
73
|
+
// A class that stands. Its kind is its own, as `kind.ts` says.
|
|
74
|
+
export type BeingClass = (new (stance: Stance) => BeingLike) & { readonly name: string; readonly kind: string };
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// `nervur/browser`: the browser ground, in a page or a worker. A harbor
|
|
3
|
+
// keeps its sealed memory and its seed in one IndexedDB database, named
|
|
4
|
+
// by `where`, speaks the web, and holds its database to one run with a
|
|
5
|
+
// Web Lock of the same name.
|
|
6
|
+
//
|
|
7
|
+
// entries one sealed entry per key [place, name]
|
|
8
|
+
// custody the seed, sealed under an AES-GCM key the engine never
|
|
9
|
+
// hands out, itself kept as the engine keeps a CryptoKey
|
|
10
|
+
//
|
|
11
|
+
// A keep is one transaction, so it is whole or nothing by IndexedDB's own
|
|
12
|
+
// rule.
|
|
13
|
+
import { Custody, Memory, type Entropy, type Module } from '../contract/index.ts';
|
|
14
|
+
import type { GroundProbe } from '../harbor/index.ts';
|
|
15
|
+
import { webFaculty, WEB_OPENING, webPush } from '../line/index.ts';
|
|
16
|
+
import { CryptoEntropy, HeldLoader, PointerTerrain } from '../pointer/index.ts';
|
|
17
|
+
|
|
18
|
+
export { ringAll, rootOf, serveWorker, type Reaching } from './worker.ts';
|
|
19
|
+
|
|
20
|
+
export const BROWSER = 'browser';
|
|
21
|
+
// The database a harbor keeps itself in unless named.
|
|
22
|
+
export const BROWSER_HARBOR = 'nervur';
|
|
23
|
+
|
|
24
|
+
const ENTRIES = 'entries';
|
|
25
|
+
const CUSTODY = 'custody';
|
|
26
|
+
const SEED = 'seed';
|
|
27
|
+
// Past every lowercase hex name, so [place, END] bounds a place's keys.
|
|
28
|
+
const END = '';
|
|
29
|
+
|
|
30
|
+
type Sealed = { key: CryptoKey; iv: Uint8Array<ArrayBuffer>; sealed: ArrayBuffer };
|
|
31
|
+
|
|
32
|
+
const done = <T>(request: IDBRequest<T>): Promise<T> =>
|
|
33
|
+
new Promise((resolve, reject) => {
|
|
34
|
+
request.onsuccess = () => resolve(request.result);
|
|
35
|
+
request.onerror = () => reject(request.error ?? new Error('IndexedDB refused'));
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// A transaction's end. A request that fails aborts it, and the abort
|
|
39
|
+
// carries that request's error.
|
|
40
|
+
const finished = (tx: IDBTransaction): Promise<void> =>
|
|
41
|
+
new Promise((resolve, reject) => {
|
|
42
|
+
tx.oncomplete = () => resolve();
|
|
43
|
+
tx.onabort = () => reject(tx.error ?? new Error('the keep was aborted'));
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// One database, opened once per body and made on first open.
|
|
47
|
+
class Database {
|
|
48
|
+
readonly name: string;
|
|
49
|
+
#open: Promise<IDBDatabase> | undefined;
|
|
50
|
+
constructor(name: string) {
|
|
51
|
+
this.name = name;
|
|
52
|
+
}
|
|
53
|
+
get(): Promise<IDBDatabase> {
|
|
54
|
+
this.#open ??= new Promise((resolve, reject) => {
|
|
55
|
+
const request = indexedDB.open(this.name, 1);
|
|
56
|
+
request.onupgradeneeded = () => {
|
|
57
|
+
request.result.createObjectStore(ENTRIES);
|
|
58
|
+
request.result.createObjectStore(CUSTODY);
|
|
59
|
+
};
|
|
60
|
+
request.onsuccess = () => resolve(request.result);
|
|
61
|
+
request.onerror = () => reject(request.error ?? new Error(`${this.name} did not open`));
|
|
62
|
+
});
|
|
63
|
+
return this.#open;
|
|
64
|
+
}
|
|
65
|
+
async close(): Promise<void> {
|
|
66
|
+
const open = this.#open;
|
|
67
|
+
this.#open = undefined;
|
|
68
|
+
if (open) (await open).close();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export class IdbMemory extends Memory {
|
|
73
|
+
readonly #db: Database;
|
|
74
|
+
constructor(name: string = BROWSER_HARBOR) {
|
|
75
|
+
super();
|
|
76
|
+
this.#db = new Database(name);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// The database it keeps in.
|
|
80
|
+
get name(): string {
|
|
81
|
+
return this.#db.name;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async read(place: string): Promise<Map<string, Uint8Array>> {
|
|
85
|
+
const store = (await this.#db.get()).transaction(ENTRIES).objectStore(ENTRIES);
|
|
86
|
+
const range = IDBKeyRange.bound([place, ''], [place, END]);
|
|
87
|
+
const [keys, values] = await Promise.all([done(store.getAllKeys(range)), done(store.getAll(range))]);
|
|
88
|
+
return new Map(keys.map((key, i) => [(key as [string, string])[1], new Uint8Array(values[i] as ArrayBuffer)]));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async write(place: string, entries: ReadonlyMap<string, Uint8Array | null>): Promise<void> {
|
|
92
|
+
const tx = (await this.#db.get()).transaction(ENTRIES, 'readwrite');
|
|
93
|
+
const kept = finished(tx);
|
|
94
|
+
const store = tx.objectStore(ENTRIES);
|
|
95
|
+
try {
|
|
96
|
+
for (const [name, bytes] of entries) this.put(store, [place, name], bytes === null ? null : bytes.slice().buffer);
|
|
97
|
+
} catch (e) {
|
|
98
|
+
tx.abort();
|
|
99
|
+
await kept.catch(() => undefined);
|
|
100
|
+
throw e;
|
|
101
|
+
}
|
|
102
|
+
await kept;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// One entry of a keep: its bytes, or null where it goes.
|
|
106
|
+
protected put(store: IDBObjectStore, key: [string, string], bytes: ArrayBuffer | null): void {
|
|
107
|
+
if (bytes === null) store.delete(key);
|
|
108
|
+
else store.put(bytes, key);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async places(): Promise<string[]> {
|
|
112
|
+
const keys = await done((await this.#db.get()).transaction(ENTRIES).objectStore(ENTRIES).getAllKeys());
|
|
113
|
+
return [...new Set(keys.map((key) => (key as [string, string])[0]))];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async forget(place: string): Promise<void> {
|
|
117
|
+
const tx = (await this.#db.get()).transaction(ENTRIES, 'readwrite');
|
|
118
|
+
tx.objectStore(ENTRIES).delete(IDBKeyRange.bound([place, ''], [place, END]));
|
|
119
|
+
await finished(tx);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
close(): Promise<void> {
|
|
123
|
+
return this.#db.close();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export class IdbCustody extends Custody {
|
|
128
|
+
readonly #db: Database;
|
|
129
|
+
readonly #entropy: Entropy;
|
|
130
|
+
constructor(name: string = BROWSER_HARBOR, entropy: Entropy = new CryptoEntropy()) {
|
|
131
|
+
super();
|
|
132
|
+
this.#db = new Database(name);
|
|
133
|
+
this.#entropy = entropy;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// The seed the database holds, or a drawn one sealed there the first
|
|
137
|
+
// time. Two first asks at once keep the one added first.
|
|
138
|
+
async seed(): Promise<Uint8Array> {
|
|
139
|
+
const db = await this.#db.get();
|
|
140
|
+
const kept = (await done(db.transaction(CUSTODY).objectStore(CUSTODY).get(SEED))) as Sealed | undefined;
|
|
141
|
+
if (kept) return new Uint8Array(await crypto.subtle.decrypt({ name: 'AES-GCM', iv: kept.iv }, kept.key, kept.sealed));
|
|
142
|
+
const seed = this.#entropy.draw(32);
|
|
143
|
+
const key = await crypto.subtle.generateKey({ name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt']);
|
|
144
|
+
const iv = new Uint8Array(this.#entropy.draw(12));
|
|
145
|
+
const sealed = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, new Uint8Array(seed));
|
|
146
|
+
const tx = db.transaction(CUSTODY, 'readwrite');
|
|
147
|
+
tx.objectStore(CUSTODY).add({ key, iv, sealed } satisfies Sealed, SEED);
|
|
148
|
+
try {
|
|
149
|
+
await finished(tx);
|
|
150
|
+
} catch (e) {
|
|
151
|
+
if ((e as { name?: string }).name === 'ConstraintError') return this.seed();
|
|
152
|
+
throw e;
|
|
153
|
+
}
|
|
154
|
+
return seed;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
close(): Promise<void> {
|
|
158
|
+
return this.#db.close();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export type BrowserParts = { readonly where?: string; readonly modules?: readonly Module[] };
|
|
163
|
+
|
|
164
|
+
// A harbor of the browser: its database, the web opened at genesis, Web
|
|
165
|
+
// Push beside it, and one run to a database, held by a Web Lock where the
|
|
166
|
+
// engine has them.
|
|
167
|
+
export class BrowserTerrain extends PointerTerrain {
|
|
168
|
+
readonly where: string;
|
|
169
|
+
#release: (() => void) | undefined;
|
|
170
|
+
|
|
171
|
+
constructor(parts: BrowserParts = {}) {
|
|
172
|
+
const where = parts.where ?? BROWSER_HARBOR;
|
|
173
|
+
const entropy = new CryptoEntropy();
|
|
174
|
+
super({ entropy, loader: new HeldLoader(parts.modules ?? []), custody: new IdbCustody(where, entropy), memory: new IdbMemory(where), faculties: [webFaculty(), webPush], opens: [WEB_OPENING] });
|
|
175
|
+
this.where = where;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
override async claim(): Promise<void> {
|
|
179
|
+
const locks = (globalThis as { navigator?: { locks?: LockManager } }).navigator?.locks;
|
|
180
|
+
if (!locks) return;
|
|
181
|
+
const held = await new Promise<boolean>((granted) => {
|
|
182
|
+
void locks.request(`nervur:${this.where}`, { ifAvailable: true }, (lock) => {
|
|
183
|
+
if (lock === null) {
|
|
184
|
+
granted(false);
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
granted(true);
|
|
188
|
+
return new Promise<void>((release) => (this.#release = release));
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
if (!held) throw new Error(`a harbor is already open on ${this.where}`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
override async release(): Promise<void> {
|
|
195
|
+
this.#release?.();
|
|
196
|
+
this.#release = undefined;
|
|
197
|
+
await (this.memory as IdbMemory).close();
|
|
198
|
+
await (this.custody as IdbCustody).close();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// A page or a worker with IndexedDB and Web Crypto.
|
|
203
|
+
export const browserGround: GroundProbe = {
|
|
204
|
+
name: BROWSER,
|
|
205
|
+
fits: () => typeof indexedDB !== 'undefined' && typeof crypto?.subtle !== 'undefined',
|
|
206
|
+
terrain: ({ modules, where }) => new BrowserTerrain({ ...(where ? { where } : {}), ...(modules ? { modules } : {}) }),
|
|
207
|
+
};
|