@superdoc/sdk 2.7.0 → 2.9.0-next.1
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/README.md +21 -0
- package/dist/generated/client.d.ts +1 -0
- package/dist/generated/contract.cjs +9 -0
- package/dist/generated/contract.js +10 -0
- package/dist/index.cjs +22 -12
- package/dist/index.d.ts +4 -3
- package/dist/index.js +22 -12
- package/dist/presets/core.cjs +62 -10
- package/dist/presets/core.js +62 -10
- package/dist/runtime/embedded-platform.cjs +18 -12
- package/dist/runtime/embedded-platform.js +18 -12
- package/dist/runtime/host.cjs +192 -7
- package/dist/runtime/host.d.ts +18 -2
- package/dist/runtime/host.js +193 -9
- package/dist/runtime/process.cjs +6 -1
- package/dist/runtime/process.d.ts +4 -3
- package/dist/runtime/process.js +5 -1
- 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 +22 -0
- package/dist/runtime/transport-common.d.ts +11 -0
- package/dist/runtime/transport-common.js +21 -0
- package/package.json +8 -7
- package/tools/__pycache__/__init__.cpython-311.pyc +0 -0
- package/tools/__pycache__/intent_dispatch_generated.cpython-311.pyc +0 -0
package/dist/runtime/host.cjs
CHANGED
|
@@ -8,6 +8,7 @@ var documentRpc = require('./document-rpc.cjs');
|
|
|
8
8
|
|
|
9
9
|
const HOST_PROTOCOL_VERSION = '1.0';
|
|
10
10
|
const CLI_HOST_REQUIRED_FEATURES = ['cli.invoke', 'host.shutdown'];
|
|
11
|
+
const COLLABORATION_AUTH_PER_OPEN_FEATURE = 'collaboration.auth.perOpen';
|
|
11
12
|
const DOCUMENT_HOST_REQUIRED_FEATURES = [...documentRpc.DOCUMENT_RPC_FEATURES, 'host.shutdown'];
|
|
12
13
|
const CHANGE_MODES = ['direct', 'tracked'];
|
|
13
14
|
const FORWARD_HOST_STDERR = typeof process !== 'undefined' && typeof process.env?.SUPERDOC_DEBUG_TEXT_REWRITE === 'string'
|
|
@@ -98,9 +99,15 @@ class HostTransport {
|
|
|
98
99
|
pending = new Map();
|
|
99
100
|
nextRequestId = 1;
|
|
100
101
|
connecting = null;
|
|
102
|
+
disposePromise = null;
|
|
103
|
+
terminalDisposalError = null;
|
|
101
104
|
stopping = false;
|
|
105
|
+
activeCalls = new Set();
|
|
102
106
|
hostFeatures = new Set();
|
|
103
107
|
documentRpcSessions = new Set();
|
|
108
|
+
dirtyDocumentRpcSessions = new Set();
|
|
109
|
+
documentRpcMutationStates = new Map();
|
|
110
|
+
indeterminateDiscardCloses = new Set();
|
|
104
111
|
constructor(options) {
|
|
105
112
|
this.hostBin = options.hostBin;
|
|
106
113
|
this.processMode = options.processMode ?? 'cli';
|
|
@@ -121,12 +128,43 @@ class HostTransport {
|
|
|
121
128
|
this.documentRpcEnabled = this.processMode === 'document' || documentRpc.documentRpcEnabled(options.env);
|
|
122
129
|
}
|
|
123
130
|
async connect() {
|
|
124
|
-
await this.ensureConnected();
|
|
131
|
+
await this.runWhileActive(() => this.ensureConnected());
|
|
125
132
|
}
|
|
126
133
|
async dispose() {
|
|
127
|
-
if (
|
|
134
|
+
if (this.disposePromise)
|
|
135
|
+
return this.disposePromise;
|
|
136
|
+
if (this.terminalDisposalError)
|
|
137
|
+
throw this.terminalDisposalError;
|
|
138
|
+
if (!this.child && !this.connecting && this.activeCalls.size === 0)
|
|
128
139
|
return;
|
|
129
140
|
this.stopping = true;
|
|
141
|
+
const disposal = this.disposeAfterActiveCalls();
|
|
142
|
+
this.disposePromise = disposal;
|
|
143
|
+
try {
|
|
144
|
+
await disposal;
|
|
145
|
+
}
|
|
146
|
+
finally {
|
|
147
|
+
if (this.disposePromise === disposal)
|
|
148
|
+
this.disposePromise = null;
|
|
149
|
+
this.stopping = false;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
async disposeAfterActiveCalls() {
|
|
153
|
+
await Promise.allSettled(Array.from(this.activeCalls));
|
|
154
|
+
if (this.terminalDisposalError)
|
|
155
|
+
throw this.terminalDisposalError;
|
|
156
|
+
if (!this.child)
|
|
157
|
+
return;
|
|
158
|
+
try {
|
|
159
|
+
await this.reconcileIndeterminateDiscardCloses();
|
|
160
|
+
await this.persistDirtyDocumentRpcSessions();
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
if (!this.child && error instanceof errors.SuperDocCliError) {
|
|
164
|
+
this.terminalDisposalError = error;
|
|
165
|
+
}
|
|
166
|
+
throw error;
|
|
167
|
+
}
|
|
130
168
|
const child = this.child;
|
|
131
169
|
try {
|
|
132
170
|
await this.sendJsonRpcRequest('host.shutdown', {}, this.shutdownTimeoutMs);
|
|
@@ -145,10 +183,32 @@ class HostTransport {
|
|
|
145
183
|
});
|
|
146
184
|
});
|
|
147
185
|
this.cleanupProcess(null);
|
|
148
|
-
this.stopping = false;
|
|
149
186
|
}
|
|
150
187
|
async invoke(operation, params = {}, options = {}) {
|
|
188
|
+
return this.runWhileActive(() => this.invokeWhileActive(operation, params, options));
|
|
189
|
+
}
|
|
190
|
+
async invokeWhileActive(operation, params, options) {
|
|
191
|
+
const collaborationAuth = options.collaborationAuth === undefined ? undefined : transportCommon.normalizeCollaborationAuth(options.collaborationAuth);
|
|
192
|
+
if (collaborationAuth !== undefined) {
|
|
193
|
+
if (operation.operationId !== 'doc.open') {
|
|
194
|
+
throw new errors.SuperDocCliError('collaborationAuth is supported only for doc.open requests.', {
|
|
195
|
+
code: 'INVALID_ARGUMENT',
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
if (this.processMode !== 'cli') {
|
|
199
|
+
throw new errors.SuperDocCliError('Per-open collaboration authentication is not supported by the standalone document host.', {
|
|
200
|
+
code: 'CAPABILITY_UNSUPPORTED',
|
|
201
|
+
details: { feature: COLLABORATION_AUTH_PER_OPEN_FEATURE },
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
151
205
|
await this.ensureConnected();
|
|
206
|
+
if (collaborationAuth !== undefined && !this.hostFeatures.has(COLLABORATION_AUTH_PER_OPEN_FEATURE)) {
|
|
207
|
+
throw new errors.SuperDocCliError('The connected CLI host does not support per-open collaboration authentication.', {
|
|
208
|
+
code: 'CAPABILITY_UNSUPPORTED',
|
|
209
|
+
details: { feature: COLLABORATION_AUTH_PER_OPEN_FEATURE },
|
|
210
|
+
});
|
|
211
|
+
}
|
|
152
212
|
const shouldOpenThroughCliTimeoutFallback = this.processMode === 'cli' &&
|
|
153
213
|
(this.requestTimeoutMs !== undefined ||
|
|
154
214
|
(options.timeoutMs !== undefined && !this.hostFeatures.has(documentRpc.DOCUMENT_RPC_REQUEST_TIMEOUT_FEATURE)));
|
|
@@ -163,6 +223,7 @@ class HostTransport {
|
|
|
163
223
|
const response = await this.sendJsonRpcRequest('document.open', opened.params, this.resolveWatchdogTimeout(options.timeoutMs), { requestTimeoutMs });
|
|
164
224
|
const result = documentRpc.mapDocumentOpenResult(response, opened.sessionId, params.doc);
|
|
165
225
|
this.documentRpcSessions.add(opened.sessionId);
|
|
226
|
+
this.indeterminateDiscardCloses.delete(opened.sessionId);
|
|
166
227
|
return result;
|
|
167
228
|
}
|
|
168
229
|
const sessionId = typeof params.sessionId === 'string' ? params.sessionId : undefined;
|
|
@@ -172,10 +233,50 @@ class HostTransport {
|
|
|
172
233
|
const requestTimeoutMs = supportsResponseTimeout
|
|
173
234
|
? this.resolveDocumentRequestTimeout(options.timeoutMs)
|
|
174
235
|
: undefined;
|
|
175
|
-
const
|
|
176
|
-
const
|
|
177
|
-
|
|
236
|
+
const documentOperation = operation;
|
|
237
|
+
const requestOptions = typeof request.params.options === 'object' &&
|
|
238
|
+
request.params.options !== null &&
|
|
239
|
+
!Array.isArray(request.params.options)
|
|
240
|
+
? request.params.options
|
|
241
|
+
: undefined;
|
|
242
|
+
const tracksMutation = request.method === 'document.invoke' && documentOperation.mutates === true && requestOptions?.dryRun !== true;
|
|
243
|
+
if (tracksMutation)
|
|
244
|
+
this.beginDocumentRpcMutation(sessionId);
|
|
245
|
+
let result;
|
|
246
|
+
try {
|
|
247
|
+
const response = await this.sendJsonRpcRequest(request.method, request.params, this.resolveWatchdogTimeout(options.timeoutMs), { requestTimeoutMs });
|
|
248
|
+
result = documentRpc.mapDocumentLifecycleResult(operation.operationId, response, sessionId, operation.operationId === 'doc.save' ? request.params.path === undefined : undefined);
|
|
249
|
+
}
|
|
250
|
+
catch (error) {
|
|
251
|
+
if (tracksMutation) {
|
|
252
|
+
const indeterminate = error instanceof errors.SuperDocCliError && ['TIMEOUT', 'HOST_TIMEOUT', 'HOST_DISCONNECTED'].includes(error.code);
|
|
253
|
+
this.settleDocumentRpcMutation(sessionId, indeterminate ? 'indeterminate' : 'not-applied');
|
|
254
|
+
}
|
|
255
|
+
if (operation.operationId === 'doc.close' &&
|
|
256
|
+
request.params.discard === true &&
|
|
257
|
+
error instanceof errors.SuperDocCliError &&
|
|
258
|
+
error.code === 'TIMEOUT') {
|
|
259
|
+
this.indeterminateDiscardCloses.add(sessionId);
|
|
260
|
+
}
|
|
261
|
+
throw error;
|
|
262
|
+
}
|
|
263
|
+
if (tracksMutation) {
|
|
264
|
+
const failedReceipt = typeof result === 'object' &&
|
|
265
|
+
result !== null &&
|
|
266
|
+
!Array.isArray(result) &&
|
|
267
|
+
result.success === false;
|
|
268
|
+
this.settleDocumentRpcMutation(sessionId, failedReceipt ? 'not-applied' : 'applied');
|
|
269
|
+
}
|
|
270
|
+
if (operation.operationId === 'doc.close') {
|
|
178
271
|
this.documentRpcSessions.delete(sessionId);
|
|
272
|
+
this.clearDirtyDocumentRpcSession(sessionId);
|
|
273
|
+
this.indeterminateDiscardCloses.delete(sessionId);
|
|
274
|
+
}
|
|
275
|
+
else if (operation.operationId === 'doc.save') {
|
|
276
|
+
if (request.params.mode === undefined || request.params.mode === 'review-preserving') {
|
|
277
|
+
this.markDocumentRpcSessionSaved(sessionId);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
179
280
|
return result;
|
|
180
281
|
}
|
|
181
282
|
if (this.processMode === 'document') {
|
|
@@ -202,6 +303,7 @@ class HostTransport {
|
|
|
202
303
|
const response = await this.sendJsonRpcRequest('cli.invoke', {
|
|
203
304
|
argv,
|
|
204
305
|
stdinBase64,
|
|
306
|
+
...(collaborationAuth === undefined ? {} : { collaborationAuth }),
|
|
205
307
|
}, watchdogTimeout);
|
|
206
308
|
if (typeof response !== 'object' || response == null || Array.isArray(response)) {
|
|
207
309
|
throw new errors.SuperDocCliError('Host returned invalid cli.invoke result.', {
|
|
@@ -212,6 +314,20 @@ class HostTransport {
|
|
|
212
314
|
const resultRecord = response;
|
|
213
315
|
return mapCliInvocationResult(operation, resultRecord.data);
|
|
214
316
|
}
|
|
317
|
+
runWhileActive(start) {
|
|
318
|
+
if (this.terminalDisposalError) {
|
|
319
|
+
return Promise.reject(this.terminalDisposalError);
|
|
320
|
+
}
|
|
321
|
+
if (this.stopping) {
|
|
322
|
+
return Promise.reject(new errors.SuperDocCliError('Host is disposing.', {
|
|
323
|
+
code: 'HOST_DISCONNECTED',
|
|
324
|
+
}));
|
|
325
|
+
}
|
|
326
|
+
const activeCall = start();
|
|
327
|
+
this.activeCalls.add(activeCall);
|
|
328
|
+
void activeCall.then(() => this.activeCalls.delete(activeCall), () => this.activeCalls.delete(activeCall));
|
|
329
|
+
return activeCall;
|
|
330
|
+
}
|
|
215
331
|
async ensureConnected() {
|
|
216
332
|
if (this.connecting) {
|
|
217
333
|
await this.connecting;
|
|
@@ -264,7 +380,7 @@ class HostTransport {
|
|
|
264
380
|
}));
|
|
265
381
|
});
|
|
266
382
|
child.on('close', (code, signal) => {
|
|
267
|
-
if (this.stopping) {
|
|
383
|
+
if (this.stopping && this.dirtyDocumentRpcSessions.size === 0) {
|
|
268
384
|
this.cleanupProcess(null);
|
|
269
385
|
return;
|
|
270
386
|
}
|
|
@@ -350,6 +466,65 @@ class HostTransport {
|
|
|
350
466
|
details: { feature: documentRpc.DOCUMENT_RPC_REQUEST_TIMEOUT_FEATURE },
|
|
351
467
|
});
|
|
352
468
|
}
|
|
469
|
+
beginDocumentRpcMutation(sessionId) {
|
|
470
|
+
const state = this.documentRpcMutationStates.get(sessionId) ?? {
|
|
471
|
+
confirmedDirty: this.dirtyDocumentRpcSessions.has(sessionId),
|
|
472
|
+
pendingCount: 0,
|
|
473
|
+
};
|
|
474
|
+
state.pendingCount += 1;
|
|
475
|
+
this.documentRpcMutationStates.set(sessionId, state);
|
|
476
|
+
this.dirtyDocumentRpcSessions.add(sessionId);
|
|
477
|
+
}
|
|
478
|
+
settleDocumentRpcMutation(sessionId, outcome) {
|
|
479
|
+
const state = this.documentRpcMutationStates.get(sessionId);
|
|
480
|
+
if (!state)
|
|
481
|
+
return;
|
|
482
|
+
state.pendingCount = Math.max(0, state.pendingCount - 1);
|
|
483
|
+
if (outcome !== 'not-applied')
|
|
484
|
+
state.confirmedDirty = true;
|
|
485
|
+
if (state.confirmedDirty || state.pendingCount > 0) {
|
|
486
|
+
this.dirtyDocumentRpcSessions.add(sessionId);
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
this.clearDirtyDocumentRpcSession(sessionId);
|
|
490
|
+
}
|
|
491
|
+
clearDirtyDocumentRpcSession(sessionId) {
|
|
492
|
+
this.dirtyDocumentRpcSessions.delete(sessionId);
|
|
493
|
+
this.documentRpcMutationStates.delete(sessionId);
|
|
494
|
+
}
|
|
495
|
+
markDocumentRpcSessionSaved(sessionId) {
|
|
496
|
+
const state = this.documentRpcMutationStates.get(sessionId);
|
|
497
|
+
if (!state || state.pendingCount === 0) {
|
|
498
|
+
this.clearDirtyDocumentRpcSession(sessionId);
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
state.confirmedDirty = false;
|
|
502
|
+
this.dirtyDocumentRpcSessions.add(sessionId);
|
|
503
|
+
}
|
|
504
|
+
async persistDirtyDocumentRpcSessions() {
|
|
505
|
+
if (this.dirtyDocumentRpcSessions.size === 0)
|
|
506
|
+
return;
|
|
507
|
+
if (!this.hostFeatures.has(documentRpc.DOCUMENT_RPC_SOURCE_SAVE_FEATURE)) {
|
|
508
|
+
throw new errors.SuperDocCliError('Document host cannot persist dirty sessions during client disposal because it does not support source saves.', {
|
|
509
|
+
code: 'DOCUMENT_RPC_INPUT_UNSUPPORTED',
|
|
510
|
+
details: { feature: documentRpc.DOCUMENT_RPC_SOURCE_SAVE_FEATURE },
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
for (const sessionId of this.dirtyDocumentRpcSessions) {
|
|
514
|
+
const response = await this.sendJsonRpcRequest('document.save', { sessionId }, this.resolveWatchdogTimeout(undefined));
|
|
515
|
+
documentRpc.mapDocumentLifecycleResult('doc.save', response, sessionId, true);
|
|
516
|
+
this.markDocumentRpcSessionSaved(sessionId);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
async reconcileIndeterminateDiscardCloses() {
|
|
520
|
+
for (const sessionId of this.indeterminateDiscardCloses) {
|
|
521
|
+
const response = await this.sendJsonRpcRequest('document.close', { sessionId, discard: true }, this.resolveWatchdogTimeout(undefined));
|
|
522
|
+
documentRpc.mapDocumentLifecycleResult('doc.close', response, sessionId);
|
|
523
|
+
this.documentRpcSessions.delete(sessionId);
|
|
524
|
+
this.clearDirtyDocumentRpcSession(sessionId);
|
|
525
|
+
this.indeterminateDiscardCloses.delete(sessionId);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
353
528
|
async sendJsonRpcRequest(method, params, watchdogTimeoutMs, metadata = {}) {
|
|
354
529
|
const child = this.child;
|
|
355
530
|
if (!child || !child.stdin.writable) {
|
|
@@ -481,8 +656,17 @@ class HostTransport {
|
|
|
481
656
|
this.cleanupProcess(error);
|
|
482
657
|
}
|
|
483
658
|
cleanupProcess(error) {
|
|
659
|
+
if (error && this.dirtyDocumentRpcSessions.size > 0 && !this.terminalDisposalError) {
|
|
660
|
+
this.terminalDisposalError = new errors.SuperDocCliError('Document host disconnected before dirty sessions could be persisted.', {
|
|
661
|
+
code: 'HOST_DISCONNECTED',
|
|
662
|
+
details: { causeCode: error.code, causeDetails: error.details },
|
|
663
|
+
});
|
|
664
|
+
}
|
|
484
665
|
this.hostFeatures.clear();
|
|
485
666
|
this.documentRpcSessions.clear();
|
|
667
|
+
this.dirtyDocumentRpcSessions.clear();
|
|
668
|
+
this.documentRpcMutationStates.clear();
|
|
669
|
+
this.indeterminateDiscardCloses.clear();
|
|
486
670
|
const child = this.child;
|
|
487
671
|
if (child) {
|
|
488
672
|
child.removeAllListeners();
|
|
@@ -507,6 +691,7 @@ class HostTransport {
|
|
|
507
691
|
}
|
|
508
692
|
}
|
|
509
693
|
|
|
694
|
+
exports.COLLABORATION_AUTH_PER_OPEN_FEATURE = COLLABORATION_AUTH_PER_OPEN_FEATURE;
|
|
510
695
|
exports.HostTransport = HostTransport;
|
|
511
696
|
exports.buildDocumentHostSpawnArgs = buildDocumentHostSpawnArgs;
|
|
512
697
|
exports.buildHostSpawnArgs = buildHostSpawnArgs;
|
package/dist/runtime/host.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type OperationSpec, type SuperDocClientOptions, type TransportInvokeOptions } from './transport-common.js';
|
|
2
|
+
export declare const COLLABORATION_AUTH_PER_OPEN_FEATURE = "collaboration.auth.perOpen";
|
|
2
3
|
export declare function mapCliInvocationResult(operation: OperationSpec, value: unknown): unknown;
|
|
3
4
|
/**
|
|
4
5
|
* Builds the argv passed to `spawn` for `superdoc host --stdio`. Propagates
|
|
@@ -46,21 +47,36 @@ export declare class HostTransport {
|
|
|
46
47
|
private readonly pending;
|
|
47
48
|
private nextRequestId;
|
|
48
49
|
private connecting;
|
|
50
|
+
private disposePromise;
|
|
51
|
+
private terminalDisposalError;
|
|
49
52
|
private stopping;
|
|
53
|
+
private readonly activeCalls;
|
|
50
54
|
private readonly hostFeatures;
|
|
51
55
|
private readonly documentRpcSessions;
|
|
56
|
+
private readonly dirtyDocumentRpcSessions;
|
|
57
|
+
private readonly documentRpcMutationStates;
|
|
58
|
+
private readonly indeterminateDiscardCloses;
|
|
52
59
|
constructor(options: {
|
|
53
60
|
hostBin: string;
|
|
54
61
|
processMode?: 'cli' | 'document';
|
|
55
62
|
} & SuperDocClientOptions);
|
|
56
63
|
connect(): Promise<void>;
|
|
57
64
|
dispose(): Promise<void>;
|
|
58
|
-
|
|
65
|
+
private disposeAfterActiveCalls;
|
|
66
|
+
invoke<TData = unknown>(operation: OperationSpec, params?: Record<string, unknown>, options?: TransportInvokeOptions): Promise<TData>;
|
|
67
|
+
private invokeWhileActive;
|
|
68
|
+
private runWhileActive;
|
|
59
69
|
private ensureConnected;
|
|
60
70
|
private startHostProcess;
|
|
61
71
|
private assertCapabilities;
|
|
62
72
|
private resolveWatchdogTimeout;
|
|
63
73
|
private resolveDocumentRequestTimeout;
|
|
74
|
+
private beginDocumentRpcMutation;
|
|
75
|
+
private settleDocumentRpcMutation;
|
|
76
|
+
private clearDirtyDocumentRpcSession;
|
|
77
|
+
private markDocumentRpcSessionSaved;
|
|
78
|
+
private persistDirtyDocumentRpcSessions;
|
|
79
|
+
private reconcileIndeterminateDiscardCloses;
|
|
64
80
|
private sendJsonRpcRequest;
|
|
65
81
|
private onStdoutLine;
|
|
66
82
|
private mapJsonRpcError;
|
package/dist/runtime/host.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { createInterface } from 'node:readline';
|
|
3
|
-
import { buildOperationArgv, resolveInvocation, } from './transport-common.js';
|
|
3
|
+
import { buildOperationArgv, normalizeCollaborationAuth, resolveInvocation, } from './transport-common.js';
|
|
4
4
|
import { SuperDocCliError } from './errors.js';
|
|
5
|
-
import { DOCUMENT_RPC_FEATURES, DOCUMENT_RPC_DEFAULT_CHANGE_MODE_FEATURE, DOCUMENT_RPC_REQUEST_TIMEOUT_FEATURE, buildDocumentInvokeParams, buildDocumentOpenParams, canOpenWithDocumentRpc, documentRpcRequestSupportsResponseTimeout, documentRpcEnabled, mapDocumentLifecycleResult, mapDocumentOpenResult, supportsDocumentRpc, } from './document-rpc.js';
|
|
5
|
+
import { DOCUMENT_RPC_FEATURES, DOCUMENT_RPC_DEFAULT_CHANGE_MODE_FEATURE, DOCUMENT_RPC_REQUEST_TIMEOUT_FEATURE, DOCUMENT_RPC_SOURCE_SAVE_FEATURE, buildDocumentInvokeParams, buildDocumentOpenParams, canOpenWithDocumentRpc, documentRpcRequestSupportsResponseTimeout, documentRpcEnabled, mapDocumentLifecycleResult, mapDocumentOpenResult, supportsDocumentRpc, } from './document-rpc.js';
|
|
6
6
|
const HOST_PROTOCOL_VERSION = '1.0';
|
|
7
7
|
const CLI_HOST_REQUIRED_FEATURES = ['cli.invoke', 'host.shutdown'];
|
|
8
|
+
export const COLLABORATION_AUTH_PER_OPEN_FEATURE = 'collaboration.auth.perOpen';
|
|
8
9
|
const DOCUMENT_HOST_REQUIRED_FEATURES = [...DOCUMENT_RPC_FEATURES, 'host.shutdown'];
|
|
9
10
|
const CHANGE_MODES = ['direct', 'tracked'];
|
|
10
11
|
const FORWARD_HOST_STDERR = typeof process !== 'undefined' && typeof process.env?.SUPERDOC_DEBUG_TEXT_REWRITE === 'string'
|
|
@@ -95,9 +96,15 @@ export class HostTransport {
|
|
|
95
96
|
pending = new Map();
|
|
96
97
|
nextRequestId = 1;
|
|
97
98
|
connecting = null;
|
|
99
|
+
disposePromise = null;
|
|
100
|
+
terminalDisposalError = null;
|
|
98
101
|
stopping = false;
|
|
102
|
+
activeCalls = new Set();
|
|
99
103
|
hostFeatures = new Set();
|
|
100
104
|
documentRpcSessions = new Set();
|
|
105
|
+
dirtyDocumentRpcSessions = new Set();
|
|
106
|
+
documentRpcMutationStates = new Map();
|
|
107
|
+
indeterminateDiscardCloses = new Set();
|
|
101
108
|
constructor(options) {
|
|
102
109
|
this.hostBin = options.hostBin;
|
|
103
110
|
this.processMode = options.processMode ?? 'cli';
|
|
@@ -118,12 +125,43 @@ export class HostTransport {
|
|
|
118
125
|
this.documentRpcEnabled = this.processMode === 'document' || documentRpcEnabled(options.env);
|
|
119
126
|
}
|
|
120
127
|
async connect() {
|
|
121
|
-
await this.ensureConnected();
|
|
128
|
+
await this.runWhileActive(() => this.ensureConnected());
|
|
122
129
|
}
|
|
123
130
|
async dispose() {
|
|
124
|
-
if (
|
|
131
|
+
if (this.disposePromise)
|
|
132
|
+
return this.disposePromise;
|
|
133
|
+
if (this.terminalDisposalError)
|
|
134
|
+
throw this.terminalDisposalError;
|
|
135
|
+
if (!this.child && !this.connecting && this.activeCalls.size === 0)
|
|
125
136
|
return;
|
|
126
137
|
this.stopping = true;
|
|
138
|
+
const disposal = this.disposeAfterActiveCalls();
|
|
139
|
+
this.disposePromise = disposal;
|
|
140
|
+
try {
|
|
141
|
+
await disposal;
|
|
142
|
+
}
|
|
143
|
+
finally {
|
|
144
|
+
if (this.disposePromise === disposal)
|
|
145
|
+
this.disposePromise = null;
|
|
146
|
+
this.stopping = false;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
async disposeAfterActiveCalls() {
|
|
150
|
+
await Promise.allSettled(Array.from(this.activeCalls));
|
|
151
|
+
if (this.terminalDisposalError)
|
|
152
|
+
throw this.terminalDisposalError;
|
|
153
|
+
if (!this.child)
|
|
154
|
+
return;
|
|
155
|
+
try {
|
|
156
|
+
await this.reconcileIndeterminateDiscardCloses();
|
|
157
|
+
await this.persistDirtyDocumentRpcSessions();
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
if (!this.child && error instanceof SuperDocCliError) {
|
|
161
|
+
this.terminalDisposalError = error;
|
|
162
|
+
}
|
|
163
|
+
throw error;
|
|
164
|
+
}
|
|
127
165
|
const child = this.child;
|
|
128
166
|
try {
|
|
129
167
|
await this.sendJsonRpcRequest('host.shutdown', {}, this.shutdownTimeoutMs);
|
|
@@ -142,10 +180,32 @@ export class HostTransport {
|
|
|
142
180
|
});
|
|
143
181
|
});
|
|
144
182
|
this.cleanupProcess(null);
|
|
145
|
-
this.stopping = false;
|
|
146
183
|
}
|
|
147
184
|
async invoke(operation, params = {}, options = {}) {
|
|
185
|
+
return this.runWhileActive(() => this.invokeWhileActive(operation, params, options));
|
|
186
|
+
}
|
|
187
|
+
async invokeWhileActive(operation, params, options) {
|
|
188
|
+
const collaborationAuth = options.collaborationAuth === undefined ? undefined : normalizeCollaborationAuth(options.collaborationAuth);
|
|
189
|
+
if (collaborationAuth !== undefined) {
|
|
190
|
+
if (operation.operationId !== 'doc.open') {
|
|
191
|
+
throw new SuperDocCliError('collaborationAuth is supported only for doc.open requests.', {
|
|
192
|
+
code: 'INVALID_ARGUMENT',
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
if (this.processMode !== 'cli') {
|
|
196
|
+
throw new SuperDocCliError('Per-open collaboration authentication is not supported by the standalone document host.', {
|
|
197
|
+
code: 'CAPABILITY_UNSUPPORTED',
|
|
198
|
+
details: { feature: COLLABORATION_AUTH_PER_OPEN_FEATURE },
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
148
202
|
await this.ensureConnected();
|
|
203
|
+
if (collaborationAuth !== undefined && !this.hostFeatures.has(COLLABORATION_AUTH_PER_OPEN_FEATURE)) {
|
|
204
|
+
throw new SuperDocCliError('The connected CLI host does not support per-open collaboration authentication.', {
|
|
205
|
+
code: 'CAPABILITY_UNSUPPORTED',
|
|
206
|
+
details: { feature: COLLABORATION_AUTH_PER_OPEN_FEATURE },
|
|
207
|
+
});
|
|
208
|
+
}
|
|
149
209
|
const shouldOpenThroughCliTimeoutFallback = this.processMode === 'cli' &&
|
|
150
210
|
(this.requestTimeoutMs !== undefined ||
|
|
151
211
|
(options.timeoutMs !== undefined && !this.hostFeatures.has(DOCUMENT_RPC_REQUEST_TIMEOUT_FEATURE)));
|
|
@@ -160,6 +220,7 @@ export class HostTransport {
|
|
|
160
220
|
const response = await this.sendJsonRpcRequest('document.open', opened.params, this.resolveWatchdogTimeout(options.timeoutMs), { requestTimeoutMs });
|
|
161
221
|
const result = mapDocumentOpenResult(response, opened.sessionId, params.doc);
|
|
162
222
|
this.documentRpcSessions.add(opened.sessionId);
|
|
223
|
+
this.indeterminateDiscardCloses.delete(opened.sessionId);
|
|
163
224
|
return result;
|
|
164
225
|
}
|
|
165
226
|
const sessionId = typeof params.sessionId === 'string' ? params.sessionId : undefined;
|
|
@@ -169,10 +230,50 @@ export class HostTransport {
|
|
|
169
230
|
const requestTimeoutMs = supportsResponseTimeout
|
|
170
231
|
? this.resolveDocumentRequestTimeout(options.timeoutMs)
|
|
171
232
|
: undefined;
|
|
172
|
-
const
|
|
173
|
-
const
|
|
174
|
-
|
|
233
|
+
const documentOperation = operation;
|
|
234
|
+
const requestOptions = typeof request.params.options === 'object' &&
|
|
235
|
+
request.params.options !== null &&
|
|
236
|
+
!Array.isArray(request.params.options)
|
|
237
|
+
? request.params.options
|
|
238
|
+
: undefined;
|
|
239
|
+
const tracksMutation = request.method === 'document.invoke' && documentOperation.mutates === true && requestOptions?.dryRun !== true;
|
|
240
|
+
if (tracksMutation)
|
|
241
|
+
this.beginDocumentRpcMutation(sessionId);
|
|
242
|
+
let result;
|
|
243
|
+
try {
|
|
244
|
+
const response = await this.sendJsonRpcRequest(request.method, request.params, this.resolveWatchdogTimeout(options.timeoutMs), { requestTimeoutMs });
|
|
245
|
+
result = mapDocumentLifecycleResult(operation.operationId, response, sessionId, operation.operationId === 'doc.save' ? request.params.path === undefined : undefined);
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
if (tracksMutation) {
|
|
249
|
+
const indeterminate = error instanceof SuperDocCliError && ['TIMEOUT', 'HOST_TIMEOUT', 'HOST_DISCONNECTED'].includes(error.code);
|
|
250
|
+
this.settleDocumentRpcMutation(sessionId, indeterminate ? 'indeterminate' : 'not-applied');
|
|
251
|
+
}
|
|
252
|
+
if (operation.operationId === 'doc.close' &&
|
|
253
|
+
request.params.discard === true &&
|
|
254
|
+
error instanceof SuperDocCliError &&
|
|
255
|
+
error.code === 'TIMEOUT') {
|
|
256
|
+
this.indeterminateDiscardCloses.add(sessionId);
|
|
257
|
+
}
|
|
258
|
+
throw error;
|
|
259
|
+
}
|
|
260
|
+
if (tracksMutation) {
|
|
261
|
+
const failedReceipt = typeof result === 'object' &&
|
|
262
|
+
result !== null &&
|
|
263
|
+
!Array.isArray(result) &&
|
|
264
|
+
result.success === false;
|
|
265
|
+
this.settleDocumentRpcMutation(sessionId, failedReceipt ? 'not-applied' : 'applied');
|
|
266
|
+
}
|
|
267
|
+
if (operation.operationId === 'doc.close') {
|
|
175
268
|
this.documentRpcSessions.delete(sessionId);
|
|
269
|
+
this.clearDirtyDocumentRpcSession(sessionId);
|
|
270
|
+
this.indeterminateDiscardCloses.delete(sessionId);
|
|
271
|
+
}
|
|
272
|
+
else if (operation.operationId === 'doc.save') {
|
|
273
|
+
if (request.params.mode === undefined || request.params.mode === 'review-preserving') {
|
|
274
|
+
this.markDocumentRpcSessionSaved(sessionId);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
176
277
|
return result;
|
|
177
278
|
}
|
|
178
279
|
if (this.processMode === 'document') {
|
|
@@ -199,6 +300,7 @@ export class HostTransport {
|
|
|
199
300
|
const response = await this.sendJsonRpcRequest('cli.invoke', {
|
|
200
301
|
argv,
|
|
201
302
|
stdinBase64,
|
|
303
|
+
...(collaborationAuth === undefined ? {} : { collaborationAuth }),
|
|
202
304
|
}, watchdogTimeout);
|
|
203
305
|
if (typeof response !== 'object' || response == null || Array.isArray(response)) {
|
|
204
306
|
throw new SuperDocCliError('Host returned invalid cli.invoke result.', {
|
|
@@ -209,6 +311,20 @@ export class HostTransport {
|
|
|
209
311
|
const resultRecord = response;
|
|
210
312
|
return mapCliInvocationResult(operation, resultRecord.data);
|
|
211
313
|
}
|
|
314
|
+
runWhileActive(start) {
|
|
315
|
+
if (this.terminalDisposalError) {
|
|
316
|
+
return Promise.reject(this.terminalDisposalError);
|
|
317
|
+
}
|
|
318
|
+
if (this.stopping) {
|
|
319
|
+
return Promise.reject(new SuperDocCliError('Host is disposing.', {
|
|
320
|
+
code: 'HOST_DISCONNECTED',
|
|
321
|
+
}));
|
|
322
|
+
}
|
|
323
|
+
const activeCall = start();
|
|
324
|
+
this.activeCalls.add(activeCall);
|
|
325
|
+
void activeCall.then(() => this.activeCalls.delete(activeCall), () => this.activeCalls.delete(activeCall));
|
|
326
|
+
return activeCall;
|
|
327
|
+
}
|
|
212
328
|
async ensureConnected() {
|
|
213
329
|
if (this.connecting) {
|
|
214
330
|
await this.connecting;
|
|
@@ -261,7 +377,7 @@ export class HostTransport {
|
|
|
261
377
|
}));
|
|
262
378
|
});
|
|
263
379
|
child.on('close', (code, signal) => {
|
|
264
|
-
if (this.stopping) {
|
|
380
|
+
if (this.stopping && this.dirtyDocumentRpcSessions.size === 0) {
|
|
265
381
|
this.cleanupProcess(null);
|
|
266
382
|
return;
|
|
267
383
|
}
|
|
@@ -347,6 +463,65 @@ export class HostTransport {
|
|
|
347
463
|
details: { feature: DOCUMENT_RPC_REQUEST_TIMEOUT_FEATURE },
|
|
348
464
|
});
|
|
349
465
|
}
|
|
466
|
+
beginDocumentRpcMutation(sessionId) {
|
|
467
|
+
const state = this.documentRpcMutationStates.get(sessionId) ?? {
|
|
468
|
+
confirmedDirty: this.dirtyDocumentRpcSessions.has(sessionId),
|
|
469
|
+
pendingCount: 0,
|
|
470
|
+
};
|
|
471
|
+
state.pendingCount += 1;
|
|
472
|
+
this.documentRpcMutationStates.set(sessionId, state);
|
|
473
|
+
this.dirtyDocumentRpcSessions.add(sessionId);
|
|
474
|
+
}
|
|
475
|
+
settleDocumentRpcMutation(sessionId, outcome) {
|
|
476
|
+
const state = this.documentRpcMutationStates.get(sessionId);
|
|
477
|
+
if (!state)
|
|
478
|
+
return;
|
|
479
|
+
state.pendingCount = Math.max(0, state.pendingCount - 1);
|
|
480
|
+
if (outcome !== 'not-applied')
|
|
481
|
+
state.confirmedDirty = true;
|
|
482
|
+
if (state.confirmedDirty || state.pendingCount > 0) {
|
|
483
|
+
this.dirtyDocumentRpcSessions.add(sessionId);
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
this.clearDirtyDocumentRpcSession(sessionId);
|
|
487
|
+
}
|
|
488
|
+
clearDirtyDocumentRpcSession(sessionId) {
|
|
489
|
+
this.dirtyDocumentRpcSessions.delete(sessionId);
|
|
490
|
+
this.documentRpcMutationStates.delete(sessionId);
|
|
491
|
+
}
|
|
492
|
+
markDocumentRpcSessionSaved(sessionId) {
|
|
493
|
+
const state = this.documentRpcMutationStates.get(sessionId);
|
|
494
|
+
if (!state || state.pendingCount === 0) {
|
|
495
|
+
this.clearDirtyDocumentRpcSession(sessionId);
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
state.confirmedDirty = false;
|
|
499
|
+
this.dirtyDocumentRpcSessions.add(sessionId);
|
|
500
|
+
}
|
|
501
|
+
async persistDirtyDocumentRpcSessions() {
|
|
502
|
+
if (this.dirtyDocumentRpcSessions.size === 0)
|
|
503
|
+
return;
|
|
504
|
+
if (!this.hostFeatures.has(DOCUMENT_RPC_SOURCE_SAVE_FEATURE)) {
|
|
505
|
+
throw new SuperDocCliError('Document host cannot persist dirty sessions during client disposal because it does not support source saves.', {
|
|
506
|
+
code: 'DOCUMENT_RPC_INPUT_UNSUPPORTED',
|
|
507
|
+
details: { feature: DOCUMENT_RPC_SOURCE_SAVE_FEATURE },
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
for (const sessionId of this.dirtyDocumentRpcSessions) {
|
|
511
|
+
const response = await this.sendJsonRpcRequest('document.save', { sessionId }, this.resolveWatchdogTimeout(undefined));
|
|
512
|
+
mapDocumentLifecycleResult('doc.save', response, sessionId, true);
|
|
513
|
+
this.markDocumentRpcSessionSaved(sessionId);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
async reconcileIndeterminateDiscardCloses() {
|
|
517
|
+
for (const sessionId of this.indeterminateDiscardCloses) {
|
|
518
|
+
const response = await this.sendJsonRpcRequest('document.close', { sessionId, discard: true }, this.resolveWatchdogTimeout(undefined));
|
|
519
|
+
mapDocumentLifecycleResult('doc.close', response, sessionId);
|
|
520
|
+
this.documentRpcSessions.delete(sessionId);
|
|
521
|
+
this.clearDirtyDocumentRpcSession(sessionId);
|
|
522
|
+
this.indeterminateDiscardCloses.delete(sessionId);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
350
525
|
async sendJsonRpcRequest(method, params, watchdogTimeoutMs, metadata = {}) {
|
|
351
526
|
const child = this.child;
|
|
352
527
|
if (!child || !child.stdin.writable) {
|
|
@@ -478,8 +653,17 @@ export class HostTransport {
|
|
|
478
653
|
this.cleanupProcess(error);
|
|
479
654
|
}
|
|
480
655
|
cleanupProcess(error) {
|
|
656
|
+
if (error && this.dirtyDocumentRpcSessions.size > 0 && !this.terminalDisposalError) {
|
|
657
|
+
this.terminalDisposalError = new SuperDocCliError('Document host disconnected before dirty sessions could be persisted.', {
|
|
658
|
+
code: 'HOST_DISCONNECTED',
|
|
659
|
+
details: { causeCode: error.code, causeDetails: error.details },
|
|
660
|
+
});
|
|
661
|
+
}
|
|
481
662
|
this.hostFeatures.clear();
|
|
482
663
|
this.documentRpcSessions.clear();
|
|
664
|
+
this.dirtyDocumentRpcSessions.clear();
|
|
665
|
+
this.documentRpcMutationStates.clear();
|
|
666
|
+
this.indeterminateDiscardCloses.clear();
|
|
483
667
|
const child = this.child;
|
|
484
668
|
if (child) {
|
|
485
669
|
child.removeAllListeners();
|
package/dist/runtime/process.cjs
CHANGED
|
@@ -32,6 +32,10 @@ function resolveRuntimeProcess(options = {}, embedded = {
|
|
|
32
32
|
processMode: 'cli',
|
|
33
33
|
};
|
|
34
34
|
}
|
|
35
|
+
function toSafeInvokeTraceOptions(options) {
|
|
36
|
+
const { collaborationAuth: _collaborationAuth, ...traceOptions } = options;
|
|
37
|
+
return traceOptions;
|
|
38
|
+
}
|
|
35
39
|
/**
|
|
36
40
|
* Internal runtime that delegates operations to a persistent host transport.
|
|
37
41
|
*
|
|
@@ -60,7 +64,7 @@ class SuperDocRuntime {
|
|
|
60
64
|
phase: 'start',
|
|
61
65
|
operationId: operation.operationId,
|
|
62
66
|
params,
|
|
63
|
-
options,
|
|
67
|
+
options: toSafeInvokeTraceOptions(options),
|
|
64
68
|
});
|
|
65
69
|
try {
|
|
66
70
|
const result = await this.transport.invoke(operation, params, options);
|
|
@@ -88,3 +92,4 @@ class SuperDocRuntime {
|
|
|
88
92
|
|
|
89
93
|
exports.SuperDocRuntime = SuperDocRuntime;
|
|
90
94
|
exports.resolveRuntimeProcess = resolveRuntimeProcess;
|
|
95
|
+
exports.toSafeInvokeTraceOptions = toSafeInvokeTraceOptions;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DocumentRuntimeKind, InvokeOptions, OperationParamSpec, OperationSpec, RuntimeInvoker, SuperDocClientOptions } from './transport-common.js';
|
|
1
|
+
import type { CollaborationAuth, DocumentRuntimeKind, DocOpenOptions, InvokeOptions, TransportInvokeOptions, OperationParamSpec, OperationSpec, RuntimeInvoker, SuperDocClientOptions } from './transport-common.js';
|
|
2
2
|
type RuntimeProcessResolution = {
|
|
3
3
|
hostBin: string;
|
|
4
4
|
processMode: 'cli' | 'document';
|
|
@@ -8,6 +8,7 @@ type EmbeddedRuntimeResolvers = {
|
|
|
8
8
|
documentHost: () => string;
|
|
9
9
|
};
|
|
10
10
|
export declare function resolveRuntimeProcess(options?: SuperDocClientOptions, embedded?: EmbeddedRuntimeResolvers): RuntimeProcessResolution;
|
|
11
|
+
export declare function toSafeInvokeTraceOptions(options: TransportInvokeOptions): InvokeOptions;
|
|
11
12
|
/**
|
|
12
13
|
* Internal runtime that delegates operations to a persistent host transport.
|
|
13
14
|
*
|
|
@@ -19,6 +20,6 @@ export declare class SuperDocRuntime {
|
|
|
19
20
|
constructor(options?: SuperDocClientOptions);
|
|
20
21
|
connect(): Promise<void>;
|
|
21
22
|
dispose(): Promise<void>;
|
|
22
|
-
invoke<TData = unknown>(operation: OperationSpec, params?: Record<string, unknown>, options?:
|
|
23
|
+
invoke<TData = unknown>(operation: OperationSpec, params?: Record<string, unknown>, options?: TransportInvokeOptions): Promise<TData>;
|
|
23
24
|
}
|
|
24
|
-
export type { DocumentRuntimeKind, InvokeOptions, OperationParamSpec, OperationSpec, RuntimeInvoker, SuperDocClientOptions, };
|
|
25
|
+
export type { CollaborationAuth, DocumentRuntimeKind, DocOpenOptions, InvokeOptions, OperationParamSpec, OperationSpec, RuntimeInvoker, SuperDocClientOptions, };
|