@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
@@ -108,14 +108,14 @@ export function saveHistoryView(directory, view) {
108
108
  for (const dependency of dependencies)
109
109
  commitParentPin(dependency.directory, file);
110
110
  return id;
111
- });
111
+ }, { operation: 'view pin publication' });
112
112
  }
113
113
  /** What an inherited history's content stands on, as file facts: every segment file the view names
114
114
  * (and the view document itself) and each named directory's ancestry record (its generation). A read cache keyed by this sees an
115
115
  * ancestor's retained history truncated, replaced or its directory re-created, which the full read
116
116
  * refuses (`historyEntries`), instead of serving a tree folded from the history it replaced. */
117
- export function inheritedHistoryKey(service, root) {
118
- const parent = readBranchMeta(service, root)?.parent;
117
+ export function inheritedHistoryKey(service, root, meta) {
118
+ const parent = readBranchMeta(service, root, meta)?.parent;
119
119
  if (!parent?.view)
120
120
  return '';
121
121
  const store = getActiveWorldStore();
@@ -170,7 +170,7 @@ export function captureHistory(service, root) {
170
170
  descriptor.segments.push({ directory, generation, file, start: 0, count: entries.length, digest: historyDigest(entries) });
171
171
  }
172
172
  return { view: saveHistoryView(directory, descriptor), position: historyLength(descriptor.layout), descriptor };
173
- });
173
+ }, { operation: 'capture' });
174
174
  }
175
175
  export function originHead(service, root) {
176
176
  const raw = getActiveWorldStore().read(join(worldPaths(service, root).dir, 'origin-head.json'));
@@ -213,7 +213,7 @@ export function publishOriginHistory(service, root, remoteView, layout, entries)
213
213
  const view = saveHistoryView(directory, { version: 1, owner: { directory, generation }, layout, segments });
214
214
  store.writeAtomic(join(directory, 'origin-head.json'), JSON.stringify({ view, remoteView }));
215
215
  return { view, appended };
216
- });
216
+ }, { operation: 'origin fetch' });
217
217
  }
218
218
  /** A changeset binds the complete inherited view, including this root's observations. */
219
219
  export function captureParentHistory(service, root) {
@@ -221,7 +221,7 @@ export function captureParentHistory(service, root) {
221
221
  const head = captureHistory(service, root);
222
222
  const position = head.position - head.descriptor.layout.branch;
223
223
  return { view: saveHistoryView(worldPaths(service, root).dir, historyPrefix(head.descriptor, position)), position };
224
- });
224
+ }, { operation: 'capture' });
225
225
  }
226
226
  /** Compare evaluated views, so removals, reordered rows and layout changes count as drift. */
227
227
  export function historyChanges(service, before, after) {
@@ -295,7 +295,7 @@ export function historyAtInstant(service, instant, root, selectedView) {
295
295
  }
296
296
  const view = saveHistoryView(directory, { version: 1, owner: source.owner, layout, segments, ...(source.origin ? { origin: { ...source.origin, ...(keep.slice(0, source.origin.position).some(v => !v) ? { incomplete: true } : {}) } } : {}) });
297
297
  return { view, position: historyLength(layout) };
298
- });
298
+ }, { operation: 'capture' });
299
299
  }
300
300
  /** Replace only the tracked origin layer; inherited local layers keep their exact projection order. */
301
301
  export function replaceHistoryOrigin(previous, origin, next) {
@@ -40,7 +40,7 @@ export { answerVendorErrors, RefusedWriteError } from './head.js';
40
40
  export { GraphqlError, type GraphqlPart, type GraphqlResolver } from './graphql-wire.js';
41
41
  export { VendorUnreachableError } from './vendor-call.js';
42
42
  export { isReadOnlyRequest, READ_ONLY_REQUEST_HEADER } from './request-scope.js';
43
- export { serveHttp, serveStream, StreamListenerUnavailable, type HttpHandler, type HttpServer, type ServeHttpOptions } from './serve-http.js';
43
+ export { serveHttp, serveStream, nodeBuiltin, StreamListenerUnavailable, type HttpHandler, type HttpServer, type ServeHttpOptions } from './serve-http.js';
44
44
  export type { TwinStream, TwinStreamConnection, TwinStreamSink } from './twin-fetch.js';
45
45
  export { privateTransportKey } from './private-transport.js';
46
46
  export { siteLabel, siteUrlOf, twinSiteUrl, twinVendorUrl } from './twin-fetch.js';
@@ -58,6 +58,7 @@ export { EngineDatabaseError, EngineUnavailableError, quoteIdent } from './manag
58
58
  export type { Batch as EngineBatch, ManagedDatabase, Statement as EngineStatement, StatementResult as EngineStatementResult } from './managed-database.js';
59
59
  export type { Mail, SmtpRoute } from './smtp.js';
60
60
  export * as s3 from './s3/wire.js';
61
+ export * as xml from './xml.js';
61
62
  export * as clickhouse from './clickhouse/index.js';
62
63
  export { sampleFromSchema } from './schema-sample.js';
63
64
  export * as openaiWire from './openai-wire.js';
@@ -70,6 +71,7 @@ export { presignSigV4, signSigV4, verifySigV4, type SigV4Verdict } from './sigv4
70
71
  export * as redis from './redis/index.js';
71
72
  export * as mysql from './mysql/index.js';
72
73
  export * as live from './live/index.js';
74
+ export * as mongodb from './mongodb/index.js';
73
75
  export * as git from './git/index.js';
74
76
  export { parseScenarioDocument, scenarioFaultResult, ScenarioEngine, ScenarioError } from './scenario.js';
75
77
  export type { PackScenarioAdapter, ScenarioDecision, ScenarioDocument, ScenarioExtractorSpec, ScenarioFault, ScenarioFaultResult, ScenarioFeatures, ScenarioHandler, ScenarioMatcher, ScenarioMissRecord, ScenarioStatus } from './scenario.js';
@@ -77,3 +79,5 @@ export { verifyAttestationBundle } from './attestation.js';
77
79
  export { browserSessionToken, startBrowserSession, endBrowserSession } from './browser-session.js';
78
80
  export { readHostPort } from './host-port.js';
79
81
  export { assertBrowserName, browserOrigin, validateBrowserState, validateBrowserStorageState, validateBrowserContext, browserAcceptLanguage, cookieMatchesHost, type BrowserCookie, type BrowserState, type BrowserStorageState, type BrowserContext, type BrowserIndexedDB } from './browser-state.js';
82
+ export * as managedOriginalInputs from './managed-original-inputs.js';
83
+ export * as managedGoPrograms from './managed-go-programs.js';
package/dist/src/index.js CHANGED
@@ -29,7 +29,7 @@ export { GraphqlError } from "./graphql-wire.js";
29
29
  export { VendorUnreachableError } from "./vendor-call.js";
30
30
  export { isReadOnlyRequest, READ_ONLY_REQUEST_HEADER } from "./request-scope.js";
31
31
  // ── the serve factory ──────────────────────────────────────────────────────────────────────────
32
- export { serveHttp, serveStream, StreamListenerUnavailable } from "./serve-http.js";
32
+ export { serveHttp, serveStream, nodeBuiltin, StreamListenerUnavailable } from "./serve-http.js";
33
33
  export { privateTransportKey } from "./private-transport.js";
34
34
  // where a page links a hostname its twin answers, inside the World that shows it
35
35
  export { siteLabel, siteUrlOf, twinSiteUrl, twinVendorUrl } from "./twin-fetch.js";
@@ -49,6 +49,8 @@ export { passwordHash, personOf, personWith, recordPerson } from "./people.js";
49
49
  export { EngineDatabaseError, EngineUnavailableError, quoteIdent } from "./managed-database.js";
50
50
  // S3's wire (contract "S3's wire is a kernel library too") and the git plane (contract "The git plane is a kernel library")
51
51
  export * as s3 from "./s3/wire.js";
52
+ // Generic strict XML grammar; vendor interpretation remains in its consumer.
53
+ export * as xml from "./xml.js";
52
54
  export * as clickhouse from "./clickhouse/index.js";
53
55
  export { sampleFromSchema } from "./schema-sample.js";
54
56
  export * as openaiWire from "./openai-wire.js";
@@ -61,6 +63,7 @@ export { presignSigV4, signSigV4, verifySigV4 } from "./sigv4.js";
61
63
  export * as redis from "./redis/index.js";
62
64
  export * as mysql from "./mysql/index.js";
63
65
  export * as live from "./live/index.js";
66
+ export * as mongodb from "./mongodb/index.js";
64
67
  export * as git from "./git/index.js";
65
68
  // ── the scenario grammar (architecture, "Behaviour") ───────────────────────────────────────────
66
69
  export { parseScenarioDocument, scenarioFaultResult, ScenarioEngine, ScenarioError } from "./scenario.js";
@@ -68,3 +71,5 @@ export { verifyAttestationBundle } from "./attestation.js";
68
71
  export { browserSessionToken, startBrowserSession, endBrowserSession } from "./browser-session.js";
69
72
  export { readHostPort } from "./host-port.js";
70
73
  export { assertBrowserName, browserOrigin, validateBrowserState, validateBrowserStorageState, validateBrowserContext, browserAcceptLanguage, cookieMatchesHost } from "./browser-state.js";
74
+ export * as managedOriginalInputs from "./managed-original-inputs.js";
75
+ export * as managedGoPrograms from "./managed-go-programs.js";
@@ -25,7 +25,6 @@ export function liveIngest(options) {
25
25
  let priorInit;
26
26
  let started = false;
27
27
  let metadata;
28
- let refusedCodec;
29
28
  const recording = () => {
30
29
  const row = store.rows(LIVE).find((r) => r.id === stream);
31
30
  return recordingOf(row, segmentsOf(store, stream));
@@ -116,7 +115,12 @@ export function liveIngest(options) {
116
115
  app: ask.app, key: ask.key, state: 'publishing', startedAt: typeof prior?.startedAt === 'string' ? prior.startedAt : now, endedAt: null,
117
116
  publishes: Number(prior?.publishes ?? 0) + 1, window: ruling.window ?? 6, segmentSeconds: ruling.segmentSeconds ?? 2, ...(ruling.retain !== undefined ? { retain: ruling.retain } : {}),
118
117
  }, 'live.publish'));
118
+ // Every admitted publish has its own encoder and hook state. Continuation comes from the recording above,
119
+ // including on a connection reused after FCUnpublish, never from the prior publish's in-memory state.
120
+ initIndex = -1;
119
121
  priorInit = last?.init;
122
+ started = false;
123
+ metadata = undefined;
120
124
  segmenter = new Segmenter({ targetMs: Math.max(0.5, ruling.segmentSeconds ?? 2) * 1000, offsetMs, firstSequence: last ? last.seq + 1 : 0 });
121
125
  stream = named;
122
126
  admitted = ruling;
@@ -137,12 +141,8 @@ export function liveIngest(options) {
137
141
  if (!segmenter)
138
142
  return;
139
143
  const tag = readVideoTag(payload);
140
- if (tag.kind === 'unsupported') {
141
- if (refusedCodec !== tag.codec)
142
- log(`${stream}: ${tag.codec} video is not recorded (H.264 is)`);
143
- refusedCodec = tag.codec;
144
- return;
145
- }
144
+ if (tag.kind === 'unsupported')
145
+ throw new RtmpRefusal('NetStream.Failed', `${tag.codec} video is not supported; publish H.264.`);
146
146
  const s = segmenter;
147
147
  if (tag.kind === 'config')
148
148
  await keepAll(s.videoConfig(tag.config));
@@ -153,12 +153,8 @@ export function liveIngest(options) {
153
153
  if (!segmenter)
154
154
  return;
155
155
  const tag = readAudioTag(payload);
156
- if (tag.kind === 'unsupported') {
157
- if (refusedCodec !== tag.codec)
158
- log(`${stream}: ${tag.codec} audio is not recorded (AAC is)`);
159
- refusedCodec = tag.codec;
160
- return;
161
- }
156
+ if (tag.kind === 'unsupported')
157
+ throw new RtmpRefusal('NetStream.Failed', `${tag.codec} audio is not supported; publish AAC.`);
162
158
  const s = segmenter;
163
159
  if (tag.kind === 'config')
164
160
  await keepAll(s.audioConfig(tag.config));
@@ -98,6 +98,9 @@ export function rtmpSession(sink, publisher, log = () => { }) {
98
98
  chunks.clear();
99
99
  current = undefined;
100
100
  held = 0;
101
+ // A refused/ended transport owns the publish's cleanup too. An admission already awaiting the dialect settles
102
+ // first; messages queued behind the refusal cannot admit a new publish on the closed connection.
103
+ chain = chain.then(() => stop()).catch((error) => log(`the publish's end failed: ${String(error?.message ?? error)}`));
101
104
  try {
102
105
  sink.end();
103
106
  }
@@ -165,6 +168,7 @@ export function rtmpSession(sink, publisher, log = () => { }) {
165
168
  close();
166
169
  return;
167
170
  }
171
+ ended = false;
168
172
  publishing = true;
169
173
  publishStream = stream;
170
174
  // user control StreamBegin (event 0) for the publish's stream, then its start
@@ -281,7 +285,7 @@ export function rtmpSession(sink, publisher, log = () => { }) {
281
285
  overrun(`more than ${MAX_QUEUED} bytes of messages wait to be recorded`);
282
286
  return;
283
287
  }
284
- chain = chain.then(() => (phase === 'closed' && message.type !== 20 ? undefined : onMessage(message))).catch((error) => {
288
+ chain = chain.then(() => (phase === 'closed' ? undefined : onMessage(message))).catch((error) => {
285
289
  log(`a message failed: ${String(error?.message ?? error)}`);
286
290
  close();
287
291
  }).finally(() => { queued -= payload.length; });
@@ -447,10 +451,8 @@ export function rtmpSession(sink, publisher, log = () => { }) {
447
451
  // message's bytes are not in it: only more bytes finish it, and what it may hold is bounded (MAX_HELD)
448
452
  backlog: () => queued + pending.length,
449
453
  close() {
450
- clearTimeout(deadline);
451
- // the connection ended: a publish still open ends with it, after every message already read
452
- chain = chain.then(() => stop()).catch((error) => log(`the publish's end failed: ${String(error?.message ?? error)}`));
453
- phase = 'closed';
454
+ // Peer close and server refusal share one end, after any admission or write already under way settles.
455
+ close();
454
456
  },
455
457
  };
456
458
  }
@@ -39,14 +39,20 @@ export class Segmenter {
39
39
  this.avc = config;
40
40
  this.freshInit = true;
41
41
  this.keyed = false;
42
+ this.unkeyedSince = undefined;
42
43
  return out;
43
44
  }
44
45
  audioConfig(config) {
45
46
  if (this.aac && same(this.aac.asc, config.asc))
46
47
  return [];
47
- const out = this.aac ? this.flush() : [];
48
+ const out = this.flush();
48
49
  this.aac = config;
49
50
  this.freshInit = true;
51
+ // A configuration boundary has no running video segment. Its first frame must be a new keyframe, and the
52
+ // audio sample clock belongs to the new configuration (which may have a different sample rate).
53
+ this.keyed = false;
54
+ this.unkeyedSince = undefined;
55
+ this.audioNext = undefined;
50
56
  return out;
51
57
  }
52
58
  /** A video frame at its decode time: the segment it closes, if it closes one. */
package/dist/src/log.d.ts CHANGED
@@ -102,7 +102,7 @@ export declare function originLogPath(service: string, root?: string): string;
102
102
  /** A parent is at a URL when it is not a path on this machine. */
103
103
  export declare function isUrlParent(at: string): boolean;
104
104
  export declare function checkpointPath(service: string, root?: string): string;
105
- export declare function readBranchMeta(service: string, root?: string): BranchMeta | null;
105
+ export declare function readBranchMeta(service: string, root?: string, read?: string | null): BranchMeta | null;
106
106
  export declare function writeBranchMeta(service: string, meta: BranchMeta, root?: string): void;
107
107
  /** A v1 observed row as an entry: a delta event folds its `changed.*.after`; anything else folds
108
108
  * nothing and rides along for `listEvents`. An entry row is itself. */
@@ -130,13 +130,21 @@ export declare function landedIds(parent: Entry[]): Set<string>;
130
130
  * before it had observed the vendor's own subject under that id (its tree was behind) must not fold
131
131
  * the vendor's subject onto the landed one, nor send a caller who addresses the vendor's id there. */
132
132
  export declare function aliasesFrom(parent: Entry[]): Map<string, string>;
133
- type Subject = {
133
+ export type Subject = {
134
134
  type: string;
135
135
  id: string;
136
136
  updatedAt: string;
137
137
  fields: SubjectFields;
138
138
  };
139
139
  export type Tree = Map<string, Subject>;
140
+ /** One subject read by the same guarded key as a write's answer; restore the vendor's own metadata fields. */
141
+ export declare function heldSubject(service: string, type: string, id: string, root?: string): Subject | undefined;
142
+ /**
143
+ * One local write folded onto its subject as the tree folds its entry (`overlay`): the subject as it was (undefined
144
+ * for one new to the tree) is copied, never changed. A command's writes staged in `ctx.batch` are read back by
145
+ * folding them this way, so a read inside the batch sees what the tree will hold once they land (derived-core.ts).
146
+ */
147
+ export declare function foldLocalWrite(previous: Subject | undefined, type: string, id: string, fields: SubjectFields, at: string): Subject;
140
148
  /** Fold entries onto a tree, in order: `set` overlays its fields and applies its projection; a
141
149
  * reverted entry folds nothing; a `confirm` row (v1) folds nothing. `skip` names entries the
142
150
  * parent already holds. */
@@ -152,6 +160,15 @@ export declare function ownFields(r: TwinResource): Record<string, unknown>;
152
160
  export declare function dropCheckpoint(service: string, root?: string): void;
153
161
  /** Preserve each ancestor's observed-then-local overlay, including landed IDs and aliases. */
154
162
  export declare function foldHistory(entries: Entry[], layout: HistoryLayout, service?: string): Tree;
163
+ /**
164
+ * One subject of the current tree as a resource, by its type and id: the resource `projectResources(...).find` gives
165
+ * for the same type and id, read by the subject's key without cloning or projecting the rest of the tree. Every subject is
166
+ * held under `${type}:${id}` (overlay is the only setter, and an alias re-keys through it), so the key is the
167
+ * subject. A write's answer read back the whole tree this way (serve.ts, applyTwinWrite's `resource`), once per
168
+ * written subject: a derived pack writing each document, index and collection of a database's startup paid a
169
+ * projection of every subject the World holds for each (S2 R17).
170
+ */
171
+ export declare function readSubject(service: string, type: string, id: string, root?: string): TwinResource | undefined;
155
172
  export declare function readTree(service: string, root?: string, opts?: {
156
173
  until?: string;
157
174
  at?: number;
@@ -171,6 +188,10 @@ export declare function readTreeMap(service: string, root?: string, opts?: {
171
188
  at?: number;
172
189
  view?: string;
173
190
  }): Tree;
191
+ /** Append `data` (whole JSONL lines of a service's branch log, `append` writing them) and, when the reads' memo held
192
+ * the tree as it stood, extend it by them. Called holding the directory's history lock. Answers the log's facts
193
+ * after the append (`size:mtimeMs`) when it read them, for a caller keyed on the same. */
194
+ export declare function appendToBranchLog(service: string, root: string | undefined, data: string, append: () => void): string | undefined;
174
195
  /** What changed in a service's tree since `since` (a `treeStamp` an earlier read was made at): the subjects
175
196
  * written since, as resources; the keys (`type:id`) of those no longer in the tree; and, of the changed, the keys
176
197
  * now at the tree's end in the order they stand there (new to the tree, or deleted and made again), which a
@@ -257,4 +278,3 @@ export declare function subjectHistory(service: string, subject: {
257
278
  type: string;
258
279
  id?: string;
259
280
  }, root?: string): Entry[];
260
- export {};
package/dist/src/log.js CHANGED
@@ -29,14 +29,14 @@ import { canonicalStatePath, checkParent, commitParentPin, pinParent, withAncest
29
29
  import { resolveReferences } from "./references.js";
30
30
  import { hashFieldValue } from "./hash.js";
31
31
  import { worldPaths } from "./storage.js";
32
- import { getActiveWorldStore } from "./world-store.js";
32
+ import { getActiveWorldStore, TREE_FILES } from "./world-store.js";
33
33
  export const CHECKPOINT_EVERY = 100;
34
34
  // Retained checkpoints and process memos are disposable projections, never history.
35
35
  const TREE_PROJECTION_VERSION = 2;
36
36
  export const DELTA_SUFFIX = '.delta';
37
37
  export function parentLogPath(service, root) { return worldPaths(service, root).events; }
38
- export function branchLogPath(service, root) { return join(dirname(worldPaths(service, root).events), 'actions.jsonl'); }
39
- export function branchMetaPath(service, root) { return join(dirname(worldPaths(service, root).events), 'branch.json'); }
38
+ export function branchLogPath(service, root) { return join(dirname(worldPaths(service, root).events), TREE_FILES.branch); }
39
+ export function branchMetaPath(service, root) { return join(dirname(worldPaths(service, root).events), TREE_FILES.pointer); }
40
40
  /** The fetched cache of a URL parent's whole log — what `fetch` extends and a read folds up to the position. */
41
41
  export function originLogPath(service, root) { return join(dirname(worldPaths(service, root).events), 'origin.jsonl'); }
42
42
  /** A parent is at a URL when it is not a path on this machine. */
@@ -114,8 +114,9 @@ function readRows(path) {
114
114
  cache.set(path, tail !== undefined ? { rows, lines: lines.length - 1, bytes: Buffer.byteLength(content), tail } : { content, rows, lines: lines.length - 1 });
115
115
  return rows.slice();
116
116
  }
117
- export function readBranchMeta(service, root) {
118
- const raw = getActiveWorldStore().read(branchMetaPath(service, root));
117
+ export function readBranchMeta(service, root, read) {
118
+ // `read`: branch.json's text where the caller has just read it (a tree's stamp reads it once for both uses).
119
+ const raw = read !== undefined ? read : getActiveWorldStore().read(branchMetaPath(service, root));
119
120
  if (raw === null)
120
121
  return null;
121
122
  const meta = parseExactJson(raw);
@@ -179,8 +180,8 @@ export function writeBranchMeta(service, meta, root) {
179
180
  getActiveWorldStore().writeAtomic(path, `${JSON.stringify(meta, null, 2)}\n`);
180
181
  if (meta.parent?.directory && !isUrlParent(meta.parent.at))
181
182
  commitParentPin(meta.parent.directory, path);
182
- });
183
- });
183
+ }, { operation: 'pointer publication' });
184
+ }, { operation: 'pointer publication' });
184
185
  }
185
186
  /** A v1 observed row as an entry: a delta event folds its `changed.*.after`; anything else folds
186
187
  * nothing and rides along for `listEvents`. An entry row is itself. */
@@ -251,7 +252,7 @@ export function appendOriginEntry(entry, root) {
251
252
  return { appended: false };
252
253
  store.append(path, `${JSON.stringify(entry)}\n`);
253
254
  return { appended: true };
254
- }));
255
+ }, { operation: 'origin append' }), { operation: 'origin append' });
255
256
  }
256
257
  /** Where the parent stands now: the length of its whole log (live at a path, the cache for a URL). */
257
258
  export function parentPosition(service, root) {
@@ -300,6 +301,22 @@ function overlay(tree, type, id, fields, at) {
300
301
  delete merged.deleted;
301
302
  tree.set(key, { ...existing, updatedAt: at, fields: merged });
302
303
  }
304
+ /** One subject read by the same guarded key as a write's answer; restore the vendor's own metadata fields. */
305
+ export function heldSubject(service, type, id, root) {
306
+ const row = readSubject(service, type, id, root);
307
+ return row ? { type: row.type, id: row.id, updatedAt: row.updatedAt, fields: ownFields(row) } : undefined;
308
+ }
309
+ /**
310
+ * One local write folded onto its subject as the tree folds its entry (`overlay`): the subject as it was (undefined
311
+ * for one new to the tree) is copied, never changed. A command's writes staged in `ctx.batch` are read back by
312
+ * folding them this way, so a read inside the batch sees what the tree will hold once they land (derived-core.ts).
313
+ */
314
+ export function foldLocalWrite(previous, type, id, fields, at) {
315
+ const key = `${type}:${id}`;
316
+ const tree = previous ? new Map([[key, { ...previous, fields: { ...previous.fields } }]]) : new Map();
317
+ overlay(tree, type, id, fields, at);
318
+ return tree.get(key);
319
+ }
303
320
  /** Fold entries onto a tree, in order: `set` overlays its fields and applies its projection; a
304
321
  * reverted entry folds nothing; a `confirm` row (v1) folds nothing. `skip` names entries the
305
322
  * parent already holds. */
@@ -402,6 +419,22 @@ export function foldHistory(entries, layout, service) {
402
419
  foldAliases(tree, aliasesFrom(parent), service);
403
420
  return tree;
404
421
  }
422
+ /**
423
+ * One subject of the current tree as a resource, by its type and id: the resource `projectResources(...).find` gives
424
+ * for the same type and id, read by the subject's key without cloning or projecting the rest of the tree. Every subject is
425
+ * held under `${type}:${id}` (overlay is the only setter, and an alias re-keys through it), so the key is the
426
+ * subject. A write's answer read back the whole tree this way (serve.ts, applyTwinWrite's `resource`), once per
427
+ * written subject: a derived pack writing each document, index and collection of a database's startup paid a
428
+ * projection of every subject the World holds for each (S2 R17).
429
+ */
430
+ export function readSubject(service, type, id, root) {
431
+ const key = `${type}:${id}`;
432
+ const held = currentTree(service, root).tree.get(key);
433
+ // a type or id holding ':' can share a key with another (type, id): only the subject itself is it
434
+ if (!held || held.type !== type || held.id !== id)
435
+ return undefined;
436
+ return treeResources(new Map([[key, { ...held, fields: { ...held.fields } }]]))[0];
437
+ }
405
438
  export function readTree(service, root, opts = {}) {
406
439
  return treeResources(readTreeMap(service, root, opts));
407
440
  }
@@ -414,7 +447,7 @@ export function readParentTreeMap(service, root) {
414
447
  return foldHistory([...(inherited ? historyEntries(inherited) : []), ...own], {
415
448
  ...(inherited ? { base: inherited.layout } : {}), parent: own.length, branch: 0,
416
449
  }, service);
417
- });
450
+ }, { operation: 'tree fold' });
418
451
  }
419
452
  const treeMemos = new WeakMap();
420
453
  /** How many extensions a memo remembers the touched subjects of; a projection further behind refolds. */
@@ -422,11 +455,44 @@ const TREE_STEPS_KEPT = 64;
422
455
  function statKey(path) { const s = getActiveWorldStore().stat(path); return s ? `${s.size}:${s.mtimeMs}` : '-'; }
423
456
  /** What a service's PARENT side at `root` is folded from (its parent log, branch.json and inherited history), as one
424
457
  * string: while it stands, its aliases and landed ids are the same, whatever the branch's own log does. */
425
- export function parentStamp(service, root) { return parentKeyOf(service, root); }
426
- function parentKeyOf(service, root) { return `${TREE_PROJECTION_VERSION}|${statKey(parentLogPath(service, root))}|${getActiveWorldStore().read(branchMetaPath(service, root)) ?? ''}|${inheritedHistoryKey(service, root)}`; }
458
+ export function parentStamp(service, root) { return treeFacts(service, root).parentKey; }
459
+ function parentKeyOf(service, root) { return parentFacts(service, root).key; }
460
+ /** The parent side's key, and whether it names a file outside the tree's own directory (an inherited view). */
461
+ function parentFacts(service, root) {
462
+ // branch.json is small and every stamp needs it: kept while its own facts stand (world-store.ts readKept), so an
463
+ // unchanged pointer costs one check. Its text is still in the key, so a changed pointer still moves the stamp.
464
+ const store = getActiveWorldStore(), pointer = branchMetaPath(service, root);
465
+ const meta = store.readKept ? store.readKept(pointer) : store.read(pointer);
466
+ const inherited = inheritedHistoryKey(service, root, meta);
467
+ return { key: `${TREE_PROJECTION_VERSION}|${statKey(parentLogPath(service, root))}|${meta ?? ''}|${inherited}`, inherits: inherited !== '' };
468
+ }
469
+ const keptFacts = new WeakMap();
470
+ function treeFacts(service, root) {
471
+ const store = getActiveWorldStore();
472
+ const branchLog = branchLogPath(service, root);
473
+ if (!store.generation)
474
+ return { parentKey: parentKeyOf(service, root), branchKey: statKey(branchLog) };
475
+ const directory = dirname(branchLog);
476
+ const generation = store.generation(directory);
477
+ let kept = keptFacts.get(store);
478
+ if (!kept) {
479
+ kept = new Map();
480
+ keptFacts.set(store, kept);
481
+ }
482
+ const held = kept.get(directory);
483
+ if (generation !== null && held?.generation === generation)
484
+ return held;
485
+ const parent = parentFacts(service, root);
486
+ const facts = { parentKey: parent.key, branchKey: statKey(branchLog) };
487
+ if (generation !== null && !parent.inherits)
488
+ kept.set(directory, { ...facts, generation });
489
+ else
490
+ kept.delete(directory);
491
+ return facts;
492
+ }
427
493
  /** What a service's tree (and every subject's history) at `root` is folded from, as one string: while it
428
494
  * stands, nothing was written. A pack that projects the tree into its own state memoizes on it. */
429
- export function treeStamp(service, root) { return `${parentKeyOf(service, root)}#${statKey(branchLogPath(service, root))}`; }
495
+ export function treeStamp(service, root) { const facts = treeFacts(service, root); return `${facts.parentKey}#${facts.branchKey}`; }
430
496
  const cloneTree = (tree) => new Map([...tree].map(([k, s]) => [k, { ...s, fields: { ...s.fields } }]));
431
497
  const memoStamp = (m) => `${m.parentKey}#${m.branchKey}`;
432
498
  /** The subject keys entries write (as foldEntries folds them), and the vendor rows an alias moves a local one onto. */
@@ -484,8 +550,11 @@ function currentTree(service, root) {
484
550
  // A hit needs no lock: a memo is only ever stored from a fold made under the history lock, and while the
485
551
  // facts still match that fold, nothing was written since. A writer mid-batch has moved them.
486
552
  const unlocked = memos.get(slot);
487
- if (unlocked && unlocked.parentKey === parentKeyOf(service, root) && unlocked.branchKey === statKey(branchLogPath(service, root)))
488
- return { tree: unlocked.tree, memo: unlocked };
553
+ if (unlocked) {
554
+ const facts = treeFacts(service, root);
555
+ if (unlocked.parentKey === facts.parentKey && unlocked.branchKey === facts.branchKey)
556
+ return { tree: unlocked.tree, memo: unlocked };
557
+ }
489
558
  return withHistoryLock(worldPaths(service, root).dir, () => {
490
559
  const parentKey = parentKeyOf(service, root);
491
560
  const branchKey = statKey(branchLogPath(service, root));
@@ -497,24 +566,7 @@ function currentTree(service, root) {
497
566
  if (held && held.parentKey === parentKey && branch.length >= held.branchCount
498
567
  && (held.branchCount === 0 ? held.lastBranchId === null : branch[held.branchCount - 1]?.id === held.lastBranchId)
499
568
  && !branch.slice(held.branchCount).some((e) => e.op === 'revert')) {
500
- const fresh = branch.slice(held.branchCount);
501
- const keys = touchedKeys(fresh, held.aliases);
502
- const from = memoStamp(held);
503
- let appended;
504
- try {
505
- appended = insertionsDuring(held.tree, () => {
506
- foldEntries(held.tree, fresh, held.landed);
507
- foldAliases(held.tree, held.aliases, service, keys);
508
- });
509
- }
510
- catch (error) {
511
- memos.delete(slot);
512
- throw error;
513
- } // a half-folded memo is never read again
514
- Object.assign(held, { branchKey, branchCount: branch.length, lastBranchId: last(branch) });
515
- held.steps.push({ from, to: memoStamp(held), keys, appended });
516
- if (held.steps.length > TREE_STEPS_KEPT)
517
- held.steps.splice(0, held.steps.length - TREE_STEPS_KEPT);
569
+ extendMemo(memos, slot, held, branch.slice(held.branchCount), branchKey, service);
518
570
  return { tree: held.tree, memo: held };
519
571
  }
520
572
  const tree = readTreeMapFull(service, root);
@@ -526,7 +578,78 @@ function currentTree(service, root) {
526
578
  return { tree, memo };
527
579
  }
528
580
  return { tree };
529
- });
581
+ }, { operation: 'tree fold' });
582
+ }
583
+ /** The memo extended in place by `fresh`, the branch log's entries after its own, which leave the log at `branchKey`,
584
+ * with the step that records the subjects they touched. */
585
+ function extendMemo(memos, slot, held, fresh, branchKey, service) {
586
+ const keys = touchedKeys(fresh, held.aliases);
587
+ const from = memoStamp(held);
588
+ let appended;
589
+ try {
590
+ appended = insertionsDuring(held.tree, () => {
591
+ foldEntries(held.tree, fresh, held.landed);
592
+ foldAliases(held.tree, held.aliases, service, keys);
593
+ });
594
+ }
595
+ catch (error) {
596
+ memos.delete(slot);
597
+ throw error;
598
+ } // a half-folded memo is never read again
599
+ Object.assign(held, { branchKey, branchCount: held.branchCount + fresh.length, lastBranchId: fresh.length ? fresh[fresh.length - 1].id : held.lastBranchId });
600
+ held.steps.push({ from, to: memoStamp(held), keys, appended });
601
+ if (held.steps.length > TREE_STEPS_KEPT)
602
+ held.steps.splice(0, held.steps.length - TREE_STEPS_KEPT);
603
+ }
604
+ // AN OWN APPEND EXTENDS THE READS' MEMO (S2 R17c). A write appended its entries and the next read found the facts
605
+ // moved, so it took the history lock, read the facts again and the log's new bytes, and extended the memo with the
606
+ // entries this process had just written: in a tab, where each of those is a call into the kernel, about twenty calls
607
+ // per command that writes, taken again by the command after it. The writer, still holding the history lock, extends
608
+ // the memo with its own entries and keeps the tree's facts under the generation its own write left, so the next read
609
+ // asks one question. It does so only where the store can say its write was the one write since the generation it
610
+ // read just before (`generationAfterOwnWrite`), the facts were kept under that generation and the memo stood at them,
611
+ // the parent side's facts read after the append are still the kept ones, and the log grew by exactly its bytes;
612
+ // otherwise it extends nothing, and the next read checks as before.
613
+ /** Append `data` (whole JSONL lines of a service's branch log, `append` writing them) and, when the reads' memo held
614
+ * the tree as it stood, extend it by them. Called holding the directory's history lock. Answers the log's facts
615
+ * after the append (`size:mtimeMs`) when it read them, for a caller keyed on the same. */
616
+ export function appendToBranchLog(service, root, data, append) {
617
+ const store = getActiveWorldStore();
618
+ const memos = treeMemos.get(store);
619
+ const slot = `${service}\u0000${root ?? ''}`;
620
+ const held = memos?.get(slot);
621
+ const branchLog = branchLogPath(service, root);
622
+ const directory = dirname(branchLog);
623
+ const kept = keptFacts.get(store)?.get(directory);
624
+ const standing = held !== undefined && kept !== undefined && kept.parentKey === held.parentKey && kept.branchKey === held.branchKey;
625
+ const before = standing && store.generation && store.generationAfterOwnWrite ? store.generation(directory) : null;
626
+ append();
627
+ if (!memos || !held || !kept || before === null || kept.generation !== before)
628
+ return undefined;
629
+ const after = store.generationAfterOwnWrite(directory, before);
630
+ if (after === null)
631
+ return undefined;
632
+ // read after the generation, as treeFacts reads its facts. The parent side is read again too: a writer the generation
633
+ // does not see (an older version's, or one that died before its second byte) may have moved it under the kept
634
+ // generation, and carried forward its old key would outlive every append this process makes (S2 R17c review).
635
+ const branchKey = statKey(branchLog);
636
+ if (parentKeyOf(service, root) !== kept.parentKey)
637
+ return branchKey;
638
+ const grown = (key) => (key === '-' ? 0 : Number(key.slice(0, key.indexOf(':'))));
639
+ const fresh = [];
640
+ // line numbers in a refusal count from this append's first line
641
+ parseLines(`${branchLog} (appended)`, data.split('\n'), 0, fresh);
642
+ if (grown(branchKey) !== grown(held.branchKey) + Buffer.byteLength(data) || fresh.some((e) => e.op === 'revert'))
643
+ return branchKey;
644
+ // the write has landed: a fold that fails here drops the memo (extendMemo) and the next read folds in full, never failing the write
645
+ try {
646
+ extendMemo(memos, slot, held, fresh, branchKey, service);
647
+ }
648
+ catch {
649
+ return branchKey;
650
+ }
651
+ keptFacts.get(store).set(directory, { parentKey: kept.parentKey, branchKey, generation: after });
652
+ return branchKey;
530
653
  }
531
654
  /** What changed in a service's tree since `since` (a `treeStamp` an earlier read was made at): the subjects
532
655
  * written since, as resources; the keys (`type:id`) of those no longer in the tree; and, of the changed, the keys
@@ -637,7 +760,7 @@ function readTreeMapFull(service, root, opts = {}) {
637
760
  getActiveWorldStore().writeAtomic(checkpointPath(service, root), JSON.stringify({ parentDigest, branchDigest, branchCount: branch.length, subjects: [...tree.values()], ...(now === facts ? { facts } : {}) }));
638
761
  }
639
762
  return tree;
640
- });
763
+ }, { operation: 'tree fold' });
641
764
  }
642
765
  export function cutCheckpoint(service, root) {
643
766
  return withHistoryLock(worldPaths(service, root).dir, () => {
@@ -646,7 +769,7 @@ export function cutCheckpoint(service, root) {
646
769
  const inherited = inheritedHistory(service, root);
647
770
  const own = ownParentEntries(service, root);
648
771
  getActiveWorldStore().writeAtomic(checkpointPath(service, root), JSON.stringify({ parentDigest: hashFieldValue({ projectionVersion: TREE_PROJECTION_VERSION, inherited, own }), branchDigest: hashFieldValue(branchEntries(service, root)), branchCount: branchEntries(service, root).length, subjects }));
649
- });
772
+ }, { operation: 'checkpoint' });
650
773
  }
651
774
  /** The branch's entries the parent does not hold: `set` rows, not reverted, not landed. */
652
775
  export function unpushedEntries(service, root) {
@@ -745,7 +868,7 @@ export function appendParentEntry(entry, root) {
745
868
  index.ids.add(entry.id);
746
869
  index.key = statKey(path);
747
870
  return { appended: true };
748
- }));
871
+ }, { operation: 'parent append' }), { operation: 'parent append' });
749
872
  }
750
873
  /** The whole log of a twin as a served world hands it out: the parent view, then the branch's own
751
874
  * entries, one sequence with a position. A clone's parent is exactly this. */
@@ -823,7 +946,7 @@ export function rebaseBranch(service, root, opts = {}) {
823
946
  dropCheckpoint(service, root);
824
947
  }
825
948
  return { moved, from, to, conflicts };
826
- });
949
+ }, { operation: 'branch-pointer adjustment' });
827
950
  }
828
951
  /**
829
952
  * HISTORY through the kernel: every entry that touched a subject (or every subject of a type), parent
@@ -14,6 +14,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
14
14
  // is reached only when a compute pool is asked to act. The light pool delegates to the host's Substrate adapter.
15
15
  import { refuseReadOnlyWrite } from "./request-scope.js";
16
16
  import { nodeBuiltin } from "./serve-http.js";
17
+ import { refuseRequiredMachineTransport } from '../required-credential-transport.cjs';
17
18
  /** A declared compute capability gap, distinct from a failed admitted guest. */
18
19
  export function isMachineCapabilityGap(error) {
19
20
  return typeof error === 'object' && error !== null && 'code' in error && error.code === 'WORLD_MACHINE_CAPABILITY_GAP';
@@ -60,6 +61,7 @@ function dockerPool(world) {
60
61
  });
61
62
  return {
62
63
  kind: 'docker',
64
+ async admitRun() { refuseRequiredMachineTransport(process.env, 'Docker machine application transport'); },
63
65
  async status(name) {
64
66
  const state = JSON.parse(await docker(['inspect', '--format', '{{json .State}}', containerName(world, name)]));
65
67
  return state.Running ? { state: 'started' } : { state: 'exited', exitCode: state.ExitCode, ...(state.Error ? { reason: state.Error } : {}) };
@@ -67,6 +69,7 @@ function dockerPool(world) {
67
69
  async ports() { throw new Error('port discovery requires the light guest backing'); },
68
70
  async files() { throw new Error('file access requires the light guest backing'); },
69
71
  async run(spec) {
72
+ refuseRequiredMachineTransport(process.env, 'Docker machine application transport');
70
73
  const name = containerName(world, spec.name);
71
74
  await docker(['rm', '-f', name]).catch(() => undefined);
72
75
  const env = Object.entries(spec.env ?? {}).flatMap(([k, v]) => ['-e', `${k}=${v}`]);
@@ -80,6 +83,7 @@ function dockerPool(world) {
80
83
  return { ports: published };
81
84
  },
82
85
  async exec(name, spec) {
86
+ refuseRequiredMachineTransport(process.env, 'Docker machine exec transport');
83
87
  const { spawn } = nodeBuiltin('node:child_process');
84
88
  const env = Object.entries(spec.env ?? {}).flatMap(([k, v]) => ['-e', `${k}=${v}`]);
85
89
  // the shell names its own pid first, then becomes the process: its pid is the process's
@@ -165,7 +169,7 @@ function dockerPool(world) {
165
169
  return fetch(`http://127.0.0.1:${host}${source.pathname}${source.search}`, { method: request.method, headers, body, redirect: 'manual', signal: request.signal });
166
170
  },
167
171
  async pause(name) { await docker(['pause', containerName(world, name)]); },
168
- async resume(name) { await docker(['unpause', containerName(world, name)]); },
172
+ async resume(name) { refuseRequiredMachineTransport(process.env, 'Docker machine resume transport'); await docker(['unpause', containerName(world, name)]); },
169
173
  async stop(name) { await docker(['stop', containerName(world, name)]).catch(() => undefined); },
170
174
  async remove(name) { await docker(['rm', '-f', containerName(world, name)]).catch(() => undefined); },
171
175
  };