deepspace 0.20.0 → 0.21.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/CHANGELOG.md +18 -0
- package/dist/{chunk-A7UFIOWO.js → chunk-KDCDRHBA.js} +7 -2
- package/dist/chunk-KDCDRHBA.js.map +1 -0
- package/dist/cli.js +193 -62
- package/dist/cli.js.map +1 -1
- package/dist/documentation-client-core.d.ts +3 -2
- package/dist/documentation-client-core.js +1 -1
- package/dist/documentation-default-renderer.cjs +48 -48
- package/dist/documentation-react.d.ts +2 -1
- package/dist/documentation-react.js +1 -1
- package/dist/documentation-runtime.js +7 -7
- package/dist/documentation-server-core.d.ts +3 -2
- package/dist/documentation-server-core.js +1 -1
- package/dist/documentation.d.ts +40 -12
- package/dist/documentation.js +40 -7
- package/dist/documentation.js.map +1 -1
- package/dist/index.d.ts +225 -21
- package/dist/index.js +239 -100
- package/dist/index.js.map +1 -1
- package/dist/{public-CFAj1hL2.d.ts → public-DenQ1TsZ.d.ts} +36 -11
- package/dist/server.d.ts +21 -0
- package/dist/server.js +148 -39
- package/dist/server.js.map +1 -1
- package/dist/testing-mcp.d.ts +69 -0
- package/dist/testing-mcp.js +99 -0
- package/dist/testing-mcp.js.map +1 -0
- package/dist/testing.d.ts +17 -20
- package/dist/testing.js +47 -25
- package/dist/testing.js.map +1 -1
- package/dist/worker.d.ts +48 -3
- package/dist/worker.js +234 -62
- package/dist/worker.js.map +1 -1
- package/features/ai-chat/feature.json +2 -2
- package/features/ai-chat/src/AiChatPage.tsx +56 -21
- package/features/ai-chat/src/ChatPanel.tsx +33 -10
- package/features/ai-chat/src/__tests__/ChatPanel.lifecycle.test.tsx +156 -0
- package/features/cron/src/CronLogPage.tsx +107 -69
- package/features/file-attachments/feature.json +2 -1
- package/features/file-manager/feature.json +2 -1
- package/features/file-manager/src/FileManagerPage.tsx +227 -194
- package/features/integration-test/src/IntegrationTestPage.tsx +54 -34
- package/package.json +5 -1
- package/dist/chunk-A7UFIOWO.js.map +0 -1
|
@@ -1,4 +1,24 @@
|
|
|
1
1
|
import { ReactNode, ReactElement, ComponentType } from 'react';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
declare const themeObjectSchema: z.ZodObject<{
|
|
5
|
+
accent: z.ZodOptional<z.ZodString>;
|
|
6
|
+
accentDark: z.ZodOptional<z.ZodString>;
|
|
7
|
+
background: z.ZodOptional<z.ZodString>;
|
|
8
|
+
density: z.ZodOptional<z.ZodEnum<{
|
|
9
|
+
comfortable: "comfortable";
|
|
10
|
+
compact: "compact";
|
|
11
|
+
}>>;
|
|
12
|
+
logo: z.ZodOptional<z.ZodString>;
|
|
13
|
+
logoDark: z.ZodOptional<z.ZodString>;
|
|
14
|
+
favicon: z.ZodOptional<z.ZodString>;
|
|
15
|
+
defaultMode: z.ZodOptional<z.ZodEnum<{
|
|
16
|
+
light: "light";
|
|
17
|
+
dark: "dark";
|
|
18
|
+
system: "system";
|
|
19
|
+
}>>;
|
|
20
|
+
strictMode: z.ZodOptional<z.ZodBoolean>;
|
|
21
|
+
}, z.core.$strip>;
|
|
2
22
|
|
|
3
23
|
type DocumentationAssistantAccess = 'disabled' | 'public' | 'authenticated';
|
|
4
24
|
type DocumentationMcpAccess = 'disabled' | 'public';
|
|
@@ -10,11 +30,21 @@ interface DocumentationFontConfig {
|
|
|
10
30
|
source?: string;
|
|
11
31
|
format?: string;
|
|
12
32
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
33
|
+
/**
|
|
34
|
+
* The `theme` object as authored in documentation.json — exactly the keys
|
|
35
|
+
* the config schema accepts under `theme.*`, derived from that schema so
|
|
36
|
+
* the two can never drift. Everything else (fonts, background decoration,
|
|
37
|
+
* styling, logo href) is configured through the Mintlify-style top-level
|
|
38
|
+
* keys and only exists on the resolved theme.
|
|
39
|
+
*/
|
|
40
|
+
type DocumentationThemeConfig = z.infer<typeof themeObjectSchema>;
|
|
41
|
+
/**
|
|
42
|
+
* The fully resolved theme after config loading merges `theme.*` with the
|
|
43
|
+
* Mintlify top-level keys (`colors`, `logo`, `favicon`, `appearance`,
|
|
44
|
+
* `background`, `fonts`, `styling`). This is what the template and runtime
|
|
45
|
+
* consume; it is an output shape, never accepted as `theme.*` input.
|
|
46
|
+
*/
|
|
47
|
+
interface ResolvedDocumentationTheme extends DocumentationThemeConfig {
|
|
18
48
|
backgroundDark?: string;
|
|
19
49
|
backgroundDecoration?: 'none' | 'gradient' | 'grid';
|
|
20
50
|
bodyFont?: DocumentationFontConfig;
|
|
@@ -22,12 +52,7 @@ interface DocumentationThemeConfig {
|
|
|
22
52
|
monoFont?: DocumentationFontConfig;
|
|
23
53
|
codeBlockMode?: 'dark' | 'system';
|
|
24
54
|
eyebrowStyle?: 'section' | 'breadcrumbs' | 'none';
|
|
25
|
-
logo?: string;
|
|
26
|
-
logoDark?: string;
|
|
27
55
|
logoHref?: string;
|
|
28
|
-
favicon?: string;
|
|
29
|
-
defaultMode?: 'light' | 'dark' | 'system';
|
|
30
|
-
strictMode?: boolean;
|
|
31
56
|
}
|
|
32
57
|
interface DocumentationLinkConfig {
|
|
33
58
|
label: string;
|
|
@@ -79,7 +104,7 @@ interface DocumentationConfig {
|
|
|
79
104
|
url?: string;
|
|
80
105
|
/** Explicit custom hostnames that mount this documentation at `/`. */
|
|
81
106
|
domains: string[];
|
|
82
|
-
theme:
|
|
107
|
+
theme: ResolvedDocumentationTheme;
|
|
83
108
|
navigation?: DocumentationNavigationItem[];
|
|
84
109
|
links: DocumentationLinkConfig[];
|
|
85
110
|
footer: DocumentationLinkConfig[];
|
package/dist/server.d.ts
CHANGED
|
@@ -1532,6 +1532,7 @@ declare const MSG: {
|
|
|
1532
1532
|
readonly CRON_PAUSE: "cron.pause";
|
|
1533
1533
|
readonly CRON_RESUME: "cron.resume";
|
|
1534
1534
|
readonly CRON_STATUS: "cron.status";
|
|
1535
|
+
readonly CRON_ACK: "cron.ack";
|
|
1535
1536
|
readonly JOB_ENQUEUE: "job.enqueue";
|
|
1536
1537
|
readonly JOB_CANCEL: "job.cancel";
|
|
1537
1538
|
readonly JOB_RETRY: "job.retry";
|
|
@@ -1699,6 +1700,16 @@ type ServerMessage = BaseMessage<typeof MSG.QUERY_RESULT, {
|
|
|
1699
1700
|
}> | BaseMessage<typeof MSG.CRON_STATUS, {
|
|
1700
1701
|
tasks: unknown;
|
|
1701
1702
|
recentHistory: unknown;
|
|
1703
|
+
}> | BaseMessage<typeof MSG.CRON_ACK, {
|
|
1704
|
+
requestId: string;
|
|
1705
|
+
taskName: string;
|
|
1706
|
+
ok: true;
|
|
1707
|
+
} | {
|
|
1708
|
+
requestId: string;
|
|
1709
|
+
taskName?: string;
|
|
1710
|
+
ok: false;
|
|
1711
|
+
reason: 'read_only' | 'unknown_task' | 'failed';
|
|
1712
|
+
error?: string;
|
|
1702
1713
|
}> | BaseMessage<typeof MSG.JOB_UPDATE, {
|
|
1703
1714
|
kind: 'snapshot' | 'enqueued' | 'progress' | 'succeeded' | 'failed' | 'canceled' | 'retried';
|
|
1704
1715
|
job?: unknown;
|
|
@@ -2313,12 +2324,22 @@ declare abstract class CronRoom<E = Record<string, unknown>> extends BaseRoom<E>
|
|
|
2313
2324
|
type: string;
|
|
2314
2325
|
[key: string]: unknown;
|
|
2315
2326
|
}): Promise<void>;
|
|
2327
|
+
private dispatchMessage;
|
|
2316
2328
|
protected onAlarm(): Promise<void>;
|
|
2317
2329
|
private executeTask;
|
|
2318
2330
|
private scheduleNextAlarm;
|
|
2319
2331
|
private getTaskStates;
|
|
2320
2332
|
private getRecentHistory;
|
|
2321
2333
|
private broadcastStatus;
|
|
2334
|
+
/**
|
|
2335
|
+
* Receipt for a mutation frame, addressed only to its sender. Receipts
|
|
2336
|
+
* are opt-in by correlation id: frames without a `requestId` keep the
|
|
2337
|
+
* fire-and-forget contract, so the (untyped) `requestId` from the wire
|
|
2338
|
+
* is the single gate here rather than a check at every call site.
|
|
2339
|
+
* Frames go through the shared `serverBuild` constructors so the wire
|
|
2340
|
+
* shape is enforced by the protocol types, not re-spelled here.
|
|
2341
|
+
*/
|
|
2342
|
+
private ackMutation;
|
|
2322
2343
|
/**
|
|
2323
2344
|
* Execute a scheduled task by name.
|
|
2324
2345
|
* Called both by the alarm scheduler and manual trigger.
|
package/dist/server.js
CHANGED
|
@@ -740,6 +740,9 @@ function declaredLength(request, what) {
|
|
|
740
740
|
function isKeyWithinScope(key, prefix, excludedPrefixes) {
|
|
741
741
|
return key.startsWith(prefix) && !excludedPrefixes.some((excluded) => key.startsWith(excluded));
|
|
742
742
|
}
|
|
743
|
+
function scopeRelativeKey(key, prefix) {
|
|
744
|
+
return key.slice(prefix.length);
|
|
745
|
+
}
|
|
743
746
|
function anchorKey(raw, prefix, excludedPrefixes) {
|
|
744
747
|
const safe = sanitizeSubpath(raw);
|
|
745
748
|
if (safe === null) return fail2(400, "Invalid key: traversal not allowed", "invalid_key");
|
|
@@ -939,7 +942,7 @@ async function handleUpload(request, url, bucket, prefix, excludedPrefixes, auth
|
|
|
939
942
|
admission.markerKey,
|
|
940
943
|
admission.markerKey !== null || fileData.byteLength < replacedBytes
|
|
941
944
|
);
|
|
942
|
-
return storedFileJson(located.key, fileName, url, scope);
|
|
945
|
+
return storedFileJson(located.key, prefix, fileName, url, scope);
|
|
943
946
|
} catch (err) {
|
|
944
947
|
const msg = err instanceof Error ? err.message : "Upload failed";
|
|
945
948
|
console.error("[handleUpload] Error:", msg, err instanceof Error ? err.stack : "");
|
|
@@ -961,11 +964,17 @@ function fileMetadata(fileName, userId, reservationId, declaredBytes) {
|
|
|
961
964
|
uploadedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
962
965
|
};
|
|
963
966
|
}
|
|
964
|
-
function storedFileJson(key, name, url, scope) {
|
|
967
|
+
function storedFileJson(key, prefix, name, url, scope) {
|
|
965
968
|
const fileUrl = new URL(`/api/files/${encodeKeyPath(key)}`, url.origin);
|
|
966
969
|
fileUrl.searchParams.set("scope", scope);
|
|
967
970
|
return Response.json(
|
|
968
|
-
{
|
|
971
|
+
{
|
|
972
|
+
success: true,
|
|
973
|
+
key,
|
|
974
|
+
relativeKey: scopeRelativeKey(key, prefix),
|
|
975
|
+
url: fileUrl.toString(),
|
|
976
|
+
name
|
|
977
|
+
},
|
|
969
978
|
{ headers: CORS_HEADERS2 }
|
|
970
979
|
);
|
|
971
980
|
}
|
|
@@ -1245,7 +1254,7 @@ async function completedReservationObject(bucket, session) {
|
|
|
1245
1254
|
const object = await bucket.head(session.key);
|
|
1246
1255
|
return object?.customMetadata?.reservationId === session.reservationId ? object : null;
|
|
1247
1256
|
}
|
|
1248
|
-
async function finishCompletedMultipart(bucket, session, reservation, object, auth, url, scope) {
|
|
1257
|
+
async function finishCompletedMultipart(bucket, session, reservation, object, auth, url, scope, prefix) {
|
|
1249
1258
|
const declaredMetadata = Number(object.customMetadata?.declaredBytes);
|
|
1250
1259
|
const declaredBytes = reservation?.declaredBytes ?? (Number.isSafeInteger(declaredMetadata) && declaredMetadata >= 0 ? declaredMetadata : MAX_APP_FILE_BYTES);
|
|
1251
1260
|
const refusal = overCeiling(object.size) ?? (object.size > declaredBytes ? fail2(
|
|
@@ -1279,7 +1288,7 @@ async function finishCompletedMultipart(bucket, session, reservation, object, au
|
|
|
1279
1288
|
}
|
|
1280
1289
|
}
|
|
1281
1290
|
const name = object.customMetadata?.originalName ?? session.uploadKey.split("/").pop() ?? "file";
|
|
1282
|
-
return storedFileJson(session.key, name, url, scope);
|
|
1291
|
+
return storedFileJson(session.key, prefix, name, url, scope);
|
|
1283
1292
|
}
|
|
1284
1293
|
async function multipartComplete(request, url, bucket, prefix, excludedPrefixes, auth, scope) {
|
|
1285
1294
|
const body = await readControlJson(request);
|
|
@@ -1300,7 +1309,7 @@ async function multipartComplete(request, url, bucket, prefix, excludedPrefixes,
|
|
|
1300
1309
|
if (auth.storage && !reservation) {
|
|
1301
1310
|
const completed = await completedReservationObject(bucket, session);
|
|
1302
1311
|
if (completed) {
|
|
1303
|
-
return finishCompletedMultipart(bucket, session, null, completed, auth, url, scope);
|
|
1312
|
+
return finishCompletedMultipart(bucket, session, null, completed, auth, url, scope, prefix);
|
|
1304
1313
|
}
|
|
1305
1314
|
await abandonMultipart(bucket, session, auth, null);
|
|
1306
1315
|
return quotaUnavailable("This upload reservation is missing or does not match.");
|
|
@@ -1339,14 +1348,23 @@ async function multipartComplete(request, url, bucket, prefix, excludedPrefixes,
|
|
|
1339
1348
|
} catch (error) {
|
|
1340
1349
|
const completed = await completedReservationObject(bucket, session);
|
|
1341
1350
|
if (completed) {
|
|
1342
|
-
return finishCompletedMultipart(
|
|
1351
|
+
return finishCompletedMultipart(
|
|
1352
|
+
bucket,
|
|
1353
|
+
session,
|
|
1354
|
+
reservation,
|
|
1355
|
+
completed,
|
|
1356
|
+
auth,
|
|
1357
|
+
url,
|
|
1358
|
+
scope,
|
|
1359
|
+
prefix
|
|
1360
|
+
);
|
|
1343
1361
|
}
|
|
1344
1362
|
if (r2ErrorCode(error) === 10024 && reservation) {
|
|
1345
1363
|
await releaseQuotaMarker(bucket, auth.storage, reservation.key, true);
|
|
1346
1364
|
}
|
|
1347
1365
|
return multipartFailure(error);
|
|
1348
1366
|
}
|
|
1349
|
-
return finishCompletedMultipart(bucket, session, reservation, object, auth, url, scope);
|
|
1367
|
+
return finishCompletedMultipart(bucket, session, reservation, object, auth, url, scope, prefix);
|
|
1350
1368
|
}
|
|
1351
1369
|
async function multipartAbort(url, bucket, prefix, excludedPrefixes, auth) {
|
|
1352
1370
|
const session = resolveSession(url, prefix, excludedPrefixes);
|
|
@@ -1366,37 +1384,49 @@ async function handleList(bucket, prefix, excludedPrefixes, url, scope, auth) {
|
|
|
1366
1384
|
const listPrefix = `${prefix}${userPrefix}`;
|
|
1367
1385
|
const requestedLimit = Number(url.searchParams.get("limit") ?? 100);
|
|
1368
1386
|
const limit = Number.isSafeInteger(requestedLimit) ? Math.min(1e3, Math.max(1, requestedLimit)) : 100;
|
|
1387
|
+
const requestCursor = url.searchParams.get("cursor");
|
|
1369
1388
|
if (excludedPrefixes.some((excluded) => listPrefix.startsWith(excluded))) {
|
|
1370
1389
|
return Response.json({ files: [], truncated: false }, { headers: CORS_HEADERS2 });
|
|
1371
1390
|
}
|
|
1372
|
-
const
|
|
1391
|
+
const withMetadata = (options) => ({ ...options, include: ["customMetadata"] });
|
|
1392
|
+
const listed = await bucket.list(
|
|
1393
|
+
withMetadata({
|
|
1394
|
+
prefix: listPrefix,
|
|
1395
|
+
limit,
|
|
1396
|
+
...requestCursor ? { cursor: requestCursor } : {}
|
|
1397
|
+
})
|
|
1398
|
+
);
|
|
1373
1399
|
const visible = listed.objects.filter(
|
|
1374
1400
|
(object) => isKeyWithinScope(object.key, prefix, excludedPrefixes)
|
|
1375
1401
|
);
|
|
1376
1402
|
const seen = new Set(visible.map((object) => object.key));
|
|
1377
|
-
let
|
|
1378
|
-
|
|
1379
|
-
|
|
1403
|
+
let frontier = listed;
|
|
1404
|
+
let frontierKey = listed.objects.at(-1)?.key;
|
|
1405
|
+
for (const excluded of excludedPrefixes.filter((candidate) => candidate.startsWith(listPrefix)).sort()) {
|
|
1406
|
+
if (!frontier.truncated || visible.length >= limit) break;
|
|
1380
1407
|
const excludedEnd = `${excluded}\u{10FFFF}`;
|
|
1381
|
-
const
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1408
|
+
const after = await bucket.list(
|
|
1409
|
+
withMetadata({
|
|
1410
|
+
prefix: listPrefix,
|
|
1411
|
+
startAfter: frontierKey && frontierKey > excludedEnd ? frontierKey : excludedEnd,
|
|
1412
|
+
limit: limit - visible.length
|
|
1413
|
+
})
|
|
1414
|
+
);
|
|
1387
1415
|
for (const object of after.objects) {
|
|
1388
1416
|
if (!seen.has(object.key) && isKeyWithinScope(object.key, prefix, excludedPrefixes)) {
|
|
1389
1417
|
seen.add(object.key);
|
|
1390
1418
|
visible.push(object);
|
|
1391
1419
|
}
|
|
1392
1420
|
}
|
|
1393
|
-
|
|
1421
|
+
frontierKey = after.objects.at(-1)?.key ?? frontierKey;
|
|
1422
|
+
frontier = after;
|
|
1394
1423
|
}
|
|
1395
1424
|
const files = visible.slice(0, limit).map((obj) => {
|
|
1396
1425
|
const fileUrl = new URL(`/api/files/${encodeKeyPath(obj.key)}`, url.origin);
|
|
1397
1426
|
fileUrl.searchParams.set("scope", scope);
|
|
1398
1427
|
return {
|
|
1399
1428
|
key: obj.key,
|
|
1429
|
+
relativeKey: scopeRelativeKey(obj.key, prefix),
|
|
1400
1430
|
size: obj.size,
|
|
1401
1431
|
uploaded: obj.uploaded.toISOString(),
|
|
1402
1432
|
url: fileUrl.toString(),
|
|
@@ -1411,7 +1441,12 @@ async function handleList(bucket, prefix, excludedPrefixes, url, scope, auth) {
|
|
|
1411
1441
|
if (storage && limitBytes !== null) storage.limitBytes = limitBytes;
|
|
1412
1442
|
}
|
|
1413
1443
|
return Response.json(
|
|
1414
|
-
{
|
|
1444
|
+
{
|
|
1445
|
+
files,
|
|
1446
|
+
truncated: frontier.truncated,
|
|
1447
|
+
...frontier.truncated ? { cursor: frontier.cursor } : {},
|
|
1448
|
+
...storage ? { storage } : {}
|
|
1449
|
+
},
|
|
1415
1450
|
{ headers: CORS_HEADERS2 }
|
|
1416
1451
|
);
|
|
1417
1452
|
}
|
|
@@ -2952,6 +2987,9 @@ var MSG = {
|
|
|
2952
2987
|
CRON_PAUSE: "cron.pause",
|
|
2953
2988
|
CRON_RESUME: "cron.resume",
|
|
2954
2989
|
CRON_STATUS: "cron.status",
|
|
2990
|
+
// Per-mutation receipt, sent only to the socket whose trigger/pause/
|
|
2991
|
+
// resume carried a `requestId` — frames without one stay fire-and-forget.
|
|
2992
|
+
CRON_ACK: "cron.ack",
|
|
2955
2993
|
// ---- JobRoom ---------------------------------------------------------
|
|
2956
2994
|
// Background-job queue with durable, alarm-driven execution. See
|
|
2957
2995
|
// server/rooms/job-room.ts. Clients enqueue and subscribe to updates;
|
|
@@ -3453,6 +3491,14 @@ var serverBuild = {
|
|
|
3453
3491
|
cronTasks: (tasks) => ({ type: MSG.CRON_TASKS, payload: { tasks } }),
|
|
3454
3492
|
cronHistory: (history) => ({ type: MSG.CRON_HISTORY, payload: { history } }),
|
|
3455
3493
|
cronStatus: (tasks, recentHistory) => ({ type: MSG.CRON_STATUS, payload: { tasks, recentHistory } }),
|
|
3494
|
+
cronAckSuccess: (requestId, taskName) => ({
|
|
3495
|
+
type: MSG.CRON_ACK,
|
|
3496
|
+
payload: { requestId, taskName, ok: true }
|
|
3497
|
+
}),
|
|
3498
|
+
cronAckFailure: (requestId, reason, error, taskName) => ({
|
|
3499
|
+
type: MSG.CRON_ACK,
|
|
3500
|
+
payload: { requestId, taskName, ok: false, reason, error }
|
|
3501
|
+
}),
|
|
3456
3502
|
// Jobs
|
|
3457
3503
|
jobSnapshot: (jobs) => ({ type: MSG.JOB_UPDATE, payload: { kind: "snapshot", jobs } }),
|
|
3458
3504
|
jobEnqueued: (job, requestId) => ({
|
|
@@ -6196,40 +6242,88 @@ var CronRoom = class extends BaseRoom {
|
|
|
6196
6242
|
}
|
|
6197
6243
|
async onMessage(ws, user, message) {
|
|
6198
6244
|
this.ensureInitialized();
|
|
6199
|
-
const { type, payload } = message;
|
|
6245
|
+
const { type, payload = {} } = message;
|
|
6200
6246
|
if (CRON_WRITE_TYPES.has(type) && !user.canWrite) {
|
|
6201
|
-
|
|
6202
|
-
|
|
6203
|
-
|
|
6247
|
+
const error = "Write access denied: viewer role cannot modify cron tasks";
|
|
6248
|
+
this.sendTo(ws, { type: MSG.ERROR, payload: { error } });
|
|
6249
|
+
this.ackMutation(ws, payload.requestId, {
|
|
6250
|
+
ok: false,
|
|
6251
|
+
taskName: payload.taskName,
|
|
6252
|
+
reason: "read_only",
|
|
6253
|
+
error
|
|
6204
6254
|
});
|
|
6205
6255
|
return;
|
|
6206
6256
|
}
|
|
6257
|
+
try {
|
|
6258
|
+
await this.dispatchMessage(ws, type, payload);
|
|
6259
|
+
} catch (error) {
|
|
6260
|
+
try {
|
|
6261
|
+
this.ackMutation(ws, payload.requestId, {
|
|
6262
|
+
ok: false,
|
|
6263
|
+
taskName: payload.taskName,
|
|
6264
|
+
reason: "failed",
|
|
6265
|
+
error: error instanceof Error ? error.message : String(error)
|
|
6266
|
+
});
|
|
6267
|
+
} catch {
|
|
6268
|
+
}
|
|
6269
|
+
throw error;
|
|
6270
|
+
}
|
|
6271
|
+
}
|
|
6272
|
+
async dispatchMessage(ws, type, payload) {
|
|
6207
6273
|
switch (type) {
|
|
6208
6274
|
case MSG.CRON_TRIGGER: {
|
|
6209
6275
|
const taskName = payload.taskName;
|
|
6210
6276
|
if (!taskName) {
|
|
6211
|
-
|
|
6277
|
+
const error = "Missing taskName";
|
|
6278
|
+
this.sendTo(ws, { type: MSG.ERROR, payload: { error } });
|
|
6279
|
+
this.ackMutation(ws, payload.requestId, { ok: false, reason: "failed", error });
|
|
6212
6280
|
return;
|
|
6213
6281
|
}
|
|
6214
|
-
|
|
6215
|
-
|
|
6216
|
-
|
|
6217
|
-
|
|
6282
|
+
const execution = await this.executeTask(taskName);
|
|
6283
|
+
if (!execution) {
|
|
6284
|
+
const error = `Unknown cron task: ${taskName}`;
|
|
6285
|
+
this.sendTo(ws, { type: MSG.ERROR, payload: { error } });
|
|
6286
|
+
this.ackMutation(ws, payload.requestId, {
|
|
6287
|
+
ok: false,
|
|
6288
|
+
taskName,
|
|
6289
|
+
reason: "unknown_task",
|
|
6290
|
+
error
|
|
6218
6291
|
});
|
|
6292
|
+
break;
|
|
6219
6293
|
}
|
|
6294
|
+
this.ackMutation(
|
|
6295
|
+
ws,
|
|
6296
|
+
payload.requestId,
|
|
6297
|
+
execution.success ? { ok: true, taskName } : { ok: false, taskName, reason: "failed", error: execution.error }
|
|
6298
|
+
);
|
|
6220
6299
|
break;
|
|
6221
6300
|
}
|
|
6222
|
-
case MSG.CRON_PAUSE:
|
|
6223
|
-
const taskName = payload.taskName;
|
|
6224
|
-
this.sql.exec(`UPDATE cron_tasks SET paused = 1 WHERE name = ?`, taskName);
|
|
6225
|
-
this.broadcastStatus();
|
|
6226
|
-
break;
|
|
6227
|
-
}
|
|
6301
|
+
case MSG.CRON_PAUSE:
|
|
6228
6302
|
case MSG.CRON_RESUME: {
|
|
6229
6303
|
const taskName = payload.taskName;
|
|
6230
|
-
|
|
6231
|
-
|
|
6304
|
+
if (!taskName) {
|
|
6305
|
+
const error = "Missing taskName";
|
|
6306
|
+
this.sendTo(ws, { type: MSG.ERROR, payload: { error } });
|
|
6307
|
+
this.ackMutation(ws, payload.requestId, { ok: false, reason: "failed", error });
|
|
6308
|
+
return;
|
|
6309
|
+
}
|
|
6310
|
+
const known = this.sql.exec(`SELECT name FROM cron_tasks WHERE name = ?`, taskName).toArray().length > 0;
|
|
6311
|
+
if (!known) {
|
|
6312
|
+
const error = `Unknown cron task: ${taskName}`;
|
|
6313
|
+
this.sendTo(ws, { type: MSG.ERROR, payload: { error } });
|
|
6314
|
+
this.ackMutation(ws, payload.requestId, {
|
|
6315
|
+
ok: false,
|
|
6316
|
+
taskName,
|
|
6317
|
+
reason: "unknown_task",
|
|
6318
|
+
error
|
|
6319
|
+
});
|
|
6320
|
+
break;
|
|
6321
|
+
}
|
|
6322
|
+
const paused = type === MSG.CRON_PAUSE;
|
|
6323
|
+
this.sql.exec(`UPDATE cron_tasks SET paused = ? WHERE name = ?`, paused ? 1 : 0, taskName);
|
|
6324
|
+
if (!paused) this.scheduleNextAlarm();
|
|
6232
6325
|
this.broadcastStatus();
|
|
6326
|
+
this.ackMutation(ws, payload.requestId, { ok: true, taskName });
|
|
6233
6327
|
break;
|
|
6234
6328
|
}
|
|
6235
6329
|
case MSG.CRON_TASKS: {
|
|
@@ -6272,7 +6366,7 @@ var CronRoom = class extends BaseRoom {
|
|
|
6272
6366
|
// ==========================================================================
|
|
6273
6367
|
async executeTask(taskName) {
|
|
6274
6368
|
const taskRow = this.sql.exec(`SELECT interval_minutes, schedule, timezone FROM cron_tasks WHERE name = ?`, taskName).toArray()[0];
|
|
6275
|
-
if (!taskRow) return
|
|
6369
|
+
if (!taskRow) return null;
|
|
6276
6370
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6277
6371
|
const start = Date.now();
|
|
6278
6372
|
let success = true;
|
|
@@ -6308,7 +6402,7 @@ var CronRoom = class extends BaseRoom {
|
|
|
6308
6402
|
`DELETE FROM cron_history WHERE id NOT IN (SELECT id FROM cron_history ORDER BY id DESC LIMIT 500)`
|
|
6309
6403
|
);
|
|
6310
6404
|
this.broadcastStatus();
|
|
6311
|
-
return
|
|
6405
|
+
return { taskName, startedAt, completedAt, success, durationMs, error };
|
|
6312
6406
|
}
|
|
6313
6407
|
// ==========================================================================
|
|
6314
6408
|
// Scheduling
|
|
@@ -6378,6 +6472,21 @@ var CronRoom = class extends BaseRoom {
|
|
|
6378
6472
|
}
|
|
6379
6473
|
});
|
|
6380
6474
|
}
|
|
6475
|
+
/**
|
|
6476
|
+
* Receipt for a mutation frame, addressed only to its sender. Receipts
|
|
6477
|
+
* are opt-in by correlation id: frames without a `requestId` keep the
|
|
6478
|
+
* fire-and-forget contract, so the (untyped) `requestId` from the wire
|
|
6479
|
+
* is the single gate here rather than a check at every call site.
|
|
6480
|
+
* Frames go through the shared `serverBuild` constructors so the wire
|
|
6481
|
+
* shape is enforced by the protocol types, not re-spelled here.
|
|
6482
|
+
*/
|
|
6483
|
+
ackMutation(ws, requestId, result) {
|
|
6484
|
+
if (typeof requestId !== "string") return;
|
|
6485
|
+
this.sendTo(
|
|
6486
|
+
ws,
|
|
6487
|
+
result.ok ? serverBuild.cronAckSuccess(requestId, result.taskName) : serverBuild.cronAckFailure(requestId, result.reason, result.error, result.taskName)
|
|
6488
|
+
);
|
|
6489
|
+
}
|
|
6381
6490
|
};
|
|
6382
6491
|
function computeNextRunAt(task, from) {
|
|
6383
6492
|
if (task.interval_minutes) {
|