@volter/world-core 3.0.38 → 3.0.39

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 (179) hide show
  1. package/app-route.cjs +26 -25
  2. package/app-route.d.cts +1 -0
  3. package/dist/app-route.cjs +26 -25
  4. package/dist/app-route.d.cts +1 -0
  5. package/dist/generated/pack-facts.json +409 -352
  6. package/dist/inject.cjs +474 -95
  7. package/dist/network-policy.cjs +108 -13
  8. package/dist/network-policy.d.cts +8 -3
  9. package/dist/src/actions.js +2 -1
  10. package/dist/src/anthropic-wire.d.ts +2 -0
  11. package/dist/src/anthropic-wire.js +5 -0
  12. package/dist/src/attestation.d.ts +6 -0
  13. package/dist/src/attestation.js +27 -0
  14. package/dist/src/bytes.d.ts +3 -0
  15. package/dist/src/bytes.js +17 -0
  16. package/dist/src/changeset.js +2 -1
  17. package/dist/src/clickhouse/sql.js +73 -14
  18. package/dist/src/derived-core.d.ts +210 -32
  19. package/dist/src/derived-core.js +510 -244
  20. package/dist/src/derived-real.d.ts +1 -1
  21. package/dist/src/derived-real.js +704 -93
  22. package/dist/src/derived.d.ts +18 -2
  23. package/dist/src/derived.js +21 -2
  24. package/dist/src/events.d.ts +28 -45
  25. package/dist/src/events.js +40 -58
  26. package/dist/src/exact-json.d.ts +10 -0
  27. package/dist/src/exact-json.js +98 -0
  28. package/dist/src/executor.d.ts +17 -0
  29. package/dist/src/executor.js +91 -30
  30. package/dist/src/file-response.js +1 -1
  31. package/dist/src/form.d.ts +8 -0
  32. package/dist/src/form.js +76 -0
  33. package/dist/src/git/format.d.ts +16 -0
  34. package/dist/src/git/format.js +110 -0
  35. package/dist/src/git/index.d.ts +1 -0
  36. package/dist/src/git/index.js +1 -0
  37. package/dist/src/git/lfs.d.ts +1 -1
  38. package/dist/src/git/lfs.js +4 -3
  39. package/dist/src/graphql-wire.d.ts +6 -0
  40. package/dist/src/graphql-wire.js +6 -3
  41. package/dist/src/grpc-wire.js +2 -1
  42. package/dist/src/head.d.ts +1 -0
  43. package/dist/src/head.js +15 -4
  44. package/dist/src/held-reads.d.ts +6 -0
  45. package/dist/src/held-reads.js +60 -0
  46. package/dist/src/history.js +5 -4
  47. package/dist/src/host-port.d.ts +5 -0
  48. package/dist/src/host-port.js +14 -0
  49. package/dist/src/index.d.ts +11 -3
  50. package/dist/src/index.js +9 -4
  51. package/dist/src/log.js +12 -7
  52. package/dist/src/machines.d.ts +81 -5
  53. package/dist/src/machines.js +125 -9
  54. package/dist/src/managed-database.d.ts +15 -11
  55. package/dist/src/managed-database.js +38 -37
  56. package/dist/src/multipart.d.ts +21 -3
  57. package/dist/src/multipart.js +112 -38
  58. package/dist/src/observe.d.ts +3 -0
  59. package/dist/src/observe.js +6 -2
  60. package/dist/src/openai-wire.d.ts +16 -0
  61. package/dist/src/openai-wire.js +43 -12
  62. package/dist/src/pack-fetch.d.ts +4 -6
  63. package/dist/src/pack-fetch.js +189 -57
  64. package/dist/src/packRegistry.d.ts +7 -1
  65. package/dist/src/packRegistry.js +20 -3
  66. package/dist/src/private-transport.d.ts +1 -0
  67. package/dist/src/private-transport.js +11 -0
  68. package/dist/src/protobuf.js +10 -4
  69. package/dist/src/rateBudget.d.ts +8 -1
  70. package/dist/src/rateBudget.js +1 -0
  71. package/dist/src/redis/engine.d.ts +14 -5
  72. package/dist/src/redis/engine.js +271 -72
  73. package/dist/src/redis/frames.d.ts +17 -0
  74. package/dist/src/redis/frames.js +123 -0
  75. package/dist/src/redis/index.d.ts +6 -0
  76. package/dist/src/redis/index.js +6 -0
  77. package/dist/src/redis/json.d.ts +2 -0
  78. package/dist/src/redis/json.js +14 -0
  79. package/dist/src/redis/lua-libs.d.ts +4 -0
  80. package/dist/src/redis/lua-libs.js +173 -0
  81. package/dist/src/redis/lua.d.ts +1 -0
  82. package/dist/src/redis/lua.js +36 -0
  83. package/dist/src/redis/member-storage.d.ts +5 -0
  84. package/dist/src/redis/member-storage.js +75 -0
  85. package/dist/src/redis/resp2.d.ts +4 -0
  86. package/dist/src/redis/resp2.js +29 -0
  87. package/dist/src/redis/stream.d.ts +14 -0
  88. package/dist/src/redis/stream.js +335 -0
  89. package/dist/src/remote-execute.d.ts +2 -2
  90. package/dist/src/request-body.d.ts +11 -0
  91. package/dist/src/request-body.js +11 -0
  92. package/dist/src/runtime.d.ts +6 -5
  93. package/dist/src/runtime.js +5 -5
  94. package/dist/src/s3/wire.d.ts +4 -4
  95. package/dist/src/s3/wire.js +5 -3
  96. package/dist/src/scenario.d.ts +22 -0
  97. package/dist/src/scenario.js +35 -31
  98. package/dist/src/serve-http.d.ts +2 -1
  99. package/dist/src/serve-http.js +10 -1
  100. package/dist/src/serve.js +7 -4
  101. package/dist/src/signing.d.ts +23 -5
  102. package/dist/src/signing.js +74 -11
  103. package/dist/src/sigv4.d.ts +3 -1
  104. package/dist/src/sigv4.js +4 -2
  105. package/dist/src/smtp.js +2 -1
  106. package/dist/src/sockets.js +8 -3
  107. package/dist/src/state-system.d.ts +3 -0
  108. package/dist/src/storage.js +5 -2
  109. package/dist/src/twin-fetch.d.ts +3 -1
  110. package/dist/src/twin-fetch.js +3 -2
  111. package/dist/src/vendor-call.d.ts +10 -3
  112. package/dist/src/vendor-call.js +58 -14
  113. package/dist/test-fixtures/attestation.SOURCE.md +1 -0
  114. package/dist/test-fixtures/attestation.json +50 -0
  115. package/generated/pack-facts.json +409 -352
  116. package/inject.cjs +474 -95
  117. package/network-policy.cjs +108 -13
  118. package/network-policy.d.cts +8 -3
  119. package/package.json +11 -4
  120. package/src/actions.ts +2 -1
  121. package/src/anthropic-wire.ts +6 -0
  122. package/src/attestation.ts +23 -0
  123. package/src/bytes.ts +21 -0
  124. package/src/changeset.ts +2 -1
  125. package/src/clickhouse/sql.ts +50 -16
  126. package/src/derived-core.ts +573 -249
  127. package/src/derived-real.ts +552 -42
  128. package/src/derived.ts +23 -4
  129. package/src/events.ts +54 -85
  130. package/src/exact-json.ts +71 -0
  131. package/src/executor.ts +86 -27
  132. package/src/file-response.ts +1 -1
  133. package/src/form.ts +66 -0
  134. package/src/git/format.ts +89 -0
  135. package/src/git/index.ts +1 -0
  136. package/src/git/lfs.ts +5 -4
  137. package/src/graphql-wire.ts +8 -3
  138. package/src/grpc-wire.ts +2 -1
  139. package/src/head.ts +15 -5
  140. package/src/held-reads.ts +54 -0
  141. package/src/history.ts +5 -4
  142. package/src/host-port.ts +9 -0
  143. package/src/index.ts +10 -4
  144. package/src/log.ts +11 -7
  145. package/src/machines.ts +135 -13
  146. package/src/managed-database.ts +48 -40
  147. package/src/multipart.ts +110 -35
  148. package/src/observe.ts +6 -3
  149. package/src/openai-wire.ts +45 -14
  150. package/src/pack-fetch.ts +173 -50
  151. package/src/packRegistry.ts +27 -4
  152. package/src/private-transport.ts +8 -0
  153. package/src/protobuf.ts +8 -4
  154. package/src/rateBudget.ts +6 -1
  155. package/src/redis/engine.ts +168 -63
  156. package/src/redis/frames.ts +53 -0
  157. package/src/redis/index.ts +6 -0
  158. package/src/redis/json.ts +13 -0
  159. package/src/redis/lua-libs.ts +62 -0
  160. package/src/redis/lua.ts +20 -0
  161. package/src/redis/member-storage.ts +58 -0
  162. package/src/redis/resp2.ts +25 -0
  163. package/src/redis/stream.ts +102 -0
  164. package/src/remote-execute.ts +2 -2
  165. package/src/request-body.ts +12 -0
  166. package/src/runtime.ts +6 -5
  167. package/src/s3/wire.ts +8 -5
  168. package/src/scenario.ts +34 -13
  169. package/src/serve-http.ts +11 -2
  170. package/src/serve.ts +4 -3
  171. package/src/signing.ts +71 -17
  172. package/src/sigv4.ts +5 -3
  173. package/src/smtp.ts +3 -2
  174. package/src/sockets.ts +8 -3
  175. package/src/state-system.ts +3 -0
  176. package/src/storage.ts +5 -2
  177. package/src/twin-fetch.ts +4 -3
  178. package/src/vendor-call.ts +59 -12
  179. package/test-fixtures/attestation.json +50 -0
@@ -1,4 +1,27 @@
1
1
  import { RateBudget } from "./rateBudget.js";
2
+ async function oauth1SignedHeader(credential, method, target, headers, body) {
3
+ const field = (name) => {
4
+ const value = credential[name];
5
+ if (typeof value !== 'string' || !value)
6
+ throw new Error(`remote execute: oauth1-hmac-sha1 needs sealed ${name}`);
7
+ return value;
8
+ };
9
+ const consumerKey = field('consumerKey'), consumerSecret = field('consumerSecret');
10
+ const token = field('token'), tokenSecret = field('tokenSecret');
11
+ const encode = (value) => encodeURIComponent(value).replace(/[!\x27()*]/g, c => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);
12
+ const protocol = [
13
+ ['oauth_consumer_key', consumerKey], ['oauth_token', token], ['oauth_signature_method', 'HMAC-SHA1'],
14
+ ['oauth_timestamp', String(Math.floor(Date.now() / 1000))], ['oauth_nonce', crypto.randomUUID()], ['oauth_version', '1.0'],
15
+ ];
16
+ const form = typeof body === 'string' && /^application\/x-www-form-urlencoded/i.test(headers.get('content-type') ?? '') ? [...new URLSearchParams(body)] : [];
17
+ const pairs = [...protocol, ...target.searchParams, ...form].filter(([name]) => name !== 'oauth_signature').map(([name, value]) => [encode(name), encode(value)]);
18
+ pairs.sort(([a, av], [b, bv]) => a < b ? -1 : a > b ? 1 : av < bv ? -1 : av > bv ? 1 : 0);
19
+ const base = `${method.toUpperCase()}&${encode(`${target.protocol}//${target.host.toLowerCase()}${target.pathname}`)}&${encode(pairs.map(([name, value]) => `${name}=${value}`).join('&'))}`;
20
+ const key = await crypto.subtle.importKey('raw', new TextEncoder().encode(`${encode(consumerSecret)}&${encode(tokenSecret)}`), { name: 'HMAC', hash: 'SHA-1' }, false, ['sign']);
21
+ const bytes = new Uint8Array(await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(base)));
22
+ const signature = btoa(Array.from(bytes, byte => String.fromCharCode(byte)).join(''));
23
+ return `OAuth ${[...protocol, ['oauth_signature', signature]].map(([name, value]) => `${encode(name)}="${encode(value)}"`).join(', ')}`;
24
+ }
2
25
  const MAX_ORIGIN_LENGTH = 2048;
3
26
  function isLoopbackHost(hostname) {
4
27
  return hostname === '127.0.0.1' || hostname === 'localhost' || hostname === '[::1]' || hostname === '::1';
@@ -138,7 +161,13 @@ export function buildRemoteExecute(origin, credential, auth, vendorHosts, option
138
161
  const execute = (request) => (byLane.get(request.lane ?? '') ?? headers)(request);
139
162
  return options.vendor === undefined ? execute : budgeted(options.vendor, execute, options.custody?.key ?? JSON.stringify(credential.headers));
140
163
  }
141
- const execute = auth?.in === 'exchange' ? exchangeExecute(origin, credential, auth, vendorHosts, options.custody) : direct(origin, credential, auth, vendorHosts);
164
+ if (auth?.in === 'choice') {
165
+ const picked = auth.choices.find(c => typeof credential[c.credential] === 'string' && credential[c.credential] !== '');
166
+ if (!picked)
167
+ throw new Error('remote execute: no credential matches the declared strategy choices');
168
+ return buildRemoteExecute(origin, credential, picked.strategy, vendorHosts, options);
169
+ }
170
+ const execute = auth?.in === 'exchange' ? exchangeExecute(origin, credential, auth, vendorHosts, options.custody) : direct(origin, credential, auth, vendorHosts, options.now);
142
171
  return options.vendor === undefined ? execute : budgeted(options.vendor, execute, options.custody?.key ?? JSON.stringify(credential.headers));
143
172
  }
144
173
  /** Every live call charged to the vendor's rate budget (rateBudget.ts, D8): a call the budget refuses is never made;
@@ -155,7 +184,7 @@ function budgeted(vendor, execute, token) {
155
184
  };
156
185
  }
157
186
  /** The call made as the root's credential is applied by the strategy (header replacement when none). */
158
- function direct(origin, credential, auth, vendorHosts) {
187
+ function direct(origin, credential, auth, vendorHosts, now = () => new Date()) {
159
188
  return async ({ method, path, headers, body, responseType, presigned, issued }) => {
160
189
  // A PRESIGNED UPLOAD URL the vendor returned: its own signature is its authorization, so the
161
190
  // sealed credential never goes with it, whatever host it names. A host other than the root's
@@ -193,15 +222,25 @@ function direct(origin, credential, auth, vendorHosts) {
193
222
  // the origin into USERINFO and ships the credential to evil.com. A path is a path:
194
223
  // it starts with '/', and the parse must land on the validated origin, asserted.
195
224
  let target;
225
+ // the vendor's own URL, where a loopback root stands in for it: what a signature is made over
226
+ let vendorUrl;
196
227
  if (/^https:\/\//i.test(path) && vendorHosts?.length) {
197
228
  // a second host the pack declares for this vendor, on a path its rule names: validated as the root is
198
229
  target = new URL(path);
199
230
  if (target.username || target.password)
200
231
  throw new Error('remote execute: a vendor URL carries no userinfo');
201
232
  validateRemoteOrigin(target.origin);
202
- const declared = vendorHosts.some((r) => 'host' in r && !r.exclude && r.host === target.hostname && (r.pathPattern === undefined || new RegExp(r.pathPattern).test(target.pathname)));
233
+ const named = (r) => ('host' in r ? r.host === target.hostname : 'suffix' in r ? target.hostname.endsWith(r.suffix) : new RegExp(r.hostPattern).test(target.hostname))
234
+ && (r.pathPattern === undefined || new RegExp(r.pathPattern).test(target.pathname));
235
+ const declared = vendorHosts.some((r) => !r.exclude && named(r)) && !vendorHosts.some((r) => r.exclude && named(r));
203
236
  if (!declared)
204
237
  throw new Error(`remote execute: ${target.host}${target.pathname.slice(0, 64)} is not a host this vendor's pack declares for this path`);
238
+ // A loopback root is the vendor substitute. Preserve the vendor Host while retaining
239
+ // the already-validated root's mounted path; an absolute URL never escapes that root.
240
+ if (isLoopbackHost(origin.hostname)) {
241
+ vendorUrl = target;
242
+ target = new URL(origin.href.replace(/\/+$/, '') + target.pathname + target.search);
243
+ }
205
244
  }
206
245
  else {
207
246
  if (!path.startsWith('/'))
@@ -219,6 +258,11 @@ function direct(origin, credential, auth, vendorHosts) {
219
258
  else
220
259
  for (const [name, value] of Object.entries(credential.headers))
221
260
  outbound.set(name, value);
261
+ // the vendor's host, kept over any the credential carries (a loopback twin routes by it, as the World's proxy's)
262
+ if (vendorUrl) {
263
+ outbound.set('host', vendorUrl.host);
264
+ outbound.set('x-volter-twin-original-host', vendorUrl.host);
265
+ }
222
266
  // ── THE STRATEGIES BEYOND HEADER REPLACEMENT ──────────────────────────────────────────────
223
267
  // Applied AFTER the anchoring check above, so nothing here can move the request off the
224
268
  // validated origin: `searchParams.set` only ever touches the query, and a header is a header.
@@ -226,35 +270,52 @@ function direct(origin, credential, auth, vendorHosts) {
226
270
  // FAIL CLOSED. A strategy with no secret must never send a request that merely LOOKS
227
271
  // authenticated — that silence is the whole defect this exists to remove. The refusals name the
228
272
  // strategy and the parameter, never the secret.
229
- if (auth !== undefined && issued === undefined) {
230
- const secret = credential.secret;
231
- if (typeof secret !== 'string' || secret === '') {
232
- throw new Error(`remote execute: the ${auth.in} credential strategy needs \`secret\` on the sealed credential, and it is absent`);
273
+ // an issued credential is the authorization: nothing of the sealed one signs the request
274
+ if (issued === undefined) {
275
+ if (auth?.in === 'signature' && auth.algorithm === 'oauth1-hmac-sha1') {
276
+ outbound.set('authorization', await oauth1SignedHeader(credential, method, target, outbound, body));
233
277
  }
234
- if (auth.in === 'query') {
235
- // `set`, not `append`: it REPLACES whatever placeholder the pack put in its own path,
236
- // exactly as `outbound.set` replaces a placeholder authorization header.
237
- target.searchParams.set(auth.name, secret);
238
- }
239
- else if (auth.algorithm === 'aws-sigv4') {
240
- const keyId = credential.keyId;
241
- if (typeof keyId !== 'string' || keyId === '') {
242
- throw new Error('remote execute: the aws-sigv4 credential strategy needs `keyId` on the sealed credential, and it is absent');
278
+ else if (auth !== undefined) {
279
+ const secret = credential.secret;
280
+ if (typeof secret !== 'string' || secret === '') {
281
+ throw new Error(`remote execute: the ${auth.in} credential strategy needs \`secret\` on the sealed credential, and it is absent`);
282
+ }
283
+ if (auth.in === 'query') {
284
+ // `set`, not `append`: it REPLACES whatever placeholder the pack put in its own path,
285
+ // exactly as `outbound.set` replaces a placeholder authorization header.
286
+ target.searchParams.set(auth.name, secret);
287
+ }
288
+ else if (auth.in === 'path') {
289
+ // the key's place in the path, filled after the origin is validated; a secret that could end the segment or
290
+ // the path (a slash, a query, a fragment, an escape) is refused, so the request cannot leave the validated root
291
+ if (/[/?#%\\]/.test(secret))
292
+ throw new Error('remote execute: the path credential strategy refuses a secret that is not one path segment');
293
+ const place = new RegExp(auth.pattern, 'd').exec(target.pathname)?.indices?.[1];
294
+ if (!place)
295
+ throw new Error(`remote execute: the path credential strategy's pattern does not name the key's place in ${target.pathname.slice(0, 64)}`);
296
+ target.pathname = target.pathname.slice(0, place[0]) + secret + target.pathname.slice(place[1]);
297
+ }
298
+ else if (auth.algorithm === 'aws-sigv4') {
299
+ const keyId = credential.keyId;
300
+ if (typeof keyId !== 'string' || keyId === '') {
301
+ throw new Error('remote execute: the aws-sigv4 credential strategy needs `keyId` on the sealed credential, and it is absent');
302
+ }
303
+ // the HOST goes with it: an AWS endpoint names its own region, and only the pack knows how
304
+ const signedUrl = vendorUrl ?? target;
305
+ const { region, service } = typeof auth.scope === 'function' ? auth.scope({ method, path, host: signedUrl.host, headers: Object.fromEntries(outbound.entries()), ...(body === undefined ? {} : { body }) }) : auth.scope;
306
+ const signed = await awsSigV4Headers({ method, url: signedUrl, ...(body === undefined ? {} : { body }), keyId, secret, region, service, headers: outbound, at: now() });
307
+ for (const [name, value] of Object.entries(signed))
308
+ outbound.set(name, value);
309
+ }
310
+ else {
311
+ // `null` is the vendor's own exemption, declared by the pack — not an error, and not a
312
+ // reason to send an unsigned header.
313
+ if (body instanceof Uint8Array)
314
+ throw new Error('remote execute: this signature canonicalizer accepts text bodies only');
315
+ const payload = auth.canonical({ method, path, ...(body === undefined ? {} : { body }) });
316
+ if (payload !== null)
317
+ outbound.set(auth.header, await hmacSha256(secret, payload, auth.encoding));
243
318
  }
244
- // the HOST goes with it: an AWS endpoint names its own region, and only the pack knows how
245
- const { region, service } = typeof auth.scope === 'function' ? auth.scope({ method, path, host: target.host, headers: Object.fromEntries(outbound.entries()), ...(body === undefined ? {} : { body }) }) : auth.scope;
246
- const signed = await awsSigV4Headers({ method, url: target, ...(body === undefined ? {} : { body }), keyId, secret, region, service, headers: outbound, at: new Date() });
247
- for (const [name, value] of Object.entries(signed))
248
- outbound.set(name, value);
249
- }
250
- else {
251
- // `null` is the vendor's own exemption, declared by the pack — not an error, and not a
252
- // reason to send an unsigned header.
253
- if (body instanceof Uint8Array)
254
- throw new Error('remote execute: this signature canonicalizer accepts text bodies only');
255
- const payload = auth.canonical({ method, path, ...(body === undefined ? {} : { body }) });
256
- if (payload !== null)
257
- outbound.set(auth.header, await hmacSha256(secret, payload, auth.encoding));
258
319
  }
259
320
  }
260
321
  const response = await fetch(target, {
@@ -32,7 +32,7 @@ export function bytesResponse(request, bytes, contentType, cacheControl = 'publi
32
32
  const range = /^bytes=(\d*)-(\d*)$/.exec((request.headers.get('range') ?? '').trim());
33
33
  let start = 0;
34
34
  let end = total - 1;
35
- const valid = range !== null && (range[1] !== '' || range[2] !== '') && !(range[1] !== '' && range[2] !== '' && Number(range[1]) > Number(range[2]));
35
+ const valid = range !== null && (range[1] !== '' || range[2] !== '') && !(range[1] !== '' && range[2] !== '' && BigInt(range[1]) > BigInt(range[2]));
36
36
  if (valid) {
37
37
  if (range[1] === '') {
38
38
  start = Math.max(0, total - Number(range[2]));
@@ -0,0 +1,8 @@
1
+ /** A field set as the object's own, whatever its name (`__proto__`, `constructor` and `prototype` are data a caller
2
+ * may name: Stripe's metadata keys are the caller's), never through what the object inherits. */
3
+ export declare function ownField(node: Record<string, unknown> | unknown[], key: string, value: unknown): void;
4
+ /** A form body's bracket notation (`a[b][0][c]=v`, `expand[]=x`) as nested objects and arrays. A form carries only
5
+ * text: a field the operation's spec types as a number or a boolean (`scalars`, by bracket path, written by
6
+ * world-tooling's formScalarsOf) is read as one when its text is that literal; every other field stays the text sent
7
+ * (`metadata[order]=007`, `name=2024`). */
8
+ export declare function parseBracketForm(text: string, scalars?: Readonly<Record<string, string | undefined>>): Record<string, unknown>;
@@ -0,0 +1,76 @@
1
+ import { exactInteger } from "./exact-json.js";
2
+ // Side-effect-free URL-encoded form reading, shared by requests and published examples.
3
+ /** A field set as the object's own, whatever its name (`__proto__`, `constructor` and `prototype` are data a caller
4
+ * may name: Stripe's metadata keys are the caller's), never through what the object inherits. */
5
+ export function ownField(node, key, value) {
6
+ Object.defineProperty(node, key, { value, writable: true, enumerable: true, configurable: true });
7
+ }
8
+ /** A field the object holds as its own, never one it inherits (`constructor` is every object's). */
9
+ const ownValue = (node, key) => (Object.hasOwn(node, key) ? node[key] : undefined);
10
+ /** A form body's bracket notation (`a[b][0][c]=v`, `expand[]=x`) as nested objects and arrays. A form carries only
11
+ * text: a field the operation's spec types as a number or a boolean (`scalars`, by bracket path, written by
12
+ * world-tooling's formScalarsOf) is read as one when its text is that literal; every other field stays the text sent
13
+ * (`metadata[order]=007`, `name=2024`). */
14
+ export function parseBracketForm(text, scalars) {
15
+ const out = {};
16
+ for (const [rawKey, raw] of new URLSearchParams(text)) {
17
+ const parts = rawKey.replace(/\]/g, '').split('[');
18
+ const kind = scalars && scalarAt(scalars, parts);
19
+ const value = kind === 'boolean' ? (raw === 'true' ? true : raw === 'false' ? false : raw)
20
+ : kind === 'integer' ? (/^-?\d+$/.test(raw) ? exactInteger(raw) : raw)
21
+ : kind === 'number' ? (/^-?\d+$/.test(raw) ? exactInteger(raw) : /^-?\d+\.\d+$/.test(raw) ? Number(raw) : raw)
22
+ : raw;
23
+ // every field is the node's own, read and written as such: a key naming an object's machinery (`__proto__[x]`,
24
+ // `constructor[prototype][x]`) is a field of that name, never a walk into what every object inherits
25
+ let node = out;
26
+ for (const [i, key] of parts.entries()) {
27
+ // an array holds its items by index (`a[]`, `a[0]`): a named key under one (`a[]=1&a[length]=-1`) has nowhere to
28
+ // go, as a key under text has none (`a=1&a[b]=2`): the first shape stands
29
+ if (Array.isArray(node) && key !== '' && !/^\d+$/.test(key))
30
+ break;
31
+ if (i === parts.length - 1) {
32
+ if (key === '') {
33
+ if (Array.isArray(node))
34
+ node.push(value);
35
+ }
36
+ else {
37
+ const held = ownValue(node, key);
38
+ if (held === undefined)
39
+ ownField(node, key, value);
40
+ else if (Array.isArray(held))
41
+ held.push(value);
42
+ else
43
+ ownField(node, key, [held, value]);
44
+ }
45
+ break;
46
+ }
47
+ const next = parts[i + 1];
48
+ if (key === '')
49
+ break;
50
+ let child = ownValue(node, key);
51
+ if (child === undefined) {
52
+ child = next === '' || /^\d+$/.test(next) ? [] : {};
53
+ ownField(node, key, child);
54
+ }
55
+ if (child === null || typeof child !== 'object')
56
+ break;
57
+ node = child;
58
+ }
59
+ }
60
+ return out;
61
+ }
62
+ /** The type `scalars` gives a form key's parts: a segment is a property name, `[]` an array's item (an index or empty),
63
+ * `*` a map's key. */
64
+ function scalarAt(scalars, parts) {
65
+ let paths = [''];
66
+ for (const [i, part] of parts.entries()) {
67
+ const segs = part === '' || /^\d+$/.test(part) ? ['[]', '*'] : [part, '*'];
68
+ paths = paths.flatMap((p) => segs.map((seg) => (i === 0 ? seg : `${p}.${seg}`)));
69
+ if (paths.length > 64)
70
+ paths = paths.filter((p) => Object.keys(scalars).some((k) => k === p || k.startsWith(`${p}.`)));
71
+ }
72
+ for (const p of paths)
73
+ if (scalars[p])
74
+ return scalars[p];
75
+ return undefined;
76
+ }
@@ -0,0 +1,16 @@
1
+ import { type GitObjectStore } from './objects.js';
2
+ /** A complete unified diff between two trees or commits, with null as the empty preimage. */
3
+ export declare function diffText(store: GitObjectStore, from: string | null, to: string): Promise<string>;
4
+ /** One commit as an email patch; series metadata numbers the subject for a comparison's commits. */
5
+ export declare function formatPatch(store: GitObjectStore, sha: string, series?: {
6
+ number: number;
7
+ total: number;
8
+ }): Promise<string | null>;
9
+ /** Submodule paths and URLs declared by a .gitmodules file (git configuration syntax). */
10
+ export declare function parseSubmodules(text: string): Array<{
11
+ name: string;
12
+ path: string;
13
+ url: string;
14
+ }>;
15
+ /** A repository-relative symbolic-link target; targets escaping the repository have no local path. */
16
+ export declare function symlinkPath(path: string, target: string): string | undefined;
@@ -0,0 +1,110 @@
1
+ // Git text representations over the object store (architecture, The engine slot).
2
+ import { decodeCommit } from "./objects.js";
3
+ import { diffTrees, lineDiff, unifiedPatch } from "./history.js";
4
+ const decoder = new TextDecoder();
5
+ const binary = (bytes) => bytes.slice(0, 8000).includes(0);
6
+ const pathName = (path) => /[\s"\\\x00-\x1f]/.test(path) ? JSON.stringify(path) : path;
7
+ async function bytesOf(store, sha, mode) {
8
+ if (!sha)
9
+ return new Uint8Array();
10
+ if (mode === '160000')
11
+ return new TextEncoder().encode(`Subproject commit ${sha}\n`);
12
+ return (await store.read(sha))?.payload ?? new Uint8Array();
13
+ }
14
+ async function fileDiff(store, change) {
15
+ const oldBytes = await bytesOf(store, change.oldSha, change.oldMode);
16
+ const newBytes = await bytesOf(store, change.newSha, change.newMode);
17
+ const left = pathName(`a/${change.path}`), right = pathName(`b/${change.path}`);
18
+ let text = `diff --git ${left} ${right}\n`;
19
+ if (!change.oldSha)
20
+ text += `new file mode ${change.newMode}\n`;
21
+ else if (!change.newSha)
22
+ text += `deleted file mode ${change.oldMode}\n`;
23
+ else if (change.oldMode !== change.newMode)
24
+ text += `old mode ${change.oldMode}\nnew mode ${change.newMode}\n`;
25
+ if (change.oldSha === change.newSha)
26
+ return { text, additions: 0, deletions: 0 };
27
+ text += `index ${(change.oldSha ?? '0000000').slice(0, 7)}..${(change.newSha ?? '0000000').slice(0, 7)}${change.oldSha && change.newSha && change.oldMode === change.newMode ? ` ${change.newMode}` : ''}\n`;
28
+ if (binary(oldBytes) || binary(newBytes))
29
+ return { text: `${text}Binary files ${change.oldSha ? left : '/dev/null'} and ${change.newSha ? right : '/dev/null'} differ\n`, additions: 0, deletions: 0 };
30
+ const oldText = decoder.decode(oldBytes), newText = decoder.decode(newBytes);
31
+ const edits = lineDiff(oldText, newText);
32
+ text += `--- ${change.oldSha ? left : '/dev/null'}\n+++ ${change.newSha ? right : '/dev/null'}\n${unifiedPatch(oldText, newText)}\n`;
33
+ return { text, additions: edits.filter((edit) => edit.kind === '+').length, deletions: edits.filter((edit) => edit.kind === '-').length };
34
+ }
35
+ /** A complete unified diff between two trees or commits, with null as the empty preimage. */
36
+ export async function diffText(store, from, to) {
37
+ const changes = await diffTrees(store, from, to);
38
+ return (await Promise.all(changes.map((change) => fileDiff(store, change)))).map((file) => file.text).join('');
39
+ }
40
+ function mailDate(author) {
41
+ const sign = author.tz.startsWith('-') ? -1 : 1;
42
+ const offset = sign * (Number(author.tz.slice(1, 3)) * 60 + Number(author.tz.slice(3, 5)));
43
+ const date = new Date((author.time + offset * 60) * 1000);
44
+ const weekdays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
45
+ const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
46
+ const pad = (n) => String(n).padStart(2, '0');
47
+ return `${weekdays[date.getUTCDay()]}, ${date.getUTCDate()} ${months[date.getUTCMonth()]} ${date.getUTCFullYear()} ${pad(date.getUTCHours())}:${pad(date.getUTCMinutes())}:${pad(date.getUTCSeconds())} ${author.tz}`;
48
+ }
49
+ /** One commit as an email patch; series metadata numbers the subject for a comparison's commits. */
50
+ export async function formatPatch(store, sha, series) {
51
+ const object = await store.read(sha);
52
+ if (!object || object.type !== 'commit')
53
+ return null;
54
+ const commit = decodeCommit(object.payload);
55
+ const changes = await diffTrees(store, commit.parents[0] ?? null, sha);
56
+ const files = await Promise.all(changes.map((change) => fileDiff(store, change)));
57
+ const width = Math.max(0, ...changes.map((change) => change.path.length));
58
+ const stats = changes.map((change, i) => {
59
+ const file = files[i];
60
+ return ` ${change.path.padEnd(width)} | ${file.additions + file.deletions} ${'+'.repeat(file.additions)}${'-'.repeat(file.deletions)}\n`;
61
+ }).join('');
62
+ const additions = files.reduce((sum, file) => sum + file.additions, 0), deletions = files.reduce((sum, file) => sum + file.deletions, 0);
63
+ const summary = `${changes.length} file${changes.length === 1 ? '' : 's'} changed${additions ? `, ${additions} insertion${additions === 1 ? '' : 's'}(+)` : ''}${deletions ? `, ${deletions} deletion${deletions === 1 ? '' : 's'}(-)` : ''}`;
64
+ const [title = '', ...rest] = commit.message.trimEnd().split('\n');
65
+ const body = rest.join('\n').trim();
66
+ const numbered = series && series.total > 1 ? ` ${series.number}/${series.total}` : '';
67
+ return `From ${sha} Mon Sep 17 00:00:00 2001\nFrom: ${commit.author.name} <${commit.author.email}>\nDate: ${mailDate(commit.author)}\nSubject: [PATCH${numbered}] ${title}\n\n${body ? `${body}\n\n` : ''}---\n${stats} ${summary}\n\n${files.map((file) => file.text).join('')}`;
68
+ }
69
+ /** Submodule paths and URLs declared by a .gitmodules file (git configuration syntax). */
70
+ export function parseSubmodules(text) {
71
+ const modules = [];
72
+ let current;
73
+ for (const raw of text.split(/\r?\n/)) {
74
+ const line = raw.trim();
75
+ const section = /^\[submodule\s+"((?:[^"\\]|\\.)*)"\]\s*(?:[#;].*)?$/.exec(line);
76
+ if (section) {
77
+ current = { name: section[1].replace(/\\(.)/g, '$1'), path: '', url: '' };
78
+ modules.push(current);
79
+ continue;
80
+ }
81
+ if (line.startsWith('[')) {
82
+ current = undefined;
83
+ continue;
84
+ }
85
+ const field = /^(path|url)\s*=\s*(.*)$/i.exec(line);
86
+ if (!current || !field)
87
+ continue;
88
+ const value = field[2].replace(/\s+[#;].*$/, '').trim();
89
+ current[field[1].toLowerCase()] = value.startsWith('"') && value.endsWith('"') ? value.slice(1, -1).replace(/\\(.)/g, '$1') : value;
90
+ }
91
+ return modules.filter((module) => module.path && module.url);
92
+ }
93
+ /** A repository-relative symbolic-link target; targets escaping the repository have no local path. */
94
+ export function symlinkPath(path, target) {
95
+ if (target.startsWith('/'))
96
+ return undefined;
97
+ const segments = path.split('/').slice(0, -1);
98
+ for (const part of target.split('/')) {
99
+ if (!part || part === '.')
100
+ continue;
101
+ if (part === '..') {
102
+ if (!segments.length)
103
+ return undefined;
104
+ segments.pop();
105
+ }
106
+ else
107
+ segments.push(part);
108
+ }
109
+ return segments.join('/');
110
+ }
@@ -5,3 +5,4 @@ export * from './refs.js';
5
5
  export * from './smart-http.js';
6
6
  export * from './history.js';
7
7
  export * from './lfs.js';
8
+ export * from './format.js';
@@ -5,3 +5,4 @@ export * from "./refs.js";
5
5
  export * from "./smart-http.js";
6
6
  export * from "./history.js";
7
7
  export * from "./lfs.js";
8
+ export * from "./format.js";
@@ -1,7 +1,7 @@
1
1
  export declare const LFS_TYPE = "application/vnd.git-lfs+json";
2
2
  export type LfsObject = {
3
3
  oid: string;
4
- size: number;
4
+ size: number | string;
5
5
  };
6
6
  /** Where a pack keeps its objects: by oid. */
7
7
  export type LfsStore = {
@@ -1,3 +1,4 @@
1
+ import { exactInteger } from "../exact-json.js";
1
2
  // GIT LFS (architecture, "The tree contract": Git LFS is the git library's too): the batch API git-lfs speaks
2
3
  // (https://github.com/git-lfs/git-lfs/blob/main/docs/api/batch.md, the basic transfer), a pointer file read
3
4
  // (https://github.com/git-lfs/git-lfs/blob/main/docs/spec.md), and an upload accepted only when its bytes hash to the
@@ -15,7 +16,7 @@ export function parseLfsPointer(file) {
15
16
  return null;
16
17
  const oid = /^oid sha256:([0-9a-f]{64})$/m.exec(text)?.[1];
17
18
  const size = /^size (\d+)$/m.exec(text)?.[1];
18
- return oid && size ? { oid, size: Number(size) } : null;
19
+ return oid && size ? { oid, size: exactInteger(size) } : null;
19
20
  }
20
21
  /** A pointer file's text for an object. */
21
22
  export const lfsPointer = (o) => `version https://git-lfs.github.com/spec/v1\noid sha256:${o.oid}\nsize ${o.size}\n`;
@@ -39,9 +40,9 @@ export async function lfsBatch(body, opts) {
39
40
  const operation = b.operation;
40
41
  const objects = [];
41
42
  for (const raw of b.objects) {
42
- const o = { oid: String(raw?.oid ?? ''), size: Number(raw?.size) };
43
+ const o = { oid: String(raw?.oid ?? ''), size: /^-?\d+$/.test(String(raw?.size)) ? exactInteger(String(raw?.size)) : Number(raw?.size) };
43
44
  // source: https://github.com/git-lfs/git-lfs/blob/main/docs/api/batch.md "The object is invalid (for example, an invalid oid or size)."
44
- if (!OID.test(o.oid) || !Number.isSafeInteger(o.size) || o.size < 0) {
45
+ if (!OID.test(o.oid) || !Number.isSafeInteger(o.size) || Number(o.size) < 0) {
45
46
  objects.push({ ...o, error: { code: 422, message: 'Invalid object' } });
46
47
  continue;
47
48
  }
@@ -2,6 +2,12 @@
2
2
  export type GraphqlResolver<C = unknown> = (source: Record<string, unknown>, args: Record<string, unknown>, ctx: C) => unknown;
3
3
  /** What a pack's `semantics/graphql.ts` exports as `graphql`. */
4
4
  export type GraphqlPart<C = unknown> = {
5
+ /** The root operations derived from the SDL, for dispatch ownership and gap reporting. */
6
+ operations?: ReadonlyArray<{
7
+ id: string;
8
+ kind: 'query' | 'mutation';
9
+ field: string;
10
+ }>;
5
11
  /** resolvers by `Type.field` (`Query.repository`, `PullRequest.state`) */
6
12
  resolvers: Record<string, GraphqlResolver<C>>;
7
13
  /** per type, a GraphQL field name → the object's field that holds it (`nameWithOwner` → `full_name`) */
@@ -45,7 +45,10 @@ function connection(items, args) {
45
45
  export function graphqlEndpoint(options) {
46
46
  let schema;
47
47
  const { resolvers, aliases, stored } = options.part;
48
- const modeled = (type, field) => field === '__typename' || `${type}.${field}` in resolvers || !!aliases?.[type]?.[field] || !!stored?.[type]?.includes(field) || !!stored?.[type]?.includes(snake(field));
48
+ // introspection (`__typename`, `__type`, `__schema` and the `__Type`/`__Field`/… meta types) is the vendor's schema
49
+ // describing itself, answered by graphql-js from the SDL: modeled as the schema is (gh asks `__type(name:"PullRequest")`
50
+ // before it merges)
51
+ const modeled = (type, field) => field.startsWith('__') || type.startsWith('__') || `${type}.${field}` in resolvers || !!aliases?.[type]?.[field] || !!stored?.[type]?.includes(field) || !!stored?.[type]?.includes(snake(field));
49
52
  return async (request) => {
50
53
  const g = await graphqlLib();
51
54
  schema ??= g.buildSchema(options.sdl, { assumeValidSDL: true });
@@ -80,10 +83,10 @@ export function graphqlEndpoint(options) {
80
83
  const ctx = await options.context(request);
81
84
  const result = await g.execute({
82
85
  schema, document, variableValues: payload.variables ?? {}, ...(payload.operationName ? { operationName: payload.operationName } : {}), contextValue: ctx,
83
- fieldResolver: (source, args, context, info) => {
86
+ fieldResolver: async (source, args, context, info) => {
84
87
  const own = resolvers[`${info.parentType.name}.${info.fieldName}`];
85
88
  const obj = (source ?? {});
86
- let value = own ? own(obj, args, context) : (obj[aliases?.[info.parentType.name]?.[info.fieldName] ?? info.fieldName] ?? obj[snake(info.fieldName)] ?? null);
89
+ let value = own ? await own(obj, args, context) : (obj[aliases?.[info.parentType.name]?.[info.fieldName] ?? info.fieldName] ?? obj[snake(info.fieldName)] ?? null);
87
90
  const named = g.getNamedType(info.returnType);
88
91
  if (Array.isArray(value) && g.isObjectType(named) && named.name.endsWith('Connection'))
89
92
  value = connection(value, args);
@@ -1,3 +1,4 @@
1
+ import { parseExactJson } from "./exact-json.js";
1
2
  // THE gRPC WIRE (architecture, "Other wires": gRPC): unary gRPC calls over the kernel's HTTP/2 server (./h2.ts), each
2
3
  // request's one length-prefixed message decoded by the proto unit's schema (./protobuf.ts) and handed to the pack's
3
4
  // handler for its operation, whose JSON answer is encoded back with the status trailers
@@ -144,7 +145,7 @@ export function grpcOverFetch(fetch) {
144
145
  const text = await res.text();
145
146
  let body;
146
147
  try {
147
- body = text ? JSON.parse(text) : {};
148
+ body = text ? parseExactJson(text) : {};
148
149
  }
149
150
  catch {
150
151
  body = undefined;
@@ -15,6 +15,7 @@ export type PushOutcome = {
15
15
  also?: Array<{
16
16
  actionId: string;
17
17
  externalId: string;
18
+ data?: Record<string, unknown>;
18
19
  }>;
19
20
  };
20
21
  /** What a perform is handed beside the executor: a resolver from a local id to the vendor's (an
package/dist/src/head.js CHANGED
@@ -363,6 +363,15 @@ async function performNow(opts) {
363
363
  report.skipped.push(...group.map((a) => a.id));
364
364
  continue;
365
365
  }
366
+ // a write of a unit the pack sends nowhere (`vendorBacked.none` beside vendor-backed lanes) is the World's alone
367
+ const unsent = adapters.unsent?.(group[0].input);
368
+ if (unsent) {
369
+ const at = opts.at ?? worldNow();
370
+ for (const action of group)
371
+ settle(action, at, { status: 'landed', reason: unsent });
372
+ report.worldOnly.push(...group.map((a) => a.id));
373
+ continue;
374
+ }
366
375
  if (named(group[0]).some((v) => refusedIds.has(v))) {
367
376
  const at = opts.at ?? worldNow();
368
377
  for (const action of group) {
@@ -396,12 +405,14 @@ async function performNow(opts) {
396
405
  // the vendor's answer, when the adapter returned one as fields, is what the primary entry's landed copy holds
397
406
  const answered = outcome.data !== null && typeof outcome.data === 'object' && !Array.isArray(outcome.data) ? outcome.data : {};
398
407
  const primary = resolved.find((a) => a.id === outcome.actionId) ?? resolved[0];
399
- const also = new Map((outcome.also ?? []).map((a) => [a.actionId, a.externalId]));
408
+ const also = new Map((outcome.also ?? []).map((a) => [a.actionId, a]));
400
409
  for (const action of resolved) {
401
- const externalId = action === primary ? outcome.externalId : also.get(action.id);
402
- settle(action, at, { status: 'deployed', ...(action === primary && outcome.url ? { url: outcome.url } : {}) }, action === primary ? { ...action.fields, ...answered } : action.fields ?? {}, externalId || undefined);
410
+ const answeredId = action === primary ? outcome.externalId : also.get(action.id)?.externalId;
411
+ const externalId = answeredId || aliases.get(`${action.subject.type}:${action.subject.id}`);
412
+ settle(action, at, { status: 'deployed', ...(action === primary && outcome.url ? { url: outcome.url } : {}) }, { ...action.fields, ...(action === primary ? answered : also.get(action.id)?.data ?? {}) }, externalId || undefined);
413
+ const data = action === primary ? outcome.data : also.get(action.id)?.data;
403
414
  report.pushed += 1;
404
- report.deployed.push({ actionId: action.id, at, ...(externalId ? { externalId } : {}), ...(action === primary && outcome.data !== undefined ? { data: outcome.data } : {}) });
415
+ report.deployed.push({ actionId: action.id, at, ...(externalId ? { externalId } : {}), ...(data !== undefined ? { data } : {}) });
405
416
  }
406
417
  }
407
418
  catch (error) {
@@ -0,0 +1,6 @@
1
+ /** Tell every read held in this World that a subject of `storedType` was written. */
2
+ export declare function heldReadsWrite(service: string, root: string | undefined, storedType: string): void;
3
+ /** Wait until a write of one of `storedTypes` lands in this World (true), or `ms` pass or `signal` aborts (false). */
4
+ export declare function awaitWrite(service: string, root: string | undefined, storedTypes: ReadonlyArray<string>, ms: number, signal?: AbortSignal): Promise<boolean>;
5
+ /** Hold a read until its condition holds, within the caller's timeout. The kernel owns elapsed time. */
6
+ export declare function awaitCondition(service: string, root: string | undefined, storedTypes: ReadonlyArray<string>, ms: number, pollMs: number, condition: () => Promise<boolean>, signal?: AbortSignal): Promise<boolean>;
@@ -0,0 +1,60 @@
1
+ // A read held until the World writes (architecture, "Other wires": a read held until the World writes): HTTP long
2
+ // polling, as Telegram's getUpdates is, where a client asks again at once on an empty answer. A handler that finds
3
+ // nothing to answer awaits `ctx.awaitWrite(types, ms)`: it settles true when a write of one of the stored types lands in
4
+ // the same World (the derived core's one write path tells it, as it tells the open sockets), false when the time runs
5
+ // out or the client hangs up. The wait is the connection's, never World state: the handler reads the tree again and
6
+ // answers what it holds. Nothing here runs at module scope beyond the empty registry.
7
+ const waiting = new Map();
8
+ const keyOf = (service, root) => `${service}\0${root ?? ''}`;
9
+ /** Tell every read held in this World that a subject of `storedType` was written. */
10
+ export function heldReadsWrite(service, root, storedType) {
11
+ const held = waiting.get(keyOf(service, root));
12
+ if (!held?.size)
13
+ return;
14
+ for (const w of [...held])
15
+ if (w.types.has(storedType))
16
+ w.settle(true);
17
+ }
18
+ /** Wait until a write of one of `storedTypes` lands in this World (true), or `ms` pass or `signal` aborts (false). */
19
+ export function awaitWrite(service, root, storedTypes, ms, signal) {
20
+ if (!(ms > 0) || signal?.aborted)
21
+ return Promise.resolve(false);
22
+ const key = keyOf(service, root);
23
+ return new Promise((resolve) => {
24
+ let held = waiting.get(key);
25
+ if (!held) {
26
+ held = new Set();
27
+ waiting.set(key, held);
28
+ }
29
+ const set = held;
30
+ const waiter = {
31
+ types: new Set(storedTypes),
32
+ settle: (wrote) => {
33
+ clearTimeout(timer);
34
+ signal?.removeEventListener('abort', onAbort);
35
+ set.delete(waiter);
36
+ if (!set.size)
37
+ waiting.delete(key);
38
+ resolve(wrote);
39
+ },
40
+ };
41
+ const onAbort = () => waiter.settle(false);
42
+ const timer = setTimeout(() => waiter.settle(false), ms);
43
+ signal?.addEventListener('abort', onAbort, { once: true });
44
+ set.add(waiter);
45
+ });
46
+ }
47
+ /** Hold a read until its condition holds, within the caller's timeout. The kernel owns elapsed time. */
48
+ export async function awaitCondition(service, root, storedTypes, ms, pollMs, condition, signal) {
49
+ if (!(pollMs > 0))
50
+ throw new Error('a held condition requires a positive observation cadence');
51
+ const deadline = Date.now() + Math.max(0, ms);
52
+ for (;;) {
53
+ if (await condition())
54
+ return true;
55
+ const remaining = deadline - Date.now();
56
+ if (remaining <= 0 || signal?.aborted)
57
+ return false;
58
+ await awaitWrite(service, root, storedTypes, Math.min(remaining, pollMs), signal);
59
+ }
60
+ }