@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.
- package/dist/agent/actions.cjs +1869 -160
- package/dist/agent/actions.d.ts +77 -10
- package/dist/agent/actions.js +1870 -161
- package/dist/agent/catalog.cjs +102 -9
- package/dist/agent/catalog.d.ts +243 -0
- package/dist/agent/catalog.js +99 -9
- package/dist/agent/doc-snapshot.cjs +200 -2
- package/dist/agent/doc-snapshot.d.ts +91 -0
- package/dist/agent/doc-snapshot.js +199 -2
- package/dist/agent/runtime.cjs +9 -1
- package/dist/agent/runtime.d.ts +8 -0
- package/dist/agent/runtime.js +9 -1
- package/dist/generated/client.cjs +754 -770
- package/dist/generated/client.d.ts +10 -9
- package/dist/generated/client.js +754 -770
- package/dist/generated/contract.cjs +16178 -272
- package/dist/generated/contract.d.ts +38 -0
- package/dist/generated/contract.js +17419 -1510
- package/dist/index.cjs +6 -5
- package/dist/index.d.ts +2 -2
- package/dist/index.js +6 -5
- package/dist/introspection.cjs +59 -0
- package/dist/introspection.d.ts +3 -0
- package/dist/introspection.js +53 -0
- package/dist/runtime/document-rpc.cjs +179 -40
- package/dist/runtime/document-rpc.d.ts +13 -4
- package/dist/runtime/document-rpc.js +175 -40
- package/dist/runtime/embedded-cli.cjs +5 -68
- package/dist/runtime/embedded-cli.js +5 -67
- package/dist/runtime/embedded-document-host.cjs +28 -0
- package/dist/runtime/embedded-document-host.d.ts +1 -0
- package/dist/runtime/embedded-document-host.js +23 -0
- package/dist/runtime/embedded-platform.cjs +108 -0
- package/dist/runtime/embedded-platform.d.ts +5 -0
- package/dist/runtime/embedded-platform.js +99 -0
- package/dist/runtime/host.cjs +237 -24
- package/dist/runtime/host.d.ts +17 -0
- package/dist/runtime/host.js +237 -25
- package/dist/runtime/process.cjs +30 -9
- package/dist/runtime/process.d.ts +9 -0
- package/dist/runtime/process.js +29 -9
- package/dist/runtime/sdk-version.generated.cjs +8 -0
- package/dist/runtime/sdk-version.generated.d.ts +1 -0
- package/dist/runtime/sdk-version.generated.js +4 -0
- package/dist/runtime/transport-common.cjs +1 -0
- package/dist/runtime/transport-common.d.ts +28 -10
- package/dist/runtime/transport-common.js +1 -1
- package/package.json +7 -7
- package/tools/__pycache__/__init__.cpython-311.pyc +0 -0
- package/tools/__pycache__/intent_dispatch_generated.cpython-311.pyc +0 -0
- 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
|
+
}
|
package/dist/runtime/host.cjs
CHANGED
|
@@ -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 (
|
|
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
|
-
|
|
149
|
-
const
|
|
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
|
|
158
|
-
const
|
|
159
|
-
|
|
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
|
-
...
|
|
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'
|
|
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
|
-
|
|
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;
|
package/dist/runtime/host.d.ts
CHANGED
|
@@ -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;
|