nervur 0.21.2 → 0.21.4
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 +35 -30
- package/dist/browser/index.d.ts +1 -0
- package/dist/browser/index.js +8 -0
- package/dist/cli/command.js +12 -11
- package/dist/cli/edge.d.ts +1 -1
- package/dist/cli/edge.js +30 -11
- package/dist/cli/harbor.d.ts +1 -1
- package/dist/cli/harbor.js +18 -6
- package/dist/contract/index.d.ts +1 -0
- package/dist/contract/index.js +5 -0
- package/dist/edge/index.d.ts +23 -13
- package/dist/edge/index.js +103 -37
- package/dist/harbor/catalogue.d.ts +1 -0
- package/dist/harbor/catalogue.js +2 -2
- package/dist/harbor/dock.d.ts +1 -0
- package/dist/harbor/dock.js +8 -3
- package/dist/harbor/harbor.d.ts +2 -0
- package/dist/harbor/harbor.js +6 -0
- package/dist/harbor/terrain.d.ts +1 -0
- package/dist/harbor/terrain.js +6 -0
- package/dist/pointer/bodies.d.ts +1 -0
- package/dist/pointer/bodies.js +7 -1
- package/dist/proof/law.js +1 -1
- package/package.json +1 -1
- package/src/browser/index.ts +9 -0
- package/src/cli/command.ts +12 -11
- package/src/cli/edge.ts +30 -11
- package/src/cli/harbor.ts +15 -6
- package/src/contract/index.ts +5 -0
- package/src/edge/index.ts +142 -65
- package/src/harbor/catalogue.ts +4 -2
- package/src/harbor/dock.ts +8 -2
- package/src/harbor/harbor.ts +8 -0
- package/src/harbor/terrain.ts +6 -0
- package/src/pointer/bodies.ts +7 -1
- package/src/proof/law.ts +1 -1
package/src/edge/index.ts
CHANGED
|
@@ -4,23 +4,25 @@
|
|
|
4
4
|
// object answers Quo over the web, a post or a held line, since a worker
|
|
5
5
|
// takes no inbound TCP; and it asks over TCP and the web.
|
|
6
6
|
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
7
|
+
// One worker carries many harbors, each a Durable Object by its name,
|
|
8
|
+
// answering at `/<name>/quo`, and one secret, `NERVUR_SECRET`, for all of
|
|
9
|
+
// them. A harbor born on another ground is carried here in a pack: by
|
|
10
|
+
// name, its seed sealed under that secret and its sealed places, bundled
|
|
11
|
+
// with the worker and landed the first time its object starts. So a
|
|
12
|
+
// harbor is added by a deploy, never by a new secret. Its owner holds the
|
|
13
|
+
// owner invitation minted where it was born, and pilots it over the web.
|
|
14
|
+
// A harbor born here draws its seed and seals it the same way.
|
|
13
15
|
//
|
|
14
16
|
// m/<place>/<name> one sealed entry of the harbor's memory
|
|
15
|
-
// c/seed
|
|
17
|
+
// c/seed the seed, sealed under a key from the secret
|
|
16
18
|
//
|
|
17
19
|
// A keep is one storage transaction, whole or nothing. The storage and
|
|
18
20
|
// state types name only what the kit uses, so no Cloudflare package is a
|
|
19
21
|
// dependency.
|
|
20
22
|
import type { AskSpec, JsonObject } from '../being/index.ts';
|
|
21
23
|
import { Custody, Memory, type Entropy, type Module } from '../contract/index.ts';
|
|
22
|
-
import { decrypt, encrypt, hkdf, isHex, unhex, utf8 } from '../crypto/index.ts';
|
|
23
|
-
import { addGround, CarrierFaculty, Harbor, landPlaces, UNDIALED, type Carried, type Dialed, type Ground, type GroundFaculty, type GroundProbe } from '../harbor/index.ts';
|
|
24
|
+
import { concat, decrypt, encrypt, hex, hkdf, isHex, unhex, utf8 } from '../crypto/index.ts';
|
|
25
|
+
import { addGround, CarrierFaculty, Harbor, landPlaces, readCarried, UNDIALED, type Carried, type Dialed, type Ground, type GroundFaculty, type GroundProbe } from '../harbor/index.ts';
|
|
24
26
|
import { answerHeld, answerPost, askFrame, FrameReader, TCP, TCP_OPENING, WEB_OPENING, webFaculty, webPush, type WebListen, type WebListener } from '../line/index.ts';
|
|
25
27
|
import { CryptoEntropy, HeldLoader, PointerTerrain } from '../pointer/index.ts';
|
|
26
28
|
import { tcpAddress, tcpAt, type TcpAddress } from '../quo/index.ts';
|
|
@@ -47,10 +49,42 @@ export interface EdgeState {
|
|
|
47
49
|
readonly storage: EdgeStorage;
|
|
48
50
|
blockConcurrencyWhile<T>(work: () => Promise<T>): Promise<T>;
|
|
49
51
|
}
|
|
50
|
-
export type EdgeEnv = { readonly
|
|
52
|
+
export type EdgeEnv = { readonly NERVUR_SECRET?: string };
|
|
51
53
|
// What the object hands its harbor: its state, the worker's environment,
|
|
52
|
-
// and a
|
|
53
|
-
export type EdgeGiven = { readonly state: EdgeState; readonly env: EdgeEnv; readonly package?:
|
|
54
|
+
// and a packed harbor to land where the object keeps no seed yet.
|
|
55
|
+
export type EdgeGiven = { readonly state: EdgeState; readonly env: EdgeEnv; readonly package?: Packed };
|
|
56
|
+
|
|
57
|
+
// One harbor packed for an edge: its seed sealed under the worker's
|
|
58
|
+
// secret, as hex of the nonce and the sealed bytes, and its sealed places.
|
|
59
|
+
// A pack is every harbor of a worker, by name.
|
|
60
|
+
export type Packed = { readonly sealed: string; readonly places: Carried['places'] };
|
|
61
|
+
export type Pack = Record<string, Packed>;
|
|
62
|
+
|
|
63
|
+
// A harbor's name on the edge: one path segment, so it is where the
|
|
64
|
+
// harbor answers, `/<name>/quo`.
|
|
65
|
+
export const isHarborName = (name: unknown): name is string => typeof name === 'string' && /^[a-z0-9][a-z0-9-]{0,62}$/.test(name);
|
|
66
|
+
const NONCE = 12;
|
|
67
|
+
|
|
68
|
+
// A pack as JSON reads, or null for anything else.
|
|
69
|
+
export const readPack = (value: unknown): Pack | null => {
|
|
70
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) return null;
|
|
71
|
+
const pack: Record<string, Packed> = {};
|
|
72
|
+
for (const [name, entry] of Object.entries(value)) {
|
|
73
|
+
const { sealed, places } = (entry ?? {}) as { sealed?: unknown; places?: unknown };
|
|
74
|
+
const carried = readCarried({ places });
|
|
75
|
+
if (!isHarborName(name) || !isHex(sealed) || (sealed as string).length <= NONCE * 2 || carried === null) return null;
|
|
76
|
+
pack[name] = { sealed: sealed as string, places: carried.places };
|
|
77
|
+
}
|
|
78
|
+
return pack;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const sealKey = (secret: string): Promise<Uint8Array> => hkdf(utf8(secret), SEAL_INFO, 32);
|
|
82
|
+
|
|
83
|
+
// A seed sealed under a worker's secret, as a pack keeps it.
|
|
84
|
+
export const sealSeed = async (secret: string, seed: Uint8Array, entropy: Entropy = new CryptoEntropy()): Promise<string> => {
|
|
85
|
+
const nonce = entropy.draw(NONCE);
|
|
86
|
+
return hex(concat(nonce, await encrypt(await sealKey(secret), nonce, new Uint8Array(0), seed)));
|
|
87
|
+
};
|
|
54
88
|
|
|
55
89
|
const chunks = <T>(items: readonly T[]): T[][] => Array.from({ length: Math.ceil(items.length / BATCH) }, (_, i) => items.slice(i * BATCH, (i + 1) * BATCH));
|
|
56
90
|
const bytesOf = (value: unknown): Uint8Array => new Uint8Array(value instanceof ArrayBuffer ? value : (value as Uint8Array));
|
|
@@ -99,33 +133,36 @@ export class EdgeMemory extends Memory {
|
|
|
99
133
|
}
|
|
100
134
|
}
|
|
101
135
|
|
|
102
|
-
// The seed
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
// does not stand.
|
|
106
|
-
export type EdgeKeys = { readonly seed?: string | undefined; readonly secret?: string | undefined };
|
|
107
|
-
|
|
136
|
+
// The seed, kept sealed under a key the worker's secret gives, so the
|
|
137
|
+
// storage alone opens nothing: the one a pack landed, or one drawn the
|
|
138
|
+
// first time it is asked for. A worker with no secret stands no harbor.
|
|
108
139
|
export class EdgeCustody extends Custody {
|
|
109
140
|
readonly #storage: EdgeStorage;
|
|
110
|
-
readonly #
|
|
141
|
+
readonly #secret: string | undefined;
|
|
111
142
|
readonly #entropy: Entropy;
|
|
112
143
|
readonly #key: string;
|
|
113
|
-
constructor(storage: EdgeStorage,
|
|
144
|
+
constructor(storage: EdgeStorage, secret: string | undefined, entropy: Entropy = new CryptoEntropy(), root = '') {
|
|
114
145
|
super();
|
|
115
146
|
this.#storage = storage;
|
|
116
|
-
this.#
|
|
147
|
+
this.#secret = secret;
|
|
117
148
|
this.#entropy = entropy;
|
|
118
149
|
this.#key = `${root}${SEED}`;
|
|
119
150
|
}
|
|
120
151
|
|
|
152
|
+
// Whether a seed is kept, so a pack lands only where none is.
|
|
153
|
+
async kept(): Promise<boolean> {
|
|
154
|
+
return (await this.#storage.get(this.#key)) !== undefined;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// A packed seed, sealed as `sealSeed` sealed it, kept as it came.
|
|
158
|
+
async land(sealed: string, txn: EdgeStorage = this.#storage): Promise<void> {
|
|
159
|
+
const bytes = unhex(sealed);
|
|
160
|
+
await txn.put({ [this.#key]: { nonce: bytes.slice(0, NONCE), sealed: bytes.slice(NONCE) } });
|
|
161
|
+
}
|
|
162
|
+
|
|
121
163
|
async seed(): Promise<Uint8Array> {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if (!isHex(named.trim(), 32)) throw new Error('NERVUR_SEED is not 64 hex');
|
|
125
|
-
return unhex(named.trim());
|
|
126
|
-
}
|
|
127
|
-
if (!secret) throw new Error('the worker holds no NERVUR_SECRET');
|
|
128
|
-
const key = await hkdf(utf8(secret), SEAL_INFO, 32);
|
|
164
|
+
if (!this.#secret) throw new Error('the worker holds no NERVUR_SECRET');
|
|
165
|
+
const key = await sealKey(this.#secret);
|
|
129
166
|
return this.#storage.transaction(async (txn) => {
|
|
130
167
|
const kept = (await txn.get(this.#key)) as { nonce: Uint8Array; sealed: Uint8Array } | undefined;
|
|
131
168
|
if (kept) {
|
|
@@ -134,7 +171,7 @@ export class EdgeCustody extends Custody {
|
|
|
134
171
|
return seed;
|
|
135
172
|
}
|
|
136
173
|
const seed = this.#entropy.draw(32);
|
|
137
|
-
const nonce = this.#entropy.draw(
|
|
174
|
+
const nonce = this.#entropy.draw(NONCE);
|
|
138
175
|
await txn.put({ [this.#key]: { nonce, sealed: await encrypt(key, nonce, new Uint8Array(0), seed) } });
|
|
139
176
|
return seed.slice();
|
|
140
177
|
});
|
|
@@ -303,9 +340,7 @@ export class EdgeTerrain extends PointerTerrain {
|
|
|
303
340
|
super({
|
|
304
341
|
entropy,
|
|
305
342
|
loader: new HeldLoader(parts.modules ?? []),
|
|
306
|
-
|
|
307
|
-
// other object of the worker draws its own.
|
|
308
|
-
custody: new EdgeCustody(storage, { seed: parts.given.package ? parts.given.env.NERVUR_SEED : undefined, secret: parts.given.env.NERVUR_SECRET }, entropy),
|
|
343
|
+
custody: new EdgeCustody(storage, parts.given.env.NERVUR_SECRET, entropy),
|
|
309
344
|
memory: new EdgeMemory(storage),
|
|
310
345
|
faculties: [edgeTcp, webFaculty(edgeServe), webPush],
|
|
311
346
|
opens: [TCP_OPENING, WEB_OPENING],
|
|
@@ -313,13 +348,16 @@ export class EdgeTerrain extends PointerTerrain {
|
|
|
313
348
|
this.given = parts.given;
|
|
314
349
|
}
|
|
315
350
|
|
|
316
|
-
// A
|
|
317
|
-
//
|
|
351
|
+
// A packed harbor lands where the object keeps no seed yet: its places
|
|
352
|
+
// first and its seed last, so a start cut short lands it whole again,
|
|
353
|
+
// and once the seed is kept the storage is the harbor and a new deploy
|
|
354
|
+
// leaves it be. A seed sealed under another secret does not open.
|
|
318
355
|
override async claim(): Promise<void> {
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
if (
|
|
322
|
-
await landPlaces(this.memory,
|
|
356
|
+
const packed = this.given.package;
|
|
357
|
+
const custody = this.custody as EdgeCustody;
|
|
358
|
+
if (!packed || (await custody.kept())) return;
|
|
359
|
+
await landPlaces(this.memory, { places: packed.places });
|
|
360
|
+
await custody.land(packed.sealed);
|
|
323
361
|
}
|
|
324
362
|
|
|
325
363
|
override stand(harbor: Harbor): Promise<void> {
|
|
@@ -358,52 +396,77 @@ export const edgeGround: GroundProbe = {
|
|
|
358
396
|
terrain: ({ given, modules }) => new EdgeTerrain({ given: given as EdgeGiven, ...(modules ? { modules } : {}) }),
|
|
359
397
|
};
|
|
360
398
|
|
|
399
|
+
// The harbor a request is for: the first segment of its path.
|
|
400
|
+
export const harborNameOf = (request: Request): string | null => {
|
|
401
|
+
const name = new URL(request.url).pathname.split('/')[1];
|
|
402
|
+
return isHarborName(name) ? name : null;
|
|
403
|
+
};
|
|
404
|
+
|
|
361
405
|
// What a Durable Object class of harbors is made of:
|
|
362
406
|
//
|
|
363
407
|
// modules the code its harbors run
|
|
364
|
-
//
|
|
365
|
-
// first time
|
|
366
|
-
// environment giving
|
|
367
|
-
//
|
|
368
|
-
//
|
|
369
|
-
//
|
|
408
|
+
// pack the harbors carried here, from `nervur edge pack`, each
|
|
409
|
+
// landed the first time its object starts; or a function of
|
|
410
|
+
// the worker's environment giving them
|
|
411
|
+
// born the names a harbor may be born under here, with a seed of
|
|
412
|
+
// its own, where the pack holds none; none unless named, so a
|
|
413
|
+
// stranger's request mints no harbor
|
|
414
|
+
// stood the worker's own code, the root, run once a harbor stands,
|
|
415
|
+
// with the harbor's name
|
|
370
416
|
export type HarborObjectParts = {
|
|
371
417
|
readonly modules: readonly Module[];
|
|
372
|
-
readonly
|
|
373
|
-
readonly
|
|
374
|
-
readonly stood?: (harbor: Harbor) => Promise<void>;
|
|
418
|
+
readonly pack?: Pack | ((env: EdgeEnv) => Pack | undefined);
|
|
419
|
+
readonly born?: (name: string) => boolean;
|
|
420
|
+
readonly stood?: (harbor: Harbor, name: string) => Promise<void>;
|
|
375
421
|
};
|
|
376
|
-
export type
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
//
|
|
380
|
-
//
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
422
|
+
export type HarborObject = { harborFor(request: Request): Promise<Harbor | null>; fetch(request: Request): Promise<Response> };
|
|
423
|
+
export type HarborObjectClass = new (state: EdgeState, env: EdgeEnv) => HarborObject;
|
|
424
|
+
|
|
425
|
+
// A Durable Object class whose every instance is one harbor, by the name
|
|
426
|
+
// the worker reached it under: opened at its first request, the one its
|
|
427
|
+
// pack holds under that name or one `born` allows, its web carrier opened,
|
|
428
|
+
// and every request it receives answered by that carrier at
|
|
429
|
+
// `/<name>/quo`. An object answers the one name it opened under, and a
|
|
430
|
+
// name it may not open is 404, with nothing written.
|
|
431
|
+
export const harborObject = ({ modules, pack, born, stood }: HarborObjectParts): HarborObjectClass =>
|
|
432
|
+
class implements HarborObject {
|
|
433
|
+
readonly #state: EdgeState;
|
|
434
|
+
readonly #env: EdgeEnv;
|
|
435
|
+
#name: string | undefined;
|
|
436
|
+
#opening: Promise<Harbor> | undefined;
|
|
389
437
|
|
|
390
438
|
constructor(state: EdgeState, env: EdgeEnv) {
|
|
391
439
|
addGround(edgeGround);
|
|
392
|
-
|
|
393
|
-
|
|
440
|
+
this.#state = state;
|
|
441
|
+
this.#env = env;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// The harbor a request names, opened once, or null where it names
|
|
445
|
+
// another harbor or none.
|
|
446
|
+
harborFor(request: Request): Promise<Harbor | null> {
|
|
447
|
+
const name = harborNameOf(request);
|
|
448
|
+
if (name === null || (this.#name !== undefined && this.#name !== name)) return Promise.resolve(null);
|
|
449
|
+
const packed = typeof pack === 'function' ? pack(this.#env) : pack;
|
|
450
|
+
const held = packed && Object.hasOwn(packed, name) ? packed[name] : undefined;
|
|
451
|
+
if (this.#name === undefined && !held && !born?.(name)) return Promise.resolve(null);
|
|
452
|
+
this.#name = name;
|
|
394
453
|
// Opened inside the object's block, so no request reaches it half
|
|
395
454
|
// open and its storage answers the opening.
|
|
396
|
-
this.#opening
|
|
455
|
+
this.#opening ??= this.#state.blockConcurrencyWhile(async () => {
|
|
456
|
+
const given: EdgeGiven = { state: this.#state, env: this.#env, ...(held ? { package: held } : {}) };
|
|
397
457
|
const harbor = await Harbor.open({ modules, given });
|
|
398
458
|
if (!harbor.box.being(WEB_OPENING.key)) await harbor.root('open', { key: WEB_OPENING.key, class: WEB_OPENING.kind });
|
|
399
|
-
await stood?.(harbor);
|
|
459
|
+
await stood?.(harbor, name);
|
|
400
460
|
return harbor;
|
|
401
461
|
});
|
|
462
|
+
return this.#opening;
|
|
402
463
|
}
|
|
403
464
|
|
|
404
465
|
async fetch(request: Request): Promise<Response> {
|
|
405
|
-
const harbor = await this
|
|
466
|
+
const harbor = await this.harborFor(request);
|
|
467
|
+
if (!harbor) return new Response(null, { status: 404 });
|
|
406
468
|
const terrain = harbor.terrain as EdgeTerrain;
|
|
469
|
+
const path = `/${this.#name!}/quo`;
|
|
407
470
|
if (terrain.listening?.path !== path) {
|
|
408
471
|
const url = new URL(request.url);
|
|
409
472
|
await harbor.root('ask', { being: WEB_OPENING.key, method: 'listen', args: { host: url.hostname, port: url.port === '' ? 0 : Number(url.port), path } });
|
|
@@ -411,3 +474,17 @@ export const harborObject = ({ modules, package: carried, path = '/quo', stood }
|
|
|
411
474
|
return terrain.answer(request);
|
|
412
475
|
}
|
|
413
476
|
};
|
|
477
|
+
|
|
478
|
+
type Namespace = { idFromName(name: string): unknown; get(id: unknown): { fetch(request: Request): Promise<Response> } };
|
|
479
|
+
|
|
480
|
+
// A worker of many harbors: each request goes to the object of the
|
|
481
|
+
// harbor its path names, in the Durable Object namespace bound as
|
|
482
|
+
// `binding`, and a path naming none is 404.
|
|
483
|
+
export const harborWorker = (binding = 'HARBOR') => ({
|
|
484
|
+
fetch(request: Request, env: Record<string, unknown>): Promise<Response> {
|
|
485
|
+
const name = harborNameOf(request);
|
|
486
|
+
const objects = env[binding] as Namespace | undefined;
|
|
487
|
+
if (name === null || !objects) return Promise.resolve(new Response(null, { status: 404 }));
|
|
488
|
+
return objects.get(objects.idFromName(name)).fetch(request);
|
|
489
|
+
},
|
|
490
|
+
});
|
package/src/harbor/catalogue.ts
CHANGED
|
@@ -29,6 +29,8 @@ export type ModuleRow = { version: string; from: string; kinds: string[] };
|
|
|
29
29
|
// What the catalogue asks of her harbor.
|
|
30
30
|
export interface Cataloguing {
|
|
31
31
|
load(from: string): Promise<Loaded | null>;
|
|
32
|
+
// Why nothing loads from a name, where the loader says more.
|
|
33
|
+
missing(from: string): string | null;
|
|
32
34
|
// Why these modules cannot run together, or null.
|
|
33
35
|
check(modules: readonly Loaded[]): string | null;
|
|
34
36
|
// The modules run from now on, or why they cannot be.
|
|
@@ -72,7 +74,7 @@ export class Catalogue extends Faculty {
|
|
|
72
74
|
loaded instanceof Error
|
|
73
75
|
? loaded.message
|
|
74
76
|
: loaded === null
|
|
75
|
-
? `not found at ${row.from}`
|
|
77
|
+
? (this.#harbor.missing(row.from) ?? `not found at ${row.from}`)
|
|
76
78
|
: loaded.module !== module
|
|
77
79
|
? `${row.from} is module ${loaded.module}`
|
|
78
80
|
: this.#harbor.check([...this.#running.values(), loaded]);
|
|
@@ -86,7 +88,7 @@ export class Catalogue extends Faculty {
|
|
|
86
88
|
if (!text(args.from)) return { error: 'from is text' };
|
|
87
89
|
const loaded = await this.#harbor.load(args.from).catch((e: Error) => e);
|
|
88
90
|
if (loaded instanceof Error) return { error: loaded.message };
|
|
89
|
-
if (loaded === null) return { error: 'no such module' };
|
|
91
|
+
if (loaded === null) return { error: this.#harbor.missing(args.from) ?? 'no such module' };
|
|
90
92
|
const running = new Map(this.#running);
|
|
91
93
|
running.set(loaded.module, loaded);
|
|
92
94
|
const refused = this.#harbor.run([...running.values()]);
|
package/src/harbor/dock.ts
CHANGED
|
@@ -46,6 +46,8 @@ export interface Hosting {
|
|
|
46
46
|
keep(): Promise<boolean>;
|
|
47
47
|
// What did not stand when the harbor opened.
|
|
48
48
|
failures(): Failures;
|
|
49
|
+
// Whether the harbor lives only as long as a screen.
|
|
50
|
+
readonly foreground: boolean;
|
|
49
51
|
// Every place of the memory the dock does not name, gone.
|
|
50
52
|
sweep(): Promise<string[]>;
|
|
51
53
|
// Whether a kind is a carrier this harbor's ground ships.
|
|
@@ -92,10 +94,14 @@ export class Dock extends WardBeing implements Lender {
|
|
|
92
94
|
this.#hosting = hosting;
|
|
93
95
|
}
|
|
94
96
|
|
|
95
|
-
// The census adds what did not stand, for whoever pilots
|
|
97
|
+
// The census adds what did not stand, for whoever pilots, and whether
|
|
98
|
+
// the harbor lives only as long as a screen.
|
|
96
99
|
override describe(asker: Asker): Blueprint {
|
|
97
100
|
const blueprint = super.describe(asker);
|
|
98
|
-
if (pilots(asker, this.notes(asker)))
|
|
101
|
+
if (!pilots(asker, this.notes(asker))) return blueprint;
|
|
102
|
+
const notes = blueprint.notes as JsonObject;
|
|
103
|
+
notes.failed = this.#hosting.failures();
|
|
104
|
+
if (this.#hosting.foreground) notes.foreground = true;
|
|
99
105
|
return blueprint;
|
|
100
106
|
}
|
|
101
107
|
|
package/src/harbor/harbor.ts
CHANGED
|
@@ -175,6 +175,10 @@ export class Harbor extends Carrier implements Hosting, Cataloguing, Resolving {
|
|
|
175
175
|
});
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
+
get foreground(): boolean {
|
|
179
|
+
return this.terrain.foreground;
|
|
180
|
+
}
|
|
181
|
+
|
|
178
182
|
// Hosting: what did not stand when the harbor opened, each alone.
|
|
179
183
|
failures(): Failures {
|
|
180
184
|
return { wards: { ...this.#failed.wards }, routes: { ...this.#failed.routes } };
|
|
@@ -246,6 +250,10 @@ export class Harbor extends Carrier implements Hosting, Cataloguing, Resolving {
|
|
|
246
250
|
return this.terrain.loader.load(from);
|
|
247
251
|
}
|
|
248
252
|
|
|
253
|
+
missing(from: string): string | null {
|
|
254
|
+
return this.terrain.loader.missing(from);
|
|
255
|
+
}
|
|
256
|
+
|
|
249
257
|
check(modules: readonly Loaded[]): string | null {
|
|
250
258
|
return Registry.refusal(this.#kit, modules);
|
|
251
259
|
}
|
package/src/harbor/terrain.ts
CHANGED
|
@@ -25,6 +25,12 @@ export abstract class Terrain {
|
|
|
25
25
|
// Whether its carriers wake and listen where their cells say. A harbor
|
|
26
26
|
// opened for one ask and closed again dials and hears nothing.
|
|
27
27
|
readonly wakes: boolean = true;
|
|
28
|
+
// Whether the harbor lives only as long as a screen: a tab's, or a worker
|
|
29
|
+
// one tab owns. It is reached only while that screen is open, and hears
|
|
30
|
+
// no push, so its census says so.
|
|
31
|
+
get foreground(): boolean {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
28
34
|
|
|
29
35
|
// Before the harbor opens: a terrain whose ground another run holds
|
|
30
36
|
// refuses here. Once it stands: whatever the ground serves beside it.
|
package/src/pointer/bodies.ts
CHANGED
|
@@ -32,7 +32,9 @@ export class HeldCustody extends Custody {
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
// Modules handed to this object, found by the name each declares
|
|
35
|
+
// Modules handed to this object, found by the name each declares: the
|
|
36
|
+
// bundle a ground with no disk to load from carries, an edge's or a
|
|
37
|
+
// browser's. A path, a package or any other name is no module of it.
|
|
36
38
|
export class HeldLoader extends Loader {
|
|
37
39
|
readonly #modules = new Map<string, Module>();
|
|
38
40
|
constructor(modules: readonly Module[] = []) {
|
|
@@ -43,6 +45,10 @@ export class HeldLoader extends Loader {
|
|
|
43
45
|
const m = this.#modules.get(from);
|
|
44
46
|
return Promise.resolve(m ? { module: m.module, version: m.version, classes: m.classes, from } : null);
|
|
45
47
|
}
|
|
48
|
+
override missing(from: string): string {
|
|
49
|
+
const carried = [...this.#modules.keys()];
|
|
50
|
+
return `${from} is no module this bundle carries${carried.length === 0 ? ', and it carries none' : `: it carries ${carried.join(', ')}`}`;
|
|
51
|
+
}
|
|
46
52
|
}
|
|
47
53
|
|
|
48
54
|
// Entries that live as long as this object, copied in and out, which is
|
package/src/proof/law.ts
CHANGED
|
@@ -203,7 +203,7 @@ export const lawScenes = (t: Terrain, expect: Expect): [string, () => Promise<vo
|
|
|
203
203
|
expect.same(lost.names, ['w'], 'the rest of the harbor stands');
|
|
204
204
|
expect.same(((await census(lost)) as { beings: Record<string, { absent: boolean }> }).beings.echo?.absent, true);
|
|
205
205
|
expect.same(await lost.root('ask', { being: 'catalogue', method: 'modules' }), {
|
|
206
|
-
answer: { [law.module]: { version: '1', running: null, from: law.module, failed:
|
|
206
|
+
answer: { [law.module]: { version: '1', running: null, from: law.module, failed: `${law.module} is no module this bundle carries, and it carries none` } },
|
|
207
207
|
});
|
|
208
208
|
const again = await world.restart('h', { ...t.again(bodies), loader: new HeldLoader([law]) });
|
|
209
209
|
expect.same(((await census(again)) as { beings: Record<string, { absent: boolean }> }).beings.echo?.absent, false);
|