@volter/world-core 3.0.163 → 3.0.165
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/dist/generated/pack-facts.json +44 -3660
- package/dist/pack-facts.cjs +4 -4
- package/dist/src/browser-session.d.ts +7 -1
- package/dist/src/browser-session.js +21 -10
- package/dist/src/browser.d.ts +4 -0
- package/dist/src/browser.js +10 -0
- package/dist/src/derived-core.d.ts +46 -6
- package/dist/src/derived-core.js +60 -13
- package/dist/src/derived-real.js +159 -5
- package/dist/src/derived.d.ts +1 -1
- package/dist/src/events.d.ts +6 -3
- package/dist/src/events.js +52 -7
- package/dist/src/executor.d.ts +2 -2
- package/dist/src/executor.js +13 -0
- package/dist/src/file-response.d.ts +6 -2
- package/dist/src/file-response.js +29 -5
- package/dist/src/git/index.d.ts +1 -0
- package/dist/src/git/index.js +1 -0
- package/dist/src/git/smart-http-client.d.ts +28 -0
- package/dist/src/git/smart-http-client.js +95 -0
- package/dist/src/git/smart-http.d.ts +3 -0
- package/dist/src/git/smart-http.js +9 -0
- package/dist/src/head.d.ts +16 -1
- package/dist/src/head.js +52 -7
- package/dist/src/html.d.ts +29 -0
- package/dist/src/html.js +64 -0
- package/dist/src/index.d.ts +5 -2
- package/dist/src/index.js +5 -1
- package/dist/src/managed-database.d.ts +4 -2
- package/dist/src/managed-database.js +1 -0
- package/dist/src/media-header.d.ts +34 -0
- package/dist/src/media-header.js +129 -0
- package/dist/src/pack-fetch.d.ts +24 -3
- package/dist/src/pack-fetch.js +155 -96
- package/dist/src/pack-paths.d.ts +2 -3
- package/dist/src/pack-paths.js +3 -13
- package/dist/src/packRegistry.d.ts +17 -1
- package/dist/src/request-body.d.ts +2 -3
- package/dist/src/request-body.js +2 -3
- package/dist/src/runtime.d.ts +4 -1
- package/dist/src/runtime.js +5 -1
- package/dist/src/serve-http.d.ts +2 -2
- package/dist/src/serve-http.js +104 -19
- package/dist/src/smtp-server.d.ts +78 -0
- package/dist/src/smtp-server.js +160 -0
- package/dist/src/smtp.d.ts +29 -11
- package/dist/src/smtp.js +221 -52
- package/generated/pack-facts.json +44 -3660
- package/pack-facts.cjs +4 -4
- package/package.json +4 -2
- package/src/browser-session.ts +17 -5
- package/src/browser.ts +10 -0
- package/src/derived-core.ts +77 -19
- package/src/derived-real.ts +131 -4
- package/src/derived.ts +2 -2
- package/src/events.ts +54 -9
- package/src/executor.ts +13 -2
- package/src/file-response.ts +28 -6
- package/src/git/index.ts +1 -0
- package/src/git/smart-http-client.ts +99 -0
- package/src/git/smart-http.ts +7 -0
- package/src/head.ts +37 -6
- package/src/html.ts +72 -0
- package/src/index.ts +6 -2
- package/src/managed-database.ts +4 -3
- package/src/media-header.ts +108 -0
- package/src/pack-fetch.ts +142 -87
- package/src/pack-paths.ts +3 -11
- package/src/packRegistry.ts +9 -1
- package/src/request-body.ts +2 -3
- package/src/runtime.ts +5 -1
- package/src/serve-http.ts +81 -19
- package/src/smtp-server.ts +166 -0
- package/src/smtp.ts +199 -53
package/dist/src/events.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// Imports stay free of side effects: the delivery's transport (Node's http, the World's egress policy) is loaded when a
|
|
8
8
|
// delivery is made, never when a browser client imports the kernel.
|
|
9
9
|
import { createHash, createHmac } from 'node:crypto';
|
|
10
|
+
import { resolve } from 'node:path';
|
|
10
11
|
import { applyTwinWrite, applyTwinWriteAtomic, twinResources } from "./serve.js";
|
|
11
12
|
import { equalSecrets as same, ksuidFrom } from "./signing.js";
|
|
12
13
|
import { deliveryTraceHeaders } from "./trace-context.js";
|
|
@@ -223,6 +224,28 @@ const worldTransport = async (url, body, headers) => {
|
|
|
223
224
|
return 0;
|
|
224
225
|
}
|
|
225
226
|
};
|
|
227
|
+
const pendingDeliveries = new WeakMap();
|
|
228
|
+
function forgetDeliveries(store, root, deliveries) {
|
|
229
|
+
const roots = pendingDeliveries.get(store), pending = roots?.get(root);
|
|
230
|
+
if (!pending)
|
|
231
|
+
return;
|
|
232
|
+
for (const delivery of deliveries)
|
|
233
|
+
pending.delete(delivery);
|
|
234
|
+
if (!pending.size)
|
|
235
|
+
roots.delete(root);
|
|
236
|
+
if (!roots.size)
|
|
237
|
+
pendingDeliveries.delete(store);
|
|
238
|
+
}
|
|
239
|
+
/** Await already-scheduled first dispatches and consume their retained persistence failures. */
|
|
240
|
+
export async function settleEventDeliveries(root) {
|
|
241
|
+
const store = getActiveWorldStore(), path = resolve(root);
|
|
242
|
+
const deliveries = [...(pendingDeliveries.get(store)?.get(path) ?? [])];
|
|
243
|
+
const outcomes = await Promise.allSettled(deliveries);
|
|
244
|
+
forgetDeliveries(store, path, deliveries);
|
|
245
|
+
const failures = outcomes.flatMap((outcome) => outcome.status === 'rejected' ? [outcome.reason] : []);
|
|
246
|
+
if (failures.length)
|
|
247
|
+
throw new AggregateError(failures, 'Declared-event dispatch failed');
|
|
248
|
+
}
|
|
226
249
|
let deliveryObserver;
|
|
227
250
|
export function setDeliveryObserver(observer) {
|
|
228
251
|
const previous = deliveryObserver;
|
|
@@ -266,12 +289,16 @@ export async function askApplication(url, init, within, standIn) {
|
|
|
266
289
|
}
|
|
267
290
|
/** Every event a write sends, rendered, stored when the vendor keeps its events, signed, delivered to each endpoint
|
|
268
291
|
* that takes it, and recorded. */
|
|
269
|
-
export async function deliverEvents(service, decl, write, data, transport = worldTransport, values) {
|
|
292
|
+
export async function deliverEvents(service, decl, write, data, transport = worldTransport, values, application) {
|
|
270
293
|
if (write.storedType === decl.store)
|
|
271
294
|
return [];
|
|
295
|
+
const dispatch = application ? async (url, body, headers) => {
|
|
296
|
+
const answer = await application(url, { method: 'POST', headers: { 'content-type': 'application/json', ...headers }, body });
|
|
297
|
+
return answer === undefined ? transport(url, body, headers) : answer === 'unreachable' ? 0 : answer.status;
|
|
298
|
+
} : transport;
|
|
272
299
|
const sent = [];
|
|
273
300
|
for (const type of eventTypesOf(decl, write.operation))
|
|
274
|
-
sent.push(...(await deliverEventType(service, decl, write, data,
|
|
301
|
+
sent.push(...(await deliverEventType(service, decl, write, data, dispatch, values, type)));
|
|
275
302
|
return sent;
|
|
276
303
|
}
|
|
277
304
|
/** One event type a write sends: to the live endpoints that take it, stored when the vendor keeps its events. */
|
|
@@ -364,11 +391,29 @@ async function deliverEventType(service, decl, write, data, transport, values, t
|
|
|
364
391
|
const store = getActiveWorldStore(), deliveryRoot = worldPaths(service, write.root).root;
|
|
365
392
|
for (const d of sent)
|
|
366
393
|
deliveryObserver?.({ ...d, service });
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
394
|
+
const roots = pendingDeliveries.get(store) ?? new Map();
|
|
395
|
+
pendingDeliveries.set(store, roots);
|
|
396
|
+
const pending = roots.get(deliveryRoot) ?? new Set();
|
|
397
|
+
roots.set(deliveryRoot, pending);
|
|
398
|
+
for (const packet of recorded) {
|
|
399
|
+
let finish;
|
|
400
|
+
let fail;
|
|
401
|
+
const dispatched = new Promise((done, reject) => { finish = done; fail = reject; });
|
|
402
|
+
// Live writes return before dispatch; an explicit settlement reports persistence failures.
|
|
403
|
+
void dispatched.catch(() => { });
|
|
404
|
+
pending.add(dispatched);
|
|
405
|
+
setTimeout(async () => {
|
|
406
|
+
try {
|
|
407
|
+
await withWorldStore(store, () => sendRecordedEvent(service, packet.type, packet.id, deliveryRoot, transport, trace));
|
|
408
|
+
finish();
|
|
409
|
+
forgetDeliveries(store, deliveryRoot, [dispatched]);
|
|
410
|
+
}
|
|
411
|
+
catch (error) {
|
|
412
|
+
// Keep the rejection until explicit settlement consumes it. The queued/sending claim remains for
|
|
413
|
+
// native recovery; this must not fail the already-committed vendor write or cause a blind resend.
|
|
414
|
+
fail(error);
|
|
415
|
+
}
|
|
372
416
|
}, 0);
|
|
417
|
+
}
|
|
373
418
|
return sent;
|
|
374
419
|
}
|
package/dist/src/executor.d.ts
CHANGED
|
@@ -132,8 +132,8 @@ export declare function validateRemoteOrigin(origin: string, opts?: {
|
|
|
132
132
|
/** `vendorHosts`: the pack's declared vendor host rules (its descriptor's `hosts`). An absolute https URL
|
|
133
133
|
* on one of them — an exact `host` rule whose `pathPattern` the path matches — is a second host of the
|
|
134
134
|
* same vendor that takes the credential (LinkedIn's image uploads on www.linkedin.com/dms-uploads,
|
|
135
|
-
* Instagram's rupload host); it is sent credentialed like an anchored call
|
|
136
|
-
*
|
|
135
|
+
* Instagram's rupload host); it is sent credentialed like an anchored call, as HTTP Basic when a rule naming
|
|
136
|
+
* it names a `basic` user. Any other absolute URL is refused. */
|
|
137
137
|
/** Where a credential is kept (the head's and refresh's root: head.ts rootCustody): `key` names the credential stably
|
|
138
138
|
* across rotations and anew when another is sealed in its place (the token cache is keyed by it, not by the secret's value), `open` reads it as sealed now, `seal` keeps a rotated one. */
|
|
139
139
|
/** `unsealed`: where a rotation the seal failed to keep is recorded durably (the field and the cause, never a value), since
|
package/dist/src/executor.js
CHANGED
|
@@ -224,6 +224,8 @@ function direct(origin, credential, auth, vendorHosts, now = () => new Date()) {
|
|
|
224
224
|
let target;
|
|
225
225
|
// the vendor's own URL, where a loopback root stands in for it: what a signature is made over
|
|
226
226
|
let vendorUrl;
|
|
227
|
+
// the user a host that takes the token as HTTP Basic's password names it under (HostRule.basic)
|
|
228
|
+
let basicUser;
|
|
227
229
|
if (/^https:\/\//i.test(path) && vendorHosts?.length) {
|
|
228
230
|
// a second host the pack declares for this vendor, on a path its rule names: validated as the root is
|
|
229
231
|
target = new URL(path);
|
|
@@ -235,6 +237,7 @@ function direct(origin, credential, auth, vendorHosts, now = () => new Date()) {
|
|
|
235
237
|
const declared = vendorHosts.some((r) => !r.exclude && named(r)) && !vendorHosts.some((r) => r.exclude && named(r));
|
|
236
238
|
if (!declared)
|
|
237
239
|
throw new Error(`remote execute: ${target.host}${target.pathname.slice(0, 64)} is not a host this vendor's pack declares for this path`);
|
|
240
|
+
basicUser = vendorHosts.find((r) => !r.exclude && r.basic !== undefined && named(r))?.basic;
|
|
238
241
|
// A loopback root is the vendor substitute. Preserve the vendor Host while retaining
|
|
239
242
|
// the already-validated root's mounted path; an absolute URL never escapes that root.
|
|
240
243
|
if (isLoopbackHost(origin.hostname)) {
|
|
@@ -258,6 +261,16 @@ function direct(origin, credential, auth, vendorHosts, now = () => new Date()) {
|
|
|
258
261
|
else
|
|
259
262
|
for (const [name, value] of Object.entries(credential.headers))
|
|
260
263
|
outbound.set(name, value);
|
|
264
|
+
// A HOST THAT TAKES THE TOKEN AS HTTP BASIC: the sealed authorization header's credentials, after its scheme, are
|
|
265
|
+
// Basic's password under the rule's user; one sealed as Basic goes as it is, and none sealed refuses before egress
|
|
266
|
+
if (issued === undefined && basicUser !== undefined) {
|
|
267
|
+
const sealed = outbound.get('authorization') ?? '';
|
|
268
|
+
const token = /^\S+\s+(\S.*)$/.exec(sealed)?.[1];
|
|
269
|
+
if (token === undefined)
|
|
270
|
+
throw new Error(`remote execute: ${target.host} takes the credential as HTTP Basic, and the sealed credential carries no authorization header`);
|
|
271
|
+
if (!/^basic\s/i.test(sealed))
|
|
272
|
+
outbound.set('authorization', `Basic ${btoa(`${basicUser}:${token}`)}`);
|
|
273
|
+
}
|
|
261
274
|
// the vendor's host, kept over any the credential carries (a loopback twin routes by it, as the World's proxy's)
|
|
262
275
|
if (vendorUrl) {
|
|
263
276
|
outbound.set('host', vendorUrl.host);
|
|
@@ -5,6 +5,10 @@ export declare function fileResponse(path: string, init?: ResponseInit): Respons
|
|
|
5
5
|
* Stored bytes as a response, honouring a single `Range: bytes=` as RFC 9110 §14 has it: every satisfiable range is a
|
|
6
6
|
* 206 with its Content-Range, `bytes=0-` (the whole body) included; a range that does not parse, a first-byte-pos past
|
|
7
7
|
* its last-byte-pos (`bytes=5-2`) included, is ignored and the whole body is a 200; a well-formed range that starts past
|
|
8
|
-
* the end (any range of an empty body) is a 416.
|
|
8
|
+
* the end (any range of an empty body) is a 416. Lazy sources supply inclusive ranges and stream in bounded reads.
|
|
9
|
+
* `nosniff` stops a browser second-guessing the content type the vendor gave.
|
|
9
10
|
*/
|
|
10
|
-
export declare function bytesResponse(request: Request, bytes: Uint8Array
|
|
11
|
+
export declare function bytesResponse(request: Request, bytes: Uint8Array | {
|
|
12
|
+
size: number;
|
|
13
|
+
range(start: number, end: number): Promise<Uint8Array | null>;
|
|
14
|
+
}, contentType: string, cacheControl?: string): Response;
|
|
@@ -24,10 +24,11 @@ export function fileResponse(path, init) {
|
|
|
24
24
|
* Stored bytes as a response, honouring a single `Range: bytes=` as RFC 9110 §14 has it: every satisfiable range is a
|
|
25
25
|
* 206 with its Content-Range, `bytes=0-` (the whole body) included; a range that does not parse, a first-byte-pos past
|
|
26
26
|
* its last-byte-pos (`bytes=5-2`) included, is ignored and the whole body is a 200; a well-formed range that starts past
|
|
27
|
-
* the end (any range of an empty body) is a 416.
|
|
27
|
+
* the end (any range of an empty body) is a 416. Lazy sources supply inclusive ranges and stream in bounded reads.
|
|
28
|
+
* `nosniff` stops a browser second-guessing the content type the vendor gave.
|
|
28
29
|
*/
|
|
29
30
|
export function bytesResponse(request, bytes, contentType, cacheControl = 'public, max-age=604800, immutable') {
|
|
30
|
-
const total = bytes.length;
|
|
31
|
+
const total = bytes instanceof Uint8Array ? bytes.length : bytes.size;
|
|
31
32
|
const base = { 'content-type': contentType, 'x-content-type-options': 'nosniff', 'accept-ranges': 'bytes', 'cache-control': cacheControl };
|
|
32
33
|
const range = /^bytes=(\d*)-(\d*)$/.exec((request.headers.get('range') ?? '').trim());
|
|
33
34
|
let start = 0;
|
|
@@ -47,9 +48,32 @@ export function bytesResponse(request, bytes, contentType, cacheControl = 'publi
|
|
|
47
48
|
// every satisfiable range is answered as one, the whole body's `bytes=0-` included: a media player (Safari's) reads a
|
|
48
49
|
// 206 with its Content-Range as the server taking ranges, and a 200 as one it cannot seek in
|
|
49
50
|
const partial = valid;
|
|
50
|
-
|
|
51
|
-
|
|
51
|
+
let body = null;
|
|
52
|
+
if (request.method !== 'HEAD') {
|
|
53
|
+
if (bytes instanceof Uint8Array)
|
|
54
|
+
body = new Blob([bytes.slice(start, end + 1)]);
|
|
55
|
+
else {
|
|
56
|
+
let at = start;
|
|
57
|
+
body = new ReadableStream({
|
|
58
|
+
async pull(controller) {
|
|
59
|
+
if (at > end) {
|
|
60
|
+
controller.close();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const last = Math.min(at + 8 * 1024 * 1024 - 1, end);
|
|
64
|
+
const chunk = await bytes.range(at, last);
|
|
65
|
+
if (!chunk) {
|
|
66
|
+
controller.error(new Error('stored bytes are gone'));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
controller.enqueue(chunk);
|
|
70
|
+
at = last + 1;
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return new Response(body, {
|
|
52
76
|
status: partial ? 206 : 200,
|
|
53
|
-
headers: { ...base, 'content-length': String(
|
|
77
|
+
headers: { ...base, 'content-length': String(end + 1 - start), ...(partial ? { 'content-range': `bytes ${start}-${end}/${total}` } : {}) },
|
|
54
78
|
});
|
|
55
79
|
}
|
package/dist/src/git/index.d.ts
CHANGED
package/dist/src/git/index.js
CHANGED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type GitObjectStore } from './objects.js';
|
|
2
|
+
import { type Service } from './smart-http.js';
|
|
3
|
+
/** One exchange: `GET info/refs?service=<service>` when there is no body, `POST <service>` when there is. It answers
|
|
4
|
+
* the response's bytes, and throws when the server did not answer 200. */
|
|
5
|
+
export type SmartHttpTransport = (request: {
|
|
6
|
+
service: Service;
|
|
7
|
+
body?: Uint8Array;
|
|
8
|
+
}) => Promise<Uint8Array>;
|
|
9
|
+
/** What a server advertises: its refs (peeled `^{}` lines dropped), the ref its HEAD names, and its capabilities. */
|
|
10
|
+
export type RemoteRefs = {
|
|
11
|
+
refs: Map<string, string>;
|
|
12
|
+
head: string | null;
|
|
13
|
+
caps: string[];
|
|
14
|
+
};
|
|
15
|
+
/** `GET info/refs?service=<service>`, read. */
|
|
16
|
+
export declare function lsRemote(transport: SmartHttpTransport, service: Service): Promise<RemoteRefs>;
|
|
17
|
+
/** `POST git-upload-pack` with `done`: every object `wants` reach and `haves` do not, read into `store`. */
|
|
18
|
+
export declare function fetchPack(transport: SmartHttpTransport, store: GitObjectStore, wants: string[], haves: string[]): Promise<void>;
|
|
19
|
+
/** One ref update a push asks for: `old` is the server's value as advertised (ZERO_SHA to create), `new` the object
|
|
20
|
+
* to point it at (ZERO_SHA to delete). */
|
|
21
|
+
export type PushCommand = {
|
|
22
|
+
name: string;
|
|
23
|
+
old: string;
|
|
24
|
+
new: string;
|
|
25
|
+
};
|
|
26
|
+
/** `POST git-receive-pack`: the commands and the pack of what they need beyond `haves`, atomic when `atomic` (the
|
|
27
|
+
* server offered it). Answers each command's outcome by ref name: null when the server took it, else its reason. */
|
|
28
|
+
export declare function sendPack(transport: SmartHttpTransport, store: GitObjectStore, commands: PushCommand[], haves: string[], atomic?: boolean): Promise<Map<string, string | null>>;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// THE CLIENT HALF OF SMART HTTP (gitprotocol-http, protocol v0): what `git fetch` and `git push` send to a server,
|
|
2
|
+
// over a transport the caller supplies. The library opens no socket, holds no credential and names no host; a root's
|
|
3
|
+
// refresh and deploy reach the vendor's git through it over the kernel executor (derived-real.ts, ADR 0031). It asks
|
|
4
|
+
// only for what it reads: no side-band (the pack arrives bare), no multi_ack (one answer), no thin pack (every delta
|
|
5
|
+
// base is in the pack or the store), ofs-delta (readPack resolves both delta kinds), and on a push report-status, with
|
|
6
|
+
// atomic when the server offers it.
|
|
7
|
+
import { concat } from "./objects.js";
|
|
8
|
+
import { readPack } from "./pack.js";
|
|
9
|
+
import { FLUSH, ZERO_SHA, packObjects, pktLine, readPktLines } from "./smart-http.js";
|
|
10
|
+
const DEC = new TextDecoder();
|
|
11
|
+
const SHA = /^[0-9a-f]{40}$/;
|
|
12
|
+
/** `GET info/refs?service=<service>`, read. */
|
|
13
|
+
export async function lsRemote(transport, service) {
|
|
14
|
+
let { lines, rest } = readPktLines(await transport({ service }));
|
|
15
|
+
if (lines[0]?.startsWith('# service='))
|
|
16
|
+
({ lines, rest } = readPktLines(rest));
|
|
17
|
+
const refs = new Map();
|
|
18
|
+
let head = null;
|
|
19
|
+
let caps = [];
|
|
20
|
+
for (const raw of lines) {
|
|
21
|
+
const [line, offered] = raw.replace(/\n$/, '').split('\0');
|
|
22
|
+
if (line.startsWith('ERR '))
|
|
23
|
+
throw new Error(`git ${service}: ${line.slice(4)}`);
|
|
24
|
+
if (offered !== undefined) {
|
|
25
|
+
caps = offered.split(' ').filter(Boolean);
|
|
26
|
+
const symref = caps.find((c) => c.startsWith('symref=HEAD:'));
|
|
27
|
+
if (symref)
|
|
28
|
+
head = symref.slice('symref=HEAD:'.length);
|
|
29
|
+
}
|
|
30
|
+
const sha = line.slice(0, 40);
|
|
31
|
+
const name = line.slice(41);
|
|
32
|
+
if (!SHA.test(sha) || sha === ZERO_SHA || name === 'HEAD' || name.endsWith('^{}'))
|
|
33
|
+
continue;
|
|
34
|
+
refs.set(name, sha);
|
|
35
|
+
}
|
|
36
|
+
return { refs, head, caps };
|
|
37
|
+
}
|
|
38
|
+
const isPack = (bytes, at) => DEC.decode(bytes.subarray(at, at + 4)) === 'PACK';
|
|
39
|
+
/** `POST git-upload-pack` with `done`: every object `wants` reach and `haves` do not, read into `store`. */
|
|
40
|
+
export async function fetchPack(transport, store, wants, haves) {
|
|
41
|
+
if (wants.length === 0)
|
|
42
|
+
return;
|
|
43
|
+
const answer = await transport({
|
|
44
|
+
service: 'git-upload-pack',
|
|
45
|
+
body: concat(pktLine(`want ${wants[0]} ofs-delta no-progress agent=volter-twin\n`), ...wants.slice(1).map((w) => pktLine(`want ${w}\n`)), FLUSH, ...haves.map((h) => pktLine(`have ${h}\n`)), pktLine('done\n')),
|
|
46
|
+
});
|
|
47
|
+
// the negotiation's ACK / NAK lines, then the pack
|
|
48
|
+
let at = 0;
|
|
49
|
+
while (at + 4 <= answer.length && !isPack(answer, at)) {
|
|
50
|
+
const len = parseInt(DEC.decode(answer.subarray(at, at + 4)), 16);
|
|
51
|
+
if (Number.isNaN(len))
|
|
52
|
+
break;
|
|
53
|
+
if (len < 4) {
|
|
54
|
+
at += 4;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const line = DEC.decode(answer.subarray(at + 4, at + len));
|
|
58
|
+
if (line.startsWith('ERR '))
|
|
59
|
+
throw new Error(`git upload-pack: ${line.slice(4).trim()}`);
|
|
60
|
+
at += len;
|
|
61
|
+
}
|
|
62
|
+
if (!isPack(answer, at))
|
|
63
|
+
throw new Error('git upload-pack: the server answered no pack');
|
|
64
|
+
await readPack(answer.subarray(at), store);
|
|
65
|
+
}
|
|
66
|
+
/** `POST git-receive-pack`: the commands and the pack of what they need beyond `haves`, atomic when `atomic` (the
|
|
67
|
+
* server offered it). Answers each command's outcome by ref name: null when the server took it, else its reason. */
|
|
68
|
+
export async function sendPack(transport, store, commands, haves, atomic = false) {
|
|
69
|
+
const updates = commands.filter((c) => c.new !== ZERO_SHA).map((c) => c.new);
|
|
70
|
+
const caps = `report-status${atomic ? ' atomic' : ''} agent=volter-twin`;
|
|
71
|
+
const answer = await transport({
|
|
72
|
+
service: 'git-receive-pack',
|
|
73
|
+
body: concat(...commands.map((c, i) => pktLine(`${c.old} ${c.new} ${c.name}${i === 0 ? `\0${caps}` : ''}\n`)), FLUSH,
|
|
74
|
+
// a push of deletions alone sends no pack (gitprotocol-pack, "Pushing data to a server")
|
|
75
|
+
...(updates.length ? [await packObjects(store, updates, haves)] : [])),
|
|
76
|
+
});
|
|
77
|
+
const report = readPktLines(answer).lines.map((l) => l.replace(/\n$/, ''));
|
|
78
|
+
const failure = report.find((l) => l.startsWith('ERR '));
|
|
79
|
+
if (failure)
|
|
80
|
+
throw new Error(`git receive-pack: ${failure.slice(4)}`);
|
|
81
|
+
const unpack = report.find((l) => l.startsWith('unpack '))?.slice('unpack '.length);
|
|
82
|
+
if (unpack === undefined)
|
|
83
|
+
throw new Error('git receive-pack: the server sent no report');
|
|
84
|
+
const out = new Map(commands.map((c) => [c.name, unpack === 'ok' ? 'the server reported nothing for it' : `unpacker error: ${unpack}`]));
|
|
85
|
+
for (const l of report) {
|
|
86
|
+
if (l.startsWith('ok '))
|
|
87
|
+
out.set(l.slice(3), null);
|
|
88
|
+
else if (l.startsWith('ng ')) {
|
|
89
|
+
const rest = l.slice(3);
|
|
90
|
+
const sp = rest.indexOf(' ');
|
|
91
|
+
out.set(sp === -1 ? rest : rest.slice(0, sp), sp === -1 ? 'refused' : rest.slice(sp + 1));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
@@ -16,6 +16,9 @@ export declare function readAllPktLines(bytes: Uint8Array): {
|
|
|
16
16
|
export type Service = 'git-upload-pack' | 'git-receive-pack';
|
|
17
17
|
/** `GET info/refs?service=<service>`: the ref advertisement, HEAD first as a symref. */
|
|
18
18
|
export declare function advertisement(service: Service, refs: GitRefs): Uint8Array;
|
|
19
|
+
/** The pack of every object `wants` reach that the `haves` the store holds do not: what a push sends (a client's
|
|
20
|
+
* send-pack, smart-http-client.ts). */
|
|
21
|
+
export declare function packObjects(store: GitObjectStore, wants: string[], haves: string[]): Promise<Uint8Array>;
|
|
19
22
|
/** `POST git-upload-pack`: wants and haves in, NAK and (once `done`) the pack out. A request with `deepen <n>` is shallow:
|
|
20
23
|
* its answer opens with the shallow-update section (`shallow <sha>` for each boundary commit, `unshallow <sha>` for a
|
|
21
24
|
* commit the client held shallow whose parents it now receives, then a flush), and its pack stops at the boundary. */
|
|
@@ -105,6 +105,15 @@ async function closure(store, roots, seen) {
|
|
|
105
105
|
return out;
|
|
106
106
|
}
|
|
107
107
|
async function reachable(store, roots) { const seen = new Set(); await closure(store, roots, seen); return seen; }
|
|
108
|
+
/** The pack of every object `wants` reach that the `haves` the store holds do not: what a push sends (a client's
|
|
109
|
+
* send-pack, smart-http-client.ts). */
|
|
110
|
+
export async function packObjects(store, wants, haves) {
|
|
111
|
+
const known = [];
|
|
112
|
+
for (const h of haves)
|
|
113
|
+
if (await store.has(h))
|
|
114
|
+
known.push(h);
|
|
115
|
+
return writePack(await closure(store, wants, await reachable(store, known)));
|
|
116
|
+
}
|
|
108
117
|
/**
|
|
109
118
|
* A shallow request's commits (gitprotocol-pack, "Shallow requests"): every commit within `depth` of a want (a want is
|
|
110
119
|
* depth 1), and the boundary, the commits whose parents the client will not have. The client's own shallow commits it
|
package/dist/src/head.d.ts
CHANGED
|
@@ -19,6 +19,12 @@ export type PushOutcome = {
|
|
|
19
19
|
externalId: string;
|
|
20
20
|
data?: Record<string, unknown>;
|
|
21
21
|
}>;
|
|
22
|
+
/** the group's entries the vendor refused while it took the rest (a git push to a root without `atomic`, each ref its
|
|
23
|
+
* own outcome: ADR 0031); each settles refused with its reason */
|
|
24
|
+
refused?: Array<{
|
|
25
|
+
actionId: string;
|
|
26
|
+
reason: string;
|
|
27
|
+
}>;
|
|
22
28
|
};
|
|
23
29
|
/** What a perform is handed beside the executor: a resolver from a local id to the vendor's (an
|
|
24
30
|
* entry authored against `twin-1` crosses against `REAL-42` once that subject's landing adopted it). */
|
|
@@ -140,6 +146,11 @@ export type DeployReport = {
|
|
|
140
146
|
actionId: string;
|
|
141
147
|
error: string;
|
|
142
148
|
};
|
|
149
|
+
/** a group the vendor took in part: its entries it refused, each with its reason; nothing after it was attempted */
|
|
150
|
+
partial?: Array<{
|
|
151
|
+
actionId: string;
|
|
152
|
+
reason: string;
|
|
153
|
+
}>;
|
|
143
154
|
/** entries after the refusal or failure, not attempted */
|
|
144
155
|
skipped: string[];
|
|
145
156
|
/** entries whose landed copy already said `deployed` — not performed again */
|
|
@@ -149,7 +160,7 @@ export type DeployReport = {
|
|
|
149
160
|
};
|
|
150
161
|
/** The branch entries a root twin still has to settle: `set` rows, not reverted, whose landed copy is absent or says
|
|
151
162
|
* `failed`/`skipped` (a `deployed` or `refused` copy settles the entry), split into what is offered to the vendor (a
|
|
152
|
-
* surface write on a subject the vendor can have) and what is the World's own: a write a door, a screen, the gap, the
|
|
163
|
+
* surface write or a git push on a subject the vendor can have) and what is the World's own: a write a door, a screen, the gap, the
|
|
153
164
|
* clock or a seed made, and any write on a subject only the World's own writes made. */
|
|
154
165
|
export declare function settlingEntries(service: string, root?: string): {
|
|
155
166
|
offered: TwinAction[];
|
|
@@ -196,6 +207,10 @@ export declare function deferPerform<T>(request: string, fn: () => Promise<T>):
|
|
|
196
207
|
export declare function performRequestGroup(service: string, request: string, root?: string): Promise<{
|
|
197
208
|
ids: Map<string, string>;
|
|
198
209
|
response?: RemoteExecuteResponse;
|
|
210
|
+
refused?: Array<{
|
|
211
|
+
actionId: string;
|
|
212
|
+
reason: string;
|
|
213
|
+
}>;
|
|
199
214
|
} | RefusedWriteError | VendorWriteError>;
|
|
200
215
|
/** Wrap a pack's fetch: a `RefusedWriteError` or `VendorWriteError` the head raised becomes the
|
|
201
216
|
* vendor's own error body (the pack's `shape`), so an app under live use sees what the vendor
|
package/dist/src/head.js
CHANGED
|
@@ -262,11 +262,12 @@ function receiptsByEntry(parent) {
|
|
|
262
262
|
/** Whether an entry records a call of the vendor's surface (an operation or a GraphQL mutation): what a perform can send
|
|
263
263
|
* again (the real-system adapters, "What the derived core records with a write"). */
|
|
264
264
|
function surfaceWrite(e) {
|
|
265
|
-
|
|
265
|
+
// a push to a World repository is the vendor's too: a deploy sends it as a git push (derived-core.ts pushedInput)
|
|
266
|
+
return typeof e.input?.operationId === 'string' || (e.input?.graphql !== undefined && e.input.graphql !== null) || (e.input?.git !== undefined && e.input.git !== null);
|
|
266
267
|
}
|
|
267
268
|
/** The branch entries a root twin still has to settle: `set` rows, not reverted, whose landed copy is absent or says
|
|
268
269
|
* `failed`/`skipped` (a `deployed` or `refused` copy settles the entry), split into what is offered to the vendor (a
|
|
269
|
-
* surface write on a subject the vendor can have) and what is the World's own: a write a door, a screen, the gap, the
|
|
270
|
+
* surface write or a git push on a subject the vendor can have) and what is the World's own: a write a door, a screen, the gap, the
|
|
270
271
|
* clock or a seed made, and any write on a subject only the World's own writes made. */
|
|
271
272
|
export function settlingEntries(service, root) {
|
|
272
273
|
const parent = parentEntries(service, root);
|
|
@@ -435,7 +436,14 @@ async function performNow(opts) {
|
|
|
435
436
|
const answered = outcome.data !== null && typeof outcome.data === 'object' && !Array.isArray(outcome.data) ? outcome.data : {};
|
|
436
437
|
const primary = resolved.find((a) => a.id === outcome.actionId) ?? resolved[0];
|
|
437
438
|
const also = new Map((outcome.also ?? []).map((a) => [a.actionId, a]));
|
|
439
|
+
const refusedHere = new Map((outcome.refused ?? []).map((r) => [r.actionId, r.reason]));
|
|
438
440
|
for (const action of resolved) {
|
|
441
|
+
const refusal = refusedHere.get(action.id);
|
|
442
|
+
if (refusal !== undefined) {
|
|
443
|
+
settle(action, at, { status: 'refused', reason: refusal });
|
|
444
|
+
refusedIds.add(action.subject.id);
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
439
447
|
const answeredId = action === primary ? outcome.externalId : also.get(action.id)?.externalId;
|
|
440
448
|
const externalId = answeredId || aliases.get(`${action.subject.type}:${action.subject.id}`);
|
|
441
449
|
settle(action, at, { status: 'deployed', ...(action === primary && outcome.url ? { url: outcome.url } : {}) }, { ...action.fields, ...(action === primary ? answered : also.get(action.id)?.data ?? {}) }, externalId || undefined);
|
|
@@ -443,6 +451,10 @@ async function performNow(opts) {
|
|
|
443
451
|
report.pushed += 1;
|
|
444
452
|
report.deployed.push({ actionId: action.id, at, ...(externalId ? { externalId } : {}), ...(data !== undefined ? { data } : {}), ...(action === primary && outcome.response ? { response: outcome.response } : {}) });
|
|
445
453
|
}
|
|
454
|
+
if (refusedHere.size) {
|
|
455
|
+
report.partial = [...refusedHere].map(([actionId, reason]) => ({ actionId, reason }));
|
|
456
|
+
stopped = true;
|
|
457
|
+
}
|
|
446
458
|
}
|
|
447
459
|
catch (error) {
|
|
448
460
|
// a write the vendor refuses (a 4xx: it can never be sent as it is) settles the group refused, as a check's
|
|
@@ -545,24 +557,57 @@ export async function performRequestGroup(service, request, root) {
|
|
|
545
557
|
}
|
|
546
558
|
catch (error) {
|
|
547
559
|
if (error instanceof DeploymentOrderError) {
|
|
548
|
-
|
|
549
|
-
revertAction({ service, actionId: e.id, occurredAt: worldNow(), ...(root !== undefined ? { root } : {}) });
|
|
560
|
+
revertRequest(service, request, group, root);
|
|
550
561
|
return new VendorWriteError(`An earlier ${service} write is still pending; this request was rolled back before reaching the vendor. Retry after that write finishes.`, group[0].id);
|
|
551
562
|
}
|
|
552
563
|
throw error instanceof HeadError ? error : new HeadError(error instanceof Error ? error.message : String(error));
|
|
553
564
|
}
|
|
554
565
|
if (report.refused || report.failed) {
|
|
555
|
-
|
|
556
|
-
revertAction({ service, actionId: e.id, occurredAt: worldNow(), ...(root !== undefined ? { root } : {}) });
|
|
566
|
+
revertRequest(service, request, group, root);
|
|
557
567
|
return report.refused ? new RefusedWriteError(report.refused.check, report.refused.reason, report.refused.actionId) : new VendorWriteError(report.failed.error, report.failed.actionId);
|
|
558
568
|
}
|
|
569
|
+
if (report.partial)
|
|
570
|
+
revertRequest(service, request, group.filter((e) => report.partial.some((r) => r.actionId === e.id)), root);
|
|
559
571
|
for (const d of report.deployed) {
|
|
560
572
|
const e = group.find((g) => g.id === d.actionId);
|
|
561
573
|
if (e && d.externalId && d.externalId !== e.subject.id)
|
|
562
574
|
ids.set(e.subject.id, d.externalId);
|
|
563
575
|
}
|
|
564
576
|
const response = report.deployed.find((entry) => entry.response)?.response;
|
|
565
|
-
return { ids, ...(response ? { response } : {}) };
|
|
577
|
+
return { ids, ...(response ? { response } : {}), ...(report.partial ? { refused: report.partial } : {}) };
|
|
578
|
+
}
|
|
579
|
+
/** Revert what a request's refused entries made, latest first. An entry that is a declared push a screen recorded
|
|
580
|
+
* (`input.git`, ADR 0031) owns the request's writes after it up to the next such push, its push's effects (the
|
|
581
|
+
* reactions and vendor moves the pack runs on it), which go with it; any other refused entry goes alone, as the group
|
|
582
|
+
* it is in. */
|
|
583
|
+
function revertRequest(service, request, refused, root) {
|
|
584
|
+
const pushed = (e) => e.input?.git !== undefined && e.input.git !== null;
|
|
585
|
+
const undo = new Set();
|
|
586
|
+
if (refused.some(pushed)) {
|
|
587
|
+
const made = branchEntries(service, root).filter((e) => e.correlationId === request && e.op === 'set');
|
|
588
|
+
for (const r of refused) {
|
|
589
|
+
if (!pushed(r)) {
|
|
590
|
+
undo.add(r.id);
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
const at = made.findIndex((e) => e.id === r.id);
|
|
594
|
+
if (at < 0) {
|
|
595
|
+
undo.add(r.id);
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
598
|
+
undo.add(r.id);
|
|
599
|
+
for (const e of made.slice(at + 1)) {
|
|
600
|
+
if (pushed(e))
|
|
601
|
+
break;
|
|
602
|
+
undo.add(e.id);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
else
|
|
607
|
+
for (const r of refused)
|
|
608
|
+
undo.add(r.id);
|
|
609
|
+
for (const e of branchEntries(service, root).filter((x) => undo.has(x.id)).reverse())
|
|
610
|
+
revertAction({ service, actionId: e.id, occurredAt: worldNow(), ...(root !== undefined ? { root } : {}) });
|
|
566
611
|
}
|
|
567
612
|
/** Wrap a pack's fetch: a `RefusedWriteError` or `VendorWriteError` the head raised becomes the
|
|
568
613
|
* vendor's own error body (the pack's `shape`), so an app under live use sees what the vendor
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export type HtmlAttribute = {
|
|
2
|
+
name: string;
|
|
3
|
+
value: string;
|
|
4
|
+
prefix?: string;
|
|
5
|
+
namespace?: string;
|
|
6
|
+
};
|
|
7
|
+
export type HtmlElement = {
|
|
8
|
+
type: 'element';
|
|
9
|
+
name: string;
|
|
10
|
+
namespace: string;
|
|
11
|
+
attributes: HtmlAttribute[];
|
|
12
|
+
children: HtmlNode[];
|
|
13
|
+
};
|
|
14
|
+
export type HtmlText = {
|
|
15
|
+
type: 'text';
|
|
16
|
+
value: string;
|
|
17
|
+
};
|
|
18
|
+
export type HtmlNode = HtmlElement | HtmlText;
|
|
19
|
+
export declare class HtmlError extends Error {
|
|
20
|
+
readonly code: 'parser-unavailable';
|
|
21
|
+
constructor(code: 'parser-unavailable');
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* A fragment parsed as WHATWG fragment parsing does in an HTML `<body>` context: elements and text, comments dropped,
|
|
25
|
+
* an HTML `<template>`'s content carried as that element's children. Importing this module does not load the grammar.
|
|
26
|
+
*/
|
|
27
|
+
export declare function parseFragment(source: string): Promise<HtmlNode[]>;
|
|
28
|
+
/** Nodes serialized by the WHATWG HTML fragment serialization: its escaping, its void elements, its foreign names. */
|
|
29
|
+
export declare function serialize(nodes: HtmlNode[]): Promise<string>;
|
package/dist/src/html.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export class HtmlError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
constructor(code) {
|
|
4
|
+
super('HTML parser parse5 7.3.0 is unavailable');
|
|
5
|
+
this.code = code;
|
|
6
|
+
this.name = 'HtmlError';
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
let grammar;
|
|
10
|
+
function loadGrammar() {
|
|
11
|
+
return grammar ??= import('parse5').catch(() => { grammar = undefined; throw new HtmlError('parser-unavailable'); });
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* A fragment parsed as WHATWG fragment parsing does in an HTML `<body>` context: elements and text, comments dropped,
|
|
15
|
+
* an HTML `<template>`'s content carried as that element's children. Importing this module does not load the grammar.
|
|
16
|
+
*/
|
|
17
|
+
export async function parseFragment(source) {
|
|
18
|
+
const { parseFragment: parse, defaultTreeAdapter: tree, html } = await loadGrammar();
|
|
19
|
+
const convert = (nodes) => {
|
|
20
|
+
const out = [];
|
|
21
|
+
for (const node of nodes) {
|
|
22
|
+
if (node.nodeName === '#text')
|
|
23
|
+
out.push({ type: 'text', value: node.value });
|
|
24
|
+
else if ('tagName' in node) {
|
|
25
|
+
const element = node;
|
|
26
|
+
// only an HTML template has content; an SVG or MathML `template` is a foreign element with children
|
|
27
|
+
const children = element.tagName === 'template' && element.namespaceURI === html.NS.HTML
|
|
28
|
+
? element.content.childNodes
|
|
29
|
+
: element.childNodes;
|
|
30
|
+
out.push({
|
|
31
|
+
type: 'element', name: element.tagName, namespace: element.namespaceURI,
|
|
32
|
+
attributes: element.attrs.map(attr => ({ name: attr.name, value: attr.value, ...(attr.prefix ? { prefix: attr.prefix } : {}), ...(attr.namespace ? { namespace: attr.namespace } : {}) })),
|
|
33
|
+
children: convert(children),
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
};
|
|
39
|
+
return convert(parse(tree.createElement('body', html.NS.HTML, []), source, {}).childNodes);
|
|
40
|
+
}
|
|
41
|
+
/** Nodes serialized by the WHATWG HTML fragment serialization: its escaping, its void elements, its foreign names. */
|
|
42
|
+
export async function serialize(nodes) {
|
|
43
|
+
const { serialize: write, defaultTreeAdapter: tree, html } = await loadGrammar();
|
|
44
|
+
const append = (parent, list) => {
|
|
45
|
+
for (const node of list) {
|
|
46
|
+
if (node.type === 'text') {
|
|
47
|
+
tree.insertText(parent, node.value);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
const element = tree.createElement(node.name, node.namespace, node.attributes.map(({ name, value, prefix, namespace }) => ({ name, value, ...(prefix ? { prefix } : {}), ...(namespace ? { namespace } : {}) })));
|
|
51
|
+
tree.appendChild(parent, element);
|
|
52
|
+
if (node.name === 'template' && node.namespace === html.NS.HTML) {
|
|
53
|
+
const content = tree.createDocumentFragment();
|
|
54
|
+
tree.setTemplateContent(element, content);
|
|
55
|
+
append(content, node.children);
|
|
56
|
+
}
|
|
57
|
+
else
|
|
58
|
+
append(element, node.children);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
const fragment = tree.createDocumentFragment();
|
|
62
|
+
append(fragment, nodes);
|
|
63
|
+
return write(fragment);
|
|
64
|
+
}
|