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.
Files changed (157) hide show
  1. package/README.md +229 -20
  2. package/dist/being/being.d.ts +1 -0
  3. package/dist/being/being.js +5 -1
  4. package/dist/being/faculty.d.ts +1 -0
  5. package/dist/being/faculty.js +13 -7
  6. package/dist/being/index.d.ts +1 -0
  7. package/dist/being/index.js +1 -0
  8. package/dist/being/kind.d.ts +4 -0
  9. package/dist/being/kind.js +28 -0
  10. package/dist/being/types.d.ts +3 -4
  11. package/dist/browser/index.d.ts +35 -0
  12. package/dist/browser/index.js +188 -0
  13. package/dist/browser/worker.d.ts +12 -0
  14. package/dist/browser/worker.js +40 -0
  15. package/dist/browser.d.ts +1 -0
  16. package/dist/browser.js +7 -0
  17. package/dist/cli/command.d.ts +13 -0
  18. package/dist/cli/command.js +237 -0
  19. package/dist/cli/harbor.d.ts +19 -0
  20. package/dist/cli/harbor.js +99 -0
  21. package/dist/cli/main.d.ts +2 -0
  22. package/dist/cli/main.js +14 -0
  23. package/dist/contract/index.d.ts +16 -2
  24. package/dist/contract/index.js +13 -3
  25. package/dist/edge/index.d.ts +57 -0
  26. package/dist/edge/index.js +306 -0
  27. package/dist/edge.d.ts +1 -0
  28. package/dist/edge.js +8 -0
  29. package/dist/folder/index.d.ts +9 -4
  30. package/dist/folder/index.js +89 -84
  31. package/dist/harbor/carrying.d.ts +17 -0
  32. package/dist/harbor/carrying.js +38 -0
  33. package/dist/harbor/catalogue.d.ts +27 -0
  34. package/dist/harbor/catalogue.js +101 -0
  35. package/dist/harbor/dock.d.ts +32 -3
  36. package/dist/harbor/dock.js +98 -12
  37. package/dist/harbor/harbor.d.ts +27 -8
  38. package/dist/harbor/harbor.js +217 -26
  39. package/dist/harbor/index.d.ts +8 -2
  40. package/dist/harbor/index.js +8 -1
  41. package/dist/harbor/package.d.ts +12 -0
  42. package/dist/harbor/package.js +130 -0
  43. package/dist/harbor/probe.d.ts +18 -0
  44. package/dist/harbor/probe.js +15 -0
  45. package/dist/harbor/registry.d.ts +6 -3
  46. package/dist/harbor/registry.js +83 -7
  47. package/dist/harbor/relay.d.ts +15 -0
  48. package/dist/harbor/relay.js +387 -0
  49. package/dist/harbor/root-line.d.ts +9 -0
  50. package/dist/harbor/root-line.js +38 -0
  51. package/dist/harbor/terrain.d.ts +14 -1
  52. package/dist/harbor/terrain.js +19 -0
  53. package/dist/http/index.d.ts +4 -0
  54. package/dist/http/index.js +156 -0
  55. package/dist/http/websocket.d.ts +29 -0
  56. package/dist/http/websocket.js +133 -0
  57. package/dist/index.d.ts +5 -4
  58. package/dist/index.js +12 -6
  59. package/dist/line/answer.d.ts +8 -0
  60. package/dist/line/answer.js +60 -0
  61. package/dist/line/frame.d.ts +34 -0
  62. package/dist/line/frame.js +81 -0
  63. package/dist/line/ground.d.ts +3 -0
  64. package/dist/line/ground.js +9 -0
  65. package/dist/line/index.d.ts +5 -0
  66. package/dist/line/index.js +9 -0
  67. package/dist/line/push.d.ts +7 -0
  68. package/dist/line/push.js +92 -0
  69. package/dist/line/web.d.ts +30 -0
  70. package/dist/line/web.js +181 -0
  71. package/dist/node/index.d.ts +32 -0
  72. package/dist/node/index.js +164 -0
  73. package/dist/node.d.ts +1 -0
  74. package/dist/node.js +7 -0
  75. package/dist/pointer/bodies.d.ts +11 -3
  76. package/dist/pointer/bodies.js +47 -18
  77. package/dist/pointer/index.d.ts +1 -1
  78. package/dist/pointer/index.js +1 -1
  79. package/dist/pointer/world.d.ts +12 -5
  80. package/dist/pointer/world.js +29 -14
  81. package/dist/quo/address.d.ts +19 -0
  82. package/dist/quo/address.js +84 -0
  83. package/dist/quo/door.d.ts +1 -1
  84. package/dist/quo/door.js +6 -4
  85. package/dist/quo/index.d.ts +1 -0
  86. package/dist/quo/index.js +1 -0
  87. package/dist/quo/invitation.d.ts +1 -0
  88. package/dist/quo/invitation.js +9 -5
  89. package/dist/quo/keys.js +1 -1
  90. package/dist/tcp/frame.d.ts +25 -0
  91. package/dist/tcp/frame.js +64 -0
  92. package/dist/tcp/index.d.ts +24 -0
  93. package/dist/tcp/index.js +260 -0
  94. package/dist/ward/index.d.ts +3 -2
  95. package/dist/ward/index.js +2 -1
  96. package/dist/ward/pilot.d.ts +8 -0
  97. package/dist/ward/pilot.js +50 -0
  98. package/dist/ward/stance.d.ts +4 -4
  99. package/dist/ward/stance.js +10 -7
  100. package/dist/ward/ward-being.d.ts +4 -1
  101. package/dist/ward/ward-being.js +8 -2
  102. package/dist/ward/ward.d.ts +11 -2
  103. package/dist/ward/ward.js +41 -5
  104. package/package.json +28 -2
  105. package/src/being/being.ts +5 -1
  106. package/src/being/faculty.ts +14 -7
  107. package/src/being/index.ts +1 -0
  108. package/src/being/kind.ts +30 -0
  109. package/src/being/types.ts +8 -6
  110. package/src/browser/index.ts +207 -0
  111. package/src/browser/worker.ts +66 -0
  112. package/src/browser.ts +9 -0
  113. package/src/cli/command.ts +224 -0
  114. package/src/cli/harbor.ts +103 -0
  115. package/src/cli/main.ts +13 -0
  116. package/src/contract/index.ts +42 -10
  117. package/src/edge/index.ts +359 -0
  118. package/src/edge.ts +10 -0
  119. package/src/folder/index.ts +87 -84
  120. package/src/harbor/carrying.ts +56 -0
  121. package/src/harbor/catalogue.ts +125 -0
  122. package/src/harbor/dock.ts +122 -15
  123. package/src/harbor/harbor.ts +240 -36
  124. package/src/harbor/index.ts +10 -3
  125. package/src/harbor/package.ts +147 -0
  126. package/src/harbor/probe.ts +34 -0
  127. package/src/harbor/registry.ts +73 -9
  128. package/src/harbor/relay.ts +399 -0
  129. package/src/harbor/root-line.ts +39 -0
  130. package/src/harbor/terrain.ts +31 -3
  131. package/src/http/index.ts +150 -0
  132. package/src/http/websocket.ts +124 -0
  133. package/src/index.ts +39 -6
  134. package/src/line/answer.ts +59 -0
  135. package/src/line/frame.ts +88 -0
  136. package/src/line/ground.ts +19 -0
  137. package/src/line/index.ts +9 -0
  138. package/src/line/push.ts +112 -0
  139. package/src/line/web.ts +227 -0
  140. package/src/node/index.ts +170 -0
  141. package/src/node.ts +9 -0
  142. package/src/pointer/bodies.ts +45 -16
  143. package/src/pointer/index.ts +1 -1
  144. package/src/pointer/world.ts +38 -19
  145. package/src/quo/address.ts +87 -0
  146. package/src/quo/door.ts +6 -4
  147. package/src/quo/index.ts +1 -0
  148. package/src/quo/invitation.ts +10 -6
  149. package/src/quo/keys.ts +1 -1
  150. package/src/stand/main.ts +1 -0
  151. package/src/stand/stand.ts +87 -6
  152. package/src/tcp/index.ts +284 -0
  153. package/src/ward/index.ts +3 -2
  154. package/src/ward/pilot.ts +50 -0
  155. package/src/ward/stance.ts +14 -8
  156. package/src/ward/ward-being.ts +9 -3
  157. package/src/ward/ward.ts +53 -6
@@ -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(className, key, id) {
69
- return this.#live ? this.#inside.boot(this.key, className, key, id) : Promise.resolve(null);
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
- if (!this.#free(id, this.#row.standings))
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(contract.name);
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(contract.name, invitation.heir);
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>;
@@ -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
  }
@@ -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(name: string): BeingClass | undefined;
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.name;
50
+ own.class = kindOf(being.constructor);
51
51
  ward.#residents.set(WARD, { being, row: own, stance });
52
- if (parts.dock && !Object.hasOwn(own.standings, DOCK))
53
- await stance.standings.take(DOCK, parts.dock);
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, a heir the being that
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.0",
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": {
@@ -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> = {};
@@ -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 { ... } the contract
6
- // class IntervalTimer extends Timer { ... } one class that fulfils it
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. Two asks are the
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 name.
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 names: string[] = [];
20
- for (let c: unknown = C; typeof c === 'function' && c !== Faculty; c = Object.getPrototypeOf(c)) names.push((c as { name: string }).name);
21
- return names;
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 {
@@ -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
+ };
@@ -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 class name, under a key she chooses. With an
60
- // id, the new being invites her under her key, and she takes it under id.
61
- boot(className: string, key: string, id?: string): Promise<string | null>;
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 or the relation was refused.
64
- lend(contract: { readonly name: string }, id: string): Promise<string | null>;
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
- export type BeingClass = (new (stance: Stance) => BeingLike) & { readonly name: string };
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
+ };