nervur 0.21.4 → 0.22.0
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 +215 -158
- package/dist/browser.js +4 -3
- package/dist/cli.js +16 -0
- package/dist/{being → core/being}/being.js +10 -0
- package/dist/{being → core/being}/faculty.js +5 -2
- package/dist/{being → core/being}/index.d.ts +1 -1
- package/dist/{being → core/being}/index.js +1 -1
- package/dist/{being → core/being}/kind.d.ts +2 -0
- package/dist/{being → core/being}/kind.js +6 -0
- package/dist/{being → core/being}/types.js +1 -1
- package/dist/{browser → core/browser}/index.d.ts +6 -5
- package/dist/{browser → core/browser}/index.js +10 -10
- package/dist/{browser → core/browser}/worker.d.ts +2 -3
- package/dist/{browser → core/browser}/worker.js +8 -11
- package/dist/{cli → core/cli}/command.d.ts +4 -4
- package/dist/{cli → core/cli}/command.js +116 -66
- package/dist/{cli → core/cli}/edge.js +4 -2
- package/dist/core/cli/harbor.d.ts +25 -0
- package/dist/{cli → core/cli}/harbor.js +45 -33
- package/dist/{contract → core/contract}/index.d.ts +6 -5
- package/dist/{contract → core/contract}/index.js +8 -9
- package/dist/core/contract/link.d.ts +4 -0
- package/dist/core/contract/link.js +45 -0
- package/dist/{crypto → core/crypto}/ed25519.js +22 -14
- package/dist/core/crypto/subtle.d.ts +3 -0
- package/dist/core/crypto/subtle.js +11 -0
- package/dist/core/crypto/x25519.js +24 -0
- package/dist/core/edge/index.d.ts +164 -0
- package/dist/core/edge/index.js +739 -0
- package/dist/core/edge/kit.d.ts +2 -0
- package/dist/core/edge/kit.js +2 -0
- package/dist/core/git/http.d.ts +6 -0
- package/dist/core/git/http.js +102 -0
- package/dist/core/git/index.d.ts +5 -0
- package/dist/core/git/index.js +10 -0
- package/dist/core/git/object.d.ts +40 -0
- package/dist/core/git/object.js +103 -0
- package/dist/core/git/pack.d.ts +9 -0
- package/dist/core/git/pack.js +171 -0
- package/dist/core/git/store.d.ts +23 -0
- package/dist/core/git/store.js +120 -0
- package/dist/core/git/zlib.d.ts +6 -0
- package/dist/core/git/zlib.js +210 -0
- package/dist/core/harbor/carrying.d.ts +51 -0
- package/dist/core/harbor/carrying.js +69 -0
- package/dist/core/harbor/catalogue.d.ts +33 -0
- package/dist/core/harbor/catalogue.js +365 -0
- package/dist/core/harbor/dna.d.ts +13 -0
- package/dist/core/harbor/dna.js +120 -0
- package/dist/{harbor → core/harbor}/dock.d.ts +22 -13
- package/dist/core/harbor/dock.js +381 -0
- package/dist/core/harbor/harbor.d.ts +16 -0
- package/dist/core/harbor/harbor.js +438 -0
- package/dist/core/harbor/index.d.ts +11 -0
- package/dist/{harbor → core/harbor}/index.js +5 -5
- package/dist/core/harbor/memory.d.ts +10 -0
- package/dist/core/harbor/memory.js +25 -0
- package/dist/{harbor → core/harbor}/package.d.ts +0 -1
- package/dist/{harbor → core/harbor}/package.js +9 -9
- package/dist/core/harbor/probe.d.ts +22 -0
- package/dist/core/harbor/probe.js +15 -0
- package/dist/core/harbor/registry.d.ts +15 -0
- package/dist/{harbor → core/harbor}/registry.js +26 -10
- package/dist/core/harbor/root-line.d.ts +7 -0
- package/dist/core/harbor/root-line.js +9 -0
- package/dist/{harbor → core/harbor}/terrain.d.ts +9 -6
- package/dist/{harbor → core/harbor}/terrain.js +5 -4
- package/dist/core/http/index.d.ts +2 -0
- package/dist/{http → core/http}/index.js +3 -5
- package/dist/{line → core/line}/answer.d.ts +2 -2
- package/dist/{line → core/line}/frame.d.ts +0 -5
- package/dist/{line → core/line}/frame.js +0 -4
- package/dist/core/line/ground.js +7 -0
- package/dist/core/line/index.d.ts +4 -0
- package/dist/core/line/index.js +8 -0
- package/dist/core/line/web.d.ts +12 -0
- package/dist/{line → core/line}/web.js +13 -77
- package/dist/{node → core/node}/index.d.ts +2 -15
- package/dist/core/node/index.js +124 -0
- package/dist/{pointer → core/pointer}/bodies.d.ts +7 -5
- package/dist/core/pointer/bodies.js +118 -0
- package/dist/core/pointer/index.d.ts +2 -0
- package/dist/{pointer → core/pointer}/index.js +1 -1
- package/dist/{pointer → core/pointer}/world.d.ts +13 -9
- package/dist/{pointer → core/pointer}/world.js +31 -16
- package/dist/{proof → core/proof}/contracts.d.ts +6 -2
- package/dist/{proof → core/proof}/contracts.js +11 -10
- package/dist/core/proof/index.d.ts +3 -0
- package/dist/{proof → core/proof}/index.js +2 -2
- package/dist/core/proof/law.d.ts +25 -0
- package/dist/core/proof/law.js +715 -0
- package/dist/{tcp → core/tcp}/index.d.ts +7 -4
- package/dist/{tcp → core/tcp}/index.js +11 -62
- package/dist/{ward/ward.d.ts → core/ward/house.d.ts} +24 -10
- package/dist/core/ward/house.js +473 -0
- package/dist/core/ward/index.d.ts +5 -0
- package/dist/core/ward/index.js +7 -0
- package/dist/{ward → core/ward}/partition.d.ts +1 -0
- package/dist/{ward → core/ward}/stance.d.ts +2 -2
- package/dist/{ward → core/ward}/stance.js +5 -2
- package/dist/{ward/ward-being.d.ts → core/ward/ward.d.ts} +14 -6
- package/dist/core/ward/ward.js +140 -0
- package/dist/core.d.ts +10 -0
- package/dist/core.js +19 -0
- package/dist/defaults/dock/index.d.ts +4 -0
- package/dist/defaults/dock/index.js +11 -0
- package/dist/defaults/index.d.ts +5 -0
- package/dist/defaults/index.js +10 -0
- package/dist/defaults/memory/index.d.ts +12 -0
- package/dist/defaults/memory/index.js +39 -0
- package/dist/defaults/tcp/index.d.ts +16 -0
- package/dist/defaults/tcp/index.js +82 -0
- package/dist/defaults/ward/index.d.ts +4 -0
- package/dist/defaults/ward/index.js +11 -0
- package/dist/defaults/web/index.d.ts +15 -0
- package/dist/defaults/web/index.js +90 -0
- package/dist/edge.js +4 -3
- package/dist/index.d.ts +1 -8
- package/dist/index.js +13 -18
- package/dist/kit.d.ts +4 -0
- package/dist/kit.js +12 -0
- package/dist/node.js +4 -3
- package/package.json +23 -18
- package/src/browser.ts +4 -3
- package/src/cli.ts +15 -0
- package/src/{being → core/being}/being.ts +4 -0
- package/src/{being → core/being}/faculty.ts +5 -2
- package/src/{being → core/being}/index.ts +1 -1
- package/src/{being → core/being}/kind.ts +7 -0
- package/src/{being → core/being}/types.ts +1 -1
- package/src/{browser → core/browser}/index.ts +16 -13
- package/src/{browser → core/browser}/worker.ts +11 -14
- package/src/{cli → core/cli}/command.ts +113 -66
- package/src/{cli → core/cli}/edge.ts +4 -2
- package/src/{cli → core/cli}/harbor.ts +48 -31
- package/src/{contract → core/contract}/index.ts +14 -14
- package/src/core/contract/link.ts +58 -0
- package/src/{crypto → core/crypto}/ed25519.ts +22 -14
- package/src/core/crypto/subtle.ts +13 -0
- package/src/core/crypto/x25519.ts +23 -0
- package/src/core/edge/index.ts +865 -0
- package/src/core/git/http.ts +103 -0
- package/src/core/git/index.ts +10 -0
- package/src/core/git/object.ts +116 -0
- package/src/core/git/pack.ts +147 -0
- package/src/core/git/store.ts +122 -0
- package/src/core/git/zlib.ts +197 -0
- package/src/core/harbor/carrying.ts +124 -0
- package/src/core/harbor/catalogue.ts +386 -0
- package/src/core/harbor/dna.ts +116 -0
- package/src/core/harbor/dock.ts +420 -0
- package/src/core/harbor/harbor.ts +433 -0
- package/src/core/harbor/index.ts +14 -0
- package/src/core/harbor/memory.ts +31 -0
- package/src/{harbor → core/harbor}/package.ts +15 -15
- package/src/core/harbor/probe.ts +39 -0
- package/src/{harbor → core/harbor}/registry.ts +28 -11
- package/src/core/harbor/root-line.ts +19 -0
- package/src/{harbor → core/harbor}/terrain.ts +14 -7
- package/src/{http → core/http}/index.ts +5 -8
- package/src/{line → core/line}/answer.ts +3 -2
- package/src/{line → core/line}/frame.ts +0 -5
- package/src/core/line/ground.ts +18 -0
- package/src/core/line/index.ts +8 -0
- package/src/{line → core/line}/web.ts +1 -105
- package/src/core/node/index.ts +139 -0
- package/src/core/pointer/bodies.ts +122 -0
- package/src/core/pointer/index.ts +4 -0
- package/src/{pointer → core/pointer}/world.ts +45 -21
- package/src/{proof → core/proof}/contracts.ts +18 -13
- package/src/{proof → core/proof}/index.ts +3 -3
- package/src/core/proof/law.ts +742 -0
- package/src/{stand → core/stand}/stand.ts +2 -2
- package/src/{tcp → core/tcp}/index.ts +17 -74
- package/src/core/ward/house.ts +524 -0
- package/src/core/ward/index.ts +7 -0
- package/src/{ward → core/ward}/partition.ts +5 -1
- package/src/{ward → core/ward}/stance.ts +5 -2
- package/src/core/ward/ward.ts +166 -0
- package/src/core.ts +55 -0
- package/src/defaults/dock/index.ts +12 -0
- package/src/defaults/index.ts +10 -0
- package/src/defaults/memory/index.ts +50 -0
- package/src/defaults/tcp/index.ts +89 -0
- package/src/defaults/ward/index.ts +12 -0
- package/src/defaults/web/index.ts +97 -0
- package/src/edge.ts +4 -3
- package/src/index.ts +13 -50
- package/src/kit.ts +15 -0
- package/src/node.ts +4 -3
- package/dist/cli/harbor.d.ts +0 -16
- package/dist/cli/main.js +0 -14
- package/dist/crypto/subtle.d.ts +0 -10
- package/dist/crypto/subtle.js +0 -29
- package/dist/crypto/x25519.js +0 -21
- package/dist/edge/index.d.ts +0 -81
- package/dist/edge/index.js +0 -400
- package/dist/harbor/carrying.d.ts +0 -17
- package/dist/harbor/carrying.js +0 -38
- package/dist/harbor/catalogue.d.ts +0 -28
- package/dist/harbor/catalogue.js +0 -109
- package/dist/harbor/dock.js +0 -236
- package/dist/harbor/harbor.d.ts +0 -45
- package/dist/harbor/harbor.js +0 -347
- package/dist/harbor/index.d.ts +0 -11
- package/dist/harbor/probe.d.ts +0 -18
- package/dist/harbor/probe.js +0 -15
- package/dist/harbor/registry.d.ts +0 -10
- package/dist/harbor/relay.d.ts +0 -15
- package/dist/harbor/relay.js +0 -406
- package/dist/harbor/root-line.d.ts +0 -9
- package/dist/harbor/root-line.js +0 -38
- package/dist/harbor/store.d.ts +0 -13
- package/dist/harbor/store.js +0 -53
- package/dist/http/index.d.ts +0 -4
- package/dist/line/ground.js +0 -9
- package/dist/line/index.d.ts +0 -5
- package/dist/line/index.js +0 -9
- package/dist/line/push.d.ts +0 -7
- package/dist/line/push.js +0 -92
- package/dist/line/web.d.ts +0 -30
- package/dist/node/index.js +0 -236
- package/dist/pointer/bodies.js +0 -80
- package/dist/pointer/index.d.ts +0 -2
- package/dist/proof/index.d.ts +0 -3
- package/dist/proof/law.d.ts +0 -90
- package/dist/proof/law.js +0 -591
- package/dist/tcp/frame.d.ts +0 -25
- package/dist/tcp/frame.js +0 -64
- package/dist/ward/index.d.ts +0 -6
- package/dist/ward/index.js +0 -8
- package/dist/ward/pilot.d.ts +0 -8
- package/dist/ward/pilot.js +0 -50
- package/dist/ward/ward-being.js +0 -82
- package/dist/ward/ward.js +0 -290
- package/src/cli/main.ts +0 -13
- package/src/crypto/subtle.ts +0 -33
- package/src/crypto/x25519.ts +0 -21
- package/src/edge/index.ts +0 -490
- package/src/harbor/carrying.ts +0 -56
- package/src/harbor/catalogue.ts +0 -134
- package/src/harbor/dock.ts +0 -265
- package/src/harbor/harbor.ts +0 -381
- package/src/harbor/index.ts +0 -14
- package/src/harbor/probe.ts +0 -34
- package/src/harbor/relay.ts +0 -418
- package/src/harbor/root-line.ts +0 -39
- package/src/harbor/store.ts +0 -69
- package/src/line/ground.ts +0 -19
- package/src/line/index.ts +0 -9
- package/src/line/push.ts +0 -112
- package/src/node/index.ts +0 -241
- package/src/pointer/bodies.ts +0 -82
- package/src/pointer/index.ts +0 -4
- package/src/proof/law.ts +0 -591
- package/src/ward/index.ts +0 -8
- package/src/ward/pilot.ts +0 -50
- package/src/ward/ward-being.ts +0 -103
- package/src/ward/ward.ts +0 -328
- /package/dist/{cli/main.d.ts → cli.d.ts} +0 -0
- /package/dist/{being → core/being}/being.d.ts +0 -0
- /package/dist/{being → core/being}/digest.d.ts +0 -0
- /package/dist/{being → core/being}/digest.js +0 -0
- /package/dist/{being → core/being}/faculty.d.ts +0 -0
- /package/dist/{being → core/being}/types.d.ts +0 -0
- /package/dist/{being → core/being}/words.d.ts +0 -0
- /package/dist/{being → core/being}/words.js +0 -0
- /package/dist/{cli → core/cli}/edge.d.ts +0 -0
- /package/dist/{crypto → core/crypto}/aes.d.ts +0 -0
- /package/dist/{crypto → core/crypto}/aes.js +0 -0
- /package/dist/{crypto → core/crypto}/bytes.d.ts +0 -0
- /package/dist/{crypto → core/crypto}/bytes.js +0 -0
- /package/dist/{crypto → core/crypto}/ed25519.d.ts +0 -0
- /package/dist/{crypto → core/crypto}/hash.d.ts +0 -0
- /package/dist/{crypto → core/crypto}/hash.js +0 -0
- /package/dist/{crypto → core/crypto}/index.d.ts +0 -0
- /package/dist/{crypto → core/crypto}/index.js +0 -0
- /package/dist/{crypto → core/crypto}/json.d.ts +0 -0
- /package/dist/{crypto → core/crypto}/json.js +0 -0
- /package/dist/{crypto → core/crypto}/mlkem.d.ts +0 -0
- /package/dist/{crypto → core/crypto}/mlkem.js +0 -0
- /package/dist/{crypto → core/crypto}/x25519.d.ts +0 -0
- /package/dist/{folder → core/folder}/index.d.ts +0 -0
- /package/dist/{folder → core/folder}/index.js +0 -0
- /package/dist/{http → core/http}/websocket.d.ts +0 -0
- /package/dist/{http → core/http}/websocket.js +0 -0
- /package/dist/{line → core/line}/answer.js +0 -0
- /package/dist/{line → core/line}/ground.d.ts +0 -0
- /package/dist/{proof → core/proof}/expect.d.ts +0 -0
- /package/dist/{proof → core/proof}/expect.js +0 -0
- /package/dist/{quo → core/quo}/address.d.ts +0 -0
- /package/dist/{quo → core/quo}/address.js +0 -0
- /package/dist/{quo → core/quo}/door.d.ts +0 -0
- /package/dist/{quo → core/quo}/door.js +0 -0
- /package/dist/{quo → core/quo}/index.d.ts +0 -0
- /package/dist/{quo → core/quo}/index.js +0 -0
- /package/dist/{quo → core/quo}/invitation.d.ts +0 -0
- /package/dist/{quo → core/quo}/invitation.js +0 -0
- /package/dist/{quo → core/quo}/keys.d.ts +0 -0
- /package/dist/{quo → core/quo}/keys.js +0 -0
- /package/dist/{quo → core/quo}/payload.d.ts +0 -0
- /package/dist/{quo → core/quo}/payload.js +0 -0
- /package/dist/{quo → core/quo}/relations.d.ts +0 -0
- /package/dist/{quo → core/quo}/relations.js +0 -0
- /package/dist/{quo → core/quo}/reply.d.ts +0 -0
- /package/dist/{quo → core/quo}/reply.js +0 -0
- /package/dist/{quo → core/quo}/seal.d.ts +0 -0
- /package/dist/{quo → core/quo}/seal.js +0 -0
- /package/dist/{quo → core/quo}/standing.d.ts +0 -0
- /package/dist/{quo → core/quo}/standing.js +0 -0
- /package/dist/{ward → core/ward}/allowance.d.ts +0 -0
- /package/dist/{ward → core/ward}/allowance.js +0 -0
- /package/dist/{ward → core/ward}/cells.d.ts +0 -0
- /package/dist/{ward → core/ward}/cells.js +0 -0
- /package/dist/{ward → core/ward}/partition.js +0 -0
- /package/src/{being → core/being}/digest.ts +0 -0
- /package/src/{being → core/being}/words.ts +0 -0
- /package/src/{crypto → core/crypto}/aes.ts +0 -0
- /package/src/{crypto → core/crypto}/bytes.ts +0 -0
- /package/src/{crypto → core/crypto}/hash.ts +0 -0
- /package/src/{crypto → core/crypto}/index.ts +0 -0
- /package/src/{crypto → core/crypto}/json.ts +0 -0
- /package/src/{crypto → core/crypto}/mlkem.ts +0 -0
- /package/src/{folder → core/folder}/index.ts +0 -0
- /package/src/{http → core/http}/websocket.ts +0 -0
- /package/src/{proof → core/proof}/expect.ts +0 -0
- /package/src/{quo → core/quo}/address.ts +0 -0
- /package/src/{quo → core/quo}/door.ts +0 -0
- /package/src/{quo → core/quo}/index.ts +0 -0
- /package/src/{quo → core/quo}/invitation.ts +0 -0
- /package/src/{quo → core/quo}/keys.ts +0 -0
- /package/src/{quo → core/quo}/payload.ts +0 -0
- /package/src/{quo → core/quo}/relations.ts +0 -0
- /package/src/{quo → core/quo}/reply.ts +0 -0
- /package/src/{quo → core/quo}/seal.ts +0 -0
- /package/src/{quo → core/quo}/standing.ts +0 -0
- /package/src/{stand → core/stand}/main.ts +0 -0
- /package/src/{ward → core/ward}/allowance.ts +0 -0
- /package/src/{ward → core/ward}/cells.ts +0 -0
|
@@ -0,0 +1,739 @@
|
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
9
|
+
import { Custody, Loader, Memory, moduleOf } from '../contract/index.js';
|
|
10
|
+
import { concat, decrypt, encrypt, hex, hkdf, isHex, sha256, unhex, utf8 } from '../crypto/index.js';
|
|
11
|
+
import { carrierIn, Harbor, landPlaces, probeGround, readCarried, UNDIALED } from '../harbor/index.js';
|
|
12
|
+
import { answerHeld, answerPost, askFrame, FrameReader } from '../line/index.js';
|
|
13
|
+
import { CryptoEntropy, PointerTerrain } from '../pointer/index.js';
|
|
14
|
+
import { tcpAddress, tcpAt } from '../quo/index.js';
|
|
15
|
+
export const EDGE = 'edge';
|
|
16
|
+
const ENTRY = 'm/';
|
|
17
|
+
const SEED = 'c/seed';
|
|
18
|
+
// The most keys one storage call takes.
|
|
19
|
+
const BATCH = 128;
|
|
20
|
+
const SEAL_INFO = utf8('nervur edge custody');
|
|
21
|
+
const MAX_ID = 0xffff_ffff;
|
|
22
|
+
// The calls of a child, each by the number the shell gave it, and what
|
|
23
|
+
// each did to the memory: a miss or a refused keep is told to the call it
|
|
24
|
+
// happened in and to no other, and so is a keep that landed. A call that
|
|
25
|
+
// was refused and kept nothing is asked again whole in the next child. One
|
|
26
|
+
// that kept a step before it was refused, a line's seq before it asked
|
|
27
|
+
// out, is not, since a box asked again would find that step taken: it
|
|
28
|
+
// answers what the child answered, as after a crash.
|
|
29
|
+
export class ChildCalls {
|
|
30
|
+
#context;
|
|
31
|
+
#refused = new Set();
|
|
32
|
+
#kept = new Set();
|
|
33
|
+
constructor(context) {
|
|
34
|
+
this.#context = context;
|
|
35
|
+
}
|
|
36
|
+
run(call, work) {
|
|
37
|
+
return this.#context ? this.#context.run(call, work) : work();
|
|
38
|
+
}
|
|
39
|
+
// The call running now missed a module, or had a keep refused.
|
|
40
|
+
refused() {
|
|
41
|
+
const call = this.#context?.getStore();
|
|
42
|
+
if (call !== undefined)
|
|
43
|
+
this.#refused.add(call);
|
|
44
|
+
}
|
|
45
|
+
// A keep of the call running now landed.
|
|
46
|
+
kept() {
|
|
47
|
+
const call = this.#context?.getStore();
|
|
48
|
+
if (call !== undefined)
|
|
49
|
+
this.#kept.add(call);
|
|
50
|
+
}
|
|
51
|
+
// Whether a call that ended kept nothing and was refused, so it is asked
|
|
52
|
+
// again; told once per call.
|
|
53
|
+
again(call) {
|
|
54
|
+
const again = this.#refused.delete(call) && !this.#kept.has(call);
|
|
55
|
+
this.#kept.delete(call);
|
|
56
|
+
return again;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// The edge's loader: the modules its child was loaded with, each found by
|
|
60
|
+
// its blob's id. A blob it does not carry is a miss, kept with its source,
|
|
61
|
+
// so the shell loads the next child carrying it and asks again; a blob the
|
|
62
|
+
// shell found does not load here is refused with its reason. From the
|
|
63
|
+
// first miss the child is spent and its memory keeps nothing more, since a
|
|
64
|
+
// ward keeps every row it was told at once and the call that missed told
|
|
65
|
+
// some, the seq of the box it opened among them.
|
|
66
|
+
export class EdgeLoader extends Loader {
|
|
67
|
+
#carried;
|
|
68
|
+
#refused = new Map();
|
|
69
|
+
#missed = new Map();
|
|
70
|
+
#calls;
|
|
71
|
+
constructor(carried = [], calls) {
|
|
72
|
+
super();
|
|
73
|
+
this.#carried = new Map(carried.map((c) => [c.blob, c.module]));
|
|
74
|
+
this.#calls = calls;
|
|
75
|
+
}
|
|
76
|
+
get carried() {
|
|
77
|
+
return [...this.#carried.keys()];
|
|
78
|
+
}
|
|
79
|
+
refuse(refused) {
|
|
80
|
+
for (const [blob, reason] of Object.entries(refused))
|
|
81
|
+
this.#refused.set(blob, reason);
|
|
82
|
+
}
|
|
83
|
+
get refused() {
|
|
84
|
+
return this.#refused;
|
|
85
|
+
}
|
|
86
|
+
load(source, id) {
|
|
87
|
+
if (this.#carried.has(id))
|
|
88
|
+
return Promise.resolve().then(() => moduleOf(this.#carried.get(id)));
|
|
89
|
+
const refused = this.#refused.get(id);
|
|
90
|
+
if (refused !== undefined)
|
|
91
|
+
return Promise.reject(new Error(refused));
|
|
92
|
+
this.#missed.set(id, source);
|
|
93
|
+
this.#calls?.refused();
|
|
94
|
+
return Promise.reject(new Error(`blob ${id} is carried by the next child`));
|
|
95
|
+
}
|
|
96
|
+
// Whether the child missed a blob, so it is spent.
|
|
97
|
+
get missing() {
|
|
98
|
+
return this.#missed.size > 0;
|
|
99
|
+
}
|
|
100
|
+
// Every blob the child missed, with its source.
|
|
101
|
+
missed() {
|
|
102
|
+
return new Map(this.#missed);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// A harbor's name on the edge: one path segment, so it is where the
|
|
106
|
+
// harbor answers, `/<name>/quo`.
|
|
107
|
+
export const isHarborName = (name) => typeof name === 'string' && /^[a-z0-9][a-z0-9-]{0,62}$/.test(name);
|
|
108
|
+
const NONCE = 12;
|
|
109
|
+
// A pack as JSON reads, or null for anything else.
|
|
110
|
+
export const readPack = (value) => {
|
|
111
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
112
|
+
return null;
|
|
113
|
+
const pack = {};
|
|
114
|
+
for (const [name, entry] of Object.entries(value)) {
|
|
115
|
+
const { sealed, places } = (entry ?? {});
|
|
116
|
+
const carried = readCarried({ places });
|
|
117
|
+
if (!isHarborName(name) || !isHex(sealed) || sealed.length <= NONCE * 2 || carried === null)
|
|
118
|
+
return null;
|
|
119
|
+
pack[name] = { sealed: sealed, places: carried.places };
|
|
120
|
+
}
|
|
121
|
+
return pack;
|
|
122
|
+
};
|
|
123
|
+
const sealKey = (secret) => hkdf(utf8(secret), SEAL_INFO, 32);
|
|
124
|
+
// A seed sealed under a worker's secret, as a pack keeps it.
|
|
125
|
+
export const sealSeed = async (secret, seed, entropy = new CryptoEntropy()) => {
|
|
126
|
+
const nonce = entropy.draw(NONCE);
|
|
127
|
+
return hex(concat(nonce, await encrypt(await sealKey(secret), nonce, new Uint8Array(0), seed)));
|
|
128
|
+
};
|
|
129
|
+
const chunks = (items) => Array.from({ length: Math.ceil(items.length / BATCH) }, (_, i) => items.slice(i * BATCH, (i + 1) * BATCH));
|
|
130
|
+
const bytesOf = (value) => new Uint8Array(value instanceof ArrayBuffer ? value : value);
|
|
131
|
+
// A body keeps its keys under `root`, so one object may hold more than one
|
|
132
|
+
// keeping.
|
|
133
|
+
// A body refuses every keep while `voided` says so: in a child, from the
|
|
134
|
+
// moment its loader misses a module, so a spent child keeps nothing more,
|
|
135
|
+
// and tells the child's calls which keeps it refused and which landed.
|
|
136
|
+
export class EdgeMemory extends Memory {
|
|
137
|
+
#storage;
|
|
138
|
+
#entry;
|
|
139
|
+
#voided;
|
|
140
|
+
#calls;
|
|
141
|
+
constructor(storage, root = '', voided = () => false, calls) {
|
|
142
|
+
super();
|
|
143
|
+
this.#storage = storage;
|
|
144
|
+
this.#entry = `${root}${ENTRY}`;
|
|
145
|
+
this.#voided = voided;
|
|
146
|
+
this.#calls = calls;
|
|
147
|
+
}
|
|
148
|
+
#refuse() {
|
|
149
|
+
if (!this.#voided())
|
|
150
|
+
return null;
|
|
151
|
+
this.#calls?.refused();
|
|
152
|
+
return Promise.reject(new Error('a module was missed, so this call keeps nothing'));
|
|
153
|
+
}
|
|
154
|
+
async read(place) {
|
|
155
|
+
const prefix = `${this.#entry}${place}/`;
|
|
156
|
+
const listed = await this.#storage.list({ prefix });
|
|
157
|
+
return new Map([...listed].map(([key, value]) => [key.slice(prefix.length), bytesOf(value).slice()]));
|
|
158
|
+
}
|
|
159
|
+
write(place, entries) {
|
|
160
|
+
const refused = this.#refuse();
|
|
161
|
+
if (refused)
|
|
162
|
+
return refused;
|
|
163
|
+
const put = [];
|
|
164
|
+
const gone = [];
|
|
165
|
+
for (const [name, bytes] of entries) {
|
|
166
|
+
if (bytes === null)
|
|
167
|
+
gone.push(`${this.#entry}${place}/${name}`);
|
|
168
|
+
else
|
|
169
|
+
put.push([`${this.#entry}${place}/${name}`, bytes.slice()]);
|
|
170
|
+
}
|
|
171
|
+
return this.#storage
|
|
172
|
+
.transaction(async (txn) => {
|
|
173
|
+
for (const batch of chunks(put))
|
|
174
|
+
await txn.put(Object.fromEntries(batch));
|
|
175
|
+
for (const batch of chunks(gone))
|
|
176
|
+
await txn.delete(batch);
|
|
177
|
+
})
|
|
178
|
+
.then(() => this.#calls?.kept());
|
|
179
|
+
}
|
|
180
|
+
async places() {
|
|
181
|
+
const entry = this.#entry;
|
|
182
|
+
const keys = [...(await this.#storage.list({ prefix: entry })).keys()];
|
|
183
|
+
return [...new Set(keys.map((key) => key.slice(entry.length, key.indexOf('/', entry.length))))];
|
|
184
|
+
}
|
|
185
|
+
async forget(place) {
|
|
186
|
+
await this.#refuse();
|
|
187
|
+
const keys = [...(await this.#storage.list({ prefix: `${this.#entry}${place}/` })).keys()];
|
|
188
|
+
await this.#storage.transaction(async (txn) => {
|
|
189
|
+
for (const batch of chunks(keys))
|
|
190
|
+
await txn.delete(batch);
|
|
191
|
+
});
|
|
192
|
+
this.#calls?.kept();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
// The seed, kept sealed under a key the worker's secret gives, so the
|
|
196
|
+
// storage alone opens nothing: the one a pack landed, or one drawn the
|
|
197
|
+
// first time it is asked for. A worker with no secret stands no harbor.
|
|
198
|
+
export class EdgeCustody extends Custody {
|
|
199
|
+
#storage;
|
|
200
|
+
#secret;
|
|
201
|
+
#entropy;
|
|
202
|
+
#key;
|
|
203
|
+
constructor(storage, secret, entropy = new CryptoEntropy(), root = '') {
|
|
204
|
+
super();
|
|
205
|
+
this.#storage = storage;
|
|
206
|
+
this.#secret = secret;
|
|
207
|
+
this.#entropy = entropy;
|
|
208
|
+
this.#key = `${root}${SEED}`;
|
|
209
|
+
}
|
|
210
|
+
// Whether a seed is kept, so a pack lands only where none is.
|
|
211
|
+
async kept() {
|
|
212
|
+
return (await this.#storage.get(this.#key)) !== undefined;
|
|
213
|
+
}
|
|
214
|
+
// A packed seed, sealed as `sealSeed` sealed it, kept as it came.
|
|
215
|
+
async land(sealed, txn = this.#storage) {
|
|
216
|
+
const bytes = unhex(sealed);
|
|
217
|
+
await txn.put({ [this.#key]: { nonce: bytes.slice(0, NONCE), sealed: bytes.slice(NONCE) } });
|
|
218
|
+
}
|
|
219
|
+
async seed() {
|
|
220
|
+
if (!this.#secret)
|
|
221
|
+
throw new Error('the worker holds no NERVUR_SECRET');
|
|
222
|
+
const key = await sealKey(this.#secret);
|
|
223
|
+
return this.#storage.transaction(async (txn) => {
|
|
224
|
+
const kept = (await txn.get(this.#key));
|
|
225
|
+
if (kept) {
|
|
226
|
+
const seed = await decrypt(key, bytesOf(kept.nonce), new Uint8Array(0), bytesOf(kept.sealed));
|
|
227
|
+
if (seed === null)
|
|
228
|
+
throw new Error('the seed does not open under this NERVUR_SECRET');
|
|
229
|
+
return seed;
|
|
230
|
+
}
|
|
231
|
+
const seed = this.#entropy.draw(32);
|
|
232
|
+
const nonce = this.#entropy.draw(NONCE);
|
|
233
|
+
await txn.put({ [this.#key]: { nonce, sealed: await encrypt(key, nonce, new Uint8Array(0), seed) } });
|
|
234
|
+
return seed.slice();
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
class EdgeLine {
|
|
239
|
+
#socket;
|
|
240
|
+
#writer;
|
|
241
|
+
#waiting = new Map();
|
|
242
|
+
#next = 1;
|
|
243
|
+
#closed = false;
|
|
244
|
+
constructor(socket, onClose) {
|
|
245
|
+
this.#socket = socket;
|
|
246
|
+
this.#writer = socket.writable.getWriter();
|
|
247
|
+
const reader = new FrameReader();
|
|
248
|
+
void (async () => {
|
|
249
|
+
try {
|
|
250
|
+
for await (const chunk of socket.readable) {
|
|
251
|
+
for (const frame of reader.push(new Uint8Array(chunk))) {
|
|
252
|
+
if (frame.kind === 'ask')
|
|
253
|
+
continue;
|
|
254
|
+
const done = this.#waiting.get(frame.id);
|
|
255
|
+
this.#waiting.delete(frame.id);
|
|
256
|
+
done?.(frame.kind === 'reply' ? frame.box : null);
|
|
257
|
+
}
|
|
258
|
+
if (reader.bad)
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
// A line that broke answers nothing more.
|
|
264
|
+
}
|
|
265
|
+
this.#closed = true;
|
|
266
|
+
onClose();
|
|
267
|
+
for (const done of this.#waiting.values())
|
|
268
|
+
done(null);
|
|
269
|
+
this.#waiting.clear();
|
|
270
|
+
await socket.close().catch(() => undefined);
|
|
271
|
+
})();
|
|
272
|
+
}
|
|
273
|
+
ask(pk, box) {
|
|
274
|
+
if (this.#closed)
|
|
275
|
+
return Promise.resolve(null);
|
|
276
|
+
const id = this.#next;
|
|
277
|
+
this.#next = this.#next === MAX_ID ? 1 : this.#next + 1;
|
|
278
|
+
return new Promise((resolve) => {
|
|
279
|
+
this.#waiting.set(id, resolve);
|
|
280
|
+
this.#writer.write(askFrame(id, pk, box)).catch(() => resolve(null));
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
close() {
|
|
284
|
+
this.#closed = true;
|
|
285
|
+
return this.#socket.close().catch(() => undefined);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
const sockets = 'cloudflare:sockets';
|
|
289
|
+
const connector = async () => {
|
|
290
|
+
try {
|
|
291
|
+
return (await import(__rewriteRelativeImportExtension(/* @vite-ignore */ sockets))).connect;
|
|
292
|
+
}
|
|
293
|
+
catch {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
// The worker's sockets under the TCP carrier: it dials on the worker's
|
|
298
|
+
// `connect`, one line per address, and takes no inbound TCP.
|
|
299
|
+
export class EdgeSockets {
|
|
300
|
+
#lines = new Map();
|
|
301
|
+
async dial(address, pk, bytes) {
|
|
302
|
+
const at = tcpAddress(address);
|
|
303
|
+
if (at === null || !isHex(pk, 64))
|
|
304
|
+
return UNDIALED;
|
|
305
|
+
const line = await this.#line(at);
|
|
306
|
+
return line ? line.ask(unhex(pk), bytes) : UNDIALED;
|
|
307
|
+
}
|
|
308
|
+
#line({ host, port }) {
|
|
309
|
+
const at = tcpAt(host, port);
|
|
310
|
+
let line = this.#lines.get(at);
|
|
311
|
+
if (!line) {
|
|
312
|
+
line = (async () => {
|
|
313
|
+
const connect = await connector();
|
|
314
|
+
if (!connect)
|
|
315
|
+
return null;
|
|
316
|
+
try {
|
|
317
|
+
const socket = connect({ hostname: host, port });
|
|
318
|
+
await socket.opened;
|
|
319
|
+
return new EdgeLine(socket, () => this.#lines.delete(at));
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
})();
|
|
325
|
+
this.#lines.set(at, line);
|
|
326
|
+
void line.then((open) => open ?? this.#lines.delete(at));
|
|
327
|
+
}
|
|
328
|
+
return line;
|
|
329
|
+
}
|
|
330
|
+
listen() {
|
|
331
|
+
return Promise.reject(new Error('an edge takes no inbound TCP'));
|
|
332
|
+
}
|
|
333
|
+
async close() {
|
|
334
|
+
const lines = [...this.#lines.values()];
|
|
335
|
+
this.#lines.clear();
|
|
336
|
+
for (const line of await Promise.all(lines))
|
|
337
|
+
await line?.close();
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
const edgeServe = (listening, listen) => {
|
|
341
|
+
listening.at = listen;
|
|
342
|
+
const authority = listen.port === 0 ? listen.host : `${tcpAt(listen.host, listen.port).slice('tcp://'.length)}`;
|
|
343
|
+
const [post, held] = listen.port === 0 ? ['https', 'wss'] : ['http', 'ws'];
|
|
344
|
+
return Promise.resolve({
|
|
345
|
+
port: listen.port,
|
|
346
|
+
at: [`${post}://${authority}${listen.path}`, `${held}://${authority}${listen.path}`],
|
|
347
|
+
close: () => {
|
|
348
|
+
if (listening.at === listen)
|
|
349
|
+
listening.at = undefined;
|
|
350
|
+
return Promise.resolve();
|
|
351
|
+
},
|
|
352
|
+
});
|
|
353
|
+
};
|
|
354
|
+
export class EdgeTerrain extends PointerTerrain {
|
|
355
|
+
given;
|
|
356
|
+
#listening;
|
|
357
|
+
constructor(parts) {
|
|
358
|
+
const entropy = new CryptoEntropy();
|
|
359
|
+
const { storage } = parts.given.state;
|
|
360
|
+
const loader = parts.given.loader ?? new EdgeLoader();
|
|
361
|
+
const listening = { at: undefined };
|
|
362
|
+
super({
|
|
363
|
+
entropy,
|
|
364
|
+
loader,
|
|
365
|
+
custody: new EdgeCustody(storage, parts.given.env.NERVUR_SECRET, entropy),
|
|
366
|
+
memory: new EdgeMemory(storage, '', () => loader.missing, parts.given.calls),
|
|
367
|
+
tcp: () => new EdgeSockets(),
|
|
368
|
+
web: (_ground, listen) => edgeServe(listening, listen),
|
|
369
|
+
defaults: parts.defaults,
|
|
370
|
+
});
|
|
371
|
+
this.given = parts.given;
|
|
372
|
+
this.#listening = listening;
|
|
373
|
+
}
|
|
374
|
+
// Where the web carrier listens now, which the shell answers at.
|
|
375
|
+
get listening() {
|
|
376
|
+
return this.#listening.at;
|
|
377
|
+
}
|
|
378
|
+
// A packed harbor lands where the object keeps no seed yet: its places
|
|
379
|
+
// first and its seed last, so a start cut short lands it whole again,
|
|
380
|
+
// and once the seed is kept the storage is the harbor and a new deploy
|
|
381
|
+
// leaves it be. A seed sealed under another secret does not open.
|
|
382
|
+
async claim() {
|
|
383
|
+
const packed = this.given.package;
|
|
384
|
+
const custody = this.custody;
|
|
385
|
+
if (!packed || (await custody.kept()))
|
|
386
|
+
return;
|
|
387
|
+
await landPlaces(this.memory, { places: packed.places });
|
|
388
|
+
await custody.land(packed.sealed);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
const isGiven = (value) => {
|
|
392
|
+
const given = value;
|
|
393
|
+
return typeof given?.state?.storage?.transaction === 'function' && typeof given.env === 'object';
|
|
394
|
+
};
|
|
395
|
+
// A harbor handed a Durable Object's state and environment, and the
|
|
396
|
+
// loader of the modules its child carries.
|
|
397
|
+
export const edgeGround = {
|
|
398
|
+
name: EDGE,
|
|
399
|
+
fits: ({ given }) => isGiven(given),
|
|
400
|
+
terrain: ({ given }, defaults) => new EdgeTerrain({ given: given, defaults }),
|
|
401
|
+
};
|
|
402
|
+
// The harbor a request is for: the first segment of its path.
|
|
403
|
+
export const harborNameOf = (request) => {
|
|
404
|
+
const name = new URL(request.url).pathname.split('/')[1];
|
|
405
|
+
return isHarborName(name) ? name : null;
|
|
406
|
+
};
|
|
407
|
+
// The key a child is loaded under: the commit `live` names, where the
|
|
408
|
+
// child carries that commit's modules, and else that commit and every
|
|
409
|
+
// blob the child carries.
|
|
410
|
+
export const childKey = (live, blobs, liveBlobs) => {
|
|
411
|
+
const carried = [...new Set(blobs)].sort();
|
|
412
|
+
const same = carried.length === new Set(liveBlobs).size && liveBlobs.every((b) => carried.includes(b));
|
|
413
|
+
return same ? live : `${live}+${carried.join('+')}`;
|
|
414
|
+
};
|
|
415
|
+
// The whole harbor inside a child: its bodies on the storage the facet
|
|
416
|
+
// keeps, its loader over the modules the child's worker imported, and the
|
|
417
|
+
// web door the shell hands each box through. It opens the harbor once, and
|
|
418
|
+
// after every call says whether the shell must move to another child.
|
|
419
|
+
export class HarborChild {
|
|
420
|
+
#state;
|
|
421
|
+
#env;
|
|
422
|
+
#key;
|
|
423
|
+
#loader;
|
|
424
|
+
#calls;
|
|
425
|
+
#opening;
|
|
426
|
+
// The terrain the child opened its harbor on, which says where its web
|
|
427
|
+
// carrier listens.
|
|
428
|
+
#terrain;
|
|
429
|
+
#settled;
|
|
430
|
+
constructor(state, env, key, carried, context) {
|
|
431
|
+
this.#state = state;
|
|
432
|
+
this.#env = env;
|
|
433
|
+
this.#key = key;
|
|
434
|
+
this.#calls = new ChildCalls(context);
|
|
435
|
+
this.#loader = new EdgeLoader(carried, this.#calls);
|
|
436
|
+
}
|
|
437
|
+
// The harbor, opened inside the object's block so no call reaches it
|
|
438
|
+
// half open, its web carrier listening at `/<name>/quo`. A child that
|
|
439
|
+
// missed a module of `live` as the harbor booted stands nothing more:
|
|
440
|
+
// the shell moves to the next before it hands this one a call.
|
|
441
|
+
async open(name, host, port, packed, refused) {
|
|
442
|
+
this.#loader.refuse(refused);
|
|
443
|
+
this.#opening ??= this.#state.blockConcurrencyWhile(async () => {
|
|
444
|
+
const terrain = probeGround({ given: { state: this.#state, env: this.#env, loader: this.#loader, calls: this.#calls, ...(packed ? { package: packed } : {}) } }).terrain;
|
|
445
|
+
this.#terrain = terrain;
|
|
446
|
+
const harbor = await Harbor.open(terrain);
|
|
447
|
+
if (this.#loader.missing)
|
|
448
|
+
return harbor;
|
|
449
|
+
await harbor.ask({ method: 'stand' });
|
|
450
|
+
const { beings } = (await harbor.ask({})).answer.notes;
|
|
451
|
+
const web = carrierIn(beings, terrain.defaults.classes, 'web');
|
|
452
|
+
if (web === null)
|
|
453
|
+
throw new Error('no class this edge runs carries web');
|
|
454
|
+
if (web.open !== undefined)
|
|
455
|
+
await harbor.ask(web.open);
|
|
456
|
+
if (terrain.listening?.path !== `/${name}/quo`)
|
|
457
|
+
await harbor.ask({ method: 'ask', args: { being: web.key, method: 'listen', args: { host, port, path: `/${name}/quo` } } });
|
|
458
|
+
return harbor;
|
|
459
|
+
});
|
|
460
|
+
const harbor = await this.#opening;
|
|
461
|
+
const at = this.#terrain?.listening;
|
|
462
|
+
// A plain copy, since what crosses to the shell is cloned.
|
|
463
|
+
const listen = at ? { host: at.host, port: at.port, path: at.path, origins: [...at.origins] } : null;
|
|
464
|
+
return { listen, wanted: await this.#wanted(harbor) };
|
|
465
|
+
}
|
|
466
|
+
// One box from the web door under the shell's number for the call, its
|
|
467
|
+
// reply, and whether it is asked again. A call that lands in a spent
|
|
468
|
+
// child is asked again untouched.
|
|
469
|
+
async arrive(pk, box, call) {
|
|
470
|
+
const harbor = await this.#opening;
|
|
471
|
+
if (!harbor)
|
|
472
|
+
return { reply: null, wanted: null, again: false };
|
|
473
|
+
if (this.#loader.missing)
|
|
474
|
+
return { reply: null, wanted: await this.#wanted(harbor), again: true };
|
|
475
|
+
const reply = await this.#calls.run(call, () => harbor.arrive(pk, box)).catch(() => null);
|
|
476
|
+
return { reply, wanted: await this.#wanted(harbor), again: this.#calls.again(call) };
|
|
477
|
+
}
|
|
478
|
+
async #wanted(harbor) {
|
|
479
|
+
const missed = this.#loader.missed();
|
|
480
|
+
const catalogue = async (method) => (await harbor.ask({ method: 'ask', args: { being: 'catalogue', method } })).answer.answer;
|
|
481
|
+
const live = (await catalogue('live')).live;
|
|
482
|
+
if (missed.size === 0 && live === this.#settled)
|
|
483
|
+
return null;
|
|
484
|
+
const sources = (await catalogue('sources')).sources;
|
|
485
|
+
const modules = new Map(sources.map((s) => [s.blob, s.source]));
|
|
486
|
+
for (const [blob, source] of missed)
|
|
487
|
+
modules.set(blob, source);
|
|
488
|
+
for (const blob of this.#loader.refused.keys())
|
|
489
|
+
modules.delete(blob);
|
|
490
|
+
const liveBlobs = sources.map((s) => s.blob);
|
|
491
|
+
if (missed.size === 0 && childKey(live, [...modules.keys()], liveBlobs) === this.#key) {
|
|
492
|
+
this.#settled = live;
|
|
493
|
+
return null;
|
|
494
|
+
}
|
|
495
|
+
return { live, liveBlobs, modules: Object.fromEntries(modules), missed: [...missed.keys()] };
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
const FACET = 'harbor';
|
|
499
|
+
const CHILD = 'Harbor';
|
|
500
|
+
const MAIN = 'harbor.js';
|
|
501
|
+
const PROBE = 'probe.js';
|
|
502
|
+
const BOOT = 'boot';
|
|
503
|
+
const COMPATIBILITY = '2026-09-01';
|
|
504
|
+
// The most moves one call makes before the shell gives up on it.
|
|
505
|
+
const ROUNDS = 8;
|
|
506
|
+
// A module's file at the child's root, where its `'nervur'` names the kit.
|
|
507
|
+
const moduleFile = (blob) => `${blob}.js`;
|
|
508
|
+
// A child's main module: the kit's child as the facet's class, over the
|
|
509
|
+
// modules it carries, each imported by its blob's file.
|
|
510
|
+
const childMain = (key, blobs) => [
|
|
511
|
+
"import { DurableObject } from 'cloudflare:workers';",
|
|
512
|
+
"import { AsyncLocalStorage } from 'node:async_hooks';",
|
|
513
|
+
"import { HarborChild } from 'nervur';",
|
|
514
|
+
...blobs.map((b, i) => `import * as m${String(i)} from '${moduleFile(b)}';`),
|
|
515
|
+
`const carried = [${blobs.map((b, i) => `{ blob: '${b}', module: m${String(i)} }`).join(', ')}];`,
|
|
516
|
+
'export class Harbor extends DurableObject {',
|
|
517
|
+
` #child = new HarborChild(this.ctx, this.env, ${JSON.stringify(key)}, carried, new AsyncLocalStorage());`,
|
|
518
|
+
' open(name, host, port, packed, refused) { return this.#child.open(name, host, port, packed, refused); }',
|
|
519
|
+
' arrive(pk, box, call) { return this.#child.arrive(pk, box, call); }',
|
|
520
|
+
'}',
|
|
521
|
+
'',
|
|
522
|
+
].join('\n');
|
|
523
|
+
// A probe's main module: the module alone beside the kit, so one that
|
|
524
|
+
// does not load fails here and never in a harbor's child.
|
|
525
|
+
const probeMain = (blob) => `import '${moduleFile(blob)}';\nexport default { fetch() { return new Response(null, { status: 204 }); } };\n`;
|
|
526
|
+
const said = (e) => (e instanceof Error ? e.message : String(e));
|
|
527
|
+
// A Durable Object class whose every instance is one harbor, by the name
|
|
528
|
+
// the worker reached it under, the one its pack holds under that name. The
|
|
529
|
+
// object is a shell: it keeps the harbor's storage as its child's facet,
|
|
530
|
+
// answers the web at `/<name>/quo`, and hands each box to the child it
|
|
531
|
+
// loaded for the commit `live` names. A call that misses a module loads
|
|
532
|
+
// the next child carrying it and is asked there again, since a miss comes
|
|
533
|
+
// before `live` or any cell moves; a module that does not load on its own
|
|
534
|
+
// is refused with its reason, and `live` stays. An object answers the one
|
|
535
|
+
// name it opened under, and a name its pack does not hold is 404, with
|
|
536
|
+
// nothing written. The shell never runs a harbor's code and reads no cell.
|
|
537
|
+
export const harborShell = ({ kit, pack }) => class {
|
|
538
|
+
#state;
|
|
539
|
+
#env;
|
|
540
|
+
#refused = {};
|
|
541
|
+
#name;
|
|
542
|
+
#packed;
|
|
543
|
+
#at;
|
|
544
|
+
#kit;
|
|
545
|
+
#key;
|
|
546
|
+
#child;
|
|
547
|
+
#listen = null;
|
|
548
|
+
#standing;
|
|
549
|
+
#turn = Promise.resolve();
|
|
550
|
+
// The number the shell gives each call it hands a child.
|
|
551
|
+
#calls = 0;
|
|
552
|
+
// The calls inside the child now, and the moves waiting for them to end.
|
|
553
|
+
#inside = 0;
|
|
554
|
+
#ended = [];
|
|
555
|
+
// Where the child said the shell must go, the latest word of the child
|
|
556
|
+
// that stands.
|
|
557
|
+
#wanted;
|
|
558
|
+
constructor(state, env) {
|
|
559
|
+
this.#state = state;
|
|
560
|
+
this.#env = env;
|
|
561
|
+
}
|
|
562
|
+
async fetch(request) {
|
|
563
|
+
const name = harborNameOf(request);
|
|
564
|
+
if (name === null || (this.#name !== undefined && this.#name !== name))
|
|
565
|
+
return new Response(null, { status: 404 });
|
|
566
|
+
if (this.#name === undefined) {
|
|
567
|
+
const packed = typeof pack === 'function' ? pack(this.#env) : pack;
|
|
568
|
+
const held = packed && Object.hasOwn(packed, name) ? packed[name] : undefined;
|
|
569
|
+
if (!held)
|
|
570
|
+
return new Response(null, { status: 404 });
|
|
571
|
+
const url = new URL(request.url);
|
|
572
|
+
this.#name = name;
|
|
573
|
+
this.#packed = held;
|
|
574
|
+
this.#at = { host: url.hostname, port: url.port === '' ? 0 : Number(url.port) };
|
|
575
|
+
}
|
|
576
|
+
this.#standing ??= this.#serial(() => this.#open(BOOT, {}, 0)).catch((e) => {
|
|
577
|
+
this.#standing = undefined;
|
|
578
|
+
throw e;
|
|
579
|
+
});
|
|
580
|
+
await this.#standing;
|
|
581
|
+
const listen = this.#listen;
|
|
582
|
+
if (!listen)
|
|
583
|
+
return new Response(null, { status: 404 });
|
|
584
|
+
const door = { arrive: (pk, box) => this.#arrive(pk, box) };
|
|
585
|
+
if (request.headers.get('upgrade')?.toLowerCase() !== 'websocket')
|
|
586
|
+
return answerPost(door, listen, request);
|
|
587
|
+
const offered = (request.headers.get('sec-websocket-protocol') ?? '').split(',').map((p) => p.trim());
|
|
588
|
+
if (new URL(request.url).pathname !== listen.path || !offered.includes('quo'))
|
|
589
|
+
return new Response(null, { status: 400 });
|
|
590
|
+
const Pair = globalThis.WebSocketPair;
|
|
591
|
+
const pair = new Pair();
|
|
592
|
+
const [client, server] = [pair[0], pair[1]];
|
|
593
|
+
server.accept();
|
|
594
|
+
const side = { send: (body) => server.send(new Uint8Array(body)), close: () => server.close() };
|
|
595
|
+
server.addEventListener('message', (event) => answerHeld(door, side, typeof event.data === 'string' ? event.data : new Uint8Array(event.data)));
|
|
596
|
+
return new Response(null, { status: 101, webSocket: client, headers: { 'sec-websocket-protocol': 'quo' } });
|
|
597
|
+
}
|
|
598
|
+
// One move at a time.
|
|
599
|
+
#serial(work) {
|
|
600
|
+
const run = this.#turn.then(work, work);
|
|
601
|
+
this.#turn = run.catch(() => undefined);
|
|
602
|
+
return run;
|
|
603
|
+
}
|
|
604
|
+
#loader() {
|
|
605
|
+
const loader = this.#env.LOADER;
|
|
606
|
+
if (!loader)
|
|
607
|
+
throw new Error('the worker binds no LOADER: its wrangler.toml names a worker_loaders binding');
|
|
608
|
+
return loader;
|
|
609
|
+
}
|
|
610
|
+
#kitId() {
|
|
611
|
+
return (this.#kit ??= sha256(utf8(kit)).then((digest) => hex(digest).slice(0, 16)));
|
|
612
|
+
}
|
|
613
|
+
// The child under `key`, carrying `modules`, stood as the facet in the
|
|
614
|
+
// one before's place, and opened. A staging child carries a blob a
|
|
615
|
+
// call missed beside what `live` names, and keeps its key until that
|
|
616
|
+
// call, asked again, moves `live`.
|
|
617
|
+
async #open(key, modules, depth, staging = false) {
|
|
618
|
+
const id = `${await this.#kitId()}:${key}`;
|
|
619
|
+
const blobs = Object.keys(modules).sort();
|
|
620
|
+
const secret = this.#env.NERVUR_SECRET;
|
|
621
|
+
const code = {
|
|
622
|
+
compatibilityDate: COMPATIBILITY,
|
|
623
|
+
// `AsyncLocalStorage` alone of Node's modules, to tell calls apart.
|
|
624
|
+
compatibilityFlags: ['nodejs_als'],
|
|
625
|
+
mainModule: MAIN,
|
|
626
|
+
modules: { [MAIN]: { js: childMain(key, blobs) }, nervur: { js: kit }, ...Object.fromEntries(blobs.map((b) => [moduleFile(b), { js: modules[b] }])) },
|
|
627
|
+
env: secret === undefined ? {} : { NERVUR_SECRET: secret },
|
|
628
|
+
};
|
|
629
|
+
const worker = this.#loader().get(id, () => Promise.resolve(code));
|
|
630
|
+
if (this.#child)
|
|
631
|
+
this.#state.facets.abort(FACET, new Error('live moved'));
|
|
632
|
+
const child = this.#state.facets.get(FACET, () => Promise.resolve({ class: worker.getDurableObjectClass(CHILD) }));
|
|
633
|
+
this.#child = child;
|
|
634
|
+
this.#key = key;
|
|
635
|
+
this.#wanted = undefined;
|
|
636
|
+
const opened = await child.open(this.#name, this.#at.host, this.#at.port, this.#packed ?? null, { ...this.#refused });
|
|
637
|
+
this.#listen = opened.listen;
|
|
638
|
+
if (opened.wanted && (!staging || opened.wanted.missed.length > 0))
|
|
639
|
+
await this.#move(opened.wanted, depth + 1);
|
|
640
|
+
}
|
|
641
|
+
// Where a child said the shell must go: each blob it missed probed, one
|
|
642
|
+
// that does not load refused, and the child for what remains loaded.
|
|
643
|
+
// A call that missed kept nothing, so a fresh child takes it whole,
|
|
644
|
+
// under the same key where every blob it missed is refused.
|
|
645
|
+
async #move(wanted, depth) {
|
|
646
|
+
if (depth > ROUNDS)
|
|
647
|
+
throw new Error('the harbor settles on no child');
|
|
648
|
+
for (const blob of wanted.missed) {
|
|
649
|
+
if (Object.hasOwn(this.#refused, blob))
|
|
650
|
+
continue;
|
|
651
|
+
const reason = await this.#probe(blob, wanted.modules[blob] ?? '');
|
|
652
|
+
if (reason !== null)
|
|
653
|
+
this.#refused[blob] = reason;
|
|
654
|
+
}
|
|
655
|
+
const modules = Object.fromEntries(Object.entries(wanted.modules).filter(([blob]) => !Object.hasOwn(this.#refused, blob)));
|
|
656
|
+
const key = childKey(wanted.live, Object.keys(modules), wanted.liveBlobs);
|
|
657
|
+
const staging = wanted.missed.length > 0;
|
|
658
|
+
if (key === this.#key && !staging)
|
|
659
|
+
return;
|
|
660
|
+
return this.#open(key, modules, depth, staging);
|
|
661
|
+
}
|
|
662
|
+
// Null where a module loads on its own beside the kit, and else why not.
|
|
663
|
+
async #probe(blob, source) {
|
|
664
|
+
const code = {
|
|
665
|
+
compatibilityDate: COMPATIBILITY,
|
|
666
|
+
mainModule: PROBE,
|
|
667
|
+
modules: { [PROBE]: { js: probeMain(blob) }, nervur: { js: kit }, [moduleFile(blob)]: { js: source } },
|
|
668
|
+
env: {},
|
|
669
|
+
};
|
|
670
|
+
try {
|
|
671
|
+
const worker = this.#loader().get(`${await this.#kitId()}:probe:${blob}`, () => Promise.resolve(code));
|
|
672
|
+
const answered = await worker.getEntrypoint().fetch(new Request('https://probe.invalid/'));
|
|
673
|
+
return answered.status === 204 ? null : `the module does not load on the edge: ${await answered.text()}`;
|
|
674
|
+
}
|
|
675
|
+
catch (e) {
|
|
676
|
+
return `the module does not load on the edge: ${said(e)}`;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
// A box handed to the child, and its answer. Where the child says the
|
|
680
|
+
// shell must move, the move waits until no call is inside, so none is
|
|
681
|
+
// cut short, and no call enters while it waits. A call that missed a
|
|
682
|
+
// module told nothing, and is asked again in the next child.
|
|
683
|
+
async #arrive(pk, box) {
|
|
684
|
+
for (let round = 0; round <= ROUNDS; round += 1) {
|
|
685
|
+
let turn;
|
|
686
|
+
do {
|
|
687
|
+
turn = this.#turn;
|
|
688
|
+
await turn;
|
|
689
|
+
} while (turn !== this.#turn);
|
|
690
|
+
const child = this.#child;
|
|
691
|
+
const call = (this.#calls += 1);
|
|
692
|
+
this.#inside += 1;
|
|
693
|
+
let arrived;
|
|
694
|
+
try {
|
|
695
|
+
arrived = await child.arrive(pk, box, call);
|
|
696
|
+
}
|
|
697
|
+
catch {
|
|
698
|
+
arrived = null;
|
|
699
|
+
}
|
|
700
|
+
finally {
|
|
701
|
+
this.#inside -= 1;
|
|
702
|
+
if (this.#inside === 0)
|
|
703
|
+
for (const end of this.#ended.splice(0))
|
|
704
|
+
end();
|
|
705
|
+
}
|
|
706
|
+
if (arrived === null)
|
|
707
|
+
return null;
|
|
708
|
+
if (arrived.wanted && child === this.#child) {
|
|
709
|
+
this.#wanted = arrived.wanted;
|
|
710
|
+
void this.#serial(() => this.#moved(child)).catch(() => undefined);
|
|
711
|
+
}
|
|
712
|
+
if (!arrived.again)
|
|
713
|
+
return arrived.reply;
|
|
714
|
+
}
|
|
715
|
+
return null;
|
|
716
|
+
}
|
|
717
|
+
// The move the child that stands asked for, once no call is inside it.
|
|
718
|
+
async #moved(child) {
|
|
719
|
+
if (this.#inside > 0)
|
|
720
|
+
await new Promise((end) => this.#ended.push(end));
|
|
721
|
+
const wanted = this.#wanted;
|
|
722
|
+
if (child !== this.#child || !wanted)
|
|
723
|
+
return;
|
|
724
|
+
this.#wanted = undefined;
|
|
725
|
+
await this.#move(wanted, 0);
|
|
726
|
+
}
|
|
727
|
+
};
|
|
728
|
+
// A worker of many harbors: each request goes to the object of the
|
|
729
|
+
// harbor its path names, in the Durable Object namespace bound as
|
|
730
|
+
// `binding`, and a path naming none is 404.
|
|
731
|
+
export const harborWorker = (binding = 'HARBOR') => ({
|
|
732
|
+
fetch(request, env) {
|
|
733
|
+
const name = harborNameOf(request);
|
|
734
|
+
const objects = env[binding];
|
|
735
|
+
if (name === null || !objects)
|
|
736
|
+
return Promise.resolve(new Response(null, { status: 404 }));
|
|
737
|
+
return objects.get(objects.idFromName(name)).fetch(request);
|
|
738
|
+
},
|
|
739
|
+
});
|