nervur 0.20.4 → 0.21.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.
- package/README.md +61 -19
- package/dist/cli/command.js +18 -4
- package/dist/cli/edge.d.ts +7 -0
- package/dist/cli/edge.js +58 -0
- package/dist/contract/index.js +2 -1
- package/dist/harbor/catalogue.js +11 -3
- package/dist/harbor/dock.js +6 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/node/index.js +12 -4
- package/dist/proof/contracts.d.ts +15 -0
- package/dist/proof/contracts.js +115 -0
- package/dist/proof/expect.d.ts +6 -0
- package/dist/proof/expect.js +18 -0
- package/dist/proof/index.d.ts +3 -0
- package/dist/proof/index.js +12 -0
- package/dist/proof/law.d.ts +52 -0
- package/dist/proof/law.js +345 -0
- package/package.json +9 -2
- package/src/cli/command.ts +17 -3
- package/src/cli/edge.ts +65 -0
- package/src/contract/index.ts +2 -1
- package/src/harbor/catalogue.ts +11 -4
- package/src/harbor/dock.ts +3 -1
- package/src/index.ts +1 -1
- package/src/node/index.ts +11 -4
- package/src/proof/contracts.ts +129 -0
- package/src/proof/expect.ts +25 -0
- package/src/proof/index.ts +12 -0
- package/src/proof/law.ts +352 -0
package/README.md
CHANGED
|
@@ -43,38 +43,74 @@ terrain, as below, and that one is used.
|
|
|
43
43
|
## A harbor on the edge
|
|
44
44
|
|
|
45
45
|
An edge harbor is born on your machine and carried there, so you pilot it
|
|
46
|
-
from your own harbor, as you pilot one on a server.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
```sh
|
|
50
|
-
nervur init --dir ./edge
|
|
51
|
-
nervur reach https://harbor.example.workers.dev/quo --dir ./edge
|
|
52
|
-
nervur own pilot --dir ./edge
|
|
53
|
-
nervur edge pack ./package.json --dir ./edge
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
`own` prints the owner invitation, which never leaves your machine.
|
|
57
|
-
`edge pack` prints the harbor's seed and writes its sealed package. Keep
|
|
58
|
-
the seed as the worker's secret, `wrangler secret put NERVUR_SEED`, and
|
|
59
|
-
bundle the package with the worker:
|
|
46
|
+
from your own harbor, as you pilot one on a server. The worker is a
|
|
47
|
+
folder of its own, an ES module project (`"type": "module"` in its
|
|
48
|
+
`package.json`), holding the worker and its `wrangler.toml`:
|
|
60
49
|
|
|
61
50
|
```js
|
|
51
|
+
// worker.js
|
|
62
52
|
import { harborObject } from 'nervur/edge';
|
|
63
|
-
import carried from './package.json';
|
|
53
|
+
import carried from './package.edge.json';
|
|
64
54
|
import * as greetings from './greetings.js';
|
|
65
55
|
|
|
66
56
|
export const Harbor = harborObject({ modules: [greetings], package: carried });
|
|
57
|
+
|
|
58
|
+
export default {
|
|
59
|
+
fetch(request, env) {
|
|
60
|
+
return env.HARBOR.get(env.HARBOR.idFromName('main')).fetch(request);
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
```toml
|
|
66
|
+
# wrangler.toml
|
|
67
|
+
name = "harbor"
|
|
68
|
+
main = "worker.js"
|
|
69
|
+
compatibility_date = "2026-09-01"
|
|
70
|
+
|
|
71
|
+
[[durable_objects.bindings]]
|
|
72
|
+
name = "HARBOR"
|
|
73
|
+
class_name = "Harbor"
|
|
74
|
+
|
|
75
|
+
[[migrations]]
|
|
76
|
+
tag = "v1"
|
|
77
|
+
new_sqlite_classes = ["Harbor"]
|
|
67
78
|
```
|
|
68
79
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
80
|
+
Make the harbor and set it up with the command, from that folder, naming
|
|
81
|
+
where the edge will reach it:
|
|
82
|
+
|
|
83
|
+
```sh
|
|
84
|
+
nervur init --dir ./harbor
|
|
85
|
+
nervur module add ./greetings.js --dir ./harbor
|
|
86
|
+
nervur reach http://127.0.0.1:8787/quo --dir ./harbor
|
|
87
|
+
nervur own me --dir ./harbor
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`own` prints the owner invitation, which never leaves your machine. Run
|
|
91
|
+
it on this machine first:
|
|
92
|
+
|
|
93
|
+
```sh
|
|
94
|
+
nervur edge dev ./package.edge.json --dir ./harbor
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`edge dev` packs the harbor into `package.edge.json` and runs your
|
|
98
|
+
`wrangler dev` with its seed, keeping the object's storage in
|
|
99
|
+
`.nervur-edge`, so the harbor stands again each time you run it. The
|
|
100
|
+
first time the object starts, the package lands in its storage and the
|
|
101
|
+
harbor opens under that seed; after that the storage is the harbor, and
|
|
102
|
+
a new deploy leaves it be. Then, from your own harbor:
|
|
72
103
|
|
|
73
104
|
```sh
|
|
74
105
|
nervur pilot edge '<the owner invitation>' --dir ./mine
|
|
75
106
|
nervur host shop --via edge --dir ./mine
|
|
76
107
|
```
|
|
77
108
|
|
|
109
|
+
For Cloudflare, name the address it will answer at with `reach`, then
|
|
110
|
+
`nervur edge pack ./package.edge.json --dir ./harbor` prints the seed and
|
|
111
|
+
writes the package. Keep the seed as the worker's secret,
|
|
112
|
+
`wrangler secret put NERVUR_SEED`, and `wrangler deploy`.
|
|
113
|
+
|
|
78
114
|
`pilot` opens the web carrier where the invitation names a web address.
|
|
79
115
|
An object with no package draws its own seed, sealed under the worker's
|
|
80
116
|
`NERVUR_SECRET`, and `stood` runs the worker's own code on it as root:
|
|
@@ -182,7 +218,11 @@ stands a ward under a seed you name, or a drawn one.
|
|
|
182
218
|
and every class between it and `Faculty`.
|
|
183
219
|
- **Entropy, Clock, Custody, Memory, Carrier, Loader.** The contracts a
|
|
184
220
|
terrain fulfils. Each has one suite, and every body that fulfils it
|
|
185
|
-
passes it.
|
|
221
|
+
passes it. `nervur/proof` hands you those suites and the unpacking
|
|
222
|
+
law's as scenes, a title and a function each, so a body of your own,
|
|
223
|
+
a store in a cloud or a secret of your platform's, runs them under
|
|
224
|
+
your own runner: `for (const [title, scene] of memoryScenes('Mine',
|
|
225
|
+
make, expect)) test(title, scene)`. `@nervur-org/s3` is one such body.
|
|
186
226
|
- **Harbor, Terrain, Dock, Catalogue, Registry, WardBeing.** The onion.
|
|
187
227
|
- **World, PointerTerrain** and the pointer bodies, `HeldLoader` among
|
|
188
228
|
them. A whole world with this package alone.
|
|
@@ -204,6 +244,8 @@ stands a ward under a seed you name, or a drawn one.
|
|
|
204
244
|
- **WebDialer, webFaculty**: Quo over the web, a post to `https://` or a
|
|
205
245
|
held line to `wss://`, in any engine with `fetch` and WebSocket.
|
|
206
246
|
`webServe` and `webGround`, from `nervur/http`, listen for it on Node.
|
|
247
|
+
`answerPost(harbor, listen, request)` answers one post and nothing
|
|
248
|
+
more, for a function that opens a harbor per request and closes it.
|
|
207
249
|
|
|
208
250
|
- **The relay**, `org.nervur.relay` and `org.nervur.relay-client`, in
|
|
209
251
|
every harbor: a harbor nobody can dial is reached through one that
|
package/dist/cli/command.js
CHANGED
|
@@ -23,6 +23,10 @@
|
|
|
23
23
|
// printed, for the worker's
|
|
24
24
|
// NERVUR_SEED, and its sealed
|
|
25
25
|
// package written to the file
|
|
26
|
+
// nervur edge dev <file> [--port P] the harbor packed to the file and
|
|
27
|
+
// run by the worker's own wrangler
|
|
28
|
+
// dev, from this folder, at
|
|
29
|
+
// http://127.0.0.1:P/quo
|
|
26
30
|
// nervur open <key> <kind> a faculty stands in the box ward
|
|
27
31
|
// nervur host <ward> [--seed S] a hosted ward stands
|
|
28
32
|
// nervur route <ward pk> <address... | none> where a ward is reached, in order
|
|
@@ -51,7 +55,10 @@ import { join, resolve } from 'node:path';
|
|
|
51
55
|
import { WEB_OPENING } from '../line/index.js';
|
|
52
56
|
import { harborDir, isFile } from '../node/index.js';
|
|
53
57
|
import { readAt, webAddress } from '../quo/index.js';
|
|
58
|
+
import { edgeDev } from './edge.js';
|
|
54
59
|
import { carry, isServed, land, open, pack, request, serve } from './harbor.js';
|
|
60
|
+
// Where `edge dev` answers unless --port names another, wrangler's own.
|
|
61
|
+
const EDGE_PORT = 8787;
|
|
55
62
|
const USAGE = 'usage: nervur <init|serve|module|modules|census|sweep|open|host|route|reach|boot|unboot|public|invite|ask|own|disown|pilot|export|import|edge> [args] [--dir D] [--ward W] [--via V] [--seed S] [--with-seed] [--host H] [--port P]';
|
|
56
63
|
const FLAGS = new Set(['dir', 'ward', 'via', 'seed', 'host', 'port']);
|
|
57
64
|
// The one flag that stands alone, since what it names is a yes.
|
|
@@ -193,13 +200,20 @@ export const main = async (argv, env, print) => {
|
|
|
193
200
|
return out({ landed: await land(dir, JSON.parse(text)) });
|
|
194
201
|
}
|
|
195
202
|
if (command === 'edge') {
|
|
196
|
-
if (words[0] !== 'pack' || words.length !== 2)
|
|
197
|
-
throw new Error('edge takes pack and a file');
|
|
203
|
+
if ((words[0] !== 'pack' && words[0] !== 'dev') || words.length !== 2)
|
|
204
|
+
throw new Error('edge takes pack or dev, and a file');
|
|
198
205
|
if (flags.via !== undefined)
|
|
199
|
-
throw new Error(
|
|
206
|
+
throw new Error(`edge ${words[0]} reads this folder, so it takes no --via`);
|
|
200
207
|
if (await isServed(dir))
|
|
201
208
|
throw new Error(`a harbor is served from ${dir}: stop it first`);
|
|
202
|
-
|
|
209
|
+
if (words[0] === 'pack')
|
|
210
|
+
return out(await pack(dir, words[1]));
|
|
211
|
+
const dev = await edgeDev(dir, words[1], process.cwd(), flags.port === undefined ? EDGE_PORT : Number(flags.port));
|
|
212
|
+
const stop = () => void dev.close().then(() => process.exit(0));
|
|
213
|
+
process.once('SIGINT', stop);
|
|
214
|
+
process.once('SIGTERM', stop);
|
|
215
|
+
print(JSON.stringify({ at: dev.at, places: dev.places }));
|
|
216
|
+
return SERVING;
|
|
203
217
|
}
|
|
204
218
|
if (command === 'serve') {
|
|
205
219
|
if (words.length > 0)
|
package/dist/cli/edge.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// `nervur edge dev`: an edge harbor on this machine, as it will stand on
|
|
3
|
+
// Cloudflare. The harbor of a folder is packed into the file the worker
|
|
4
|
+
// imports, and the worker's own `wrangler dev` runs it, in the folder the
|
|
5
|
+
// command runs from, with the seed handed as `NERVUR_SEED` through the
|
|
6
|
+
// environment so no listing of processes shows it. The object's storage is
|
|
7
|
+
// kept under `.nervur-edge` there, so a harbor landed once stands again
|
|
8
|
+
// across restarts, as it does on Cloudflare.
|
|
9
|
+
//
|
|
10
|
+
// wrangler is the worker's tool and never the kit's: the one who deploys
|
|
11
|
+
// an edge has it already, and `edge dev` finds it where npm put it.
|
|
12
|
+
import { spawn } from 'node:child_process';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
import { pack } from './harbor.js';
|
|
15
|
+
export const EDGE_STATE = '.nervur-edge';
|
|
16
|
+
const READY_TRIES = 240;
|
|
17
|
+
const READY_EVERY = 250;
|
|
18
|
+
const answers = (url) => fetch(url).then(() => true, () => false);
|
|
19
|
+
// Resolved once wrangler answers at `url`, rejected once it exits or never
|
|
20
|
+
// answers.
|
|
21
|
+
const ready = async (child, url, said) => {
|
|
22
|
+
let gone = false;
|
|
23
|
+
child.once('exit', () => (gone = true));
|
|
24
|
+
for (let i = 0; i < READY_TRIES; i += 1) {
|
|
25
|
+
if (gone)
|
|
26
|
+
throw new Error(`wrangler dev exited: ${said()}`);
|
|
27
|
+
if (await answers(url))
|
|
28
|
+
return;
|
|
29
|
+
await new Promise((r) => setTimeout(r, READY_EVERY));
|
|
30
|
+
}
|
|
31
|
+
throw new Error(`wrangler dev never answered at ${url}: ${said()}`);
|
|
32
|
+
};
|
|
33
|
+
export const edgeDev = async (dir, file, cwd, port) => {
|
|
34
|
+
const { seed, places } = (await pack(dir, file));
|
|
35
|
+
const child = spawn('npx', ['--no-install', 'wrangler', 'dev', '--ip', '127.0.0.1', '--port', String(port), '--persist-to', join(cwd, EDGE_STATE), '--log-level', 'warn'], {
|
|
36
|
+
cwd,
|
|
37
|
+
env: { ...process.env, NERVUR_SEED: seed, CLOUDFLARE_INCLUDE_PROCESS_ENV: 'true' },
|
|
38
|
+
stdio: ['ignore', 'ignore', 'pipe'],
|
|
39
|
+
detached: true,
|
|
40
|
+
});
|
|
41
|
+
let err = '';
|
|
42
|
+
child.stderr.on('data', (chunk) => (err = `${err}${chunk.toString('utf8')}`.slice(-4000)));
|
|
43
|
+
const at = `http://127.0.0.1:${String(port)}/quo`;
|
|
44
|
+
const close = () => new Promise((done) => {
|
|
45
|
+
if (child.exitCode !== null)
|
|
46
|
+
return done();
|
|
47
|
+
child.once('exit', () => done());
|
|
48
|
+
process.kill(-child.pid, 'SIGTERM');
|
|
49
|
+
});
|
|
50
|
+
try {
|
|
51
|
+
await ready(child, at, () => err);
|
|
52
|
+
}
|
|
53
|
+
catch (e) {
|
|
54
|
+
await close();
|
|
55
|
+
throw e;
|
|
56
|
+
}
|
|
57
|
+
return { at, places, close };
|
|
58
|
+
};
|
package/dist/contract/index.js
CHANGED
|
@@ -18,7 +18,8 @@ export class Memory {
|
|
|
18
18
|
export class Custody {
|
|
19
19
|
}
|
|
20
20
|
// Code, by the name a harbor keeps for it, or null where this loader has
|
|
21
|
-
// none. Code
|
|
21
|
+
// none. Code it has that does not load throws, saying why. Code never
|
|
22
|
+
// crosses a door and never lives in a cell, so it is the
|
|
22
23
|
// one thing beside the seed a terrain hands in: a map, an import, a bundle.
|
|
23
24
|
export class Loader {
|
|
24
25
|
}
|
package/dist/harbor/catalogue.js
CHANGED
|
@@ -42,8 +42,14 @@ export class Catalogue extends Faculty {
|
|
|
42
42
|
// it. The rest stay out, each with its reason.
|
|
43
43
|
async wake() {
|
|
44
44
|
for (const [module, row] of Object.entries(this.#rows)) {
|
|
45
|
-
const loaded = await this.#harbor.load(row.from).catch(() =>
|
|
46
|
-
const refused = loaded
|
|
45
|
+
const loaded = await this.#harbor.load(row.from).catch((e) => e);
|
|
46
|
+
const refused = loaded instanceof Error
|
|
47
|
+
? loaded.message
|
|
48
|
+
: loaded === null
|
|
49
|
+
? `not found at ${row.from}`
|
|
50
|
+
: loaded.module !== module
|
|
51
|
+
? `${row.from} is module ${loaded.module}`
|
|
52
|
+
: this.#harbor.check([...this.#running.values(), loaded]);
|
|
47
53
|
if (refused === null)
|
|
48
54
|
this.#running.set(module, loaded);
|
|
49
55
|
else
|
|
@@ -54,7 +60,9 @@ export class Catalogue extends Faculty {
|
|
|
54
60
|
async add(args) {
|
|
55
61
|
if (!text(args.from))
|
|
56
62
|
return { error: 'from is text' };
|
|
57
|
-
const loaded = await this.#harbor.load(args.from);
|
|
63
|
+
const loaded = await this.#harbor.load(args.from).catch((e) => e);
|
|
64
|
+
if (loaded instanceof Error)
|
|
65
|
+
return { error: loaded.message };
|
|
58
66
|
if (loaded === null)
|
|
59
67
|
return { error: 'no such module' };
|
|
60
68
|
const running = new Map(this.#running);
|
package/dist/harbor/dock.js
CHANGED
|
@@ -126,8 +126,12 @@ export class Dock extends WardBeing {
|
|
|
126
126
|
}
|
|
127
127
|
async host(args) {
|
|
128
128
|
const name = args.ward;
|
|
129
|
-
if (!text(name)
|
|
130
|
-
return { error: '
|
|
129
|
+
if (!text(name))
|
|
130
|
+
return { error: 'ward is text' };
|
|
131
|
+
if (name === BOX)
|
|
132
|
+
return { error: 'box is the box ward' };
|
|
133
|
+
if (this.hosted.some((h) => h.ward === name))
|
|
134
|
+
return { error: `${name} is hosted already` };
|
|
131
135
|
if (args.seed !== undefined && !text(args.seed))
|
|
132
136
|
return { error: 'seed is text' };
|
|
133
137
|
const invitation = await this.stance.occupants.invite(name, { hosted: true });
|
package/dist/index.d.ts
CHANGED
|
@@ -5,4 +5,4 @@ export { Carrier, Clock, Custody, Entropy, Loader, Memory, type Loaded, type Mod
|
|
|
5
5
|
export { CEILING, DEFAULT, DEPTH, ownerAsk, Pilot, pilots, WardBeing, type Steward, type WardBeingStance } from './ward/index.ts';
|
|
6
6
|
export { BOX, CarrierFaculty, Catalogue, copyPackage, Dock, Harbor, addGround, grounds, readRootRequest, Registry, Push, PUSH, RELAY, RELAY_CLIENT, rootLine, Terrain, UNDIALED, type Dialed, type Failures, type Ground, type GroundFaculty, type GroundProbe, type Opening, type Probing, type RootRequest, } from './harbor/index.ts';
|
|
7
7
|
export { CryptoEntropy, HeldCustody, HeldLoader, NoCarrier, PointerCarrier, PointerTerrain, SystemClock, VolatileMemory, World, type PointerParts } from './pointer/index.ts';
|
|
8
|
-
export { NEUTRAL, WEB, WEB_OPENING, WEB_PUSH, WebDialer, webFaculty, webPush, type WebListen, type WebListener, type WebServe } from './line/index.ts';
|
|
8
|
+
export { answerPost, NEUTRAL, WEB, WEB_OPENING, WEB_PUSH, WebDialer, webFaculty, webPush, type WebListen, type WebListener, type WebServe } from './line/index.ts';
|
package/dist/index.js
CHANGED
|
@@ -15,6 +15,6 @@ export { CEILING, DEFAULT, DEPTH, ownerAsk, Pilot, pilots, WardBeing } from './w
|
|
|
15
15
|
export { BOX, CarrierFaculty, Catalogue, copyPackage, Dock, Harbor, addGround, grounds, readRootRequest, Registry, Push, PUSH, RELAY, RELAY_CLIENT, rootLine, Terrain, UNDIALED, } from './harbor/index.js';
|
|
16
16
|
// The pointer dock: a whole world in one process.
|
|
17
17
|
export { CryptoEntropy, HeldCustody, HeldLoader, NoCarrier, PointerCarrier, PointerTerrain, SystemClock, VolatileMemory, World } from './pointer/index.js';
|
|
18
|
-
export { NEUTRAL, WEB, WEB_OPENING, WEB_PUSH, WebDialer, webFaculty, webPush } from './line/index.js';
|
|
18
|
+
export { answerPost, NEUTRAL, WEB, WEB_OPENING, WEB_PUSH, WebDialer, webFaculty, webPush } from './line/index.js';
|
|
19
19
|
// The ground that fits everywhere, tried after any a platform entry adds.
|
|
20
20
|
addGround(neutralGround);
|
package/dist/node/index.js
CHANGED
|
@@ -53,8 +53,10 @@ export const isFile = (from) => /^(\.{1,2}[\\/]|[\\/]|[A-Za-z]:[\\/])/.test(from
|
|
|
53
53
|
// Code from a file or an installed package: a module that exports
|
|
54
54
|
// `module`, `version` and `classes`, found again by where it was found, a
|
|
55
55
|
// file by its absolute path and a package by its name, resolved from each
|
|
56
|
-
// folder of `bases` in order. What
|
|
57
|
-
//
|
|
56
|
+
// folder of `bases` in order. What is not there, or exports no module, is
|
|
57
|
+
// none. Code that is there and does not import throws Node's reason, so
|
|
58
|
+
// whoever added it learns why; an ES module read as CommonJS says which
|
|
59
|
+
// field its package.json lacks.
|
|
58
60
|
export class ImportLoader extends Loader {
|
|
59
61
|
#bases;
|
|
60
62
|
constructor(bases = [process.cwd()]) {
|
|
@@ -69,8 +71,14 @@ export class ImportLoader extends Loader {
|
|
|
69
71
|
try {
|
|
70
72
|
exported = (await import(__rewriteRelativeImportExtension(pathToFileURL(found).href)));
|
|
71
73
|
}
|
|
72
|
-
catch {
|
|
73
|
-
|
|
74
|
+
catch (e) {
|
|
75
|
+
const said = e.message;
|
|
76
|
+
// The name itself is nowhere, as against code there that needs
|
|
77
|
+
// something that is not.
|
|
78
|
+
if (e.code === 'ERR_MODULE_NOT_FOUND' && said.includes(`'${found}'`))
|
|
79
|
+
return null;
|
|
80
|
+
const commonjs = e instanceof SyntaxError && /\b(export|import)\b/.test(said);
|
|
81
|
+
throw new Error(`${found} did not load: ${said}${commonjs ? '; it is an ES module read as CommonJS, so set "type": "module" in its package.json' : ''}`, { cause: e });
|
|
74
82
|
}
|
|
75
83
|
const { module, version, classes } = exported;
|
|
76
84
|
if (typeof module !== 'string' || typeof version !== 'string' || !Array.isArray(classes))
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Custody, Entropy, Loader, Memory } from '../contract/index.ts';
|
|
2
|
+
import type { Expect } from './expect.ts';
|
|
3
|
+
export type Scene = [string, () => Promise<void>];
|
|
4
|
+
export declare const custodyScenes: (name: string, make: () => [Custody, Custody], expect: Expect) => Scene[];
|
|
5
|
+
export type Cut = {
|
|
6
|
+
cut: Memory;
|
|
7
|
+
fresh: Memory;
|
|
8
|
+
};
|
|
9
|
+
export declare const memoryScenes: (name: string, make: () => [Memory, Memory], expect: Expect, half?: () => Cut) => Scene[];
|
|
10
|
+
export declare const entropyScenes: (name: string, make: () => Entropy, expect: Expect) => Scene[];
|
|
11
|
+
export declare const loaderScenes: (name: string, make: () => {
|
|
12
|
+
loader: Loader;
|
|
13
|
+
found: string;
|
|
14
|
+
missing: string;
|
|
15
|
+
}, expect: Expect) => Scene[];
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// The suites of the bodies, Custody, Memory, Entropy and Loader, as scenes
|
|
3
|
+
// with no platform under them, so Node's runner, every engine and a
|
|
4
|
+
// stranger's own runner hold a body to the same assertions.
|
|
5
|
+
import { Custody, Entropy, Loader, Memory } from '../contract/index.js';
|
|
6
|
+
const bytes = (...values) => Uint8Array.from(values);
|
|
7
|
+
// A place as plain values, in name order, so two can be compared as JSON.
|
|
8
|
+
const plain = (place) => [...place].map(([n, b]) => [n, [...b]]).sort(([a], [b]) => (a < b ? -1 : 1));
|
|
9
|
+
// `make` gives two bodies over one keeping, as two runs would build them,
|
|
10
|
+
// and a fresh keeping each call.
|
|
11
|
+
export const custodyScenes = (name, make, expect) => [
|
|
12
|
+
[
|
|
13
|
+
`[contract] ${name} fulfils Custody: one seed of thirty-two bytes, the same every time`,
|
|
14
|
+
async () => {
|
|
15
|
+
const [custody, again] = make();
|
|
16
|
+
expect.ok(custody instanceof Custody);
|
|
17
|
+
const seed = await custody.seed();
|
|
18
|
+
expect.equal(seed.length, 32);
|
|
19
|
+
const first = [...seed];
|
|
20
|
+
seed.fill(0);
|
|
21
|
+
expect.same([...(await custody.seed())], first, 'a seed handed out is a copy');
|
|
22
|
+
expect.same([...(await again.seed())], first, 'another body over the keeping holds the same');
|
|
23
|
+
expect.ok([...(await make()[0].seed())].join() !== first.join(), 'another keeping holds another seed');
|
|
24
|
+
const [fresh, rival] = make();
|
|
25
|
+
const [a, b] = await Promise.all([fresh.seed(), rival.seed()]);
|
|
26
|
+
expect.same([...a], [...b], 'two first asks at once keep one seed');
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
];
|
|
30
|
+
export const memoryScenes = (name, make, expect, half) => [
|
|
31
|
+
[
|
|
32
|
+
`[contract] ${name} fulfils Memory: it names its places, and forgets one whole`,
|
|
33
|
+
async () => {
|
|
34
|
+
const [memory, again] = make();
|
|
35
|
+
expect.same(await memory.places(), []);
|
|
36
|
+
await memory.write('aa', new Map([['01', bytes(1)]]));
|
|
37
|
+
await memory.write('bb', new Map([['01', bytes(2)], ['02', bytes(3)]]));
|
|
38
|
+
expect.same([...(await again.places())].sort(), ['aa', 'bb']);
|
|
39
|
+
await memory.forget('aa');
|
|
40
|
+
expect.same(await again.places(), ['bb']);
|
|
41
|
+
expect.same(plain(await again.read('aa')), []);
|
|
42
|
+
await memory.forget('aa');
|
|
43
|
+
await memory.write('bb', new Map([['01', null], ['02', null]]));
|
|
44
|
+
expect.same(await again.places(), [], 'a place holding nothing is no place');
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
...(half
|
|
48
|
+
? [
|
|
49
|
+
[
|
|
50
|
+
`[contract] ${name} fulfils Memory: a keep that stops partway is whole or is nothing`,
|
|
51
|
+
async () => {
|
|
52
|
+
const { cut, fresh } = half();
|
|
53
|
+
await cut.write('aa', new Map([['01', bytes(1)], ['02', bytes(2)]])).catch(() => undefined);
|
|
54
|
+
const before = plain(await fresh.read('aa'));
|
|
55
|
+
await cut.write('aa', new Map([['01', bytes(9)], ['02', null], ['03', bytes(3)]])).catch(() => undefined);
|
|
56
|
+
const after = JSON.stringify(plain(await fresh.read('aa')));
|
|
57
|
+
const whole = JSON.stringify(plain(new Map([['01', bytes(9)], ['03', bytes(3)]])));
|
|
58
|
+
expect.ok(after === JSON.stringify(before) || after === whole, `a keep cut short left ${after}`);
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
]
|
|
62
|
+
: []),
|
|
63
|
+
[
|
|
64
|
+
`[contract] ${name} fulfils Memory: a place read again holds what was kept, and nothing else`,
|
|
65
|
+
async () => {
|
|
66
|
+
const [memory, again] = make();
|
|
67
|
+
expect.ok(memory instanceof Memory);
|
|
68
|
+
expect.same(plain(await memory.read('aa')), []);
|
|
69
|
+
const written = bytes(1, 2, 3);
|
|
70
|
+
await memory.write('aa', new Map([['01', written], ['02', bytes(4)]]));
|
|
71
|
+
written[0] = 9;
|
|
72
|
+
await memory.write('bb', new Map([['01', bytes(5)]]));
|
|
73
|
+
await memory.write('aa', new Map([['02', null], ['03', bytes()], ['04', null]]));
|
|
74
|
+
const read = await again.read('aa');
|
|
75
|
+
expect.same(plain(read), [['01', [1, 2, 3]], ['03', []]]);
|
|
76
|
+
expect.ok(read.get('01') instanceof Uint8Array, 'an entry is bytes');
|
|
77
|
+
read.get('01')[0] = 7;
|
|
78
|
+
expect.same([...(await memory.read('aa')).get('01')], [1, 2, 3], 'an entry handed out is a copy');
|
|
79
|
+
expect.same(plain(await again.read('bb')), [['01', [5]]]);
|
|
80
|
+
expect.same(plain(await again.read('cc')), []);
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
];
|
|
84
|
+
export const entropyScenes = (name, make, expect) => [
|
|
85
|
+
[
|
|
86
|
+
`[contract] ${name} fulfils Entropy: bytes of the length asked, never the same twice`,
|
|
87
|
+
() => {
|
|
88
|
+
const entropy = make();
|
|
89
|
+
expect.ok(entropy instanceof Entropy);
|
|
90
|
+
expect.equal(entropy.draw(0).length, 0);
|
|
91
|
+
expect.equal(entropy.drawer(33).length, 33);
|
|
92
|
+
expect.ok([...entropy.draw(32)].join() !== [...entropy.draw(32)].join(), 'two draws differ');
|
|
93
|
+
return Promise.resolve();
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
];
|
|
97
|
+
// `make` gives a loader, a name it finds a module by, and one it finds
|
|
98
|
+
// nothing by.
|
|
99
|
+
export const loaderScenes = (name, make, expect) => [
|
|
100
|
+
[
|
|
101
|
+
`[contract] ${name} fulfils Loader: a module by its name, found again by the name it gives back, and nothing else`,
|
|
102
|
+
async () => {
|
|
103
|
+
const { loader, found, missing } = make();
|
|
104
|
+
expect.ok(loader instanceof Loader);
|
|
105
|
+
const loaded = await loader.load(found);
|
|
106
|
+
expect.ok(loaded !== null, `nothing found at ${found}`);
|
|
107
|
+
expect.equal(typeof loaded.module, 'string');
|
|
108
|
+
expect.equal(typeof loaded.version, 'string');
|
|
109
|
+
expect.ok(Array.isArray(loaded.classes) && loaded.classes.every((C) => typeof C === 'function'), 'the classes are classes');
|
|
110
|
+
const again = await loader.load(loaded.from);
|
|
111
|
+
expect.same(again && { module: again.module, version: again.version, from: again.from }, { module: loaded.module, version: loaded.version, from: loaded.from });
|
|
112
|
+
expect.equal(await loader.load(missing), null);
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
];
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const fail = (message, detail) => {
|
|
2
|
+
throw new Error(`${message ? `${message}: ` : ''}${detail}`);
|
|
3
|
+
};
|
|
4
|
+
export const expect = {
|
|
5
|
+
equal: (actual, expected, message) => {
|
|
6
|
+
if (!Object.is(actual, expected))
|
|
7
|
+
fail(message, `${String(actual)} is not ${String(expected)}`);
|
|
8
|
+
},
|
|
9
|
+
same: (actual, expected, message) => {
|
|
10
|
+
const [a, b] = [JSON.stringify(actual), JSON.stringify(expected)];
|
|
11
|
+
if (a !== b)
|
|
12
|
+
fail(message, `${a} is not ${b}`);
|
|
13
|
+
},
|
|
14
|
+
ok: (value, message) => {
|
|
15
|
+
if (!value)
|
|
16
|
+
fail(message, 'not true');
|
|
17
|
+
},
|
|
18
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// `nervur/proof`: the suites a body of a stranger's passes to be supported,
|
|
3
|
+
// the kit's own. Each is a list of scenes, a title and a function, run by
|
|
4
|
+
// whatever runner the stranger keeps, in the engine the body is for:
|
|
5
|
+
//
|
|
6
|
+
// for (const [title, scene] of memoryScenes('S3Memory', make, expect, half)) test(title, scene);
|
|
7
|
+
//
|
|
8
|
+
// `expect` throws on the first assertion that fails. The law's scenes stand
|
|
9
|
+
// harbors on a stranger's bodies and hold the unpacking law over them.
|
|
10
|
+
export { entropyScenes, custodyScenes, loaderScenes, memoryScenes } from './contracts.js';
|
|
11
|
+
export { expect } from './expect.js';
|
|
12
|
+
export { law, lawScenes } from './law.js';
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Being, Faculty, type Asker, type JsonObject } from '../being/index.ts';
|
|
2
|
+
import { Memory, type Custody, type Module } from '../contract/index.ts';
|
|
3
|
+
import { type Harbor } from '../harbor/index.ts';
|
|
4
|
+
import { World } from '../pointer/index.ts';
|
|
5
|
+
import type { Expect } from './expect.ts';
|
|
6
|
+
export type Bodies = {
|
|
7
|
+
custody: Custody;
|
|
8
|
+
memory: Memory;
|
|
9
|
+
};
|
|
10
|
+
export type Terrain = {
|
|
11
|
+
name: string;
|
|
12
|
+
make(): Bodies;
|
|
13
|
+
again(bodies: Bodies): Bodies;
|
|
14
|
+
};
|
|
15
|
+
declare abstract class Echoes extends Faculty {
|
|
16
|
+
static kind: string;
|
|
17
|
+
}
|
|
18
|
+
declare class Echo extends Echoes {
|
|
19
|
+
static kind: string;
|
|
20
|
+
static asks: {
|
|
21
|
+
echo: {};
|
|
22
|
+
};
|
|
23
|
+
echo(args: JsonObject, asker: Asker): JsonObject;
|
|
24
|
+
}
|
|
25
|
+
declare class Shop extends Being {
|
|
26
|
+
static kind: string;
|
|
27
|
+
static cells: {
|
|
28
|
+
sold: string[];
|
|
29
|
+
};
|
|
30
|
+
static asks: {
|
|
31
|
+
buy: {};
|
|
32
|
+
};
|
|
33
|
+
buy(args: JsonObject, asker: Asker): JsonObject;
|
|
34
|
+
}
|
|
35
|
+
declare class Customer extends Being {
|
|
36
|
+
static kind: string;
|
|
37
|
+
static asks: {
|
|
38
|
+
join: {};
|
|
39
|
+
buy: {};
|
|
40
|
+
echo: {};
|
|
41
|
+
spoil: {};
|
|
42
|
+
};
|
|
43
|
+
spoil(): JsonObject;
|
|
44
|
+
join(args: JsonObject): Promise<JsonObject>;
|
|
45
|
+
buy(args: JsonObject): Promise<JsonObject>;
|
|
46
|
+
echo(args: JsonObject): Promise<JsonObject>;
|
|
47
|
+
}
|
|
48
|
+
export declare const classes: (typeof Echo | typeof Shop | typeof Customer)[];
|
|
49
|
+
export declare const law: Module;
|
|
50
|
+
export declare const standOne: (world: World, name: string, parts?: Partial<Bodies>) => Promise<Harbor>;
|
|
51
|
+
export declare const lawScenes: (t: Terrain, expect: Expect) => [string, () => Promise<void>][];
|
|
52
|
+
export {};
|