@components-kit/open-workbook 0.1.5 → 0.1.7
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/assets/backend/dist/addin-websocket-server.js +5 -1
- package/assets/backend/dist/addin-websocket-server.js.map +1 -1
- package/assets/backend/dist/runtime-service.d.ts +183 -8
- package/assets/backend/dist/runtime-service.d.ts.map +1 -1
- package/assets/backend/dist/runtime-service.js +642 -8
- package/assets/backend/dist/runtime-service.js.map +1 -1
- package/assets/backend/dist/state-store.d.ts +2 -1
- package/assets/backend/dist/state-store.d.ts.map +1 -1
- package/assets/backend/dist/state-store.js +1 -0
- package/assets/backend/dist/state-store.js.map +1 -1
- package/assets/excel-addin/dist/excel-executor.js +1 -1
- package/assets/excel-addin/dist/taskpane.bundle.js +10 -10
- package/assets/excel-addin/manifest.xml +1 -1
- package/assets/instructions/open-workbook-excel/SKILL.md +4 -3
- package/assets/instructions/open-workbook-excel/references/performance.md +5 -1
- package/assets/instructions/open-workbook-excel/references/tool-selection.md +10 -2
- package/assets/mcp-server/dist/index.js +209 -1
- package/assets/mcp-server/dist/index.js.map +1 -1
- package/dist/index.js +36 -10
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
|
@@ -7,7 +7,7 @@ import { getToolCatalogSummary, PromptCatalog, ResourceCatalog, makeId, runtimeE
|
|
|
7
7
|
import { SessionRegistry } from "./session-registry.js";
|
|
8
8
|
import { NativeFileBridge } from "./native-file-bridge.js";
|
|
9
9
|
import { RuntimeStateStore } from "./state-store.js";
|
|
10
|
-
const runtimeVersion = process.env.OPEN_WORKBOOK_VERSION ?? "0.1.
|
|
10
|
+
const runtimeVersion = process.env.OPEN_WORKBOOK_VERSION ?? "0.1.7";
|
|
11
11
|
export class RuntimeService {
|
|
12
12
|
sessions = new SessionRegistry();
|
|
13
13
|
backups = new BackupManager();
|
|
@@ -21,10 +21,14 @@ export class RuntimeService {
|
|
|
21
21
|
addinClients = new Map();
|
|
22
22
|
regions = new Map();
|
|
23
23
|
agents = new Map();
|
|
24
|
+
jobs = new Map();
|
|
24
25
|
collabEvents = [];
|
|
25
26
|
conflicts = [];
|
|
26
27
|
conflictTelemetry = [];
|
|
27
28
|
transactionQueue = Promise.resolve();
|
|
29
|
+
runtimeMutationActive = false;
|
|
30
|
+
runtimeMutationQueuedCount = 0;
|
|
31
|
+
cancelledQueuedTransactions = new Set();
|
|
28
32
|
defaultAgentId = "agent_daemon";
|
|
29
33
|
stateStore;
|
|
30
34
|
fileBridge;
|
|
@@ -357,14 +361,70 @@ export class RuntimeService {
|
|
|
357
361
|
return { ok: result.missingLockIds.length === 0, released: result.released, missingLockIds: result.missingLockIds };
|
|
358
362
|
}
|
|
359
363
|
listTransactions(workbookId) {
|
|
360
|
-
return {
|
|
364
|
+
return {
|
|
365
|
+
ok: true,
|
|
366
|
+
transactions: this.transactions.list(workbookId).map((transaction) => this.transactions.withQueueMetadata(transaction))
|
|
367
|
+
};
|
|
361
368
|
}
|
|
362
369
|
getTransaction(transactionId) {
|
|
363
370
|
const transaction = this.transactions.get(transactionId);
|
|
364
371
|
return transaction
|
|
365
|
-
? { ok: true, transaction }
|
|
372
|
+
? { ok: true, transaction: this.transactions.withQueueMetadata(transaction) }
|
|
366
373
|
: { ok: false, error: runtimeError("NOT_FOUND", `Transaction not found: ${transactionId}`, { retryable: false }) };
|
|
367
374
|
}
|
|
375
|
+
async waitTransaction(transactionId, timeoutMs = 30_000, pollMs = 250) {
|
|
376
|
+
const started = Date.now();
|
|
377
|
+
while (true) {
|
|
378
|
+
const transaction = this.transactions.get(transactionId);
|
|
379
|
+
if (!transaction) {
|
|
380
|
+
return { ok: false, completed: false, error: runtimeError("NOT_FOUND", `Transaction not found: ${transactionId}`, { retryable: false }) };
|
|
381
|
+
}
|
|
382
|
+
const withMetadata = this.transactions.withQueueMetadata(transaction);
|
|
383
|
+
if (isTerminalTransactionStatus(withMetadata.status)) {
|
|
384
|
+
return { ok: true, completed: true, transaction: withMetadata };
|
|
385
|
+
}
|
|
386
|
+
if (Date.now() - started >= timeoutMs) {
|
|
387
|
+
return {
|
|
388
|
+
ok: true,
|
|
389
|
+
completed: false,
|
|
390
|
+
transaction: withMetadata,
|
|
391
|
+
error: runtimeError("TIMEOUT", `Timed out waiting for transaction ${transactionId}.`, { retryable: true })
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
await sleep(Math.max(25, pollMs));
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
cancelTransaction(transactionId) {
|
|
398
|
+
const transaction = this.transactions.get(transactionId);
|
|
399
|
+
if (!transaction) {
|
|
400
|
+
return { ok: false, error: runtimeError("NOT_FOUND", `Transaction not found: ${transactionId}`, { retryable: false }) };
|
|
401
|
+
}
|
|
402
|
+
if (transaction.status !== "queued") {
|
|
403
|
+
return {
|
|
404
|
+
ok: false,
|
|
405
|
+
transaction: this.transactions.withQueueMetadata(transaction),
|
|
406
|
+
error: runtimeError("OPERATION_FAILED", `Only queued transactions can be cancelled. Current status: ${transaction.status}.`, { retryable: false })
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
this.cancelledQueuedTransactions.add(transactionId);
|
|
410
|
+
const cancelled = this.transactions.markCancelled(transactionId);
|
|
411
|
+
this.recordCollabEvent({
|
|
412
|
+
type: "transaction.cancelled",
|
|
413
|
+
workbookId: cancelled.workbookId,
|
|
414
|
+
agentId: cancelled.agentId,
|
|
415
|
+
taskId: cancelled.taskId,
|
|
416
|
+
transactionId,
|
|
417
|
+
message: cancelled.progressMessage ?? "Queued transaction cancelled.",
|
|
418
|
+
details: { transaction: cancelled }
|
|
419
|
+
});
|
|
420
|
+
if (cancelled.taskId !== undefined) {
|
|
421
|
+
this.updateTask(cancelled.taskId, { status: "cancelled", errorMessage: cancelled.errorMessage });
|
|
422
|
+
}
|
|
423
|
+
else {
|
|
424
|
+
this.persistState();
|
|
425
|
+
}
|
|
426
|
+
return { ok: true, transaction: this.transactions.withQueueMetadata(cancelled) };
|
|
427
|
+
}
|
|
368
428
|
previewTransactionRollback(transactionId) {
|
|
369
429
|
const transaction = this.transactions.get(transactionId);
|
|
370
430
|
if (!transaction) {
|
|
@@ -752,6 +812,10 @@ export class RuntimeService {
|
|
|
752
812
|
this.lockLeasePolicy = normalizeLockLeasePolicy(snapshot.lockLeasePolicy);
|
|
753
813
|
}
|
|
754
814
|
this.transactions.load(snapshot.transactions);
|
|
815
|
+
this.jobs.clear();
|
|
816
|
+
for (const job of snapshot.jobs ?? []) {
|
|
817
|
+
this.jobs.set(job.jobId, { ...job, transactionIds: [...job.transactionIds], warnings: [...job.warnings] });
|
|
818
|
+
}
|
|
755
819
|
this.conflicts.splice(0, this.conflicts.length, ...snapshot.conflicts.slice(-250));
|
|
756
820
|
this.conflictTelemetry.splice(0, this.conflictTelemetry.length, ...(snapshot.conflictTelemetry ?? []).slice(-1_000));
|
|
757
821
|
this.collabEvents.splice(0, this.collabEvents.length, ...snapshot.collaborationEvents.slice(-1_000));
|
|
@@ -817,6 +881,7 @@ export class RuntimeService {
|
|
|
817
881
|
locks: this.locks.dump(),
|
|
818
882
|
lockLeasePolicy: this.lockLeasePolicy,
|
|
819
883
|
transactions: this.transactions.dump(),
|
|
884
|
+
jobs: [...this.jobs.values()].map((job) => ({ ...job, transactionIds: [...job.transactionIds], warnings: [...job.warnings] })),
|
|
820
885
|
conflicts: this.conflicts.slice(-250),
|
|
821
886
|
conflictTelemetry: this.conflictTelemetry.slice(-1_000),
|
|
822
887
|
collaborationEvents: this.collabEvents.slice(-1_000),
|
|
@@ -1104,6 +1169,247 @@ export class RuntimeService {
|
|
|
1104
1169
|
compileBatch(request) {
|
|
1105
1170
|
return this.compiler.compile(request);
|
|
1106
1171
|
}
|
|
1172
|
+
preflightBatch(request) {
|
|
1173
|
+
const compiled = this.compiler.compile(request);
|
|
1174
|
+
const estimatedPayloadBytes = Buffer.byteLength(JSON.stringify(request.operations), "utf8");
|
|
1175
|
+
const chunkPlan = planBatchChunks(request.operations);
|
|
1176
|
+
const warnings = [];
|
|
1177
|
+
const needsQueue = request.operations.length > batchDirectOperationThreshold() ||
|
|
1178
|
+
estimatedPayloadBytes > batchDirectPayloadThresholdBytes() ||
|
|
1179
|
+
compiled.estimatedCellsTouched > batchDirectCellThreshold() ||
|
|
1180
|
+
(chunkPlan.safeToAutoChunk && chunkPlan.chunksTotal > 1);
|
|
1181
|
+
let recommendedExecutionMode = needsQueue ? "submit" : "apply";
|
|
1182
|
+
if (needsQueue && chunkPlan.safeToAutoChunk && chunkPlan.chunksTotal > 1) {
|
|
1183
|
+
recommendedExecutionMode = "chunked_submit";
|
|
1184
|
+
warnings.push({
|
|
1185
|
+
code: "LARGE_BATCH_WILL_BE_CHUNKED",
|
|
1186
|
+
message: `Batch is large and can be safely split into ${chunkPlan.chunksTotal} queued chunks.`,
|
|
1187
|
+
details: { strategy: chunkPlan.strategy, chunkSize: chunkPlan.chunkSize }
|
|
1188
|
+
});
|
|
1189
|
+
}
|
|
1190
|
+
else if (needsQueue) {
|
|
1191
|
+
warnings.push({
|
|
1192
|
+
code: "LARGE_BATCH_SHOULD_BE_QUEUED",
|
|
1193
|
+
message: "Batch is large enough that it should be submitted to the serialized queue instead of applied synchronously.",
|
|
1194
|
+
details: {
|
|
1195
|
+
operationThreshold: batchDirectOperationThreshold(),
|
|
1196
|
+
payloadThresholdBytes: batchDirectPayloadThresholdBytes(),
|
|
1197
|
+
cellThreshold: batchDirectCellThreshold()
|
|
1198
|
+
}
|
|
1199
|
+
});
|
|
1200
|
+
}
|
|
1201
|
+
return {
|
|
1202
|
+
ok: true,
|
|
1203
|
+
workbookId: request.workbookId,
|
|
1204
|
+
operationCount: request.operations.length,
|
|
1205
|
+
estimatedCellsTouched: compiled.estimatedCellsTouched,
|
|
1206
|
+
estimatedPayloadBytes,
|
|
1207
|
+
destructiveLevel: compiled.destructiveLevel,
|
|
1208
|
+
recommendedExecutionMode,
|
|
1209
|
+
safeToAutoChunk: chunkPlan.safeToAutoChunk,
|
|
1210
|
+
chunkPlan: chunkPlan.strategy === "none" ? undefined : chunkPlan,
|
|
1211
|
+
warnings
|
|
1212
|
+
};
|
|
1213
|
+
}
|
|
1214
|
+
submitChunkedBatch(request, input = {}) {
|
|
1215
|
+
if (request.mode !== "apply") {
|
|
1216
|
+
return {
|
|
1217
|
+
ok: false,
|
|
1218
|
+
error: runtimeError("INVALID_ARGUMENT", "Only apply-mode batches can be submitted as chunked jobs.", { retryable: false })
|
|
1219
|
+
};
|
|
1220
|
+
}
|
|
1221
|
+
const preflight = this.preflightBatch(request);
|
|
1222
|
+
if (!preflight.safeToAutoChunk || preflight.chunkPlan === undefined || preflight.chunkPlan.chunksTotal <= 1) {
|
|
1223
|
+
const submitted = this.submitBatch(request);
|
|
1224
|
+
return {
|
|
1225
|
+
ok: submitted.ok,
|
|
1226
|
+
status: "queued",
|
|
1227
|
+
progressMessage: "Batch was not safely chunkable, so it was submitted as one queued transaction.",
|
|
1228
|
+
preflight,
|
|
1229
|
+
transactionIds: submitted.transactionId ? [submitted.transactionId] : [],
|
|
1230
|
+
transactions: [submitted]
|
|
1231
|
+
};
|
|
1232
|
+
}
|
|
1233
|
+
const chunks = chunkBatchOperations(request.operations);
|
|
1234
|
+
const now = new Date().toISOString();
|
|
1235
|
+
const compiled = this.compiler.compile(request);
|
|
1236
|
+
const agentId = request.agentId ?? this.defaultAgentId;
|
|
1237
|
+
const job = {
|
|
1238
|
+
jobId: makeId("job"),
|
|
1239
|
+
workbookId: request.workbookId,
|
|
1240
|
+
agentId,
|
|
1241
|
+
kind: preflight.chunkPlan.strategy === "split_style_entries" ? "style_chunked" : preflight.chunkPlan.strategy === "split_matrix_rows" ? "matrix_chunked" : "batch_chunked",
|
|
1242
|
+
status: "queued",
|
|
1243
|
+
goal: input.goal ?? request.progressMessage ?? "Apply chunked Excel batch",
|
|
1244
|
+
transactionIds: [],
|
|
1245
|
+
chunksTotal: chunks.length,
|
|
1246
|
+
chunksCompleted: 0,
|
|
1247
|
+
progressMessage: `Queued ${chunks.length} workbook update chunks.`,
|
|
1248
|
+
retryStrategy: input.retryStrategy ?? preflight.chunkPlan.strategy,
|
|
1249
|
+
destructiveLevel: compiled.destructiveLevel,
|
|
1250
|
+
warnings: preflight.warnings,
|
|
1251
|
+
queuedAt: now
|
|
1252
|
+
};
|
|
1253
|
+
if (request.taskId !== undefined) {
|
|
1254
|
+
job.taskId = request.taskId;
|
|
1255
|
+
}
|
|
1256
|
+
if (request.planId !== undefined) {
|
|
1257
|
+
job.planId = request.planId;
|
|
1258
|
+
}
|
|
1259
|
+
this.jobs.set(job.jobId, job);
|
|
1260
|
+
const transactions = chunks.map((chunk, index) => this.submitBatch({
|
|
1261
|
+
...request,
|
|
1262
|
+
operations: chunk,
|
|
1263
|
+
retryStrategy: job.retryStrategy,
|
|
1264
|
+
chunksTotal: chunks.length,
|
|
1265
|
+
chunksCompleted: index,
|
|
1266
|
+
progressMessage: `Queued workbook update chunk ${index + 1} of ${chunks.length}.`
|
|
1267
|
+
}));
|
|
1268
|
+
job.transactionIds = transactions.map((transaction) => transaction.transactionId).filter(Boolean);
|
|
1269
|
+
this.refreshJob(job.jobId);
|
|
1270
|
+
this.persistState();
|
|
1271
|
+
return {
|
|
1272
|
+
ok: true,
|
|
1273
|
+
status: "queued",
|
|
1274
|
+
job: this.getJobRecord(job.jobId),
|
|
1275
|
+
jobId: job.jobId,
|
|
1276
|
+
preflight,
|
|
1277
|
+
retryStrategy: job.retryStrategy,
|
|
1278
|
+
chunksTotal: chunks.length,
|
|
1279
|
+
chunksCompleted: 0,
|
|
1280
|
+
transactionIds: job.transactionIds,
|
|
1281
|
+
transactions,
|
|
1282
|
+
progressMessage: `Batch is large, so Open Workbook queued it as job ${job.jobId} with ${chunks.length} chunks. Use excel.job.wait or excel.job.get for progress.`
|
|
1283
|
+
};
|
|
1284
|
+
}
|
|
1285
|
+
listJobs(workbookId) {
|
|
1286
|
+
return {
|
|
1287
|
+
ok: true,
|
|
1288
|
+
jobs: [...this.jobs.values()]
|
|
1289
|
+
.filter((job) => workbookId === undefined || job.workbookId === workbookId)
|
|
1290
|
+
.map((job) => this.refreshJob(job.jobId))
|
|
1291
|
+
.sort((a, b) => (b.finishedAt ?? b.startedAt ?? b.queuedAt).localeCompare(a.finishedAt ?? a.startedAt ?? a.queuedAt))
|
|
1292
|
+
};
|
|
1293
|
+
}
|
|
1294
|
+
getJob(jobId) {
|
|
1295
|
+
const job = this.getJobRecord(jobId);
|
|
1296
|
+
return job
|
|
1297
|
+
? { ok: true, job }
|
|
1298
|
+
: { ok: false, error: runtimeError("NOT_FOUND", `Job not found: ${jobId}`, { retryable: false }) };
|
|
1299
|
+
}
|
|
1300
|
+
async waitJob(jobId, timeoutMs = 30_000, pollMs = 250) {
|
|
1301
|
+
const started = Date.now();
|
|
1302
|
+
while (true) {
|
|
1303
|
+
const job = this.getJobRecord(jobId);
|
|
1304
|
+
if (!job) {
|
|
1305
|
+
return { ok: false, completed: false, error: runtimeError("NOT_FOUND", `Job not found: ${jobId}`, { retryable: false }) };
|
|
1306
|
+
}
|
|
1307
|
+
if (isTerminalJobStatus(job.status)) {
|
|
1308
|
+
return { ok: true, completed: true, job };
|
|
1309
|
+
}
|
|
1310
|
+
if (Date.now() - started >= timeoutMs) {
|
|
1311
|
+
return {
|
|
1312
|
+
ok: true,
|
|
1313
|
+
completed: false,
|
|
1314
|
+
job,
|
|
1315
|
+
error: runtimeError("TIMEOUT", `Timed out waiting for job ${jobId}.`, { retryable: true })
|
|
1316
|
+
};
|
|
1317
|
+
}
|
|
1318
|
+
await sleep(Math.max(25, pollMs));
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
cancelJob(jobId) {
|
|
1322
|
+
const job = this.getJobRecord(jobId);
|
|
1323
|
+
if (!job) {
|
|
1324
|
+
return { ok: false, error: runtimeError("NOT_FOUND", `Job not found: ${jobId}`, { retryable: false }) };
|
|
1325
|
+
}
|
|
1326
|
+
const cancelled = [];
|
|
1327
|
+
const skipped = [];
|
|
1328
|
+
for (const transactionId of job.transactionIds) {
|
|
1329
|
+
const transaction = this.transactions.get(transactionId);
|
|
1330
|
+
if (!transaction) {
|
|
1331
|
+
continue;
|
|
1332
|
+
}
|
|
1333
|
+
if (transaction.status === "queued") {
|
|
1334
|
+
const result = this.cancelTransaction(transactionId);
|
|
1335
|
+
if (result.ok && result.transaction) {
|
|
1336
|
+
cancelled.push(result.transaction);
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
else if (!isTerminalTransactionStatus(transaction.status)) {
|
|
1340
|
+
skipped.push(this.transactions.withQueueMetadata(transaction));
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
const refreshed = this.refreshJob(jobId);
|
|
1344
|
+
this.persistState();
|
|
1345
|
+
return {
|
|
1346
|
+
ok: skipped.length === 0,
|
|
1347
|
+
job: refreshed,
|
|
1348
|
+
cancelledTransactions: cancelled,
|
|
1349
|
+
skippedTransactions: skipped,
|
|
1350
|
+
progressMessage: skipped.length === 0
|
|
1351
|
+
? `Cancelled queued work for job ${jobId}.`
|
|
1352
|
+
: `Cancelled queued chunks for job ${jobId}; ${skipped.length} chunk(s) were already applying and could not be cancelled.`
|
|
1353
|
+
};
|
|
1354
|
+
}
|
|
1355
|
+
getJobRecord(jobId) {
|
|
1356
|
+
const job = this.jobs.get(jobId);
|
|
1357
|
+
return job ? { ...this.refreshJob(jobId), transactionIds: [...job.transactionIds], warnings: [...job.warnings] } : undefined;
|
|
1358
|
+
}
|
|
1359
|
+
refreshJob(jobId) {
|
|
1360
|
+
const job = this.jobs.get(jobId);
|
|
1361
|
+
if (!job) {
|
|
1362
|
+
throw new Error(`Job not found: ${jobId}`);
|
|
1363
|
+
}
|
|
1364
|
+
const transactions = job.transactionIds.map((transactionId) => this.transactions.get(transactionId)).filter((transaction) => transaction !== undefined);
|
|
1365
|
+
const appliedCount = transactions.filter((transaction) => transaction.status === "applied").length;
|
|
1366
|
+
const terminalTransactions = transactions.filter((transaction) => isTerminalTransactionStatus(transaction.status));
|
|
1367
|
+
const failed = transactions.find((transaction) => transaction.status === "failed" || transaction.status === "blocked");
|
|
1368
|
+
const applying = transactions.some((transaction) => transaction.status === "applying");
|
|
1369
|
+
const queued = transactions.some((transaction) => transaction.status === "queued");
|
|
1370
|
+
const cancelledCount = transactions.filter((transaction) => transaction.status === "cancelled").length;
|
|
1371
|
+
let status = job.status;
|
|
1372
|
+
if (transactions.length === 0) {
|
|
1373
|
+
status = "queued";
|
|
1374
|
+
}
|
|
1375
|
+
else if (appliedCount === transactions.length) {
|
|
1376
|
+
status = "applied";
|
|
1377
|
+
}
|
|
1378
|
+
else if (cancelledCount === transactions.length) {
|
|
1379
|
+
status = "cancelled";
|
|
1380
|
+
}
|
|
1381
|
+
else if (terminalTransactions.length === transactions.length && appliedCount > 0) {
|
|
1382
|
+
status = "partially_applied";
|
|
1383
|
+
}
|
|
1384
|
+
else if (terminalTransactions.length === transactions.length) {
|
|
1385
|
+
status = failed !== undefined ? "failed" : "cancelled";
|
|
1386
|
+
}
|
|
1387
|
+
else if (failed !== undefined) {
|
|
1388
|
+
status = appliedCount > 0 ? "partially_applied" : "failed";
|
|
1389
|
+
}
|
|
1390
|
+
else if (appliedCount > 0 && terminalTransactions.length < transactions.length) {
|
|
1391
|
+
status = "partially_applied";
|
|
1392
|
+
}
|
|
1393
|
+
else if (applying) {
|
|
1394
|
+
status = "applying";
|
|
1395
|
+
}
|
|
1396
|
+
else if (queued) {
|
|
1397
|
+
status = "queued";
|
|
1398
|
+
}
|
|
1399
|
+
const now = new Date().toISOString();
|
|
1400
|
+
job.status = status;
|
|
1401
|
+
job.chunksCompleted = appliedCount;
|
|
1402
|
+
job.startedAt = job.startedAt ?? transactions.find((transaction) => transaction.startedAt !== undefined)?.startedAt;
|
|
1403
|
+
if (isTerminalJobStatus(status)) {
|
|
1404
|
+
job.finishedAt = job.finishedAt ?? now;
|
|
1405
|
+
}
|
|
1406
|
+
if (failed !== undefined) {
|
|
1407
|
+
job.errorCode = failed.errorCode;
|
|
1408
|
+
job.errorMessage = failed.errorMessage;
|
|
1409
|
+
}
|
|
1410
|
+
job.progressMessage = jobProgressMessage(job, transactions.length);
|
|
1411
|
+
return job;
|
|
1412
|
+
}
|
|
1107
1413
|
async previewPlan(planId) {
|
|
1108
1414
|
const preview = this.plans.previewPlan(planId);
|
|
1109
1415
|
const client = this.getActiveAddinClient();
|
|
@@ -4142,6 +4448,34 @@ export class RuntimeService {
|
|
|
4142
4448
|
if (request.mode !== "apply") {
|
|
4143
4449
|
return this.applyBatchDirect(request);
|
|
4144
4450
|
}
|
|
4451
|
+
const returnQueuedProgress = this.isRuntimeMutationBusy();
|
|
4452
|
+
const scheduled = this.scheduleBatch(request);
|
|
4453
|
+
if (returnQueuedProgress) {
|
|
4454
|
+
void scheduled.promise.catch(() => undefined);
|
|
4455
|
+
return queuedOperationResult(this.transactions.withQueueMetadata(scheduled.transaction));
|
|
4456
|
+
}
|
|
4457
|
+
return scheduled.promise;
|
|
4458
|
+
}
|
|
4459
|
+
submitBatch(request) {
|
|
4460
|
+
if (request.mode !== "apply") {
|
|
4461
|
+
return {
|
|
4462
|
+
ok: false,
|
|
4463
|
+
error: runtimeError("INVALID_ARGUMENT", "Only apply-mode batches can be submitted to the mutation queue.", { retryable: false })
|
|
4464
|
+
};
|
|
4465
|
+
}
|
|
4466
|
+
const scheduled = this.scheduleBatch(request);
|
|
4467
|
+
void scheduled.promise.catch(() => undefined);
|
|
4468
|
+
const transaction = this.transactions.withQueueMetadata(scheduled.transaction);
|
|
4469
|
+
return {
|
|
4470
|
+
ok: true,
|
|
4471
|
+
transactionId: transaction.transactionId,
|
|
4472
|
+
status: transaction.status,
|
|
4473
|
+
queuePosition: transaction.queuePosition,
|
|
4474
|
+
progressMessage: transaction.progressMessage,
|
|
4475
|
+
transaction
|
|
4476
|
+
};
|
|
4477
|
+
}
|
|
4478
|
+
scheduleBatch(request) {
|
|
4145
4479
|
const compiled = this.compiler.compile(request);
|
|
4146
4480
|
const agentId = request.agentId ?? this.defaultAgentId;
|
|
4147
4481
|
const scopes = scopesFromBatch(request.workbookId, request.operations);
|
|
@@ -4153,7 +4487,11 @@ export class RuntimeService {
|
|
|
4153
4487
|
goal: request.operations.map((operation) => operation.reason || operation.kind).join("; ") || "Apply Excel batch",
|
|
4154
4488
|
scopes,
|
|
4155
4489
|
baseFingerprints: request.expectedTargetFingerprints ?? [],
|
|
4156
|
-
destructiveLevel: compiled.destructiveLevel
|
|
4490
|
+
destructiveLevel: compiled.destructiveLevel,
|
|
4491
|
+
progressMessage: request.progressMessage,
|
|
4492
|
+
retryStrategy: request.retryStrategy,
|
|
4493
|
+
chunksTotal: request.chunksTotal,
|
|
4494
|
+
chunksCompleted: request.chunksCompleted
|
|
4157
4495
|
});
|
|
4158
4496
|
if (request.taskId !== undefined) {
|
|
4159
4497
|
this.tasks.attachTransaction(request.taskId, transaction.transactionId);
|
|
@@ -4167,7 +4505,7 @@ export class RuntimeService {
|
|
|
4167
4505
|
transactionId: transaction.transactionId,
|
|
4168
4506
|
message: `Transaction queued: ${transaction.goal}`
|
|
4169
4507
|
});
|
|
4170
|
-
|
|
4508
|
+
const promise = this.enqueueTransaction(transaction.transactionId, async () => {
|
|
4171
4509
|
const lockResult = this.locks.acquire({
|
|
4172
4510
|
workbookId: request.workbookId,
|
|
4173
4511
|
ownerAgentId: agentId,
|
|
@@ -4200,6 +4538,8 @@ export class RuntimeService {
|
|
|
4200
4538
|
return {
|
|
4201
4539
|
ok: false,
|
|
4202
4540
|
transactionId: transaction.transactionId,
|
|
4541
|
+
transactionStatus: "blocked",
|
|
4542
|
+
progressMessage: "Workbook mutation is blocked by an active lock.",
|
|
4203
4543
|
taskId: request.taskId,
|
|
4204
4544
|
agentId,
|
|
4205
4545
|
rollbackAvailable: compiled.requiredBackups.length > 0,
|
|
@@ -4244,6 +4584,8 @@ export class RuntimeService {
|
|
|
4244
4584
|
const enriched = {
|
|
4245
4585
|
...result,
|
|
4246
4586
|
transactionId: transaction.transactionId,
|
|
4587
|
+
transactionStatus: result.ok ? "applied" : "failed",
|
|
4588
|
+
progressMessage: result.ok ? "Workbook mutation applied successfully." : (result.error?.message ?? "Workbook mutation failed."),
|
|
4247
4589
|
agentId
|
|
4248
4590
|
};
|
|
4249
4591
|
if (request.taskId !== undefined) {
|
|
@@ -4285,6 +4627,82 @@ export class RuntimeService {
|
|
|
4285
4627
|
}
|
|
4286
4628
|
return enriched;
|
|
4287
4629
|
}
|
|
4630
|
+
catch (error) {
|
|
4631
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4632
|
+
const code = /timed out|timeout/i.test(message) ? "TIMEOUT" : "TRANSACTION_FAILED";
|
|
4633
|
+
if (code === "TIMEOUT" && shouldRetryStyleBatch(request)) {
|
|
4634
|
+
const chunks = chunkOperationsForRetry(request.operations);
|
|
4635
|
+
const warnings = [
|
|
4636
|
+
{
|
|
4637
|
+
code: "RETRYING_SMALLER_BATCH",
|
|
4638
|
+
message: `Style batch timed out and was resubmitted as ${chunks.length} smaller queued chunks.`
|
|
4639
|
+
}
|
|
4640
|
+
];
|
|
4641
|
+
this.transactions.markFailed(transaction.transactionId, code, message, warnings);
|
|
4642
|
+
if (request.taskId !== undefined) {
|
|
4643
|
+
this.updateTask(request.taskId, { status: "queued", currentStep: "Retrying style update in smaller chunks" });
|
|
4644
|
+
}
|
|
4645
|
+
this.recordCollabEvent({
|
|
4646
|
+
type: "transaction.failed",
|
|
4647
|
+
workbookId: request.workbookId,
|
|
4648
|
+
agentId,
|
|
4649
|
+
taskId: request.taskId,
|
|
4650
|
+
transactionId: transaction.transactionId,
|
|
4651
|
+
message: `${message}; retrying style update in smaller chunks.`
|
|
4652
|
+
});
|
|
4653
|
+
const retryTransactions = chunks.map((chunk, index) => this.submitBatch({
|
|
4654
|
+
...request,
|
|
4655
|
+
operations: chunk,
|
|
4656
|
+
retryStrategy: "retry_timeout_split_style_entries",
|
|
4657
|
+
chunksTotal: chunks.length,
|
|
4658
|
+
chunksCompleted: index,
|
|
4659
|
+
progressMessage: `Retrying style update chunk ${index + 1} of ${chunks.length}.`
|
|
4660
|
+
}));
|
|
4661
|
+
return {
|
|
4662
|
+
ok: true,
|
|
4663
|
+
transactionId: transaction.transactionId,
|
|
4664
|
+
transactionStatus: "failed",
|
|
4665
|
+
progressMessage: `Style batch timed out, so Open Workbook queued ${chunks.length} smaller retry chunks.`,
|
|
4666
|
+
taskId: request.taskId,
|
|
4667
|
+
agentId,
|
|
4668
|
+
rollbackAvailable: false,
|
|
4669
|
+
backups: [],
|
|
4670
|
+
warnings,
|
|
4671
|
+
telemetry: { warningCount: warnings.length },
|
|
4672
|
+
data: {
|
|
4673
|
+
retryStrategy: "retry_timeout_split_style_entries",
|
|
4674
|
+
chunksTotal: chunks.length,
|
|
4675
|
+
retryTransactionIds: retryTransactions.map((retry) => retry.transactionId).filter(Boolean),
|
|
4676
|
+
retryTransactions
|
|
4677
|
+
}
|
|
4678
|
+
};
|
|
4679
|
+
}
|
|
4680
|
+
this.transactions.markFailed(transaction.transactionId, code, message);
|
|
4681
|
+
if (request.taskId !== undefined) {
|
|
4682
|
+
this.updateTask(request.taskId, { status: "failed", errorMessage: message });
|
|
4683
|
+
}
|
|
4684
|
+
this.recordCollabEvent({
|
|
4685
|
+
type: "transaction.failed",
|
|
4686
|
+
workbookId: request.workbookId,
|
|
4687
|
+
agentId,
|
|
4688
|
+
taskId: request.taskId,
|
|
4689
|
+
transactionId: transaction.transactionId,
|
|
4690
|
+
message
|
|
4691
|
+
});
|
|
4692
|
+
return {
|
|
4693
|
+
ok: false,
|
|
4694
|
+
transactionId: transaction.transactionId,
|
|
4695
|
+
transactionStatus: "failed",
|
|
4696
|
+
progressMessage: message,
|
|
4697
|
+
taskId: request.taskId,
|
|
4698
|
+
agentId,
|
|
4699
|
+
rollbackAvailable: compiled.requiredBackups.length > 0,
|
|
4700
|
+
backups: [],
|
|
4701
|
+
warnings: [],
|
|
4702
|
+
telemetry: { warningCount: 0 },
|
|
4703
|
+
error: runtimeError(code === "TIMEOUT" ? "TIMEOUT" : "OPERATION_FAILED", message, { retryable: code === "TIMEOUT" })
|
|
4704
|
+
};
|
|
4705
|
+
}
|
|
4288
4706
|
finally {
|
|
4289
4707
|
const releasedLocks = this.locks.release(lockResult.locks.map((lock) => lock.lockId));
|
|
4290
4708
|
this.markConflictTelemetryClearedByLock(releasedLocks.map((lock) => lock.lockId));
|
|
@@ -4305,15 +4723,36 @@ export class RuntimeService {
|
|
|
4305
4723
|
}
|
|
4306
4724
|
}
|
|
4307
4725
|
});
|
|
4726
|
+
return { transaction, promise };
|
|
4308
4727
|
}
|
|
4309
|
-
enqueueTransaction(work) {
|
|
4310
|
-
return this.enqueueRuntimeMutation(
|
|
4728
|
+
enqueueTransaction(transactionId, work) {
|
|
4729
|
+
return this.enqueueRuntimeMutation(async () => {
|
|
4730
|
+
const transaction = this.transactions.get(transactionId);
|
|
4731
|
+
if (this.cancelledQueuedTransactions.delete(transactionId) || transaction?.status === "cancelled") {
|
|
4732
|
+
return cancelledOperationResult(transactionId);
|
|
4733
|
+
}
|
|
4734
|
+
return work();
|
|
4735
|
+
});
|
|
4311
4736
|
}
|
|
4312
4737
|
enqueueRuntimeMutation(work) {
|
|
4313
|
-
|
|
4738
|
+
this.runtimeMutationQueuedCount += 1;
|
|
4739
|
+
const run = this.transactionQueue.then(() => this.runQueuedRuntimeMutation(work), () => this.runQueuedRuntimeMutation(work));
|
|
4314
4740
|
this.transactionQueue = run.then(() => undefined, () => undefined);
|
|
4315
4741
|
return run;
|
|
4316
4742
|
}
|
|
4743
|
+
async runQueuedRuntimeMutation(work) {
|
|
4744
|
+
this.runtimeMutationQueuedCount = Math.max(0, this.runtimeMutationQueuedCount - 1);
|
|
4745
|
+
this.runtimeMutationActive = true;
|
|
4746
|
+
try {
|
|
4747
|
+
return await work();
|
|
4748
|
+
}
|
|
4749
|
+
finally {
|
|
4750
|
+
this.runtimeMutationActive = false;
|
|
4751
|
+
}
|
|
4752
|
+
}
|
|
4753
|
+
isRuntimeMutationBusy() {
|
|
4754
|
+
return this.runtimeMutationActive || this.runtimeMutationQueuedCount > 0;
|
|
4755
|
+
}
|
|
4317
4756
|
async applyDirectTransaction(input, work) {
|
|
4318
4757
|
const agentId = input.agentId ?? this.defaultAgentId;
|
|
4319
4758
|
const transaction = this.transactions.create({
|
|
@@ -6286,6 +6725,201 @@ function telemetryBuckets(records, keySelector) {
|
|
|
6286
6725
|
}))
|
|
6287
6726
|
.sort((a, b) => b.count - a.count || b.lastSeenAt.localeCompare(a.lastSeenAt));
|
|
6288
6727
|
}
|
|
6728
|
+
function isTerminalTransactionStatus(status) {
|
|
6729
|
+
return ["applied", "failed", "blocked", "rolled_back", "cancelled"].includes(status);
|
|
6730
|
+
}
|
|
6731
|
+
function isTerminalJobStatus(status) {
|
|
6732
|
+
return ["applied", "failed", "partially_applied", "cancelled"].includes(status);
|
|
6733
|
+
}
|
|
6734
|
+
function jobProgressMessage(job, knownTransactionCount) {
|
|
6735
|
+
switch (job.status) {
|
|
6736
|
+
case "queued":
|
|
6737
|
+
return `Workbook job is queued with ${knownTransactionCount || job.chunksTotal} chunk(s).`;
|
|
6738
|
+
case "applying":
|
|
6739
|
+
return `Workbook job is applying ${job.chunksCompleted} of ${job.chunksTotal} chunk(s).`;
|
|
6740
|
+
case "partially_applied":
|
|
6741
|
+
return job.errorMessage
|
|
6742
|
+
? `Workbook job partially applied ${job.chunksCompleted} of ${job.chunksTotal} chunk(s): ${job.errorMessage}`
|
|
6743
|
+
: `Workbook job partially applied ${job.chunksCompleted} of ${job.chunksTotal} chunk(s).`;
|
|
6744
|
+
case "applied":
|
|
6745
|
+
return `Workbook job applied all ${job.chunksTotal} chunk(s).`;
|
|
6746
|
+
case "failed":
|
|
6747
|
+
return job.errorMessage ? `Workbook job failed: ${job.errorMessage}` : "Workbook job failed.";
|
|
6748
|
+
case "cancelled":
|
|
6749
|
+
return "Workbook job was cancelled before all chunks applied.";
|
|
6750
|
+
}
|
|
6751
|
+
}
|
|
6752
|
+
function planBatchChunks(operations) {
|
|
6753
|
+
const chunkedOperations = chunkBatchOperations(operations);
|
|
6754
|
+
if (chunkedOperations.length <= 1 || chunkedOperations.length === operations.length) {
|
|
6755
|
+
return {
|
|
6756
|
+
strategy: "none",
|
|
6757
|
+
chunksTotal: 1,
|
|
6758
|
+
chunkSize: operations.length,
|
|
6759
|
+
operationCount: operations.length,
|
|
6760
|
+
chunkedOperationKinds: [],
|
|
6761
|
+
safeToAutoChunk: false
|
|
6762
|
+
};
|
|
6763
|
+
}
|
|
6764
|
+
const allChunkable = operations.every((operation) => isStyleChunkableOperation(operation) || isMatrixChunkableOperation(operation));
|
|
6765
|
+
if (!allChunkable) {
|
|
6766
|
+
return {
|
|
6767
|
+
strategy: "none",
|
|
6768
|
+
chunksTotal: 1,
|
|
6769
|
+
chunkSize: operations.length,
|
|
6770
|
+
operationCount: operations.length,
|
|
6771
|
+
chunkedOperationKinds: [],
|
|
6772
|
+
safeToAutoChunk: false
|
|
6773
|
+
};
|
|
6774
|
+
}
|
|
6775
|
+
const hasStyle = operations.some(isStyleChunkableOperation);
|
|
6776
|
+
const hasMatrix = operations.some(isMatrixChunkableOperation);
|
|
6777
|
+
const chunkSize = hasMatrix ? matrixChunkRowCount() : styleBatchChunkSize();
|
|
6778
|
+
return {
|
|
6779
|
+
strategy: hasStyle && hasMatrix ? "mixed" : hasMatrix ? "split_matrix_rows" : "split_style_entries",
|
|
6780
|
+
chunksTotal: chunkedOperations.length,
|
|
6781
|
+
chunkSize,
|
|
6782
|
+
operationCount: operations.length,
|
|
6783
|
+
chunkedOperationKinds: [...new Set(operations.map((operation) => operation.kind))],
|
|
6784
|
+
safeToAutoChunk: true
|
|
6785
|
+
};
|
|
6786
|
+
}
|
|
6787
|
+
function chunkBatchOperations(operations) {
|
|
6788
|
+
if (operations.length === 0) {
|
|
6789
|
+
return [];
|
|
6790
|
+
}
|
|
6791
|
+
if (operations.every(isStyleChunkableOperation)) {
|
|
6792
|
+
return chunkArray(operations, styleBatchChunkSize());
|
|
6793
|
+
}
|
|
6794
|
+
if (!operations.every((operation) => isStyleChunkableOperation(operation) || isMatrixChunkableOperation(operation))) {
|
|
6795
|
+
return [operations];
|
|
6796
|
+
}
|
|
6797
|
+
const chunks = [];
|
|
6798
|
+
for (const operation of operations) {
|
|
6799
|
+
if (isMatrixChunkableOperation(operation)) {
|
|
6800
|
+
chunks.push(...chunkMatrixOperation(operation));
|
|
6801
|
+
}
|
|
6802
|
+
else {
|
|
6803
|
+
chunks.push([operation]);
|
|
6804
|
+
}
|
|
6805
|
+
}
|
|
6806
|
+
return chunks.length > 1 ? chunks : [operations];
|
|
6807
|
+
}
|
|
6808
|
+
function isStyleChunkableOperation(operation) {
|
|
6809
|
+
return operation.kind === "range.write_styles";
|
|
6810
|
+
}
|
|
6811
|
+
function isMatrixChunkableOperation(operation) {
|
|
6812
|
+
return operation.kind === "range.write_values" || operation.kind === "range.write_formulas" || operation.kind === "range.write_number_formats";
|
|
6813
|
+
}
|
|
6814
|
+
function chunkMatrixOperation(operation) {
|
|
6815
|
+
const matrix = matrixForOperation(operation);
|
|
6816
|
+
const rowCount = matrix.length;
|
|
6817
|
+
const chunkRows = matrixChunkRowCount();
|
|
6818
|
+
if (rowCount <= chunkRows) {
|
|
6819
|
+
return [[operation]];
|
|
6820
|
+
}
|
|
6821
|
+
const chunks = [];
|
|
6822
|
+
const parsed = parseA1Address(stripSheetName(operation.target.address));
|
|
6823
|
+
for (let start = 0; start < rowCount; start += chunkRows) {
|
|
6824
|
+
const rows = matrix.slice(start, start + chunkRows);
|
|
6825
|
+
const address = formatA1Address({
|
|
6826
|
+
...parsed,
|
|
6827
|
+
startRow: parsed.startRow + start,
|
|
6828
|
+
endRow: parsed.startRow + start + rows.length - 1
|
|
6829
|
+
});
|
|
6830
|
+
chunks.push([cloneMatrixOperation(operation, rows, address)]);
|
|
6831
|
+
}
|
|
6832
|
+
return chunks;
|
|
6833
|
+
}
|
|
6834
|
+
function matrixForOperation(operation) {
|
|
6835
|
+
switch (operation.kind) {
|
|
6836
|
+
case "range.write_values":
|
|
6837
|
+
return operation.values;
|
|
6838
|
+
case "range.write_formulas":
|
|
6839
|
+
return operation.formulas;
|
|
6840
|
+
case "range.write_number_formats":
|
|
6841
|
+
return operation.numberFormat;
|
|
6842
|
+
}
|
|
6843
|
+
}
|
|
6844
|
+
function cloneMatrixOperation(operation, rows, address) {
|
|
6845
|
+
const target = { ...operation.target, address };
|
|
6846
|
+
switch (operation.kind) {
|
|
6847
|
+
case "range.write_values":
|
|
6848
|
+
return { ...operation, operationId: makeId("op"), target, values: rows };
|
|
6849
|
+
case "range.write_formulas":
|
|
6850
|
+
return { ...operation, operationId: makeId("op"), target, formulas: rows };
|
|
6851
|
+
case "range.write_number_formats":
|
|
6852
|
+
return { ...operation, operationId: makeId("op"), target, numberFormat: rows };
|
|
6853
|
+
}
|
|
6854
|
+
}
|
|
6855
|
+
function batchDirectOperationThreshold() {
|
|
6856
|
+
return positiveIntegerEnv("OPEN_WORKBOOK_BATCH_DIRECT_OPERATION_THRESHOLD", 25);
|
|
6857
|
+
}
|
|
6858
|
+
function batchDirectPayloadThresholdBytes() {
|
|
6859
|
+
return positiveIntegerEnv("OPEN_WORKBOOK_BATCH_DIRECT_PAYLOAD_BYTES", 512_000);
|
|
6860
|
+
}
|
|
6861
|
+
function batchDirectCellThreshold() {
|
|
6862
|
+
return positiveIntegerEnv("OPEN_WORKBOOK_BATCH_DIRECT_CELL_THRESHOLD", 50_000);
|
|
6863
|
+
}
|
|
6864
|
+
function styleBatchChunkSize() {
|
|
6865
|
+
return positiveIntegerEnv("OPEN_WORKBOOK_STYLE_BATCH_CHUNK_SIZE", 25);
|
|
6866
|
+
}
|
|
6867
|
+
function matrixChunkRowCount() {
|
|
6868
|
+
return positiveIntegerEnv("OPEN_WORKBOOK_MATRIX_CHUNK_ROWS", 500);
|
|
6869
|
+
}
|
|
6870
|
+
function positiveIntegerEnv(name, fallback) {
|
|
6871
|
+
const value = Number(process.env[name] ?? fallback);
|
|
6872
|
+
return Number.isFinite(value) && value > 0 ? Math.round(value) : fallback;
|
|
6873
|
+
}
|
|
6874
|
+
function chunkArray(items, size) {
|
|
6875
|
+
const chunks = [];
|
|
6876
|
+
for (let index = 0; index < items.length; index += size) {
|
|
6877
|
+
chunks.push(items.slice(index, index + size));
|
|
6878
|
+
}
|
|
6879
|
+
return chunks;
|
|
6880
|
+
}
|
|
6881
|
+
function sleep(ms) {
|
|
6882
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
6883
|
+
}
|
|
6884
|
+
function cancelledOperationResult(transactionId) {
|
|
6885
|
+
return {
|
|
6886
|
+
ok: false,
|
|
6887
|
+
transactionId,
|
|
6888
|
+
transactionStatus: "cancelled",
|
|
6889
|
+
progressMessage: "Queued workbook mutation was cancelled before it reached Excel.",
|
|
6890
|
+
rollbackAvailable: false,
|
|
6891
|
+
backups: [],
|
|
6892
|
+
warnings: [],
|
|
6893
|
+
telemetry: { warningCount: 0 },
|
|
6894
|
+
error: runtimeError("TRANSACTION_CANCELLED", "Queued workbook mutation was cancelled before it reached Excel.", { retryable: false })
|
|
6895
|
+
};
|
|
6896
|
+
}
|
|
6897
|
+
function queuedOperationResult(transaction) {
|
|
6898
|
+
return {
|
|
6899
|
+
ok: true,
|
|
6900
|
+
transactionId: transaction.transactionId,
|
|
6901
|
+
transactionStatus: "queued",
|
|
6902
|
+
queuePosition: transaction.queuePosition,
|
|
6903
|
+
progressMessage: transaction.progressMessage ?? "Workbook mutation is queued and will apply when earlier workbook work finishes.",
|
|
6904
|
+
rollbackAvailable: false,
|
|
6905
|
+
backups: [],
|
|
6906
|
+
warnings: [],
|
|
6907
|
+
telemetry: { warningCount: 0 }
|
|
6908
|
+
};
|
|
6909
|
+
}
|
|
6910
|
+
function shouldRetryStyleBatch(request) {
|
|
6911
|
+
return request.retryStrategy !== "retry_timeout_split_style_entries"
|
|
6912
|
+
&& request.operations.length > 1
|
|
6913
|
+
&& request.operations.every((operation) => operation.kind === "range.write_styles");
|
|
6914
|
+
}
|
|
6915
|
+
function chunkOperationsForRetry(operations) {
|
|
6916
|
+
const chunkSize = Math.max(1, Math.ceil(operations.length / 2));
|
|
6917
|
+
const chunks = [];
|
|
6918
|
+
for (let index = 0; index < operations.length; index += chunkSize) {
|
|
6919
|
+
chunks.push(operations.slice(index, index + chunkSize));
|
|
6920
|
+
}
|
|
6921
|
+
return chunks;
|
|
6922
|
+
}
|
|
6289
6923
|
function scopeTelemetryKey(scope) {
|
|
6290
6924
|
switch (scope.type) {
|
|
6291
6925
|
case "workbook":
|