@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.
Files changed (74) hide show
  1. package/dist/generated/pack-facts.json +44 -3660
  2. package/dist/pack-facts.cjs +4 -4
  3. package/dist/src/browser-session.d.ts +7 -1
  4. package/dist/src/browser-session.js +21 -10
  5. package/dist/src/browser.d.ts +4 -0
  6. package/dist/src/browser.js +10 -0
  7. package/dist/src/derived-core.d.ts +46 -6
  8. package/dist/src/derived-core.js +60 -13
  9. package/dist/src/derived-real.js +159 -5
  10. package/dist/src/derived.d.ts +1 -1
  11. package/dist/src/events.d.ts +6 -3
  12. package/dist/src/events.js +52 -7
  13. package/dist/src/executor.d.ts +2 -2
  14. package/dist/src/executor.js +13 -0
  15. package/dist/src/file-response.d.ts +6 -2
  16. package/dist/src/file-response.js +29 -5
  17. package/dist/src/git/index.d.ts +1 -0
  18. package/dist/src/git/index.js +1 -0
  19. package/dist/src/git/smart-http-client.d.ts +28 -0
  20. package/dist/src/git/smart-http-client.js +95 -0
  21. package/dist/src/git/smart-http.d.ts +3 -0
  22. package/dist/src/git/smart-http.js +9 -0
  23. package/dist/src/head.d.ts +16 -1
  24. package/dist/src/head.js +52 -7
  25. package/dist/src/html.d.ts +29 -0
  26. package/dist/src/html.js +64 -0
  27. package/dist/src/index.d.ts +5 -2
  28. package/dist/src/index.js +5 -1
  29. package/dist/src/managed-database.d.ts +4 -2
  30. package/dist/src/managed-database.js +1 -0
  31. package/dist/src/media-header.d.ts +34 -0
  32. package/dist/src/media-header.js +129 -0
  33. package/dist/src/pack-fetch.d.ts +24 -3
  34. package/dist/src/pack-fetch.js +155 -96
  35. package/dist/src/pack-paths.d.ts +2 -3
  36. package/dist/src/pack-paths.js +3 -13
  37. package/dist/src/packRegistry.d.ts +17 -1
  38. package/dist/src/request-body.d.ts +2 -3
  39. package/dist/src/request-body.js +2 -3
  40. package/dist/src/runtime.d.ts +4 -1
  41. package/dist/src/runtime.js +5 -1
  42. package/dist/src/serve-http.d.ts +2 -2
  43. package/dist/src/serve-http.js +104 -19
  44. package/dist/src/smtp-server.d.ts +78 -0
  45. package/dist/src/smtp-server.js +160 -0
  46. package/dist/src/smtp.d.ts +29 -11
  47. package/dist/src/smtp.js +221 -52
  48. package/generated/pack-facts.json +44 -3660
  49. package/pack-facts.cjs +4 -4
  50. package/package.json +4 -2
  51. package/src/browser-session.ts +17 -5
  52. package/src/browser.ts +10 -0
  53. package/src/derived-core.ts +77 -19
  54. package/src/derived-real.ts +131 -4
  55. package/src/derived.ts +2 -2
  56. package/src/events.ts +54 -9
  57. package/src/executor.ts +13 -2
  58. package/src/file-response.ts +28 -6
  59. package/src/git/index.ts +1 -0
  60. package/src/git/smart-http-client.ts +99 -0
  61. package/src/git/smart-http.ts +7 -0
  62. package/src/head.ts +37 -6
  63. package/src/html.ts +72 -0
  64. package/src/index.ts +6 -2
  65. package/src/managed-database.ts +4 -3
  66. package/src/media-header.ts +108 -0
  67. package/src/pack-fetch.ts +142 -87
  68. package/src/pack-paths.ts +3 -11
  69. package/src/packRegistry.ts +9 -1
  70. package/src/request-body.ts +2 -3
  71. package/src/runtime.ts +5 -1
  72. package/src/serve-http.ts +81 -19
  73. package/src/smtp-server.ts +166 -0
  74. package/src/smtp.ts +199 -53
@@ -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, transport, values, type)));
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
- for (const packet of recorded)
368
- setTimeout(() => {
369
- // Persistence failure leaves a queued/sending claim for explicit recovery;
370
- // it must not fail the already-committed vendor write or cause a blind resend.
371
- void withWorldStore(store, () => sendRecordedEvent(service, packet.type, packet.id, deliveryRoot, transport, trace)).catch(() => { });
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
  }
@@ -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. Any other absolute URL is
136
- * refused, as before. */
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
@@ -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. `nosniff` stops a browser second-guessing the content type the vendor gave.
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, contentType: string, cacheControl?: string): Response;
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. `nosniff` stops a browser second-guessing the content type the vendor gave.
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
- const body = bytes.slice(start, end + 1);
51
- return new Response(request.method === 'HEAD' ? null : new Blob([body]), {
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(body.length), ...(partial ? { 'content-range': `bytes ${start}-${end}/${total}` } : {}) },
77
+ headers: { ...base, 'content-length': String(end + 1 - start), ...(partial ? { 'content-range': `bytes ${start}-${end}/${total}` } : {}) },
54
78
  });
55
79
  }
@@ -6,3 +6,4 @@ export * from './smart-http.js';
6
6
  export * from './history.js';
7
7
  export * from './lfs.js';
8
8
  export * from './format.js';
9
+ export * from './smart-http-client.js';
@@ -6,3 +6,4 @@ export * from "./smart-http.js";
6
6
  export * from "./history.js";
7
7
  export * from "./lfs.js";
8
8
  export * from "./format.js";
9
+ export * from "./smart-http-client.js";
@@ -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
@@ -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
- return typeof e.input?.operationId === 'string' || (e.input?.graphql !== undefined && e.input.graphql !== null);
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
- for (const e of [...group].reverse())
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
- for (const e of [...group].reverse())
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>;
@@ -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
+ }