nervur 0.23.0 → 0.24.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/CONTRIBUTING.md +13 -0
- package/LICENSE +55 -202
- package/NOTICE +3 -8
- package/README.md +14 -156
- package/RELEASING.md +15 -0
- package/SECURITY.md +9 -0
- package/bin/nervur.js +6 -0
- package/package.json +26 -81
- package/AUTHORING.md +0 -935
- package/COMMAND.md +0 -179
- package/FACES.md +0 -280
- package/FACULTIES.md +0 -578
- package/GROUNDS.md +0 -315
- package/KIT-SPEC.md +0 -304
- package/WORLD.md +0 -195
- package/dist/app/app-ground.d.ts +0 -18
- package/dist/app/app-ground.js +0 -23
- package/dist/app/index.d.ts +0 -2
- package/dist/app/index.js +0 -5
- package/dist/app/native.d.ts +0 -11
- package/dist/app/native.js +0 -13
- package/dist/being/being.d.ts +0 -306
- package/dist/being/being.js +0 -20
- package/dist/being/covers.d.ts +0 -9
- package/dist/being/covers.js +0 -21
- package/dist/being/index.d.ts +0 -4
- package/dist/being/index.js +0 -6
- package/dist/being/need.d.ts +0 -49
- package/dist/being/need.js +0 -33
- package/dist/being/schema.d.ts +0 -100
- package/dist/being/schema.js +0 -132
- package/dist/being/table.d.ts +0 -48
- package/dist/being/table.js +0 -344
- package/dist/bench/bench-ground.d.ts +0 -73
- package/dist/bench/bench-ground.js +0 -153
- package/dist/bench/bench.d.ts +0 -101
- package/dist/bench/bench.js +0 -368
- package/dist/bench/fake-clock.d.ts +0 -17
- package/dist/bench/fake-clock.js +0 -34
- package/dist/bench/fake-memory.d.ts +0 -16
- package/dist/bench/fake-memory.js +0 -46
- package/dist/bench/fake-network.d.ts +0 -66
- package/dist/bench/fake-network.js +0 -222
- package/dist/bench/fake-unlock.d.ts +0 -6
- package/dist/bench/fake-unlock.js +0 -10
- package/dist/bench/index.d.ts +0 -3
- package/dist/bench/index.js +0 -5
- package/dist/bench/seeded.d.ts +0 -11
- package/dist/bench/seeded.js +0 -37
- package/dist/bench/settle.d.ts +0 -2
- package/dist/bench/settle.js +0 -49
- package/dist/bench/stewards.d.ts +0 -169
- package/dist/bench/stewards.js +0 -128
- package/dist/bodies/class-list.d.ts +0 -14
- package/dist/bodies/class-list.js +0 -33
- package/dist/bodies/joined-carry.d.ts +0 -29
- package/dist/bodies/joined-carry.js +0 -48
- package/dist/bodies/kept.d.ts +0 -44
- package/dist/bodies/kept.js +0 -91
- package/dist/bodies/noble-crypto.d.ts +0 -22
- package/dist/bodies/noble-crypto.js +0 -88
- package/dist/bodies/seed-keys.d.ts +0 -10
- package/dist/bodies/seed-keys.js +0 -42
- package/dist/bodies/strict-tools.d.ts +0 -10
- package/dist/bodies/strict-tools.js +0 -306
- package/dist/bodies/vouch.d.ts +0 -20
- package/dist/bodies/vouch.js +0 -71
- package/dist/bodies/web-carry.d.ts +0 -72
- package/dist/bodies/web-carry.js +0 -245
- package/dist/bodies/web-clock.d.ts +0 -12
- package/dist/bodies/web-clock.js +0 -33
- package/dist/browser/browser-ground.d.ts +0 -76
- package/dist/browser/browser-ground.js +0 -294
- package/dist/browser/index.d.ts +0 -4
- package/dist/browser/index.js +0 -7
- package/dist/browser/indexeddb-memory.d.ts +0 -17
- package/dist/browser/indexeddb-memory.js +0 -97
- package/dist/browser/locked-unlock.d.ts +0 -24
- package/dist/browser/locked-unlock.js +0 -85
- package/dist/browser/origin-classes.d.ts +0 -7
- package/dist/browser/origin-classes.js +0 -31
- package/dist/edge/durable-clock.d.ts +0 -19
- package/dist/edge/durable-clock.js +0 -61
- package/dist/edge/durable-memory.d.ts +0 -5
- package/dist/edge/durable-memory.js +0 -11
- package/dist/edge/durable.d.ts +0 -18
- package/dist/edge/durable.js +0 -24
- package/dist/edge/edge-ground.d.ts +0 -49
- package/dist/edge/edge-ground.js +0 -223
- package/dist/edge/index.d.ts +0 -7
- package/dist/edge/index.js +0 -9
- package/dist/edge/native-crypto.d.ts +0 -8
- package/dist/edge/native-crypto.js +0 -102
- package/dist/edge/secret-unlock.d.ts +0 -7
- package/dist/edge/secret-unlock.js +0 -13
- package/dist/edge/socket-carry.d.ts +0 -39
- package/dist/edge/socket-carry.js +0 -111
- package/dist/foundation.d.ts +0 -162
- package/dist/foundation.js +0 -4
- package/dist/ground/dock.d.ts +0 -1460
- package/dist/ground/dock.js +0 -1159
- package/dist/ground/ground.d.ts +0 -239
- package/dist/ground/ground.js +0 -787
- package/dist/ground/views.d.ts +0 -76
- package/dist/ground/views.js +0 -208
- package/dist/house/crossing.d.ts +0 -50
- package/dist/house/crossing.js +0 -153
- package/dist/house/house.d.ts +0 -88
- package/dist/house/house.js +0 -2069
- package/dist/house/rows-shape.d.ts +0 -110
- package/dist/house/rows-shape.js +0 -12
- package/dist/house/rows.d.ts +0 -27
- package/dist/house/rows.js +0 -185
- package/dist/index.d.ts +0 -33
- package/dist/index.js +0 -29
- package/dist/node/bridge.d.ts +0 -20
- package/dist/node/bridge.js +0 -284
- package/dist/node/cli.d.ts +0 -2
- package/dist/node/cli.js +0 -225
- package/dist/node/file-memory.d.ts +0 -13
- package/dist/node/file-memory.js +0 -86
- package/dist/node/folder-classes.d.ts +0 -5
- package/dist/node/folder-classes.js +0 -40
- package/dist/node/gone.d.ts +0 -2
- package/dist/node/gone.js +0 -7
- package/dist/node/hand.d.ts +0 -39
- package/dist/node/hand.js +0 -107
- package/dist/node/http.d.ts +0 -11
- package/dist/node/http.js +0 -90
- package/dist/node/index.d.ts +0 -11
- package/dist/node/index.js +0 -13
- package/dist/node/ledger-memory.d.ts +0 -17
- package/dist/node/ledger-memory.js +0 -152
- package/dist/node/lock.d.ts +0 -10
- package/dist/node/lock.js +0 -78
- package/dist/node/node-ground.d.ts +0 -26
- package/dist/node/node-ground.js +0 -228
- package/dist/node/notify.d.ts +0 -1
- package/dist/node/notify.js +0 -18
- package/dist/node/shell.d.ts +0 -11
- package/dist/node/shell.js +0 -35
- package/dist/node/sync-folder.d.ts +0 -6
- package/dist/node/sync-folder.js +0 -18
- package/dist/node/tcp-carry.d.ts +0 -42
- package/dist/node/tcp-carry.js +0 -193
- package/dist/node/unlock.d.ts +0 -17
- package/dist/node/unlock.js +0 -86
- package/dist/node/websocket.d.ts +0 -12
- package/dist/node/websocket.js +0 -134
- package/dist/quo/frame.d.ts +0 -33
- package/dist/quo/frame.js +0 -92
- package/dist/quo/index.d.ts +0 -1
- package/dist/quo/index.js +0 -5
- package/dist/quo/room.d.ts +0 -154
- package/dist/quo/room.js +0 -362
- package/dist/serve/index.d.ts +0 -45
- package/dist/serve/index.js +0 -71
package/dist/bodies/web-carry.js
DELETED
|
@@ -1,245 +0,0 @@
|
|
|
1
|
-
import { ASK, NOTHING, PK, REPLY, body, privateHost, read } from '../quo/frame.js';
|
|
2
|
-
import { vouchesOf } from './vouch.js';
|
|
3
|
-
/** The longest post: a ward pk and the longest box. */
|
|
4
|
-
const LONGEST_POST = 1_048_640;
|
|
5
|
-
const PROTOCOL = 'quo';
|
|
6
|
-
// Errors of a connection never made: the box went nowhere.
|
|
7
|
-
const UNREACHED = new Set(['ECONNREFUSED', 'ENOTFOUND', 'EAI_AGAIN', 'EHOSTUNREACH', 'ENETUNREACH', 'UND_ERR_CONNECT_TIMEOUT']);
|
|
8
|
-
const hex = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('');
|
|
9
|
-
const unhex = (text) => Uint8Array.from(text.match(/../g) ?? [], (pair) => Number.parseInt(pair, 16));
|
|
10
|
-
const joined = (a, b) => {
|
|
11
|
-
const out = new Uint8Array(a.length + b.length);
|
|
12
|
-
out.set(a);
|
|
13
|
-
out.set(b, a.length);
|
|
14
|
-
return out;
|
|
15
|
-
};
|
|
16
|
-
export class WebCarry {
|
|
17
|
-
#doors = new Map();
|
|
18
|
-
#lines = new Map();
|
|
19
|
-
#path;
|
|
20
|
-
#addresses;
|
|
21
|
-
#origins;
|
|
22
|
-
#allowPrivate;
|
|
23
|
-
#wait;
|
|
24
|
-
/**
|
|
25
|
-
* `path` is where it listens under the ground's listener, `/quo` where
|
|
26
|
-
* omitted. `addresses` are written into invitations, or read each time
|
|
27
|
-
* where the listener's port is known only once it listens. `origins` are
|
|
28
|
-
* the pages of other origins it answers, by the CORS protocol.
|
|
29
|
-
*/
|
|
30
|
-
constructor({ path = '/quo', addresses = [], origins = [], allowPrivate = false, wait = 30_000, } = {}) {
|
|
31
|
-
this.#path = path;
|
|
32
|
-
this.#addresses = addresses;
|
|
33
|
-
this.#origins = new Set(origins);
|
|
34
|
-
this.#allowPrivate = allowPrivate;
|
|
35
|
-
this.#wait = wait;
|
|
36
|
-
}
|
|
37
|
-
/** A ward's door, answered on the post and on held lines alike. */
|
|
38
|
-
listen({ ward, door }) {
|
|
39
|
-
this.#doors.set(ward, door);
|
|
40
|
-
}
|
|
41
|
-
/** A ward this handler answers no more. */
|
|
42
|
-
unlisten({ ward }) {
|
|
43
|
-
this.#doors.delete(ward);
|
|
44
|
-
}
|
|
45
|
-
at(_options = {}) {
|
|
46
|
-
return typeof this.#addresses === 'function' ? this.#addresses() : this.#addresses;
|
|
47
|
-
}
|
|
48
|
-
/** The hosts of its `https` and `wss` addresses that vouch for the ward, each read over TLS. */
|
|
49
|
-
vouched({ ward, at }) {
|
|
50
|
-
return vouchesOf({ fetcher: (url, init) => fetch(url, init), ward, at, schemes: ['https', 'wss'], allowPrivate: this.#allowPrivate, wait: this.#wait });
|
|
51
|
-
}
|
|
52
|
-
// ---- the listener ----
|
|
53
|
-
#cors(request) {
|
|
54
|
-
const origin = request.headers.get('origin');
|
|
55
|
-
return origin !== null && (this.#origins.has(origin) || this.#origins.has('*')) ? { 'access-control-allow-origin': origin, vary: 'origin' } : {};
|
|
56
|
-
}
|
|
57
|
-
// A page reaches the door from its own host, or from an origin the carry
|
|
58
|
-
// names. A request that names no origin is no page's, and is let in.
|
|
59
|
-
#admits(request) {
|
|
60
|
-
const origin = request.headers.get('origin');
|
|
61
|
-
if (origin === null || this.#origins.has(origin) || this.#origins.has('*'))
|
|
62
|
-
return true;
|
|
63
|
-
try {
|
|
64
|
-
return new URL(origin).host === new URL(request.url).host;
|
|
65
|
-
}
|
|
66
|
-
catch {
|
|
67
|
-
return false;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
async fetch(request) {
|
|
71
|
-
if (new URL(request.url).pathname !== this.#path)
|
|
72
|
-
return null;
|
|
73
|
-
const cors = this.#cors(request);
|
|
74
|
-
if (request.method === 'OPTIONS' && 'access-control-allow-origin' in cors) {
|
|
75
|
-
return new Response(null, { status: 204, headers: { ...cors, 'access-control-allow-methods': 'POST', 'access-control-allow-headers': 'content-type' } });
|
|
76
|
-
}
|
|
77
|
-
if (request.method !== 'POST')
|
|
78
|
-
return new Response(null, { status: 405, headers: cors });
|
|
79
|
-
if (!this.#admits(request))
|
|
80
|
-
return new Response(null, { status: 403 });
|
|
81
|
-
const bytes = new Uint8Array(await request.arrayBuffer());
|
|
82
|
-
if (bytes.length <= PK || bytes.length > LONGEST_POST)
|
|
83
|
-
return new Response(null, { status: 400, headers: cors });
|
|
84
|
-
const door = this.#doors.get(hex(bytes.subarray(0, PK)));
|
|
85
|
-
if (door === undefined)
|
|
86
|
-
return new Response(null, { status: 404, headers: cors });
|
|
87
|
-
const reply = await door(bytes.slice(PK)).catch(() => null);
|
|
88
|
-
if (reply === null)
|
|
89
|
-
return new Response(null, { status: 204, headers: cors });
|
|
90
|
-
return new Response(new Uint8Array(reply), { status: 200, headers: { ...cors, 'content-type': 'application/octet-stream' } });
|
|
91
|
-
}
|
|
92
|
-
upgrade(request) {
|
|
93
|
-
if (new URL(request.url).pathname !== this.#path)
|
|
94
|
-
return null;
|
|
95
|
-
const offered = (request.headers.get('sec-websocket-protocol') ?? '').split(',').map((protocol) => protocol.trim());
|
|
96
|
-
if (!offered.includes(PROTOCOL) || !this.#admits(request))
|
|
97
|
-
return null;
|
|
98
|
-
return {
|
|
99
|
-
protocol: PROTOCOL,
|
|
100
|
-
open: (socket) => ({
|
|
101
|
-
message: (data) => {
|
|
102
|
-
const frame = typeof data === 'string' ? null : read(data);
|
|
103
|
-
if (frame === null)
|
|
104
|
-
return socket.close();
|
|
105
|
-
// A reply or a nothing frame is not a listener's to read.
|
|
106
|
-
if (frame.kind !== ASK)
|
|
107
|
-
return;
|
|
108
|
-
const door = this.#doors.get(hex(frame.rest.subarray(0, PK)));
|
|
109
|
-
return (door === undefined ? Promise.resolve(null) : door(frame.rest.slice(PK)).catch(() => null)).then((reply) => {
|
|
110
|
-
try {
|
|
111
|
-
socket.send(reply === null ? body(NOTHING, frame.id, new Uint8Array()) : body(REPLY, frame.id, reply));
|
|
112
|
-
}
|
|
113
|
-
catch {
|
|
114
|
-
// The line went while the door answered.
|
|
115
|
-
}
|
|
116
|
-
});
|
|
117
|
-
},
|
|
118
|
-
close: () => undefined,
|
|
119
|
-
}),
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
// ---- the dialer ----
|
|
123
|
-
async send({ ward, at, box, wait = this.#wait }) {
|
|
124
|
-
// A ward this carry listens for is on this ground: its door takes the box by pointer.
|
|
125
|
-
const door = this.#doors.get(ward);
|
|
126
|
-
if (door !== undefined) {
|
|
127
|
-
const reply = await door(box).catch(() => null);
|
|
128
|
-
if (reply !== null)
|
|
129
|
-
return { reply };
|
|
130
|
-
}
|
|
131
|
-
for (const address of at) {
|
|
132
|
-
let url;
|
|
133
|
-
try {
|
|
134
|
-
url = new URL(address);
|
|
135
|
-
}
|
|
136
|
-
catch {
|
|
137
|
-
continue;
|
|
138
|
-
}
|
|
139
|
-
if (!this.#allowPrivate && privateHost(url.hostname))
|
|
140
|
-
continue;
|
|
141
|
-
const scheme = url.protocol.slice(0, -1);
|
|
142
|
-
const answer = scheme === 'http' || scheme === 'https' ? await this.#post(address, ward, box, wait) : scheme === 'ws' || scheme === 'wss' ? await this.#held(address, ward, box, wait) : 'unheard';
|
|
143
|
-
// A reply ends it; an ask that may have been heard is not carried again.
|
|
144
|
-
if (answer === 'heard')
|
|
145
|
-
return { reply: null, heard: true };
|
|
146
|
-
if (answer !== 'unheard')
|
|
147
|
-
return { reply: answer, via: address };
|
|
148
|
-
}
|
|
149
|
-
return { reply: null, heard: false };
|
|
150
|
-
}
|
|
151
|
-
async #post(address, ward, box, wait) {
|
|
152
|
-
const abort = new AbortController();
|
|
153
|
-
const timer = setTimeout(() => abort.abort(), wait);
|
|
154
|
-
try {
|
|
155
|
-
// A redirect is never followed, so no answer leads the box to an address its check never read.
|
|
156
|
-
const response = await fetch(address, { method: 'POST', body: joined(unhex(ward), box), signal: abort.signal, redirect: 'error' });
|
|
157
|
-
if (response.status !== 200) {
|
|
158
|
-
await response.body?.cancel();
|
|
159
|
-
return 'unheard';
|
|
160
|
-
}
|
|
161
|
-
const reply = new Uint8Array(await response.arrayBuffer());
|
|
162
|
-
return reply.length > 0 ? reply : 'unheard';
|
|
163
|
-
}
|
|
164
|
-
catch (error) {
|
|
165
|
-
// A connection never made, or a port the fetch standard blocks, sent nothing.
|
|
166
|
-
const cause = error.cause;
|
|
167
|
-
const unsent = (typeof cause?.code === 'string' && UNREACHED.has(cause.code)) || cause?.message === 'bad port';
|
|
168
|
-
return !abort.signal.aborted && unsent ? 'unheard' : 'heard';
|
|
169
|
-
}
|
|
170
|
-
finally {
|
|
171
|
-
clearTimeout(timer);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
async #held(address, ward, box, wait) {
|
|
175
|
-
let line;
|
|
176
|
-
try {
|
|
177
|
-
line = await this.#line(address);
|
|
178
|
-
}
|
|
179
|
-
catch {
|
|
180
|
-
return 'unheard';
|
|
181
|
-
}
|
|
182
|
-
const id = line.next;
|
|
183
|
-
line.next = (line.next + 1) >>> 0;
|
|
184
|
-
const answered = new Promise((resolve) => line.pending.set(id, resolve));
|
|
185
|
-
try {
|
|
186
|
-
line.socket.send(body(ASK, id, joined(unhex(ward), box)));
|
|
187
|
-
}
|
|
188
|
-
catch {
|
|
189
|
-
line.pending.delete(id);
|
|
190
|
-
return 'unheard';
|
|
191
|
-
}
|
|
192
|
-
let timer;
|
|
193
|
-
const late = new Promise((resolve) => (timer = setTimeout(() => resolve(null), wait)));
|
|
194
|
-
const frame = await Promise.race([answered, late]);
|
|
195
|
-
clearTimeout(timer);
|
|
196
|
-
line.pending.delete(id);
|
|
197
|
-
if (frame === null)
|
|
198
|
-
return 'heard';
|
|
199
|
-
return frame.kind === REPLY ? frame.rest : 'unheard';
|
|
200
|
-
}
|
|
201
|
-
#line(address) {
|
|
202
|
-
let held = this.#lines.get(address);
|
|
203
|
-
if (held === undefined) {
|
|
204
|
-
held = new Promise((resolve, reject) => {
|
|
205
|
-
const socket = new WebSocket(address, [PROTOCOL]);
|
|
206
|
-
socket.binaryType = 'arraybuffer';
|
|
207
|
-
const line = { socket, pending: new Map(), next: 1 };
|
|
208
|
-
let open = false;
|
|
209
|
-
socket.onopen = () => {
|
|
210
|
-
if (socket.protocol !== PROTOCOL)
|
|
211
|
-
return socket.close();
|
|
212
|
-
open = true;
|
|
213
|
-
resolve(line);
|
|
214
|
-
};
|
|
215
|
-
socket.onmessage = (event) => {
|
|
216
|
-
const frame = typeof event.data === 'string' ? null : read(new Uint8Array(event.data));
|
|
217
|
-
if (frame === null)
|
|
218
|
-
return socket.close();
|
|
219
|
-
// An ask is not a dialer's to read: nothing is said to it, and the line stands.
|
|
220
|
-
if (frame.kind === ASK)
|
|
221
|
-
return;
|
|
222
|
-
line.pending.get(frame.id)?.(frame);
|
|
223
|
-
};
|
|
224
|
-
socket.onclose = () => {
|
|
225
|
-
this.#lines.delete(address);
|
|
226
|
-
for (const settle of line.pending.values())
|
|
227
|
-
settle(null);
|
|
228
|
-
line.pending.clear();
|
|
229
|
-
if (!open)
|
|
230
|
-
reject(new Error(`no held line to ${address}`));
|
|
231
|
-
};
|
|
232
|
-
socket.onerror = () => undefined;
|
|
233
|
-
});
|
|
234
|
-
this.#lines.set(address, held);
|
|
235
|
-
held.catch(() => this.#lines.delete(address));
|
|
236
|
-
}
|
|
237
|
-
return held;
|
|
238
|
-
}
|
|
239
|
-
/** Closes every held line it dialled. */
|
|
240
|
-
async close() {
|
|
241
|
-
for (const line of this.#lines.values())
|
|
242
|
-
void line.then((held) => held.socket.close(), () => undefined);
|
|
243
|
-
this.#lines.clear();
|
|
244
|
-
}
|
|
245
|
-
}
|
package/dist/bodies/web-clock.js
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
// A timer holds at most this long; a longer wait is a chain of them.
|
|
2
|
-
const LONGEST = 2 ** 31 - 1;
|
|
3
|
-
export class WebClock {
|
|
4
|
-
#waits = new Map();
|
|
5
|
-
now() {
|
|
6
|
-
return Date.now();
|
|
7
|
-
}
|
|
8
|
-
wait({ id, ms }) {
|
|
9
|
-
this.cancel({ id });
|
|
10
|
-
const at = Date.now() + Math.max(0, ms);
|
|
11
|
-
return new Promise((resolve) => {
|
|
12
|
-
const arm = () => {
|
|
13
|
-
const left = at - Date.now();
|
|
14
|
-
const timer = setTimeout(() => {
|
|
15
|
-
if (at > Date.now())
|
|
16
|
-
return arm();
|
|
17
|
-
this.#waits.delete(id);
|
|
18
|
-
resolve(true);
|
|
19
|
-
}, Math.min(Math.max(0, left), LONGEST));
|
|
20
|
-
this.#waits.set(id, { timer, resolve });
|
|
21
|
-
};
|
|
22
|
-
arm();
|
|
23
|
-
});
|
|
24
|
-
}
|
|
25
|
-
cancel({ id }) {
|
|
26
|
-
const held = this.#waits.get(id);
|
|
27
|
-
if (held === undefined)
|
|
28
|
-
return;
|
|
29
|
-
clearTimeout(held.timer);
|
|
30
|
-
this.#waits.delete(id);
|
|
31
|
-
held.resolve(false);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import type { Json } from '../being/being.ts';
|
|
2
|
-
import type { Memory } from '../foundation.ts';
|
|
3
|
-
import { Ground, type Registry, type Unlock } from '../ground/ground.ts';
|
|
4
|
-
import type { Answer } from '../house/rows-shape.ts';
|
|
5
|
-
import { type Load } from './origin-classes.ts';
|
|
6
|
-
/** What the ground takes of its engine. Each is the page's own where omitted. */
|
|
7
|
-
export interface BrowserPlatform {
|
|
8
|
-
readonly locks: Pick<LockManager, 'request'>;
|
|
9
|
-
readonly channel: (name: string) => Pick<BroadcastChannel, 'postMessage' | 'close'> & {
|
|
10
|
-
onmessage: ((event: MessageEvent) => void) | null;
|
|
11
|
-
};
|
|
12
|
-
/** Where its code stands; its classes load from under it alone. */
|
|
13
|
-
readonly origin: string;
|
|
14
|
-
/** Asks the browser to keep its storage, and answers whether it will. */
|
|
15
|
-
readonly persist: () => Promise<boolean>;
|
|
16
|
-
/** Loads a module of classes by URL: `import()`, or in a service worker a map of the worker's own imports. */
|
|
17
|
-
readonly load: Load;
|
|
18
|
-
}
|
|
19
|
-
/**
|
|
20
|
-
* Where its key and its memory rest: the terrain's own, never the
|
|
21
|
-
* drawer's, since the drawer is read with them. A BrowserGround's are the
|
|
22
|
-
* origin's IndexedDB; an AppGround's are the shell's. No entry exports
|
|
23
|
-
* this.
|
|
24
|
-
*/
|
|
25
|
-
export interface Stores {
|
|
26
|
-
readonly unlock: (name: string) => Promise<Unlock>;
|
|
27
|
-
readonly memory: (name: string) => Promise<Memory>;
|
|
28
|
-
}
|
|
29
|
-
/** The origin's ground on stores of the terrain's own: an AppGround's, or a proof's in memory. No entry exports it. */
|
|
30
|
-
export declare const openOn: (options: BrowserGroundOptions, stores: Stores) => Promise<BrowserGround>;
|
|
31
|
-
export interface BrowserGroundOptions {
|
|
32
|
-
/** The ground's name on its origin, which names its lock, its channel and its storage. `nervur` where omitted. */
|
|
33
|
-
readonly name?: string;
|
|
34
|
-
/** The page's own registry, which joins the terrain's as the ladder's first rung. */
|
|
35
|
-
readonly registry?: Registry;
|
|
36
|
-
readonly platform?: Partial<BrowserPlatform>;
|
|
37
|
-
/** The ground's bound on every ask, in milliseconds. */
|
|
38
|
-
readonly wait?: number;
|
|
39
|
-
/** Whether it dials private and loopback addresses, as a page on localhost does. */
|
|
40
|
-
readonly allowPrivate?: boolean;
|
|
41
|
-
}
|
|
42
|
-
/** One request of the hand, as every terrain serves it: `describe`, or an ask of a being in the house it names, or of the dock's steward where it names none. */
|
|
43
|
-
export interface BrowserHandRequest {
|
|
44
|
-
readonly describe?: true;
|
|
45
|
-
readonly house?: string;
|
|
46
|
-
readonly id?: string;
|
|
47
|
-
readonly method?: string;
|
|
48
|
-
readonly args?: Json;
|
|
49
|
-
readonly after?: Answer;
|
|
50
|
-
readonly cells?: true;
|
|
51
|
-
readonly call?: string;
|
|
52
|
-
}
|
|
53
|
-
export declare class BrowserGround {
|
|
54
|
-
#private;
|
|
55
|
-
private constructor();
|
|
56
|
-
/**
|
|
57
|
-
* The origin's ground, joined: this page runs it where it takes the lock,
|
|
58
|
-
* and reaches the page that runs it where another holds the lock.
|
|
59
|
-
*/
|
|
60
|
-
static open(options?: BrowserGroundOptions): Promise<BrowserGround>;
|
|
61
|
-
/** Whether this page runs the ground now. */
|
|
62
|
-
get leading(): boolean;
|
|
63
|
-
/** The ground, where this page runs it. */
|
|
64
|
-
get ground(): Ground | undefined;
|
|
65
|
-
/** Resolves once this page runs the ground, and rejects where its boot failed. */
|
|
66
|
-
led(): Promise<void>;
|
|
67
|
-
/**
|
|
68
|
-
* The hand: `describe`, or an ask of a being in a named house or of the
|
|
69
|
-
* dock's steward, answered by whichever page runs the ground.
|
|
70
|
-
*/
|
|
71
|
-
hand(request: BrowserHandRequest): Promise<Answer | {
|
|
72
|
-
readonly describe: Json;
|
|
73
|
-
}>;
|
|
74
|
-
/** This page leaves: its ground closes where it ran one, and the lock passes to the next page. */
|
|
75
|
-
close(): Promise<void>;
|
|
76
|
-
}
|
|
@@ -1,294 +0,0 @@
|
|
|
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 { s } from '../being/schema.js';
|
|
10
|
-
import { WebCarry } from '../bodies/web-carry.js';
|
|
11
|
-
import { Ground, joinedRegistry } from '../ground/ground.js';
|
|
12
|
-
import { IndexedDbMemory } from './indexeddb-memory.js';
|
|
13
|
-
import { LockedUnlock } from './locked-unlock.js';
|
|
14
|
-
import { OriginClasses } from './origin-classes.js';
|
|
15
|
-
const originStores = {
|
|
16
|
-
unlock: (unlock) => LockedUnlock.open(unlock),
|
|
17
|
-
memory: (memory) => IndexedDbMemory.open(memory),
|
|
18
|
-
};
|
|
19
|
-
let joinOn;
|
|
20
|
-
/** The origin's ground on stores of the terrain's own: an AppGround's, or a proof's in memory. No entry exports it. */
|
|
21
|
-
export const openOn = (options, stores) => joinOn(options, stores);
|
|
22
|
-
const NAME = /^[a-z0-9][a-z0-9._-]{0,63}$/;
|
|
23
|
-
const NONE = { args: s.object({}) };
|
|
24
|
-
const random = () => Array.from(crypto.getRandomValues(new Uint8Array(8)), (byte) => byte.toString(16).padStart(2, '0')).join('');
|
|
25
|
-
const defaults = (name) => ({
|
|
26
|
-
locks: navigator.locks,
|
|
27
|
-
channel: (channel) => new BroadcastChannel(channel),
|
|
28
|
-
origin: globalThis.location?.origin ?? `nervur:${name}`,
|
|
29
|
-
persist: async () => (await navigator.storage?.persist?.()) ?? false,
|
|
30
|
-
load: (href) => import(__rewriteRelativeImportExtension(href)),
|
|
31
|
-
});
|
|
32
|
-
export class BrowserGround {
|
|
33
|
-
#name;
|
|
34
|
-
#platform;
|
|
35
|
-
#stores;
|
|
36
|
-
#options;
|
|
37
|
-
#self = random();
|
|
38
|
-
#channel;
|
|
39
|
-
#abort = new AbortController();
|
|
40
|
-
// Asks sent to another page's ground, by id, with the page they went to.
|
|
41
|
-
#pending = new Map();
|
|
42
|
-
#ground;
|
|
43
|
-
#persisted = false;
|
|
44
|
-
#leader;
|
|
45
|
-
#waiting = [];
|
|
46
|
-
#release;
|
|
47
|
-
#leading;
|
|
48
|
-
#led;
|
|
49
|
-
#failed;
|
|
50
|
-
#closed = false;
|
|
51
|
-
constructor(name, platform, stores, options) {
|
|
52
|
-
this.#name = name;
|
|
53
|
-
this.#platform = platform;
|
|
54
|
-
this.#stores = stores;
|
|
55
|
-
this.#options = options;
|
|
56
|
-
this.#channel = platform.channel(`${name}-hand`);
|
|
57
|
-
this.#leading = new Promise((resolve, reject) => {
|
|
58
|
-
this.#led = resolve;
|
|
59
|
-
this.#failed = reject;
|
|
60
|
-
});
|
|
61
|
-
this.#leading.catch(() => undefined);
|
|
62
|
-
}
|
|
63
|
-
/**
|
|
64
|
-
* The origin's ground, joined: this page runs it where it takes the lock,
|
|
65
|
-
* and reaches the page that runs it where another holds the lock.
|
|
66
|
-
*/
|
|
67
|
-
static open(options = {}) {
|
|
68
|
-
return BrowserGround.#join(options, originStores);
|
|
69
|
-
}
|
|
70
|
-
static async #join(options, stores) {
|
|
71
|
-
const name = options.name ?? 'nervur';
|
|
72
|
-
if (!NAME.test(name))
|
|
73
|
-
throw new TypeError(`no ground is named ${name}`);
|
|
74
|
-
const platform = { ...defaults(name), ...options.platform };
|
|
75
|
-
const joining = new BrowserGround(name, platform, stores, options);
|
|
76
|
-
joining.#listen();
|
|
77
|
-
joining.#queue();
|
|
78
|
-
joining.#say({ kind: 'who' });
|
|
79
|
-
return joining;
|
|
80
|
-
}
|
|
81
|
-
static {
|
|
82
|
-
joinOn = (options, stores) => BrowserGround.#join(options, stores);
|
|
83
|
-
}
|
|
84
|
-
/** Whether this page runs the ground now. */
|
|
85
|
-
get leading() {
|
|
86
|
-
return this.#ground !== undefined;
|
|
87
|
-
}
|
|
88
|
-
/** The ground, where this page runs it. */
|
|
89
|
-
get ground() {
|
|
90
|
-
return this.#ground;
|
|
91
|
-
}
|
|
92
|
-
/** Resolves once this page runs the ground, and rejects where its boot failed. */
|
|
93
|
-
led() {
|
|
94
|
-
return this.#leading;
|
|
95
|
-
}
|
|
96
|
-
/**
|
|
97
|
-
* The hand: `describe`, or an ask of a being in a named house or of the
|
|
98
|
-
* dock's steward, answered by whichever page runs the ground.
|
|
99
|
-
*/
|
|
100
|
-
async hand(request) {
|
|
101
|
-
if (this.#closed)
|
|
102
|
-
return { error: { message: 'the ground is closed here' } };
|
|
103
|
-
if (this.#ground !== undefined)
|
|
104
|
-
return this.#local(request);
|
|
105
|
-
if (this.#leader === undefined)
|
|
106
|
-
await new Promise((resolve) => this.#waiting.push(resolve));
|
|
107
|
-
if (this.#ground !== undefined)
|
|
108
|
-
return this.#local(request);
|
|
109
|
-
const id = random();
|
|
110
|
-
const to = this.#leader;
|
|
111
|
-
return new Promise((settle) => {
|
|
112
|
-
this.#pending.set(id, { to, settle });
|
|
113
|
-
this.#say({ kind: 'ask', id, to, request });
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
async #local(request) {
|
|
117
|
-
const answer = await this.#ground.hand(request);
|
|
118
|
-
if (request.describe === true && 'result' in answer)
|
|
119
|
-
return { result: { ...answer.result, persisted: this.#persisted } };
|
|
120
|
-
return answer;
|
|
121
|
-
}
|
|
122
|
-
#say(said) {
|
|
123
|
-
this.#channel.postMessage(said);
|
|
124
|
-
}
|
|
125
|
-
#listen() {
|
|
126
|
-
this.#channel.onmessage = (event) => {
|
|
127
|
-
const said = event.data;
|
|
128
|
-
if (said.kind === 'who' && this.#ground !== undefined)
|
|
129
|
-
this.#say({ kind: 'up', leader: this.#self });
|
|
130
|
-
else if (said.kind === 'up')
|
|
131
|
-
this.#heard(said.leader);
|
|
132
|
-
else if (said.kind === 'answer') {
|
|
133
|
-
const pending = this.#pending.get(said.id);
|
|
134
|
-
this.#pending.delete(said.id);
|
|
135
|
-
pending?.settle(said.answer);
|
|
136
|
-
}
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
// A new page runs the ground: asks sent to the one before may never be answered, so they fail, and whoever waited goes on.
|
|
140
|
-
#heard(leader) {
|
|
141
|
-
if (leader !== this.#leader)
|
|
142
|
-
this.#orphan((to) => to !== leader);
|
|
143
|
-
this.#leader = leader;
|
|
144
|
-
const waiting = this.#waiting;
|
|
145
|
-
this.#waiting = [];
|
|
146
|
-
for (const resolve of waiting)
|
|
147
|
-
resolve();
|
|
148
|
-
}
|
|
149
|
-
#orphan(lost) {
|
|
150
|
-
for (const [id, pending] of this.#pending) {
|
|
151
|
-
if (!lost(pending.to))
|
|
152
|
-
continue;
|
|
153
|
-
this.#pending.delete(id);
|
|
154
|
-
pending.settle({ error: { message: 'the page that ran the ground closed; ask again' } });
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
// The lock, asked once and held until this page closes.
|
|
158
|
-
#queue() {
|
|
159
|
-
this.#platform.locks
|
|
160
|
-
.request(`${this.#name}-ground`, { signal: this.#abort.signal }, async () => {
|
|
161
|
-
let booted;
|
|
162
|
-
try {
|
|
163
|
-
booted = await this.#boot();
|
|
164
|
-
}
|
|
165
|
-
catch (error) {
|
|
166
|
-
this.#failed(error);
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
// A page that closed while its boot ran lets the ground go at once, and the lock with it.
|
|
170
|
-
if (this.#closed) {
|
|
171
|
-
await booted.close();
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
this.#ground = booted;
|
|
175
|
-
this.#heard(this.#self);
|
|
176
|
-
this.#say({ kind: 'up', leader: this.#self });
|
|
177
|
-
this.#led();
|
|
178
|
-
await new Promise((resolve) => {
|
|
179
|
-
this.#release = resolve;
|
|
180
|
-
});
|
|
181
|
-
})
|
|
182
|
-
.catch(() => undefined);
|
|
183
|
-
}
|
|
184
|
-
// The boot: its own unlock and memory, then the ground on them, its ladder and its drawer's houses.
|
|
185
|
-
async #boot() {
|
|
186
|
-
const name = this.#name;
|
|
187
|
-
const platform = this.#platform;
|
|
188
|
-
const stores = this.#stores;
|
|
189
|
-
this.#persisted = await platform.persist().catch(() => false);
|
|
190
|
-
// Each store it opens is let go when its body goes down, so the next ground in this page opens on none of its.
|
|
191
|
-
const closing = (opened) => () => opened.close?.();
|
|
192
|
-
const own = {
|
|
193
|
-
faculties: {
|
|
194
|
-
'store-unlock': {
|
|
195
|
-
takes: NONE,
|
|
196
|
-
up: async () => {
|
|
197
|
-
const unlock = await stores.unlock(`${name}-unlock`);
|
|
198
|
-
return { serves: 'unlock', object: unlock, down: closing(unlock) };
|
|
199
|
-
},
|
|
200
|
-
},
|
|
201
|
-
'store-memory': {
|
|
202
|
-
takes: NONE,
|
|
203
|
-
up: async () => {
|
|
204
|
-
const memory = await stores.memory(`${name}-ground`);
|
|
205
|
-
return { serves: 'memory', object: memory, down: closing(memory) };
|
|
206
|
-
},
|
|
207
|
-
},
|
|
208
|
-
// The hand, for every other page of the origin: the asks sent to this page on the channel, each answered there.
|
|
209
|
-
'channel-hand': {
|
|
210
|
-
takes: { args: s.object({ channel: s.string(), self: s.string(), persisted: s.boolean() }) },
|
|
211
|
-
up: ({ args, faculties }) => {
|
|
212
|
-
const ground = faculties.ground;
|
|
213
|
-
const channel = platform.channel(args.channel);
|
|
214
|
-
let open = true;
|
|
215
|
-
channel.onmessage = (event) => {
|
|
216
|
-
const said = event.data;
|
|
217
|
-
if (said.kind !== 'ask' || said.to !== args.self)
|
|
218
|
-
return;
|
|
219
|
-
// An ask this hand's close ended is answered by no one here: the page that runs the ground next is asked again.
|
|
220
|
-
void ground.hand(said.request).then((answer) => {
|
|
221
|
-
if (!open)
|
|
222
|
-
return;
|
|
223
|
-
channel.postMessage({
|
|
224
|
-
kind: 'answer',
|
|
225
|
-
id: said.id,
|
|
226
|
-
answer: said.request.describe === true && 'result' in answer ? { result: { ...answer.result, persisted: args.persisted === true } } : answer,
|
|
227
|
-
});
|
|
228
|
-
});
|
|
229
|
-
};
|
|
230
|
-
return {
|
|
231
|
-
serves: 'hand',
|
|
232
|
-
down: () => {
|
|
233
|
-
open = false;
|
|
234
|
-
channel.close();
|
|
235
|
-
},
|
|
236
|
-
};
|
|
237
|
-
},
|
|
238
|
-
},
|
|
239
|
-
// It only dials, so it writes no address into an invitation.
|
|
240
|
-
web: {
|
|
241
|
-
takes: { args: s.object({ allowPrivate: s.optional(s.boolean()) }) },
|
|
242
|
-
up: ({ args }) => {
|
|
243
|
-
const web = new WebCarry({ allowPrivate: args.allowPrivate === true });
|
|
244
|
-
return { serves: 'carry', schemes: ['https', 'http', 'wss', 'ws'], object: web };
|
|
245
|
-
},
|
|
246
|
-
},
|
|
247
|
-
origin: {
|
|
248
|
-
takes: NONE,
|
|
249
|
-
up: () => ({
|
|
250
|
-
serves: 'classes',
|
|
251
|
-
house: ({ args }) => {
|
|
252
|
-
if (typeof args.at !== 'string')
|
|
253
|
-
throw new Error('the origin faculty names a house’s module in at');
|
|
254
|
-
return OriginClasses.open(args.at, platform.origin, platform.load);
|
|
255
|
-
},
|
|
256
|
-
}),
|
|
257
|
-
},
|
|
258
|
-
},
|
|
259
|
-
};
|
|
260
|
-
const wait = this.#options.wait;
|
|
261
|
-
return Ground.open({
|
|
262
|
-
registry: joinedRegistry(own, this.#options.registry ?? {}),
|
|
263
|
-
primordial: {
|
|
264
|
-
unlock: { make: 'store-unlock' },
|
|
265
|
-
memory: { make: 'store-memory' },
|
|
266
|
-
crypto: { make: 'noble' },
|
|
267
|
-
tools: { make: 'strict' },
|
|
268
|
-
clock: { make: 'clock' },
|
|
269
|
-
hand: { make: 'channel-hand', args: { channel: `${name}-hand`, self: this.#self, persisted: this.#persisted }, faculties: ['ground'] },
|
|
270
|
-
},
|
|
271
|
-
entries: {
|
|
272
|
-
origin: { make: 'origin' },
|
|
273
|
-
web: { make: 'web', args: this.#options.allowPrivate === true ? { allowPrivate: true } : {} },
|
|
274
|
-
},
|
|
275
|
-
...(wait === undefined ? {} : { wait }),
|
|
276
|
-
});
|
|
277
|
-
}
|
|
278
|
-
/** This page leaves: its ground closes where it ran one, and the lock passes to the next page. */
|
|
279
|
-
async close() {
|
|
280
|
-
if (this.#closed)
|
|
281
|
-
return;
|
|
282
|
-
this.#closed = true;
|
|
283
|
-
this.#abort.abort();
|
|
284
|
-
const ground = this.#ground;
|
|
285
|
-
this.#ground = undefined;
|
|
286
|
-
await ground?.close();
|
|
287
|
-
this.#release?.();
|
|
288
|
-
this.#orphan(() => true);
|
|
289
|
-
for (const resolve of this.#waiting)
|
|
290
|
-
resolve();
|
|
291
|
-
this.#waiting = [];
|
|
292
|
-
this.#channel.close();
|
|
293
|
-
}
|
|
294
|
-
}
|
package/dist/browser/index.d.ts
DELETED
package/dist/browser/index.js
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
// `nervur/browser`: the ground of a page and its service worker, and the
|
|
3
|
-
// bodies it passes, for whoever writes a ground of their own.
|
|
4
|
-
export { BrowserGround } from './browser-ground.js';
|
|
5
|
-
export { IndexedDbMemory } from './indexeddb-memory.js';
|
|
6
|
-
export { IndexedDbShelf, LockedUnlock } from './locked-unlock.js';
|
|
7
|
-
export { OriginClasses } from './origin-classes.js';
|