@volter/world-core 3.0.155 → 3.0.157

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 (150) hide show
  1. package/attach.cjs +2 -0
  2. package/capability-decision.cjs +6 -4
  3. package/dist/attach.cjs +2 -0
  4. package/dist/capability-decision.cjs +6 -4
  5. package/dist/generated/pack-facts.json +204 -4
  6. package/dist/inject.cjs +150 -86
  7. package/dist/network-policy.cjs +11 -1
  8. package/dist/network-policy.d.cts +1 -0
  9. package/dist/pack-facts.d.cts +3 -0
  10. package/dist/required-credential-transport.cjs +29 -0
  11. package/dist/required-credential-transport.d.cts +3 -0
  12. package/dist/src/actions.d.ts +15 -0
  13. package/dist/src/actions.js +82 -21
  14. package/dist/src/ancestry.d.ts +5 -4
  15. package/dist/src/ancestry.js +53 -28
  16. package/dist/src/blob-store.d.ts +16 -0
  17. package/dist/src/blob-store.js +76 -1
  18. package/dist/src/changeset.js +1 -1
  19. package/dist/src/clickhouse/compression.d.ts +28 -0
  20. package/dist/src/clickhouse/compression.js +281 -0
  21. package/dist/src/clickhouse/index.d.ts +1 -0
  22. package/dist/src/clickhouse/index.js +1 -0
  23. package/dist/src/derived-core.d.ts +84 -7
  24. package/dist/src/derived-core.js +336 -61
  25. package/dist/src/derived-real.js +97 -15
  26. package/dist/src/derived.d.ts +5 -0
  27. package/dist/src/derived.js +3 -0
  28. package/dist/src/executor.d.ts +7 -0
  29. package/dist/src/executor.js +15 -1
  30. package/dist/src/fork.js +1 -1
  31. package/dist/src/git/lfs.d.ts +13 -3
  32. package/dist/src/git/lfs.js +41 -9
  33. package/dist/src/grpc-wire.js +2 -1
  34. package/dist/src/h2.js +63 -10
  35. package/dist/src/head.d.ts +5 -0
  36. package/dist/src/head.js +15 -0
  37. package/dist/src/history.d.ts +1 -1
  38. package/dist/src/history.js +7 -7
  39. package/dist/src/index.d.ts +5 -1
  40. package/dist/src/index.js +6 -1
  41. package/dist/src/live/ingest.js +9 -13
  42. package/dist/src/live/rtmp.js +7 -5
  43. package/dist/src/live/segmenter.js +7 -1
  44. package/dist/src/log.d.ts +23 -3
  45. package/dist/src/log.js +160 -37
  46. package/dist/src/machines.js +5 -1
  47. package/dist/src/managed-go-programs.d.ts +55 -0
  48. package/dist/src/managed-go-programs.js +133 -0
  49. package/dist/src/managed-original-inputs.d.ts +101 -0
  50. package/dist/src/managed-original-inputs.js +125 -0
  51. package/dist/src/mongodb/client.d.ts +8 -0
  52. package/dist/src/mongodb/client.js +27 -0
  53. package/dist/src/mongodb/index.d.ts +2 -0
  54. package/dist/src/mongodb/index.js +5 -0
  55. package/dist/src/mongodb/stream.d.ts +97 -0
  56. package/dist/src/mongodb/stream.js +206 -0
  57. package/dist/src/observe.js +1 -1
  58. package/dist/src/pack-fetch.d.ts +6 -1
  59. package/dist/src/pack-fetch.js +69 -14
  60. package/dist/src/pack-paths.d.ts +2 -1
  61. package/dist/src/pack-paths.js +6 -1
  62. package/dist/src/packRegistry.d.ts +10 -14
  63. package/dist/src/packRegistry.js +2 -13
  64. package/dist/src/protocol.d.ts +13 -0
  65. package/dist/src/protocol.js +13 -0
  66. package/dist/src/redis/engine.d.ts +10 -4
  67. package/dist/src/redis/engine.js +103 -32
  68. package/dist/src/redis/member-storage.js +20 -8
  69. package/dist/src/redis/service-times.d.ts +25 -0
  70. package/dist/src/redis/service-times.js +87 -0
  71. package/dist/src/redis/stream.d.ts +2 -1
  72. package/dist/src/redis/stream.js +109 -20
  73. package/dist/src/resource-blob.d.ts +4 -0
  74. package/dist/src/resource-blob.js +8 -1
  75. package/dist/src/runtime-state.d.ts +11 -0
  76. package/dist/src/runtime-state.js +13 -0
  77. package/dist/src/runtime.d.ts +7 -5
  78. package/dist/src/runtime.js +7 -4
  79. package/dist/src/serve-http.d.ts +2 -0
  80. package/dist/src/serve-http.js +73 -6
  81. package/dist/src/serve.d.ts +10 -0
  82. package/dist/src/serve.js +43 -3
  83. package/dist/src/sockets.d.ts +4 -1
  84. package/dist/src/sockets.js +59 -6
  85. package/dist/src/storage.d.ts +2 -1
  86. package/dist/src/storage.js +6 -3
  87. package/dist/src/vendor-call.js +58 -0
  88. package/dist/src/world-boot.d.ts +1 -0
  89. package/dist/src/world-boot.js +6 -0
  90. package/dist/src/world-clock.js +6 -2
  91. package/dist/src/world-store.d.ts +122 -7
  92. package/dist/src/world-store.js +959 -103
  93. package/dist/src/xml.d.ts +30 -0
  94. package/dist/src/xml.js +74 -0
  95. package/generated/pack-facts.json +204 -4
  96. package/inject.cjs +150 -86
  97. package/network-policy.cjs +11 -1
  98. package/network-policy.d.cts +1 -0
  99. package/pack-facts.d.cts +3 -0
  100. package/package.json +27 -1
  101. package/required-credential-transport.cjs +29 -0
  102. package/required-credential-transport.d.cts +3 -0
  103. package/src/actions.ts +78 -21
  104. package/src/ancestry.ts +46 -24
  105. package/src/blob-store.ts +59 -1
  106. package/src/changeset.ts +1 -1
  107. package/src/clickhouse/compression.ts +172 -0
  108. package/src/clickhouse/index.ts +2 -0
  109. package/src/derived-core.ts +356 -61
  110. package/src/derived-real.ts +80 -14
  111. package/src/derived.ts +9 -1
  112. package/src/executor.ts +17 -3
  113. package/src/fork.ts +1 -1
  114. package/src/git/lfs.ts +40 -9
  115. package/src/grpc-wire.ts +2 -1
  116. package/src/h2.ts +46 -5
  117. package/src/head.ts +15 -0
  118. package/src/history.ts +7 -7
  119. package/src/index.ts +7 -1
  120. package/src/live/ingest.ts +7 -3
  121. package/src/live/rtmp.ts +7 -5
  122. package/src/live/segmenter.ts +7 -1
  123. package/src/log.ts +147 -32
  124. package/src/machines.ts +5 -1
  125. package/src/managed-go-programs.ts +146 -0
  126. package/src/managed-original-inputs.ts +163 -0
  127. package/src/mongodb/client.ts +27 -0
  128. package/src/mongodb/index.ts +5 -0
  129. package/src/mongodb/stream.ts +223 -0
  130. package/src/observe.ts +1 -1
  131. package/src/pack-fetch.ts +66 -16
  132. package/src/pack-paths.ts +5 -1
  133. package/src/packRegistry.ts +3 -15
  134. package/src/protocol.ts +13 -0
  135. package/src/redis/engine.ts +100 -57
  136. package/src/redis/member-storage.ts +17 -9
  137. package/src/redis/service-times.ts +81 -0
  138. package/src/redis/stream.ts +66 -14
  139. package/src/resource-blob.ts +9 -1
  140. package/src/runtime-state.ts +14 -0
  141. package/src/runtime.ts +10 -3
  142. package/src/serve-http.ts +65 -6
  143. package/src/serve.ts +45 -3
  144. package/src/sockets.ts +39 -8
  145. package/src/storage.ts +5 -3
  146. package/src/vendor-call.ts +54 -0
  147. package/src/world-boot.ts +6 -0
  148. package/src/world-clock.ts +6 -2
  149. package/src/world-store.ts +768 -91
  150. package/src/xml.ts +71 -0
@@ -1,5 +1,5 @@
1
1
  import { parseExactJson, exactInteger, compareExactNumbers } from "./exact-json.js";
2
- import { aliasesFrom, parentEntries, readParentTreeMap, readTree } from "./log.js";
2
+ import { aliasesFrom, parentEntries, readParentTreeMap, readTree, wholeLog } from "./log.js";
3
3
  import { storedType } from "./derived-core.js";
4
4
  import { verifyEvent } from "./events.js";
5
5
  import { hashFieldValue } from "./hash.js";
@@ -210,13 +210,30 @@ function sentTo(m, path, params) {
210
210
  const fixedHeaders = (op) => Object.fromEntries(Object.entries(op.headers ?? {}).filter((e) => typeof e[1] === 'string'));
211
211
  /** The body as the operation's encoding sends it. */
212
212
  function encode(op, body) {
213
- if (body === undefined || op.bodyEncoding === 'none')
213
+ // a body the request carried where the spec declares none (Tinybird's Events API, NDJSON its client sends as text):
214
+ // the body as recorded, as the vendor read it
215
+ if (body === undefined)
214
216
  return {};
215
217
  if (op.bodyEncoding === 'form') {
218
+ // a form's nested values in the bracket form a form-encoded API reads (`scope[type]=account`, `items[0][price]=…`),
219
+ // the form the kernel's own parseBracketForm reads back; never a value's JSON text
216
220
  const form = new URLSearchParams();
221
+ const put = (key, v) => {
222
+ if (v === undefined || v === null)
223
+ return;
224
+ if (Array.isArray(v)) {
225
+ v.forEach((item, i) => put(`${key}[${i}]`, item));
226
+ return;
227
+ }
228
+ if (typeof v === 'object') {
229
+ for (const [k, x] of Object.entries(v))
230
+ put(`${key}[${k}]`, x);
231
+ return;
232
+ }
233
+ form.append(key, String(v));
234
+ };
217
235
  for (const [k, v] of Object.entries(object(body) ?? {}))
218
- if (v !== undefined && v !== null)
219
- form.set(k, typeof v === 'object' ? JSON.stringify(v) : String(v));
236
+ put(k, v);
220
237
  return { body: form.toString(), type: 'application/x-www-form-urlencoded' };
221
238
  }
222
239
  return { body: JSON.stringify(body), type: 'application/json' };
@@ -293,9 +310,14 @@ async function graphqlAnswers(m, surface, input, parsed) {
293
310
  /** The account a root is, before a perform sends a path naming the World's ("The account a root is"): the World's
294
311
  * account its own doors made, unadopted, adopts the one account the root's credential lists. Read once per deploy (an
295
312
  * adopted account is not asked again); a list the vendor refuses, or one naming several accounts, adopts none. */
313
+ /** The accounts a root's list already named as the World's own (the World's id is the vendor's), by service and root:
314
+ * nothing to adopt, so not asked again. */
315
+ const accountsMatched = new Map();
296
316
  async function adoptAccount(units, execute, ctx) {
297
317
  if (ctx.service === undefined)
298
318
  return;
319
+ const matched = accountsMatched.get(`${ctx.service}\u0000${ctx.root ?? ''}`) ?? new Set();
320
+ accountsMatched.set(`${ctx.service}\u0000${ctx.root ?? ''}`, matched);
299
321
  for (const u of units) {
300
322
  const m = u.manifest;
301
323
  const decl = m.account ? m.resources[m.account] : undefined;
@@ -305,13 +327,15 @@ async function adoptAccount(units, execute, ctx) {
305
327
  const local = readTree(ctx.service, ctx.root).filter((r) => r.type === type && r.deleted !== true && ctx.resolve(type, String(r.id)) === String(r.id));
306
328
  const scope = decl.refresh;
307
329
  const op = scope && 'list' in scope ? u.surface.operations.find((o) => o.id === scope.list) : undefined;
308
- if (!local.length || !op)
330
+ if (!local.length || !op || local.every((l) => matched.has(`${type}:${String(l.id)}`)))
309
331
  continue;
310
332
  const answer = await execute({ method: op.method.toUpperCase(), path: sentTo(m, marked(op, `${op.basePath ?? u.surface.basePath ?? ''}${op.path}`), {}), ...(u.lane ? { lane: u.lane } : {}), headers: fixedHeaders(op) });
311
333
  if (answer.status >= 400)
312
334
  continue;
313
335
  const items = itemsOf(op, parse(answer));
314
336
  const id = items.length === 1 ? storedIdOf(decl, items[0]) : undefined;
337
+ if (id !== undefined && local.some((l) => String(l.id) === id))
338
+ matched.add(`${type}:${id}`);
315
339
  if (id === undefined || local.some((l) => String(l.id) === id))
316
340
  continue;
317
341
  observeResources(ctx.service, [{ type, id, fields: items[0], adopts: String(local[0].id) }], ctx.root !== undefined ? { root: ctx.root } : {});
@@ -548,8 +572,28 @@ function scopeIdOf(scope, fields) {
548
572
  const value = field ? at(fields, field) : undefined;
549
573
  return value === undefined || value === null || value === '' ? undefined : String(value);
550
574
  }
575
+ /** A refresh larger than one window of the vendor's rate budget paces itself: a call the budget refuses before making
576
+ * it (RateBudgetError) waits the time the refusal names, at most two minutes a wait, and is made then. The budget is
577
+ * never exceeded and no vendor answer is retried; without it a refresh needing more than one window never finishes
578
+ * (Slack's, after a deploy spent the window: every rerun refused again from the start). */
579
+ function paced(execute) {
580
+ return async (request) => {
581
+ for (let waits = 0;; waits++) {
582
+ try {
583
+ return await execute(request);
584
+ }
585
+ catch (error) {
586
+ const wait = error.name === 'RateBudgetError' ? error.retryAfterMs : undefined;
587
+ if (wait === undefined || wait > 120_000 || waits >= 60)
588
+ throw error;
589
+ await new Promise((done) => setTimeout(done, wait + 250));
590
+ }
591
+ }
592
+ };
593
+ }
551
594
  function refreshOf(units, service) {
552
- return async (execute, opts) => {
595
+ return async (unpaced, opts) => {
596
+ const execute = paced(unpaced);
553
597
  const observed = [];
554
598
  const complete = [];
555
599
  // what the root's credential may not read (a token scoped to some of the account): left as the World holds it, said
@@ -557,6 +601,19 @@ function refreshOf(units, service) {
557
601
  let answered = 0;
558
602
  for (const { manifest: m, surface, lane } of units) {
559
603
  const onLane = lane ? { lane } : {};
604
+ const tenantBinding = {};
605
+ if (m.refreshIdentity) {
606
+ const identity = surface.operations.find(operation => operation.id === m.refreshIdentity.operation);
607
+ if (!m.tenant || !identity || identity.method !== 'get' || identity.path.includes('{'))
608
+ throw new Error(`${m.vendor}: refresh identity needs a parameterless GET and declared tenant field`);
609
+ const answer = await execute({ method: 'GET', path: `${identity.basePath ?? surface.basePath ?? ''}${identity.path}`, ...onLane, headers: fixedHeaders(identity) });
610
+ if (answer.status >= 400 || refusedIn(m, parse(answer)))
611
+ throw new Error(`${m.vendor}: root account identity refused; nothing folded`);
612
+ const tenant = at(parse(answer), m.refreshIdentity.field);
613
+ if (typeof tenant !== 'string' || !tenant)
614
+ throw new Error(`${m.vendor}: root account identity is absent; nothing folded`);
615
+ tenantBinding[m.tenant.field] = tenant;
616
+ }
560
617
  // parents before children, however deep
561
618
  const scoped = Object.entries(m.resources).filter(([, d]) => d.refresh && !('none' in d.refresh));
562
619
  const depth = (r, seen = new Set()) => { const p = m.resources[r]?.parent?.resource; return p && !seen.has(p) ? 1 + depth(p, seen.add(r)) : 0; };
@@ -565,6 +622,8 @@ function refreshOf(units, service) {
565
622
  const scope = decl.refresh;
566
623
  if ('none' in scope)
567
624
  continue;
625
+ if (m.refreshIdentity && (!('list' in scope) || 'graphql' in scope))
626
+ throw new Error(`${m.vendor}: account-bound refresh requires REST list scopes`);
568
627
  if ('graphql' in scope && scope.graphql) {
569
628
  await refreshGraphql(m, resource, decl, scope, execute, opts.scope, observed, complete);
570
629
  answered += 1;
@@ -582,6 +641,17 @@ function refreshOf(units, service) {
582
641
  if ('get' in scope && scope.known) {
583
642
  const aliases = aliasesFrom(parentEntries(service, opts.root));
584
643
  const held = [...readParentTreeMap(service, opts.root)].filter(([, r]) => r.type === type && r.fields.deleted !== true);
644
+ // and each a door made (a person's act on the vendor's site: a group they add the bot to), which the vendor holds
645
+ // as the World does though no deploy landed it
646
+ const rooted = new Set(held.map(([k]) => k));
647
+ const doorMade = new Set(wholeLog(service, opts.root).filter((e) => e.subject.type === type && e.input?.door !== undefined).map((e) => String(e.subject.id)));
648
+ for (const r of readTree(service, opts.root)) {
649
+ const key = `${type}:${String(r.id)}`;
650
+ if (r.type !== type || r.deleted === true || !doorMade.has(String(r.id)) || held.some(([k]) => k === key))
651
+ continue;
652
+ const { type: _t, id, ...fields } = r;
653
+ held.push([key, { type, id: String(id), fields }]);
654
+ }
585
655
  const read = [];
586
656
  // a 403 leaves the type as the World holds it, never complete
587
657
  let refused = false;
@@ -606,6 +676,9 @@ function refreshOf(units, service) {
606
676
  const target = marked(op, path);
607
677
  const sent = asked.length ? `${target}${target.includes('?') ? '&' : '?'}${new URLSearchParams(asked)}` : target;
608
678
  const answer = await execute({ method: op.method.toUpperCase(), path: sentTo(m, sent, subject), ...onLane, headers: fixedHeaders(op) });
679
+ // a door's subject the vendor does not show the root (another bot's private chat) stays as the World holds it
680
+ if (!rooted.has(key) && answer.status >= 400 && answer.status !== 429)
681
+ continue;
609
682
  if (answer.status === 401)
610
683
  throw new Error(`401: the vendor refused the root's credential (${op.method.toUpperCase()} ${path}); nothing folded`);
611
684
  if (answer.status === 403) {
@@ -617,7 +690,8 @@ function refreshOf(units, service) {
617
690
  throw new Error(`429: rate limited${answer.headers['retry-after'] ? `, retry after ${answer.headers['retry-after']}s` : ''}`);
618
691
  read.push(String(subject.id));
619
692
  answered += 1;
620
- if (answer.status === 404)
693
+ // the vendor's answer for a subject it no longer has: a 404, or the scope's `gone` (Telegram's 400 `chat not found`)
694
+ if (answer.status === 404 || (scope.gone && answer.status === scope.gone.status && refusalWords(m, parse(answer)).includes(scope.gone.says)))
621
695
  continue;
622
696
  if (answer.status >= 400 || refusedIn(m, parse(answer)))
623
697
  throw new Error(`${m.vendor}: ${resource} refresh ${op.id} (${op.method.toUpperCase()} ${op.path}): ${answer.status}: ${refusalWords(m, parse(answer))}`);
@@ -674,9 +748,12 @@ function refreshOf(units, service) {
674
748
  : decl.parent?.params?.length && under ? Object.fromEntries(Object.entries(under.params).filter(([n]) => !op.path.includes(`{${n}}`) && (op.query ?? []).some((x) => x.name === n))) : {};
675
749
  // a list asked by POST carries its parent in its body too, unless the operation reads it from its query
676
750
  const parentInQuery = !rpc || (op.query ?? []).some((x) => x.name === decl.parent?.param);
677
- const q = new URLSearchParams(rpc ? (parentInQuery ? byQuery : {}) : { ...page, ...byQuery });
751
+ // a parameter the operation declares in its query, and not its body, goes in the query of a list asked by POST
752
+ // too (TikTok's video list: `fields` in the query, its cursor in the body)
753
+ const inQuery = (n) => (op.query ?? []).some((x) => x.name === n) && !(op.body ?? []).some((b) => b.name === n);
754
+ const q = new URLSearchParams(rpc ? { ...Object.fromEntries(Object.entries(page).filter(([k]) => inQuery(k))), ...(parentInQuery ? byQuery : {}) } : { ...page, ...byQuery });
678
755
  // Fixed parameters and cursors take the operation's body types, not their manifest string encoding.
679
- const typedPage = Object.fromEntries(Object.entries(page).map(([k, v]) => {
756
+ const typedPage = Object.fromEntries(Object.entries(page).filter(([k]) => !rpc || !inQuery(k)).map(([k, v]) => {
680
757
  const type = (op.body ?? []).find((b) => b.name === k)?.type;
681
758
  const value = type === 'integer' ? exactInteger(v) : type === 'number' ? Number(v)
682
759
  : type === 'boolean' && (v === 'true' || v === 'false') ? v === 'true' : v;
@@ -739,9 +816,12 @@ function refreshOf(units, service) {
739
816
  const detail = surface.operations.find((o) => o.id === scope.detail);
740
817
  if (!detail)
741
818
  throw new Error(`${m.vendor}: ${resource}'s refresh detail names ${scope.detail}, no operation of the surface`);
742
- const id = mapId ?? storedIdOf(decl, item);
819
+ const id = mapId ?? storedIdOf(decl, { ...under?.params, ...item });
820
+ // the item's own id names it in the detail's path where its stored id is a composite key (a Cloudflare
821
+ // script stored `<account>/<name>` is read at `/scripts/{script_name}` by its name)
822
+ const own = item[decl.idAs ?? 'id'];
743
823
  const path = `${detail.basePath ?? surface.basePath ?? ''}${detail.path}`.replace(/\{([^}]+)\}/g, (_, n) => {
744
- const value = under?.params[n] ?? item[n] ?? id;
824
+ const value = under?.params[n] ?? item[n] ?? (decl.key && (typeof own === 'string' || typeof own === 'number') ? own : id);
745
825
  if (value === undefined)
746
826
  throw new Error(`${m.vendor}: ${resource}'s refresh detail has no value for ${n}`);
747
827
  return encodeURIComponent(String(value));
@@ -766,8 +846,10 @@ function refreshOf(units, service) {
766
846
  // twin_local_ports) is the answering World's, never the vendor's, and is not folded in
767
847
  const local = m.worldFields?.[resource] ?? {};
768
848
  const vendorShape = Object.fromEntries(Object.entries(full).filter(([k]) => !(k in local)));
769
- const fields = { ...vendorShape, ...kept, ...(decl.parent && parent !== undefined ? { [decl.parent.field]: parent } : {}) };
770
- const id = mapId ?? scopeIdOf(scope, fields) ?? storedIdOf(decl, fields);
849
+ const fields = { ...vendorShape, ...(!decl.shared ? tenantBinding : {}), ...kept, ...(decl.parent && parent !== undefined ? { [decl.parent.field]: parent } : {}) };
850
+ // a key part the item does not carry is the path parameter its parent was listed under (Cloudflare's secret,
851
+ // `{account_id}/{script_name}::{name}`, listed as its name and type alone)
852
+ const id = mapId ?? scopeIdOf(scope, fields) ?? storedIdOf(decl, { ...under?.params, ...fields });
771
853
  if (id === undefined)
772
854
  continue;
773
855
  observed.push({ type, id, fields: fields });
@@ -856,7 +938,7 @@ function refreshOf(units, service) {
856
938
  // scoped to one resource (`RootConfig.scope`, GitHub's `repos/o/r`) lists only what is under it, so a type with no
857
939
  // parent is never the account's whole list there
858
940
  if (whole && !decl.parent && !opts.scope)
859
- complete.push(type);
941
+ complete.push(m.refreshIdentity && m.tenant && !decl.shared ? { type, field: m.tenant.field, within: [String(tenantBinding[m.tenant.field])] } : type);
860
942
  else if (whole && parents.length && decl.parent)
861
943
  complete.push({ type, field: decl.parent.field, within: parents.map((p) => p.value) });
862
944
  }
@@ -971,7 +1053,7 @@ async function refreshGraphql(m, resource, decl, scope, execute, rootScope, obse
971
1053
  const items = listed(value).map(object).filter((x) => x !== undefined);
972
1054
  for (const item of items) {
973
1055
  const fields = { ...item, ...(decl.parent && under ? { [decl.parent.field]: under.value } : {}) };
974
- const id = storedIdOf(decl, fields);
1056
+ const id = storedIdOf(decl, { ...under?.params, ...fields });
975
1057
  if (id !== undefined)
976
1058
  observed.push({ type, id, fields: fields });
977
1059
  }
@@ -19,6 +19,8 @@ export type DerivedOperation = {
19
19
  emptySuccess?: boolean;
20
20
  /** a form-encoded operation's number and boolean fields, by bracket path (`items.[].quantity`, `metadata.*`) */
21
21
  scalars?: Readonly<Record<string, string | undefined>>;
22
+ /** a form-encoded operation's string fields that take only listed values, by the same bracket path */
23
+ enums?: Readonly<Record<string, ReadonlyArray<string>>>;
22
24
  /** what the success answer is: a resource, a list of them, and the envelope key holding it */
23
25
  answers?: {
24
26
  resource: string;
@@ -66,6 +68,9 @@ export type DerivedCall = {
66
68
  request: Request;
67
69
  operation: DerivedOperation;
68
70
  params: Record<string, string>;
71
+ /** a World screen's call, which no perform sends again: a body in a media type the core does not read (anything
72
+ * but JSON, a form, multipart or text) is the handler's alone, left unread and unrecorded, so an upload streams */
73
+ streamed?: true;
69
74
  };
70
75
  export declare function bodyTextOf(request: Request): Promise<string>;
71
76
  /** The body's text, as JSON, or undefined when it is not JSON; the parse is kept for the first taker. */
@@ -1,5 +1,6 @@
1
1
  import { parseExactJson } from "./exact-json.js";
2
2
  import { runWithRequestTrace } from "./trace-context.js";
3
+ import { redisServiceTimes } from "./redis/service-times.js";
3
4
  /**
4
5
  * A request's body as text, read once per request: the dispatch reads it and tags the request with it, and every clone
5
6
  * made from a tagged request carries the tag, so the malformed-JSON guard, the idempotency signature, the handler's
@@ -169,8 +170,10 @@ export function createDerivedFetch(options) {
169
170
  for (const observer of dispatchObservers ?? [])
170
171
  observer({ surface: options.surface, operation: matched.operation, owner: handlers[matched.operation.id] ? 'handler' : options.core?.owns(matched.operation) ? 'core' : 'gap' });
171
172
  // the body read once, as text, for everything the dispatch does with it (bodyTextOf)
173
+ redisServiceTimes.lap('6a around entered, guards and scenario passed, operation matched again');
172
174
  if (request.method !== 'GET' && request.method !== 'HEAD' && request.body && !(request.headers.get('content-type') ?? '').includes('multipart'))
173
175
  tagBody(request, await request.clone().text());
176
+ redisServiceTimes.lap('6b the dispatch read the body as text');
174
177
  // the path prefix's named groups (pack-fetch.ts, `pathPrefix`) are the call's parameters too
175
178
  const prefixed = request.headers.get(PREFIX_PARAMS_HEADER);
176
179
  const call = { request, operation: matched.operation, params: prefixed ? { ...parseExactJson(prefixed), ...matched.params } : matched.params };
@@ -120,6 +120,10 @@ export type TwinAuthStrategy =
120
120
  answer: string;
121
121
  credential: string;
122
122
  };
123
+ /** The answer's field giving the token's lifetime in seconds (a dotted path; OAuth's `expires_in`): a token held past
124
+ * it is exchanged again before the next call. For a vendor that answers an expired token with no `retryOn` status
125
+ * (TikTok's API for Business answers HTTP 200 with a code), and so a long-lived process never sends a dead token. */
126
+ expiresIn?: string;
123
127
  };
124
128
  /** Throws with the precise refusal; returns the parsed URL when the origin is usable. */
125
129
  export declare function validateRemoteOrigin(origin: string, opts?: {
@@ -132,10 +136,13 @@ export declare function validateRemoteOrigin(origin: string, opts?: {
132
136
  * refused, as before. */
133
137
  /** Where a credential is kept (the head's and refresh's root: head.ts rootCustody): `key` names the credential stably
134
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
+ /** `unsealed`: where a rotation the seal failed to keep is recorded durably (the field and the cause, never a value), since
140
+ * the root cannot renew once the process holding the new secret ends; a later successful seal clears it. */
135
141
  export type CredentialCustody = {
136
142
  key: string;
137
143
  open: () => Promise<CredentialPayload>;
138
144
  seal: (credential: CredentialPayload) => Promise<void> | void;
145
+ unsealed?: (field: string, cause: string) => void;
139
146
  };
140
147
  /** `vendor`: the vendor whose rate budget every call is charged to (D8): the call is priced and reserved before it is
141
148
  * made, refused when the budget is spent, and settled with the vendor's answer (its back-off honoured). */
@@ -365,6 +365,7 @@ function exchangeExecute(origin, sealed, auth, vendorHosts, custody) {
365
365
  return held;
366
366
  };
367
367
  const key = custody ? `${origin.href}\n${custody.key}` : `${origin.href}\n${JSON.stringify(sealed)}`;
368
+ let expiresAt;
368
369
  const exchange = async () => {
369
370
  // a rotating secret is read as kept NOW (another executor or process may have rotated it since this one was
370
371
  // built), or as this process holds it when its seal failed and the file still holds the spent value. Two
@@ -409,6 +410,11 @@ function exchangeExecute(origin, sealed, auth, vendorHosts, custody) {
409
410
  const code = parsed && typeof parsed === 'object' ? parsed.error : undefined;
410
411
  throw new Error(`remote execute: the token exchange answered no \`${auth.token}\`${typeof code === 'string' && code !== '' ? ` (${code.slice(0, 120)})` : ''}`);
411
412
  }
413
+ if (auth.expiresIn) {
414
+ const seconds = Number(auth.expiresIn.split('.').reduce((at, k) => (at && typeof at === 'object' ? at[k] : undefined), parsed));
415
+ // a minute short of the stated lifetime, so a call in flight never carries a token that dies on the way
416
+ expiresAt = Number.isFinite(seconds) && seconds > 0 ? Date.now() + Math.max(0, seconds - 60) * 1000 : undefined;
417
+ }
412
418
  if (auth.rotate) {
413
419
  const next = auth.rotate.answer.split('.').reduce((at, k) => (at && typeof at === 'object' ? at[k] : undefined), parsed);
414
420
  if (typeof next === 'string' && next !== '' && next !== credential[auth.rotate.credential]) {
@@ -423,6 +429,10 @@ function exchangeExecute(origin, sealed, auth, vendorHosts, custody) {
423
429
  }
424
430
  catch (error) {
425
431
  unsealedRotations.set(key, { replaced, credential });
432
+ try {
433
+ custody.unsealed?.(auth.rotate.credential, error instanceof Error ? error.message : String(error));
434
+ }
435
+ catch { /* the stderr line below still says it */ }
426
436
  console.error(`remote execute: the vendor rotated \`${auth.rotate.credential}\` and sealing the new one failed (${error instanceof Error ? error.message : String(error)}); this process holds it until a seal succeeds`);
427
437
  }
428
438
  }
@@ -432,12 +442,16 @@ function exchangeExecute(origin, sealed, auth, vendorHosts, custody) {
432
442
  };
433
443
  const current = () => {
434
444
  let held = exchangedTokens.get(key);
445
+ if (held?.expiresAt !== undefined && Date.now() >= held.expiresAt) {
446
+ exchangedTokens.delete(key);
447
+ held = undefined;
448
+ }
435
449
  if (!held) {
436
450
  const token = exchange();
437
451
  held = { token };
438
452
  exchangedTokens.set(key, held);
439
453
  const entry = held;
440
- token.catch(() => { if (exchangedTokens.get(key) === entry)
454
+ token.then(() => { entry.expiresAt = expiresAt; }, () => { if (exchangedTokens.get(key) === entry)
441
455
  exchangedTokens.delete(key); });
442
456
  }
443
457
  return held;
package/dist/src/fork.js CHANGED
@@ -56,7 +56,7 @@ export function forkTwin(opts) {
56
56
  };
57
57
  getActiveWorldStore().write(forkMetaPath(service, toRoot), `${JSON.stringify(meta, null, 2)}\n`);
58
58
  return meta;
59
- });
59
+ }, { operation: 'pointer publication' });
60
60
  }
61
61
  export function readForkMeta(service, root) {
62
62
  const path = forkMetaPath(service, root);
@@ -4,9 +4,17 @@ export type LfsObject = {
4
4
  size: number | string;
5
5
  };
6
6
  /** Where a pack keeps its objects: by oid. */
7
+ /** Where a server keeps its LFS objects: an object's size when it holds one (the batch API answers from it, never
8
+ * loading the object), and the store an upload's verified bytes go to. */
7
9
  export type LfsStore = {
8
- get(oid: string): Promise<Uint8Array | null>;
10
+ size(oid: string): Promise<number | null>;
9
11
  put(oid: string, bytes: Uint8Array): Promise<void>;
12
+ /** an upload's body written as it arrives and kept only when `accept` holds for its size and SHA-256 (`ctx.blobs.putStream`):
13
+ * a store with it never holds an object whole */
14
+ putStream?(oid: string, chunks: AsyncIterable<Uint8Array>, accept: (written: {
15
+ size: number;
16
+ sha256: string;
17
+ }) => boolean): Promise<boolean>;
10
18
  };
11
19
  /** A pointer file's object (`version https://git-lfs.github.com/spec/v1`, `oid sha256:<hex>`, `size <n>`), or null
12
20
  * for any other file. */
@@ -24,5 +32,7 @@ export declare function lfsBatch(body: unknown, opts: {
24
32
  header?: Record<string, string>;
25
33
  }): Promise<Response>;
26
34
  /** A basic transfer's upload (`PUT` of the object's bytes): kept when they hash to the oid and have the declared size
27
- * (the size is the request's Content-Length when the caller has no other), else refused and nothing kept. */
28
- export declare function lfsUpload(bytes: Uint8Array, o: LfsObject, store: LfsStore): Promise<Response>;
35
+ * (the size is the request's Content-Length when the caller has no other), else refused and nothing kept. A body
36
+ * stream goes to the store's `putStream` as it arrives, read no further than the declared size; bytes, or a store
37
+ * without one, are checked whole. */
38
+ export declare function lfsUpload(body: Uint8Array | ReadableStream<Uint8Array> | null, o: LfsObject, store: LfsStore): Promise<Response>;
@@ -46,22 +46,54 @@ export async function lfsBatch(body, opts) {
46
46
  objects.push({ ...o, error: { code: 422, message: 'Invalid object' } });
47
47
  continue;
48
48
  }
49
- const held = await opts.store.get(o.oid);
49
+ const held = await opts.store.size(o.oid);
50
50
  if (operation === 'upload') {
51
- objects.push(held ? { ...o, authenticated: true } : { ...o, authenticated: true, actions: { upload: { href: opts.href('upload', o), ...(opts.header ? { header: opts.header } : {}), expires_in: 3600 } } });
51
+ objects.push(held !== null ? { ...o, authenticated: true } : { ...o, authenticated: true, actions: { upload: { href: opts.href('upload', o), ...(opts.header ? { header: opts.header } : {}), expires_in: 3600 } } });
52
52
  }
53
53
  else {
54
54
  // source: https://github.com/git-lfs/git-lfs/blob/main/docs/api/batch.md "The object does not exist on the server."
55
- objects.push(held ? { oid: o.oid, size: held.length, authenticated: true, actions: { download: { href: opts.href('download', o), ...(opts.header ? { header: opts.header } : {}), expires_in: 3600 } } } : { ...o, error: { code: 404, message: 'Object does not exist' } });
55
+ objects.push(held !== null ? { oid: o.oid, size: held, authenticated: true, actions: { download: { href: opts.href('download', o), ...(opts.header ? { header: opts.header } : {}), expires_in: 3600 } } } : { ...o, error: { code: 404, message: 'Object does not exist' } });
56
56
  }
57
57
  }
58
58
  return lfsJson(200, { transfer: 'basic', objects, hash_algo: 'sha256' });
59
59
  }
60
60
  /** A basic transfer's upload (`PUT` of the object's bytes): kept when they hash to the oid and have the declared size
61
- * (the size is the request's Content-Length when the caller has no other), else refused and nothing kept. */
62
- export async function lfsUpload(bytes, o, store) {
63
- if (bytes.length !== o.size || (await lfsOid(bytes)) !== o.oid)
64
- return lfsJson(422, { message: `the object's bytes do not match ${o.oid} (${o.size} bytes)` });
65
- await store.put(o.oid, bytes);
66
- return new Response(null, { status: 200 });
61
+ * (the size is the request's Content-Length when the caller has no other), else refused and nothing kept. A body
62
+ * stream goes to the store's `putStream` as it arrives, read no further than the declared size; bytes, or a store
63
+ * without one, are checked whole. */
64
+ export async function lfsUpload(body, o, store) {
65
+ const refused = () => lfsJson(422, { message: `the object's bytes do not match ${o.oid} (${o.size} bytes)` });
66
+ if (body instanceof Uint8Array || body === null || !store.putStream) {
67
+ const bytes = body instanceof Uint8Array ? body : new Uint8Array(body === null ? new ArrayBuffer(0) : await new Response(body).arrayBuffer());
68
+ if (bytes.length !== o.size || (await lfsOid(bytes)) !== o.oid)
69
+ return refused();
70
+ await store.put(o.oid, bytes);
71
+ return new Response(null, { status: 200 });
72
+ }
73
+ const declared = Number(o.size);
74
+ let over = false;
75
+ async function* upTo(stream) {
76
+ const reader = stream.getReader();
77
+ let read = 0;
78
+ try {
79
+ for (;;) {
80
+ const { done, value } = await reader.read();
81
+ if (done)
82
+ return;
83
+ read += value.byteLength;
84
+ // a body longer than declared is refused without reading the rest of it
85
+ if (read > declared) {
86
+ over = true;
87
+ await reader.cancel();
88
+ return;
89
+ }
90
+ yield value;
91
+ }
92
+ }
93
+ finally {
94
+ reader.releaseLock();
95
+ }
96
+ }
97
+ const kept = await store.putStream(o.oid, upTo(body), (w) => !over && w.size === declared && w.sha256 === o.oid);
98
+ return kept ? new Response(null, { status: 200 }) : refused();
67
99
  }
@@ -7,6 +7,7 @@ import { parseExactJson } from "./exact-json.js";
7
7
  // and what they answer is the pack's. Importing it does nothing.
8
8
  import { acceptH2Connection } from "./h2.js";
9
9
  import { nodeBuiltin, WORLD_BOOT_PATH } from "./serve-http.js";
10
+ import { worldBootAnswer } from "./world-boot.js";
10
11
  import { decodeMessage, encodeMessage } from "./protobuf.js";
11
12
  /** gRPC's status codes (https://grpc.github.io/grpc/core/md_doc_statuscodes.html). */
12
13
  export const GrpcStatus = {
@@ -162,7 +163,7 @@ export function grpcOverFetch(fetch) {
162
163
  export async function serveGrpc(options) {
163
164
  const net = nodeBuiltin('node:net');
164
165
  const worldBoot = globalThis.process?.env?.VOLTER_WORLD_BOOT_ID;
165
- const fetch = worldBoot ? (request) => (new URL(request.url).pathname === WORLD_BOOT_PATH ? Promise.resolve(Response.json({ boot: worldBoot })) : options.fetch(request)) : options.fetch;
166
+ const fetch = worldBoot ? (request) => (new URL(request.url).pathname === WORLD_BOOT_PATH ? Promise.resolve(worldBootAnswer(worldBoot)) : options.fetch(request)) : options.fetch;
166
167
  const handler = grpcStreamHandler(options.schema, grpcOverFetch(fetch));
167
168
  const server = net.createServer((socket) => { acceptH2Connection(socket, handler, fetch); });
168
169
  await new Promise((resolve, reject) => { server.once('error', reject); server.listen(options.port ?? 0, '127.0.0.1', () => resolve()); });
package/dist/src/h2.js CHANGED
@@ -10,6 +10,43 @@ const MAX_CONCURRENT_STREAMS = 1000;
10
10
  /** a request body larger than this resets its stream: a memory bound, not Temporal's message limit (the
11
11
  * SDKs' gRPC default is 4 MiB; this server does not enforce that one) */
12
12
  const MAX_REQUEST_BYTES = 64 * 1024 * 1024;
13
+ /** A chunked body (RFC 9112 §7.1) from `from`: its bytes once the last chunk and the trailer section have arrived,
14
+ * 'incomplete' before, 'bad' for a malformed size line or one past MAX_REQUEST_BYTES. Extensions and trailers are
15
+ * read past and not kept. */
16
+ function chunkedBody(buffer, from) {
17
+ const parts = [];
18
+ let at = from, total = 0;
19
+ for (;;) {
20
+ const line = buffer.indexOf('\r\n', at);
21
+ if (line < 0)
22
+ return buffer.length - at > 4096 ? 'bad' : 'incomplete';
23
+ const size = /^([0-9A-Fa-f]{1,8})[ \t]*(?:;.*)?$/su.exec(buffer.subarray(at, line).toString('latin1'));
24
+ if (!size)
25
+ return 'bad';
26
+ const length = parseInt(size[1], 16);
27
+ at = line + 2;
28
+ if (length === 0)
29
+ break;
30
+ total += length;
31
+ if (total > MAX_REQUEST_BYTES)
32
+ return 'bad';
33
+ if (buffer.length < at + length + 2)
34
+ return 'incomplete';
35
+ if (buffer[at + length] !== 0x0d || buffer[at + length + 1] !== 0x0a)
36
+ return 'bad';
37
+ parts.push(buffer.subarray(at, at + length));
38
+ at += length + 2;
39
+ }
40
+ // The trailer section: field lines up to an empty line.
41
+ for (;;) {
42
+ const line = buffer.indexOf('\r\n', at);
43
+ if (line < 0)
44
+ return buffer.length - at > 64 * 1024 ? 'bad' : 'incomplete';
45
+ if (line === at)
46
+ return Buffer.concat(parts);
47
+ at = line + 2;
48
+ }
49
+ }
13
50
  /** a header block (HEADERS + CONTINUATION) larger than this closes the connection */
14
51
  const MAX_HEADER_BLOCK = 256 * 1024;
15
52
  // ---- HPACK ------------------------------------------------------------------------------------
@@ -248,18 +285,34 @@ class H2Connection {
248
285
  this.respondHttp1(new Response('bad header', { status: 400 }));
249
286
  return;
250
287
  }
251
- if (headers.get('transfer-encoding')) {
252
- this.respondHttp1(new Response('chunked request bodies are not accepted', { status: 411 }));
253
- return;
288
+ // A request with a Transfer-Encoding has a body framed by it, and its Content-Length is not read (RFC 9112 §6.3):
289
+ // undici sends a streamed body (a World's fetch forwarding a request's own body, duplex 'half') chunked.
290
+ const coding = headers.get('transfer-encoding');
291
+ let body;
292
+ if (coding !== null) {
293
+ if (coding.split(',').map((each) => each.trim().toLowerCase()).at(-1) !== 'chunked') {
294
+ this.respondHttp1(new Response('a transfer coding other than chunked is not read', { status: 400 }));
295
+ return;
296
+ }
297
+ const read = chunkedBody(this.buffer, end + 4);
298
+ if (read === 'bad') {
299
+ this.respondHttp1(new Response('bad chunked body', { status: 400 }));
300
+ return;
301
+ }
302
+ if (read === 'incomplete')
303
+ return;
304
+ body = read;
254
305
  }
255
- const length = Number(headers.get('content-length') ?? 0);
256
- if (!Number.isFinite(length) || length < 0 || length > MAX_REQUEST_BYTES) {
257
- this.respondHttp1(new Response('bad content-length', { status: 400 }));
258
- return;
306
+ else {
307
+ const length = Number(headers.get('content-length') ?? 0);
308
+ if (!Number.isFinite(length) || length < 0 || length > MAX_REQUEST_BYTES) {
309
+ this.respondHttp1(new Response('bad content-length', { status: 400 }));
310
+ return;
311
+ }
312
+ if (this.buffer.length < end + 4 + length)
313
+ return;
314
+ body = this.buffer.subarray(end + 4, end + 4 + length);
259
315
  }
260
- if (this.buffer.length < end + 4 + length)
261
- return;
262
- const body = this.buffer.subarray(end + 4, end + 4 + length);
263
316
  this.buffer = Buffer.alloc(0);
264
317
  this.socket.removeAllListeners('data');
265
318
  let request;
@@ -72,6 +72,8 @@ export declare function openRootCredential(root: BoundRoot, vendor?: string): Pr
72
72
  * fingerprint, and a new one per grant would start the ledger over), and rotatedAt says when. `source` is the record
73
73
  * the rotation began from: if another credential was set meanwhile, the rotation is dropped, never written over it. */
74
74
  export declare function resealRootCredential(root: BoundRoot, payload: CredentialPayload, vendor?: string, source?: string | null): Promise<void>;
75
+ /** Beside a root's credential, the record of a rotation its seal failed to keep (`rootCustody`'s `unsealed`). */
76
+ export declare function unsealedMarkerOf(root: BoundRoot): string;
75
77
  /** The root's custody for an exchange that rotates: the credential as sealed now, and the seal of a rotated one, which
76
78
  * lands only over the record that was opened. */
77
79
  export declare function rootCustody(root: BoundRoot, vendor?: string): CredentialCustody;
@@ -174,6 +176,9 @@ export declare function performEntries(opts: {
174
176
  * `auto` is performed now. Returns the vendor's id when it minted one, so the answer carries it.
175
177
  * Raises `RefusedWriteError` / `VendorWriteError` for the pack to answer in the vendor's shape.
176
178
  */
179
+ /** Whether `performAtHead` would perform an entry now (a twin whose root says `auto`, outside a pack fetch's deferred
180
+ * group): a caller appending several writes at once appends them one at a time then, each performed before the next. */
181
+ export declare function performsAtHeadNow(service: string, root?: string): boolean;
177
182
  export declare function performAtHead(service: string, action: Pick<TwinAction, 'id'>, root?: string): Promise<{
178
183
  performed: boolean;
179
184
  externalId?: string;
package/dist/src/head.js CHANGED
@@ -136,7 +136,10 @@ export async function resealRootCredential(root, payload, vendor = vendorOf(root
136
136
  if (store.read(root.credential) !== held)
137
137
  throw new Error(`the ${vendor} credential was set again while it rotated: the rotation is dropped`);
138
138
  store.write(root.credential, `${JSON.stringify({ ...sealed, ...(before ? { fingerprint: before.fingerprint } : {}), rotatedAt: now }, null, 2)}\n`, { secret: true });
139
+ store.remove(unsealedMarkerOf(root));
139
140
  }
141
+ /** Beside a root's credential, the record of a rotation its seal failed to keep (`rootCustody`'s `unsealed`). */
142
+ export function unsealedMarkerOf(root) { return root.credential.replace(/\.json$/, '.unsealed.json'); }
140
143
  /** The root's custody for an exchange that rotates: the credential as sealed now, and the seal of a rotated one, which
141
144
  * lands only over the record that was opened. */
142
145
  export function rootCustody(root, vendor = vendorOf(root)) {
@@ -155,6 +158,9 @@ export function rootCustody(root, vendor = vendorOf(root)) {
155
158
  return opened.payload;
156
159
  },
157
160
  seal: (credential) => resealRootCredential(root, credential, vendor, source),
161
+ // the vendor already spent the sealed secret: until a seal lands, the root renews only inside the process holding the
162
+ // new one, so the World keeps a record a person or `doctor` can read after that process ends
163
+ unsealed: (field, cause) => store.write(unsealedMarkerOf(root), `${JSON.stringify({ vendor, field, at: new Date().toISOString(), cause: cause.slice(0, 300) }, null, 2)}\n`),
158
164
  };
159
165
  }
160
166
  /** The vendor a root belongs to: its credential is sealed under the vendor's name. */
@@ -464,6 +470,15 @@ async function performNow(opts) {
464
470
  * `auto` is performed now. Returns the vendor's id when it minted one, so the answer carries it.
465
471
  * Raises `RefusedWriteError` / `VendorWriteError` for the pack to answer in the vendor's shape.
466
472
  */
473
+ /** Whether `performAtHead` would perform an entry now (a twin whose root says `auto`, outside a pack fetch's deferred
474
+ * group): a caller appending several writes at once appends them one at a time then, each performed before the next. */
475
+ export function performsAtHeadNow(service, root) {
476
+ const request = currentCorrelationId();
477
+ if (request !== undefined && deferred.has(request))
478
+ return false;
479
+ const head = headOf(service, root);
480
+ return head.kind === 'real' && head.deploy === 'auto';
481
+ }
467
482
  export async function performAtHead(service, action, root) {
468
483
  // a pack fetch performs its request's entries as one group after the handler (performRequestGroup)
469
484
  const request = currentCorrelationId();
@@ -51,7 +51,7 @@ export declare function saveHistoryView(directory: string, view: HistoryView): s
51
51
  * (and the view document itself) and each named directory's ancestry record (its generation). A read cache keyed by this sees an
52
52
  * ancestor's retained history truncated, replaced or its directory re-created, which the full read
53
53
  * refuses (`historyEntries`), instead of serving a tree folded from the history it replaced. */
54
- export declare function inheritedHistoryKey(service: string, root?: string): string;
54
+ export declare function inheritedHistoryKey(service: string, root?: string, meta?: string | null): string;
55
55
  export declare function inheritedHistory(service: string, root?: string): HistoryView | undefined;
56
56
  /** Capture under the directory's history lock, as appends and complete observation batches are made. */
57
57
  export declare function captureHistory(service: string, root?: string): {