nervur 0.22.0 → 0.22.1

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.
@@ -37,8 +37,7 @@ export declare abstract class CarrierFaculty extends Faculty {
37
37
  abstract accepts(address: string): boolean;
38
38
  abstract dial(address: string, pk: string, bytes: Uint8Array): Promise<Dialed>;
39
39
  listening(): string[];
40
- relays(_pk: string): boolean;
41
- relay(_pk: string, _bytes: Uint8Array): Promise<Uint8Array | null>;
40
+ relay(_pk: string, _bytes: Uint8Array): Promise<Uint8Array | null> | null;
42
41
  heard(): Promise<void>;
43
42
  awake(): Promise<void>;
44
43
  sleep(): Promise<void>;
@@ -34,13 +34,10 @@ export class CarrierFaculty extends Faculty {
34
34
  listening() {
35
35
  return [];
36
36
  }
37
- // Whether it answers for a ward pk no door of this harbor holds, as a
38
- // relay does, and its answer.
39
- relays(_pk) {
40
- return false;
41
- }
37
+ // Its answer to a box for a ward pk no door of this harbor holds, where
38
+ // it answers for that pk, as a relay does, and null where it does not.
42
39
  relay(_pk, _bytes) {
43
- return Promise.resolve(null);
40
+ return null;
44
41
  }
45
42
  // A push the device heard, which says only that something waits: a
46
43
  // carrier that collects for this harbor collects now.
@@ -212,7 +212,7 @@ export class Harbor {
212
212
  const ward = this.#door(pk);
213
213
  if (ward)
214
214
  return (await ward.door.arrive(bytes)).bytes;
215
- return (await this.#relaying(pk)?.relay(pk, bytes)) ?? null;
215
+ return (await this.#relayed(pk, bytes)) ?? null;
216
216
  }
217
217
  // Whether one of this harbor's doors is that ward's.
218
218
  holds(pk) {
@@ -345,8 +345,11 @@ export class Harbor {
345
345
  // order, each to the carrier that dials it, and with no route the
346
346
  // terrain's carrier.
347
347
  async #carry(pk, bytes) {
348
- if (this.holds(pk) || this.#relaying(pk))
348
+ if (this.holds(pk))
349
349
  return this.arrive(pk, bytes);
350
+ const relayed = this.#relayed(pk, bytes);
351
+ if (relayed !== undefined)
352
+ return relayed;
350
353
  const at = this.#dock.routes[pk];
351
354
  if (at === undefined)
352
355
  return this.#terrain.carrier.carry(pk, bytes);
@@ -396,8 +399,15 @@ export class Harbor {
396
399
  .map((key) => this.#box.being(key))
397
400
  .filter((being) => being instanceof CarrierFaculty);
398
401
  }
399
- #relaying(pk) {
400
- return this.#carriers.find((carrier) => carrier.relays(pk));
402
+ // The answer of the first carrier that answers for a pk no door holds,
403
+ // or undefined where none does.
404
+ #relayed(pk, bytes) {
405
+ for (const carrier of this.#carriers) {
406
+ const out = carrier.relay(pk, bytes);
407
+ if (out !== null)
408
+ return out;
409
+ }
410
+ return undefined;
401
411
  }
402
412
  #dialable(at) {
403
413
  return at.some((address) => this.#carriers.some((carrier) => carrier.accepts(address)));
@@ -56,6 +56,9 @@ export declare class House implements Behind, Inside {
56
56
  root(method?: string, args?: JsonObject): Promise<Answer>;
57
57
  get zero(): boolean;
58
58
  answer(heir: string | null, p: Payload): Promise<Choice>;
59
+ within(heir: string, take: boolean, method?: string, args?: string): Promise<Choice | {
60
+ readonly quo: 'removed';
61
+ }>;
59
62
  send(pk: string, bytes: Uint8Array): Promise<Uint8Array | null>;
60
63
  offer(contract: string): Promise<Invitation | null>;
61
64
  retract(contract: string, heir: string): Promise<void>;
@@ -223,17 +223,44 @@ export class House {
223
223
  const row = key === WARD || this.#parts.stands ? undefined : this.partition.being(key);
224
224
  return row !== undefined && this.#classOf(row) !== undefined;
225
225
  }
226
- async answer(heir, p) {
226
+ answer(heir, p) {
227
+ return this.#choose(heir, p.method, p.args);
228
+ }
229
+ // An ask on a relation between two beings of this ward, with no seal:
230
+ // both ends are this house's, so a seal would prove nothing to it. A take
231
+ // binds a fresh heir here and takes it from the door, so no box ever
232
+ // speaks under it again; an ask speaks while the heir stays bound here,
233
+ // and hears `removed` once its occupant is gone. A take that brings no
234
+ // object leaves the heir fresh on the door, as it was.
235
+ async within(heir, take, method, args) {
236
+ const { bind } = this.partition.head;
237
+ const bound = Object.hasOwn(bind, heir) ? bind[heir] : undefined;
238
+ if (!take)
239
+ return bound?.inside ? this.#choose(heir, method, args) : { quo: 'removed' };
240
+ if (!bound || bound.inside || this.door.relations.get(heir)?.state !== 'fresh')
241
+ return { silence: true };
242
+ const inside = { ...bound, inside: true };
243
+ this.door.relations.delete(heir);
244
+ bind[heir] = inside;
245
+ this.partition.toldHead();
246
+ const choice = await this.#choose(heir, method, args);
247
+ if ('object' in choice || bind[heir] !== inside)
248
+ return choice;
249
+ bind[heir] = bound;
250
+ this.door.relations.set(heir, { state: 'fresh' });
251
+ return choice;
252
+ }
253
+ async #choose(heir, method, text) {
227
254
  const bound = heir === null ? undefined : this.partition.head.bind[heir];
228
255
  const key = heir === null ? this.partition.head.public : bound?.being;
229
256
  const asker = bound ? { id: bound.id } : {};
230
- const args = p.args === undefined ? {} : readValue(p.args, DEPTH + 1);
257
+ const args = text === undefined ? {} : readValue(text, DEPTH + 1);
231
258
  if (key === null || key === undefined || typeof args !== 'object' || args === null || Array.isArray(args))
232
259
  return { silence: true };
233
260
  let seen = null;
234
261
  const out = await this.#kept([key], () => this.#with(key, silence, async (being) => {
235
- const reply = await this.#reply(being, asker, p.method, args);
236
- if (p.method !== undefined && !isSilence(reply) && !isWord(reply) && being.describe)
262
+ const reply = await this.#reply(being, asker, method, args);
263
+ if (method !== undefined && !isSilence(reply) && !isWord(reply) && being.describe)
237
264
  seen = await digest(being.describe(asker));
238
265
  return reply;
239
266
  }));
@@ -306,10 +333,9 @@ export class House {
306
333
  return row ? new HouseStance(this, key, row, this.#parts.carries ?? {}) : undefined;
307
334
  }
308
335
  // Inside: what every stance of this ward stands on.
309
- // One path through a door for every ask, this ward's own included.
310
- async send(pk, bytes) {
311
- if (pk === this.pk)
312
- return (await this.door.arrive(bytes)).bytes;
336
+ // A box to another ward's door, wherever it stands; this ward's own
337
+ // relations speak `within`.
338
+ send(pk, bytes) {
313
339
  return this.#parts.carrier.carry(pk, bytes);
314
340
  }
315
341
  // A lend: the ward lends herself, as the dock does, or asks the dock
@@ -10,6 +10,7 @@ export type Head = {
10
10
  bind: Record<string, {
11
11
  being: string;
12
12
  id: string;
13
+ inside?: true;
13
14
  }>;
14
15
  public: string | null;
15
16
  };
@@ -1,4 +1,4 @@
1
- import { type Door, type Draw, type Invitation } from '../quo/index.ts';
1
+ import { type Door, type Draw, type Invitation, type Read } from '../quo/index.ts';
2
2
  import type { Clock } from '../contract/index.ts';
3
3
  import { type JsonObject, type Occupants, type Stance, type Standings } from '../being/index.ts';
4
4
  import type { BeingRow, Partition } from './partition.ts';
@@ -9,8 +9,10 @@ export interface Inside {
9
9
  readonly door: Door;
10
10
  readonly draw: Draw;
11
11
  readonly clock: Clock;
12
+ readonly pk: string;
12
13
  live(key: string, row: BeingRow): boolean;
13
14
  send(pk: string, bytes: Uint8Array): Promise<Uint8Array | null>;
15
+ within(heir: string, take: boolean, method?: string, args?: string): Promise<Read>;
14
16
  boot(maker: string, className: string, key: string, id?: string): Promise<string | null>;
15
17
  offer(contract: string): Promise<Invitation | null>;
16
18
  retract(contract: string, heir: string): Promise<void>;
@@ -1,7 +1,8 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  // One being's stance: her cells, the ids she invited, the relations she
3
- // holds, and boot. Every relation she holds speaks through `quo/`'s
4
- // standing, one send at a time, inside her allowance.
3
+ // holds, and boot. A relation on another ward speaks through `quo/`'s
4
+ // standing, one on her own ward straight to her ward, each one send at a
5
+ // time, inside her allowance.
5
6
  import { readValue, writeValue } from '../crypto/index.js';
6
7
  import { readInvitation, Standing as Wire, freshStanding } from '../quo/index.js';
7
8
  import { OWNER, ownKind, silence, word } from '../being/index.js';
@@ -120,6 +121,8 @@ export class HouseStance {
120
121
  await this.#inside.learn(invitation.ward, invitation.at);
121
122
  const wire = new Wire(invitation, freshStanding(), this.#inside.draw);
122
123
  const read = await this.#lanes.run(`take:${id}`, async () => {
124
+ if (invitation.ward === this.#inside.pk)
125
+ return this.#inside.within(invitation.heir, true);
123
126
  // A knock that brought nothing back may have bound: the standing asks
124
127
  // under its own key, then knocks again as the same bytes.
125
128
  let out = await this.#send(wire);
@@ -157,9 +160,13 @@ export class HouseStance {
157
160
  return word('dropped');
158
161
  if (bell.rang)
159
162
  return word('late');
160
- const wire = new Wire(row.invitation, row.state, this.#inside.draw);
161
- const read = await bell.race(this.#send(wire, method, text));
162
- row.state = wire.state;
163
+ // A relation inside this ward speaks with no seal, any other
164
+ // through the far ward's door.
165
+ const inside = row.invitation.ward === this.#inside.pk;
166
+ const wire = inside ? null : new Wire(row.invitation, row.state, this.#inside.draw);
167
+ const read = await bell.race(wire ? this.#send(wire, method, text) : this.#inside.within(row.invitation.heir, false, method, text));
168
+ if (wire)
169
+ row.state = wire.state;
163
170
  if (read === LATE)
164
171
  return word('late');
165
172
  if ('object' in read)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nervur",
3
- "version": "0.22.0",
3
+ "version": "0.22.1",
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",
@@ -8,9 +8,16 @@
8
8
  "capability",
9
9
  "object-capability"
10
10
  ],
11
- "author": "Razvan Gherghina",
11
+ "author": "Bookarest Digital SRL",
12
12
  "license": "Apache-2.0",
13
13
  "homepage": "https://nervur.org",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/nervur-org/nervur.git"
17
+ },
18
+ "bugs": {
19
+ "url": "https://github.com/nervur-org/nervur/issues"
20
+ },
14
21
  "type": "module",
15
22
  "bin": {
16
23
  "nervur": "dist/cli.js"
@@ -58,8 +65,8 @@
58
65
  },
59
66
  "scripts": {
60
67
  "build": "rm -rf dist && tsc -p tsconfig.build.json && node kit.mjs",
61
- "check": "tsc -p tsconfig.json && npm run build &&node ../../test.mjs \"test/core/**/*.test.ts\" \"test/defaults/**/*.test.ts\" \"test/kit/*.test.ts\"",
62
- "deep": "node ../../test.mjs \"test/terrain/*.test.ts\"",
68
+ "check": "tsc -p tsconfig.json && npm run build &&node test.mjs \"test/core/**/*.test.ts\" \"test/defaults/**/*.test.ts\" \"test/kit/*.test.ts\"",
69
+ "deep": "node test.mjs \"test/terrain/*.test.ts\"",
63
70
  "cover": "node test/cover.mjs",
64
71
  "mutate": "node test/mutate.mjs",
65
72
  "prepublishOnly": "test \"$NERVUR_GATED\" = 1 || { echo 'a publish is /release, from the root, on the human'\"'\"'s word' >&2; exit 1; }"
@@ -82,6 +89,17 @@
82
89
  "NOTICE"
83
90
  ],
84
91
  "devDependencies": {
92
+ "@stryker-mutator/core": "^10.0.0",
93
+ "@stryker-mutator/instrumenter": "^10.0.0",
94
+ "@types/node": "^26.4.1",
95
+ "bun": "^1.4.0",
96
+ "deno": "^2.9.6",
97
+ "esbuild": "^0.28.2",
98
+ "istanbul-lib-coverage": "^3.2.2",
99
+ "playwright": "^1.62.1",
100
+ "typescript": "^7.0.2",
101
+ "v8-to-istanbul": "^9.3.0",
102
+ "workerd": "^1.20260904.1",
85
103
  "wrangler": "^4.134.0"
86
104
  }
87
105
  }
@@ -87,13 +87,10 @@ export abstract class CarrierFaculty extends Faculty {
87
87
  return [];
88
88
  }
89
89
 
90
- // Whether it answers for a ward pk no door of this harbor holds, as a
91
- // relay does, and its answer.
92
- relays(_pk: string): boolean {
93
- return false;
94
- }
95
- relay(_pk: string, _bytes: Uint8Array): Promise<Uint8Array | null> {
96
- return Promise.resolve(null);
90
+ // Its answer to a box for a ward pk no door of this harbor holds, where
91
+ // it answers for that pk, as a relay does, and null where it does not.
92
+ relay(_pk: string, _bytes: Uint8Array): Promise<Uint8Array | null> | null {
93
+ return null;
97
94
  }
98
95
 
99
96
  // A push the device heard, which says only that something waits: a
@@ -209,7 +209,7 @@ export class Harbor {
209
209
  async arrive(pk: string, bytes: Uint8Array): Promise<Uint8Array | null> {
210
210
  const ward = this.#door(pk);
211
211
  if (ward) return (await ward.door.arrive(bytes)).bytes;
212
- return (await this.#relaying(pk)?.relay(pk, bytes)) ?? null;
212
+ return (await this.#relayed(pk, bytes)) ?? null;
213
213
  }
214
214
 
215
215
  // Whether one of this harbor's doors is that ward's.
@@ -342,7 +342,9 @@ export class Harbor {
342
342
  // order, each to the carrier that dials it, and with no route the
343
343
  // terrain's carrier.
344
344
  async #carry(pk: string, bytes: Uint8Array): Promise<Uint8Array | null> {
345
- if (this.holds(pk) || this.#relaying(pk)) return this.arrive(pk, bytes);
345
+ if (this.holds(pk)) return this.arrive(pk, bytes);
346
+ const relayed = this.#relayed(pk, bytes);
347
+ if (relayed !== undefined) return relayed;
346
348
  const at = this.#dock.routes[pk];
347
349
  if (at === undefined) return this.#terrain.carrier.carry(pk, bytes);
348
350
  for (const address of at) {
@@ -390,8 +392,14 @@ export class Harbor {
390
392
  .filter((being): being is CarrierFaculty => being instanceof CarrierFaculty);
391
393
  }
392
394
 
393
- #relaying(pk: string): CarrierFaculty | undefined {
394
- return this.#carriers.find((carrier) => carrier.relays(pk));
395
+ // The answer of the first carrier that answers for a pk no door holds,
396
+ // or undefined where none does.
397
+ #relayed(pk: string, bytes: Uint8Array): Promise<Uint8Array | null> | undefined {
398
+ for (const carrier of this.#carriers) {
399
+ const out = carrier.relay(pk, bytes);
400
+ if (out !== null) return out;
401
+ }
402
+ return undefined;
395
403
  }
396
404
 
397
405
  #dialable(at: readonly string[]): boolean {
@@ -284,17 +284,43 @@ export class House implements Behind, Inside {
284
284
  return row !== undefined && this.#classOf(row) !== undefined;
285
285
  }
286
286
 
287
- async answer(heir: string | null, p: Payload): Promise<Choice> {
287
+ answer(heir: string | null, p: Payload): Promise<Choice> {
288
+ return this.#choose(heir, p.method, p.args);
289
+ }
290
+
291
+ // An ask on a relation between two beings of this ward, with no seal:
292
+ // both ends are this house's, so a seal would prove nothing to it. A take
293
+ // binds a fresh heir here and takes it from the door, so no box ever
294
+ // speaks under it again; an ask speaks while the heir stays bound here,
295
+ // and hears `removed` once its occupant is gone. A take that brings no
296
+ // object leaves the heir fresh on the door, as it was.
297
+ async within(heir: string, take: boolean, method?: string, args?: string): Promise<Choice | { readonly quo: 'removed' }> {
298
+ const { bind } = this.partition.head;
299
+ const bound = Object.hasOwn(bind, heir) ? bind[heir]! : undefined;
300
+ if (!take) return bound?.inside ? this.#choose(heir, method, args) : { quo: 'removed' };
301
+ if (!bound || bound.inside || this.door.relations.get(heir)?.state !== 'fresh') return { silence: true };
302
+ const inside = { ...bound, inside: true as const };
303
+ this.door.relations.delete(heir);
304
+ bind[heir] = inside;
305
+ this.partition.toldHead();
306
+ const choice = await this.#choose(heir, method, args);
307
+ if ('object' in choice || bind[heir] !== inside) return choice;
308
+ bind[heir] = bound;
309
+ this.door.relations.set(heir, { state: 'fresh' });
310
+ return choice;
311
+ }
312
+
313
+ async #choose(heir: string | null, method: string | undefined, text: string | undefined): Promise<Choice> {
288
314
  const bound = heir === null ? undefined : this.partition.head.bind[heir];
289
315
  const key = heir === null ? this.partition.head.public : bound?.being;
290
316
  const asker: Asker = bound ? { id: bound.id } : {};
291
- const args = p.args === undefined ? {} : readValue(p.args, DEPTH + 1);
317
+ const args = text === undefined ? {} : readValue(text, DEPTH + 1);
292
318
  if (key === null || key === undefined || typeof args !== 'object' || args === null || Array.isArray(args)) return { silence: true };
293
319
  let seen: string | null = null;
294
320
  const out = await this.#kept([key], () =>
295
321
  this.#with(key, silence, async (being) => {
296
- const reply = await this.#reply(being, asker, p.method, args);
297
- if (p.method !== undefined && !isSilence(reply) && !isWord(reply) && being.describe) seen = await digest(being.describe(asker));
322
+ const reply = await this.#reply(being, asker, method, args);
323
+ if (method !== undefined && !isSilence(reply) && !isWord(reply) && being.describe) seen = await digest(being.describe(asker));
298
324
  return reply;
299
325
  }),
300
326
  );
@@ -366,9 +392,9 @@ export class House implements Behind, Inside {
366
392
 
367
393
  // Inside: what every stance of this ward stands on.
368
394
 
369
- // One path through a door for every ask, this ward's own included.
370
- async send(pk: string, bytes: Uint8Array): Promise<Uint8Array | null> {
371
- if (pk === this.pk) return (await this.door.arrive(bytes)).bytes;
395
+ // A box to another ward's door, wherever it stands; this ward's own
396
+ // relations speak `within`.
397
+ send(pk: string, bytes: Uint8Array): Promise<Uint8Array | null> {
372
398
  return this.#parts.carrier.carry(pk, bytes);
373
399
  }
374
400
 
@@ -3,7 +3,8 @@
3
3
  // its terrain opened for it.
4
4
  //
5
5
  // rows.ward the door's lock, heirs and gone, which being and id
6
- // each heir belongs to, and the public being
6
+ // each heir belongs to and whether a being of this
7
+ // ward took it, and the public being
7
8
  // rows.beings[key] one being: her class and the registry it resolves
8
9
  // through, cells, occupants and standings
9
10
  //
@@ -21,7 +22,9 @@ export type Head = {
21
22
  lock?: KeptLock;
22
23
  heirs: Record<string, HeirRecord>;
23
24
  gone: string[];
24
- bind: Record<string, { being: string; id: string }>;
25
+ // `inside` where a being of this ward took the heir: its relation lives
26
+ // here alone and no door holds it.
27
+ bind: Record<string, { being: string; id: string; inside?: true }>;
25
28
  public: string | null;
26
29
  };
27
30
 
@@ -1,7 +1,8 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  // One being's stance: her cells, the ids she invited, the relations she
3
- // holds, and boot. Every relation she holds speaks through `quo/`'s
4
- // standing, one send at a time, inside her allowance.
3
+ // holds, and boot. A relation on another ward speaks through `quo/`'s
4
+ // standing, one on her own ward straight to her ward, each one send at a
5
+ // time, inside her allowance.
5
6
  import { readValue, writeValue } from '../crypto/index.ts';
6
7
  import { readInvitation, Standing as Wire, freshStanding, type Door, type Draw, type Invitation, type Read } from '../quo/index.ts';
7
8
  import type { Clock } from '../contract/index.ts';
@@ -18,8 +19,11 @@ export interface Inside {
18
19
  readonly door: Door;
19
20
  readonly draw: Draw;
20
21
  readonly clock: Clock;
22
+ readonly pk: string;
21
23
  live(key: string, row: BeingRow): boolean;
22
24
  send(pk: string, bytes: Uint8Array): Promise<Uint8Array | null>;
25
+ // A relation whose both ends are this ward's, taken or asked with no seal.
26
+ within(heir: string, take: boolean, method?: string, args?: string): Promise<Read>;
23
27
  boot(maker: string, className: string, key: string, id?: string): Promise<string | null>;
24
28
  // An offer from the faculty the dock opened for a contract, and its
25
29
  // retraction when the offer was not taken.
@@ -145,6 +149,7 @@ export class HouseStance implements Stance {
145
149
  if (invitation.at) await this.#inside.learn(invitation.ward, invitation.at);
146
150
  const wire = new Wire(invitation, freshStanding(), this.#inside.draw);
147
151
  const read = await this.#lanes.run(`take:${id}`, async () => {
152
+ if (invitation.ward === this.#inside.pk) return this.#inside.within(invitation.heir, true);
148
153
  // A knock that brought nothing back may have bound: the standing asks
149
154
  // under its own key, then knocks again as the same bytes.
150
155
  let out = await this.#send(wire);
@@ -179,9 +184,12 @@ export class HouseStance implements Stance {
179
184
  const row = Object.hasOwn(this.#row.standings, id) ? this.#row.standings[id] : undefined;
180
185
  if (!this.#live || row === undefined) return word('dropped');
181
186
  if (bell.rang) return word('late');
182
- const wire = new Wire(row.invitation, row.state, this.#inside.draw);
183
- const read = await bell.race(this.#send(wire, method as string | undefined, text));
184
- row.state = wire.state;
187
+ // A relation inside this ward speaks with no seal, any other
188
+ // through the far ward's door.
189
+ const inside = row.invitation.ward === this.#inside.pk;
190
+ const wire = inside ? null : new Wire(row.invitation, row.state, this.#inside.draw);
191
+ const read = await bell.race(wire ? this.#send(wire, method as string | undefined, text) : this.#inside.within(row.invitation.heir, false, method as string | undefined, text));
192
+ if (wire) row.state = wire.state;
185
193
  if (read === LATE) return word('late');
186
194
  if ('object' in read) row.seen = read.seen;
187
195
  this.#told();