@superdoc/sdk 2.6.0 → 2.8.0

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 (51) hide show
  1. package/dist/agent/actions.cjs +1869 -160
  2. package/dist/agent/actions.d.ts +77 -10
  3. package/dist/agent/actions.js +1870 -161
  4. package/dist/agent/catalog.cjs +102 -9
  5. package/dist/agent/catalog.d.ts +243 -0
  6. package/dist/agent/catalog.js +99 -9
  7. package/dist/agent/doc-snapshot.cjs +200 -2
  8. package/dist/agent/doc-snapshot.d.ts +91 -0
  9. package/dist/agent/doc-snapshot.js +199 -2
  10. package/dist/agent/runtime.cjs +9 -1
  11. package/dist/agent/runtime.d.ts +8 -0
  12. package/dist/agent/runtime.js +9 -1
  13. package/dist/generated/client.cjs +754 -770
  14. package/dist/generated/client.d.ts +10 -9
  15. package/dist/generated/client.js +754 -770
  16. package/dist/generated/contract.cjs +16178 -272
  17. package/dist/generated/contract.d.ts +38 -0
  18. package/dist/generated/contract.js +17419 -1510
  19. package/dist/index.cjs +6 -5
  20. package/dist/index.d.ts +2 -2
  21. package/dist/index.js +6 -5
  22. package/dist/introspection.cjs +59 -0
  23. package/dist/introspection.d.ts +3 -0
  24. package/dist/introspection.js +53 -0
  25. package/dist/runtime/document-rpc.cjs +179 -40
  26. package/dist/runtime/document-rpc.d.ts +13 -4
  27. package/dist/runtime/document-rpc.js +175 -40
  28. package/dist/runtime/embedded-cli.cjs +5 -68
  29. package/dist/runtime/embedded-cli.js +5 -67
  30. package/dist/runtime/embedded-document-host.cjs +28 -0
  31. package/dist/runtime/embedded-document-host.d.ts +1 -0
  32. package/dist/runtime/embedded-document-host.js +23 -0
  33. package/dist/runtime/embedded-platform.cjs +108 -0
  34. package/dist/runtime/embedded-platform.d.ts +5 -0
  35. package/dist/runtime/embedded-platform.js +99 -0
  36. package/dist/runtime/host.cjs +237 -24
  37. package/dist/runtime/host.d.ts +17 -0
  38. package/dist/runtime/host.js +237 -25
  39. package/dist/runtime/process.cjs +30 -9
  40. package/dist/runtime/process.d.ts +9 -0
  41. package/dist/runtime/process.js +29 -9
  42. package/dist/runtime/sdk-version.generated.cjs +8 -0
  43. package/dist/runtime/sdk-version.generated.d.ts +1 -0
  44. package/dist/runtime/sdk-version.generated.js +4 -0
  45. package/dist/runtime/transport-common.cjs +1 -0
  46. package/dist/runtime/transport-common.d.ts +28 -10
  47. package/dist/runtime/transport-common.js +1 -1
  48. package/package.json +7 -7
  49. package/tools/__pycache__/__init__.cpython-311.pyc +0 -0
  50. package/tools/__pycache__/intent_dispatch_generated.cpython-311.pyc +0 -0
  51. package/tools/tools-policy.json +1 -1
@@ -0,0 +1,99 @@
1
+ import { chmodSync, existsSync } from 'node:fs';
2
+ import { createRequire } from 'node:module';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { SDK_VERSION } from './sdk-version.generated.js';
6
+ const require = createRequire(import.meta.url);
7
+ const TARGET_TO_PACKAGE = {
8
+ 'darwin-arm64': '@superdoc/sdk-darwin-arm64',
9
+ 'darwin-x64': '@superdoc/sdk-darwin-x64',
10
+ 'linux-x64': '@superdoc/sdk-linux-x64',
11
+ 'linux-arm64': '@superdoc/sdk-linux-arm64',
12
+ 'windows-x64': '@superdoc/sdk-windows-x64',
13
+ };
14
+ const TARGET_TO_DIR = {
15
+ 'darwin-arm64': 'sdk-darwin-arm64',
16
+ 'darwin-x64': 'sdk-darwin-x64',
17
+ 'linux-x64': 'sdk-linux-x64',
18
+ 'linux-arm64': 'sdk-linux-arm64',
19
+ 'windows-x64': 'sdk-windows-x64',
20
+ };
21
+ export function resolveEmbeddedTarget(platform = process.platform, arch = process.arch) {
22
+ if (platform === 'darwin' && arch === 'arm64')
23
+ return 'darwin-arm64';
24
+ if (platform === 'darwin' && arch === 'x64')
25
+ return 'darwin-x64';
26
+ if (platform === 'linux' && arch === 'x64')
27
+ return 'linux-x64';
28
+ if (platform === 'linux' && arch === 'arm64')
29
+ return 'linux-arm64';
30
+ if (platform === 'win32' && arch === 'x64')
31
+ return 'windows-x64';
32
+ return null;
33
+ }
34
+ export function embeddedPlatformPackage(target) {
35
+ return TARGET_TO_PACKAGE[target];
36
+ }
37
+ /**
38
+ * The binary shipped inside this package (`platforms/<target>/bin/`). A build
39
+ * that carries its own host is authoritative — it is the only copy guaranteed
40
+ * to match this SDK's code — so it is tried before any resolver lookup.
41
+ */
42
+ function resolveFromPackageLocal(target, binaryName) {
43
+ const workspacePath = path.resolve(fileURLToPath(new URL('../../platforms', import.meta.url)), TARGET_TO_DIR[target], 'bin', binaryName);
44
+ return existsSync(workspacePath) ? workspacePath : null;
45
+ }
46
+ /**
47
+ * Resolve the platform package through Node, and reject a version that is not
48
+ * this SDK's own.
49
+ *
50
+ * Resolution starts from the installed SDK so pnpm's isolated optional
51
+ * dependency remains visible after the SDK itself is bundled. `require.resolve`
52
+ * can also fall back to NODE_PATH, so the version check prevents a host binary
53
+ * from an unrelated SDK version in a flat store from satisfying this lookup.
54
+ * optionalDependencies pin the platform package to an exact version, so a
55
+ * mismatch here is never legitimate.
56
+ */
57
+ function resolveFromPlatformPackage(target, binaryName) {
58
+ const pkg = TARGET_TO_PACKAGE[target];
59
+ let sdkRequire;
60
+ try {
61
+ sdkRequire = createRequire(require.resolve('@superdoc/sdk'));
62
+ }
63
+ catch {
64
+ sdkRequire = require;
65
+ }
66
+ let binaryPath;
67
+ try {
68
+ binaryPath = sdkRequire.resolve(`${pkg}/bin/${binaryName}`);
69
+ }
70
+ catch {
71
+ return null;
72
+ }
73
+ try {
74
+ const platformVersion = sdkRequire(sdkRequire.resolve(`${pkg}/package.json`)).version;
75
+ if (platformVersion !== SDK_VERSION)
76
+ return null;
77
+ }
78
+ catch {
79
+ // An unreadable platform manifest is itself disqualifying: an unverifiable
80
+ // host is exactly the case this guard exists to reject.
81
+ return null;
82
+ }
83
+ return binaryPath;
84
+ }
85
+ export function resolveEmbeddedPlatformBinary(target, binaryName) {
86
+ // Package-local first: a self-contained build must never be overridden by
87
+ // whatever the module resolver happens to find on NODE_PATH.
88
+ return resolveFromPackageLocal(target, binaryName) ?? resolveFromPlatformPackage(target, binaryName);
89
+ }
90
+ export function ensureEmbeddedExecutable(binaryPath) {
91
+ if (process.platform === 'win32')
92
+ return;
93
+ try {
94
+ chmodSync(binaryPath, 0o755);
95
+ }
96
+ catch {
97
+ // Non-fatal: spawn() reports the actionable execution error.
98
+ }
99
+ }
@@ -23,6 +23,21 @@ const HOST_DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
23
23
  // abort. The buffer absorbs JSON-RPC serialization, stdio drain, and event-
24
24
  // loop latency.
25
25
  const WATCHDOG_HEADROOM_MS = 5_000;
26
+ function mapCliInvocationResult(operation, value) {
27
+ const envelopeKey = operation.responseEnvelopeKey;
28
+ if (envelopeKey === null || envelopeKey === undefined)
29
+ return value;
30
+ if (typeof envelopeKey !== 'string' || envelopeKey.length === 0) {
31
+ throw new errors.SuperDocCliError('Generated operation has invalid response envelope metadata.', {
32
+ code: 'HOST_PROTOCOL_ERROR',
33
+ details: { operationId: operation.operationId, responseEnvelopeKey: envelopeKey },
34
+ });
35
+ }
36
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
37
+ return value;
38
+ const extracted = value[envelopeKey];
39
+ return extracted === undefined ? value : extracted;
40
+ }
26
41
  /**
27
42
  * Builds the argv passed to `spawn` for `superdoc host --stdio`. Propagates
28
43
  * `requestTimeoutMs` to the host via `--request-timeout-ms`, since the SDK
@@ -83,9 +98,15 @@ class HostTransport {
83
98
  pending = new Map();
84
99
  nextRequestId = 1;
85
100
  connecting = null;
101
+ disposePromise = null;
102
+ terminalDisposalError = null;
86
103
  stopping = false;
104
+ activeCalls = new Set();
87
105
  hostFeatures = new Set();
88
106
  documentRpcSessions = new Set();
107
+ dirtyDocumentRpcSessions = new Set();
108
+ documentRpcMutationStates = new Map();
109
+ indeterminateDiscardCloses = new Set();
89
110
  constructor(options) {
90
111
  this.hostBin = options.hostBin;
91
112
  this.processMode = options.processMode ?? 'cli';
@@ -101,22 +122,48 @@ class HostTransport {
101
122
  details: { defaultChangeMode: options.defaultChangeMode },
102
123
  });
103
124
  }
104
- this.defaultChangeMode = options.defaultChangeMode;
125
+ this.defaultChangeMode = options.defaultChangeMode ?? undefined;
105
126
  this.user = options.user;
106
- this.documentRpcEnabled = documentRpc.documentRpcEnabled(options.env);
107
- if (this.processMode === 'document' && !this.documentRpcEnabled) {
108
- throw new errors.SuperDocCliError('SUPERDOC_SDK_DOCUMENT_HOST_BIN requires SUPERDOC_SDK_DOCUMENT_RPC=1.', {
109
- code: 'INVALID_ARGUMENT',
110
- });
111
- }
127
+ this.documentRpcEnabled = this.processMode === 'document' || documentRpc.documentRpcEnabled(options.env);
112
128
  }
113
129
  async connect() {
114
- await this.ensureConnected();
130
+ await this.runWhileActive(() => this.ensureConnected());
115
131
  }
116
132
  async dispose() {
117
- if (!this.child)
133
+ if (this.disposePromise)
134
+ return this.disposePromise;
135
+ if (this.terminalDisposalError)
136
+ throw this.terminalDisposalError;
137
+ if (!this.child && !this.connecting && this.activeCalls.size === 0)
118
138
  return;
119
139
  this.stopping = true;
140
+ const disposal = this.disposeAfterActiveCalls();
141
+ this.disposePromise = disposal;
142
+ try {
143
+ await disposal;
144
+ }
145
+ finally {
146
+ if (this.disposePromise === disposal)
147
+ this.disposePromise = null;
148
+ this.stopping = false;
149
+ }
150
+ }
151
+ async disposeAfterActiveCalls() {
152
+ await Promise.allSettled(Array.from(this.activeCalls));
153
+ if (this.terminalDisposalError)
154
+ throw this.terminalDisposalError;
155
+ if (!this.child)
156
+ return;
157
+ try {
158
+ await this.reconcileIndeterminateDiscardCloses();
159
+ await this.persistDirtyDocumentRpcSessions();
160
+ }
161
+ catch (error) {
162
+ if (!this.child && error instanceof errors.SuperDocCliError) {
163
+ this.terminalDisposalError = error;
164
+ }
165
+ throw error;
166
+ }
120
167
  const child = this.child;
121
168
  try {
122
169
  await this.sendJsonRpcRequest('host.shutdown', {}, this.shutdownTimeoutMs);
@@ -135,29 +182,80 @@ class HostTransport {
135
182
  });
136
183
  });
137
184
  this.cleanupProcess(null);
138
- this.stopping = false;
139
185
  }
140
186
  async invoke(operation, params = {}, options = {}) {
187
+ return this.runWhileActive(() => this.invokeWhileActive(operation, params, options));
188
+ }
189
+ async invokeWhileActive(operation, params, options) {
141
190
  await this.ensureConnected();
191
+ const shouldOpenThroughCliTimeoutFallback = this.processMode === 'cli' &&
192
+ (this.requestTimeoutMs !== undefined ||
193
+ (options.timeoutMs !== undefined && !this.hostFeatures.has(documentRpc.DOCUMENT_RPC_REQUEST_TIMEOUT_FEATURE)));
142
194
  if (this.documentRpcEnabled &&
143
195
  documentRpc.supportsDocumentRpc(this.hostFeatures) &&
144
- this.defaultChangeMode === undefined &&
145
- this.user === undefined &&
196
+ (this.defaultChangeMode === undefined || this.hostFeatures.has(documentRpc.DOCUMENT_RPC_DEFAULT_CHANGE_MODE_FEATURE)) &&
146
197
  operation.operationId === 'doc.open' &&
147
- documentRpc.canOpenWithDocumentRpc(params, options)) {
148
- const opened = documentRpc.buildDocumentOpenParams(params);
149
- const response = await this.sendJsonRpcRequest('document.open', opened.params, this.resolveWatchdogTimeout(options.timeoutMs));
198
+ documentRpc.canOpenWithDocumentRpc(params, options) &&
199
+ !shouldOpenThroughCliTimeoutFallback) {
200
+ const opened = documentRpc.buildDocumentOpenParams(params, this.user, this.defaultChangeMode);
201
+ const requestTimeoutMs = this.resolveDocumentRequestTimeout(options.timeoutMs);
202
+ const response = await this.sendJsonRpcRequest('document.open', opened.params, this.resolveWatchdogTimeout(options.timeoutMs), { requestTimeoutMs });
150
203
  const result = documentRpc.mapDocumentOpenResult(response, opened.sessionId, params.doc);
151
204
  this.documentRpcSessions.add(opened.sessionId);
205
+ this.indeterminateDiscardCloses.delete(opened.sessionId);
152
206
  return result;
153
207
  }
154
208
  const sessionId = typeof params.sessionId === 'string' ? params.sessionId : undefined;
155
209
  if (sessionId !== undefined && this.documentRpcSessions.has(sessionId)) {
156
- const request = documentRpc.buildDocumentInvokeParams(sessionId, operation, params, options);
157
- const response = await this.sendJsonRpcRequest(request.method, request.params, this.resolveWatchdogTimeout(options.timeoutMs));
158
- const result = documentRpc.mapDocumentLifecycleResult(operation.operationId, response, sessionId);
159
- if (operation.operationId === 'doc.close')
210
+ const request = documentRpc.buildDocumentInvokeParams(sessionId, operation, params, options, this.hostFeatures);
211
+ const supportsResponseTimeout = documentRpc.documentRpcRequestSupportsResponseTimeout(operation, request);
212
+ const requestTimeoutMs = supportsResponseTimeout
213
+ ? this.resolveDocumentRequestTimeout(options.timeoutMs)
214
+ : undefined;
215
+ const documentOperation = operation;
216
+ const requestOptions = typeof request.params.options === 'object' &&
217
+ request.params.options !== null &&
218
+ !Array.isArray(request.params.options)
219
+ ? request.params.options
220
+ : undefined;
221
+ const tracksMutation = request.method === 'document.invoke' && documentOperation.mutates === true && requestOptions?.dryRun !== true;
222
+ if (tracksMutation)
223
+ this.beginDocumentRpcMutation(sessionId);
224
+ let result;
225
+ try {
226
+ const response = await this.sendJsonRpcRequest(request.method, request.params, this.resolveWatchdogTimeout(options.timeoutMs), { requestTimeoutMs });
227
+ result = documentRpc.mapDocumentLifecycleResult(operation.operationId, response, sessionId, operation.operationId === 'doc.save' ? request.params.path === undefined : undefined);
228
+ }
229
+ catch (error) {
230
+ if (tracksMutation) {
231
+ const indeterminate = error instanceof errors.SuperDocCliError && ['TIMEOUT', 'HOST_TIMEOUT', 'HOST_DISCONNECTED'].includes(error.code);
232
+ this.settleDocumentRpcMutation(sessionId, indeterminate ? 'indeterminate' : 'not-applied');
233
+ }
234
+ if (operation.operationId === 'doc.close' &&
235
+ request.params.discard === true &&
236
+ error instanceof errors.SuperDocCliError &&
237
+ error.code === 'TIMEOUT') {
238
+ this.indeterminateDiscardCloses.add(sessionId);
239
+ }
240
+ throw error;
241
+ }
242
+ if (tracksMutation) {
243
+ const failedReceipt = typeof result === 'object' &&
244
+ result !== null &&
245
+ !Array.isArray(result) &&
246
+ result.success === false;
247
+ this.settleDocumentRpcMutation(sessionId, failedReceipt ? 'not-applied' : 'applied');
248
+ }
249
+ if (operation.operationId === 'doc.close') {
160
250
  this.documentRpcSessions.delete(sessionId);
251
+ this.clearDirtyDocumentRpcSession(sessionId);
252
+ this.indeterminateDiscardCloses.delete(sessionId);
253
+ }
254
+ else if (operation.operationId === 'doc.save') {
255
+ if (request.params.mode === undefined || request.params.mode === 'review-preserving') {
256
+ this.markDocumentRpcSessionSaved(sessionId);
257
+ }
258
+ }
161
259
  return result;
162
260
  }
163
261
  if (this.processMode === 'document') {
@@ -166,6 +264,18 @@ class HostTransport {
166
264
  details: { operationId: operation.operationId },
167
265
  });
168
266
  }
267
+ const cliHostRequestTimeoutMs = this.requestTimeoutMs ?? HOST_DEFAULT_REQUEST_TIMEOUT_MS;
268
+ if (options.timeoutMs !== undefined && options.timeoutMs > cliHostRequestTimeoutMs) {
269
+ throw new errors.SuperDocCliError(`CLI host cannot honor timeoutMs=${options.timeoutMs} above its ${cliHostRequestTimeoutMs}ms request ceiling.`, {
270
+ code: 'DOCUMENT_RPC_INPUT_UNSUPPORTED',
271
+ details: {
272
+ feature: documentRpc.DOCUMENT_RPC_REQUEST_TIMEOUT_FEATURE,
273
+ operationId: operation.operationId,
274
+ timeoutMs: options.timeoutMs,
275
+ hostRequestTimeoutMs: cliHostRequestTimeoutMs,
276
+ },
277
+ });
278
+ }
169
279
  const argv = transportCommon.buildOperationArgv(operation, params, options, this.requestTimeoutMs, this.defaultChangeMode, this.user);
170
280
  const stdinBase64 = options.stdinBytes ? Buffer.from(options.stdinBytes).toString('base64') : '';
171
281
  const watchdogTimeout = this.resolveWatchdogTimeout(options.timeoutMs);
@@ -180,7 +290,21 @@ class HostTransport {
180
290
  });
181
291
  }
182
292
  const resultRecord = response;
183
- return resultRecord.data;
293
+ return mapCliInvocationResult(operation, resultRecord.data);
294
+ }
295
+ runWhileActive(start) {
296
+ if (this.terminalDisposalError) {
297
+ return Promise.reject(this.terminalDisposalError);
298
+ }
299
+ if (this.stopping) {
300
+ return Promise.reject(new errors.SuperDocCliError('Host is disposing.', {
301
+ code: 'HOST_DISCONNECTED',
302
+ }));
303
+ }
304
+ const activeCall = start();
305
+ this.activeCalls.add(activeCall);
306
+ void activeCall.then(() => this.activeCalls.delete(activeCall), () => this.activeCalls.delete(activeCall));
307
+ return activeCall;
184
308
  }
185
309
  async ensureConnected() {
186
310
  if (this.connecting) {
@@ -206,7 +330,7 @@ class HostTransport {
206
330
  const child = node_child_process.spawn(command, args, {
207
331
  env: {
208
332
  ...process.env,
209
- ...(this.env ?? {}),
333
+ ...this.env,
210
334
  },
211
335
  stdio: ['pipe', 'pipe', 'pipe'],
212
336
  });
@@ -234,7 +358,7 @@ class HostTransport {
234
358
  }));
235
359
  });
236
360
  child.on('close', (code, signal) => {
237
- if (this.stopping) {
361
+ if (this.stopping && this.dirtyDocumentRpcSessions.size === 0) {
238
362
  this.cleanupProcess(null);
239
363
  return;
240
364
  }
@@ -285,7 +409,13 @@ class HostTransport {
285
409
  details: { features },
286
410
  });
287
411
  }
288
- const requiredFeatures = this.processMode === 'document' ? DOCUMENT_HOST_REQUIRED_FEATURES : CLI_HOST_REQUIRED_FEATURES;
412
+ const requiredFeatures = this.processMode === 'document'
413
+ ? [
414
+ ...DOCUMENT_HOST_REQUIRED_FEATURES,
415
+ ...(this.defaultChangeMode === undefined ? [] : [documentRpc.DOCUMENT_RPC_DEFAULT_CHANGE_MODE_FEATURE]),
416
+ ...(this.requestTimeoutMs === undefined ? [] : [documentRpc.DOCUMENT_RPC_REQUEST_TIMEOUT_FEATURE]),
417
+ ]
418
+ : CLI_HOST_REQUIRED_FEATURES;
289
419
  for (const requiredFeature of requiredFeatures) {
290
420
  if (!features.includes(requiredFeature)) {
291
421
  throw new errors.SuperDocCliError(`Host does not support required feature: ${requiredFeature}`, {
@@ -301,7 +431,79 @@ class HostTransport {
301
431
  resolveWatchdogTimeout(timeoutMsOverride) {
302
432
  return resolveJsWatchdogTimeout(this.watchdogTimeoutMs, this.requestTimeoutMs, timeoutMsOverride);
303
433
  }
304
- async sendJsonRpcRequest(method, params, watchdogTimeoutMs) {
434
+ resolveDocumentRequestTimeout(timeoutMsOverride) {
435
+ const requestTimeoutMs = timeoutMsOverride ?? this.requestTimeoutMs;
436
+ if (requestTimeoutMs === undefined)
437
+ return undefined;
438
+ if (this.hostFeatures.has(documentRpc.DOCUMENT_RPC_REQUEST_TIMEOUT_FEATURE))
439
+ return requestTimeoutMs;
440
+ if (this.processMode === 'cli' && timeoutMsOverride === undefined)
441
+ return undefined;
442
+ throw new errors.SuperDocCliError(`Structured document RPC host does not support ${documentRpc.DOCUMENT_RPC_REQUEST_TIMEOUT_FEATURE}.`, {
443
+ code: 'DOCUMENT_RPC_INPUT_UNSUPPORTED',
444
+ details: { feature: documentRpc.DOCUMENT_RPC_REQUEST_TIMEOUT_FEATURE },
445
+ });
446
+ }
447
+ beginDocumentRpcMutation(sessionId) {
448
+ const state = this.documentRpcMutationStates.get(sessionId) ?? {
449
+ confirmedDirty: this.dirtyDocumentRpcSessions.has(sessionId),
450
+ pendingCount: 0,
451
+ };
452
+ state.pendingCount += 1;
453
+ this.documentRpcMutationStates.set(sessionId, state);
454
+ this.dirtyDocumentRpcSessions.add(sessionId);
455
+ }
456
+ settleDocumentRpcMutation(sessionId, outcome) {
457
+ const state = this.documentRpcMutationStates.get(sessionId);
458
+ if (!state)
459
+ return;
460
+ state.pendingCount = Math.max(0, state.pendingCount - 1);
461
+ if (outcome !== 'not-applied')
462
+ state.confirmedDirty = true;
463
+ if (state.confirmedDirty || state.pendingCount > 0) {
464
+ this.dirtyDocumentRpcSessions.add(sessionId);
465
+ return;
466
+ }
467
+ this.clearDirtyDocumentRpcSession(sessionId);
468
+ }
469
+ clearDirtyDocumentRpcSession(sessionId) {
470
+ this.dirtyDocumentRpcSessions.delete(sessionId);
471
+ this.documentRpcMutationStates.delete(sessionId);
472
+ }
473
+ markDocumentRpcSessionSaved(sessionId) {
474
+ const state = this.documentRpcMutationStates.get(sessionId);
475
+ if (!state || state.pendingCount === 0) {
476
+ this.clearDirtyDocumentRpcSession(sessionId);
477
+ return;
478
+ }
479
+ state.confirmedDirty = false;
480
+ this.dirtyDocumentRpcSessions.add(sessionId);
481
+ }
482
+ async persistDirtyDocumentRpcSessions() {
483
+ if (this.dirtyDocumentRpcSessions.size === 0)
484
+ return;
485
+ if (!this.hostFeatures.has(documentRpc.DOCUMENT_RPC_SOURCE_SAVE_FEATURE)) {
486
+ throw new errors.SuperDocCliError('Document host cannot persist dirty sessions during client disposal because it does not support source saves.', {
487
+ code: 'DOCUMENT_RPC_INPUT_UNSUPPORTED',
488
+ details: { feature: documentRpc.DOCUMENT_RPC_SOURCE_SAVE_FEATURE },
489
+ });
490
+ }
491
+ for (const sessionId of this.dirtyDocumentRpcSessions) {
492
+ const response = await this.sendJsonRpcRequest('document.save', { sessionId }, this.resolveWatchdogTimeout(undefined));
493
+ documentRpc.mapDocumentLifecycleResult('doc.save', response, sessionId, true);
494
+ this.markDocumentRpcSessionSaved(sessionId);
495
+ }
496
+ }
497
+ async reconcileIndeterminateDiscardCloses() {
498
+ for (const sessionId of this.indeterminateDiscardCloses) {
499
+ const response = await this.sendJsonRpcRequest('document.close', { sessionId, discard: true }, this.resolveWatchdogTimeout(undefined));
500
+ documentRpc.mapDocumentLifecycleResult('doc.close', response, sessionId);
501
+ this.documentRpcSessions.delete(sessionId);
502
+ this.clearDirtyDocumentRpcSession(sessionId);
503
+ this.indeterminateDiscardCloses.delete(sessionId);
504
+ }
505
+ }
506
+ async sendJsonRpcRequest(method, params, watchdogTimeoutMs, metadata = {}) {
305
507
  const child = this.child;
306
508
  if (!child || !child.stdin.writable) {
307
509
  throw new errors.SuperDocCliError('Host process is not available.', {
@@ -321,6 +523,7 @@ class HostTransport {
321
523
  id,
322
524
  method,
323
525
  params,
526
+ ...(metadata.requestTimeoutMs === undefined ? {} : { requestTimeoutMs: metadata.requestTimeoutMs }),
324
527
  });
325
528
  const promise = new Promise((resolve, reject) => {
326
529
  const timer = setTimeout(() => {
@@ -431,8 +634,17 @@ class HostTransport {
431
634
  this.cleanupProcess(error);
432
635
  }
433
636
  cleanupProcess(error) {
637
+ if (error && this.dirtyDocumentRpcSessions.size > 0 && !this.terminalDisposalError) {
638
+ this.terminalDisposalError = new errors.SuperDocCliError('Document host disconnected before dirty sessions could be persisted.', {
639
+ code: 'HOST_DISCONNECTED',
640
+ details: { causeCode: error.code, causeDetails: error.details },
641
+ });
642
+ }
434
643
  this.hostFeatures.clear();
435
644
  this.documentRpcSessions.clear();
645
+ this.dirtyDocumentRpcSessions.clear();
646
+ this.documentRpcMutationStates.clear();
647
+ this.indeterminateDiscardCloses.clear();
436
648
  const child = this.child;
437
649
  if (child) {
438
650
  child.removeAllListeners();
@@ -460,4 +672,5 @@ class HostTransport {
460
672
  exports.HostTransport = HostTransport;
461
673
  exports.buildDocumentHostSpawnArgs = buildDocumentHostSpawnArgs;
462
674
  exports.buildHostSpawnArgs = buildHostSpawnArgs;
675
+ exports.mapCliInvocationResult = mapCliInvocationResult;
463
676
  exports.resolveJsWatchdogTimeout = resolveJsWatchdogTimeout;
@@ -1,4 +1,5 @@
1
1
  import { type InvokeOptions, type OperationSpec, type SuperDocClientOptions } from './transport-common.js';
2
+ export declare function mapCliInvocationResult(operation: OperationSpec, value: unknown): unknown;
2
3
  /**
3
4
  * Builds the argv passed to `spawn` for `superdoc host --stdio`. Propagates
4
5
  * `requestTimeoutMs` to the host via `--request-timeout-ms`, since the SDK
@@ -45,20 +46,36 @@ export declare class HostTransport {
45
46
  private readonly pending;
46
47
  private nextRequestId;
47
48
  private connecting;
49
+ private disposePromise;
50
+ private terminalDisposalError;
48
51
  private stopping;
52
+ private readonly activeCalls;
49
53
  private readonly hostFeatures;
50
54
  private readonly documentRpcSessions;
55
+ private readonly dirtyDocumentRpcSessions;
56
+ private readonly documentRpcMutationStates;
57
+ private readonly indeterminateDiscardCloses;
51
58
  constructor(options: {
52
59
  hostBin: string;
53
60
  processMode?: 'cli' | 'document';
54
61
  } & SuperDocClientOptions);
55
62
  connect(): Promise<void>;
56
63
  dispose(): Promise<void>;
64
+ private disposeAfterActiveCalls;
57
65
  invoke<TData = unknown>(operation: OperationSpec, params?: Record<string, unknown>, options?: InvokeOptions): Promise<TData>;
66
+ private invokeWhileActive;
67
+ private runWhileActive;
58
68
  private ensureConnected;
59
69
  private startHostProcess;
60
70
  private assertCapabilities;
61
71
  private resolveWatchdogTimeout;
72
+ private resolveDocumentRequestTimeout;
73
+ private beginDocumentRpcMutation;
74
+ private settleDocumentRpcMutation;
75
+ private clearDirtyDocumentRpcSession;
76
+ private markDocumentRpcSessionSaved;
77
+ private persistDirtyDocumentRpcSessions;
78
+ private reconcileIndeterminateDiscardCloses;
62
79
  private sendJsonRpcRequest;
63
80
  private onStdoutLine;
64
81
  private mapJsonRpcError;