deepspace 0.3.9 → 0.3.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +96 -18
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +160 -1
- package/dist/index.js +283 -126
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +172 -2
- package/dist/server.js +581 -1
- package/dist/server.js.map +1 -1
- package/dist/worker.d.ts +252 -2
- package/dist/worker.js +588 -1
- package/dist/worker.js.map +1 -1
- package/features/admin-page/feature.json +29 -0
- package/features/admin-page/src/AdminPage.tsx +292 -0
- package/features/admin-page/src/admin-schema.ts +20 -0
- package/features/ai-chat/feature.json +41 -0
- package/features/ai-chat/src/AiChatPage.tsx +692 -0
- package/features/ai-chat/src/ChatPanel.tsx +1098 -0
- package/features/ai-chat/src/ai-chat-schema.ts +11 -0
- package/features/canvas/feature.json +45 -0
- package/features/canvas/src/CanvasEditorPage.tsx +87 -0
- package/features/canvas/src/CanvasListPage.tsx +202 -0
- package/features/canvas/src/canvas-schema.ts +33 -0
- package/features/canvas/src/components/CanvasToolbar.tsx +170 -0
- package/features/canvas/src/components/CanvasView.tsx +369 -0
- package/features/canvas/src/components/ShapeRenderer.tsx +114 -0
- package/features/cron/feature.json +33 -0
- package/features/cron/src/CronLogPage.tsx +100 -0
- package/features/cron/src/cron.ts +25 -0
- package/features/docs/feature.json +96 -0
- package/features/docs/src/DocsEditorPage.tsx +1012 -0
- package/features/docs/src/DocsListPage.tsx +951 -0
- package/features/docs/src/DocsOutlinePanel.tsx +68 -0
- package/features/docs/src/DocsPresence.tsx +138 -0
- package/features/docs/src/InviteDialog.tsx +336 -0
- package/features/docs/src/LibrarySidebar.tsx +464 -0
- package/features/docs/src/docs-favorites.ts +18 -0
- package/features/docs/src/docs-library-types.ts +15 -0
- package/features/docs/src/docs-schema.ts +80 -0
- package/features/docs/src/docs-ui.css +309 -0
- package/features/docs/src/editor/DocEditorSurface.tsx +257 -0
- package/features/docs/src/editor/DocsTiptapToolbar.tsx +560 -0
- package/features/docs/src/editor/useDocEditor.ts +206 -0
- package/features/docs/src/use-yjs-room-with-awareness.ts +285 -0
- package/features/file-attachments/feature.json +42 -0
- package/features/file-attachments/src/AttachmentsPage.tsx +864 -0
- package/features/file-attachments/src/attachments-constants.ts +9 -0
- package/features/file-attachments/src/attachments-schema.ts +45 -0
- package/features/file-attachments/src/useFilePreview.ts +471 -0
- package/features/file-attachments/src/useMimeTypeDetection.ts +138 -0
- package/features/file-manager/feature.json +24 -0
- package/features/file-manager/src/FileManagerPage.tsx +330 -0
- package/features/integration-test/feature.json +18 -0
- package/features/integration-test/src/IntegrationTestPage.tsx +423 -0
- package/features/integration-test/src/integration.spec.ts +42 -0
- package/features/items/feature.json +33 -0
- package/features/items/src/ItemsPage.tsx +325 -0
- package/features/items/src/items-constants.ts +10 -0
- package/features/items/src/items-schema.ts +27 -0
- package/features/kanban/feature.json +18 -0
- package/features/kanban/src/Kanban.tsx +283 -0
- package/features/landing/feature.json +62 -0
- package/features/landing/src/LandingPage.tsx +627 -0
- package/features/landing/src/primitives.tsx +389 -0
- package/features/landing/src/sections/FAQSection.tsx +110 -0
- package/features/landing/src/sections/FeaturesGridSection.tsx +311 -0
- package/features/landing/src/sections/HowItWorksSection.tsx +62 -0
- package/features/landing/src/sections/LogoCloudSection.tsx +58 -0
- package/features/landing/src/sections/ShowcaseSection.tsx +63 -0
- package/features/landing/src/sections/StatsSection.tsx +31 -0
- package/features/landing/src/sections/TeamSection.tsx +86 -0
- package/features/landing/src/sections/TestimonialsSection.tsx +68 -0
- package/features/landing/src/sections/VideoSection.tsx +71 -0
- package/features/leaderboard/feature.json +33 -0
- package/features/leaderboard/src/LeaderboardPage.tsx +372 -0
- package/features/leaderboard/src/leaderboard-constants.ts +21 -0
- package/features/leaderboard/src/leaderboard-schema.ts +36 -0
- package/features/messaging/feature.json +111 -0
- package/features/messaging/src/ChatMultiPage.tsx +300 -0
- package/features/messaging/src/ChatPage.tsx +75 -0
- package/features/messaging/src/components/chat/ChatHeader.tsx +76 -0
- package/features/messaging/src/components/chat/MessageActionSheet.tsx +168 -0
- package/features/messaging/src/components/chat/MessageInput.tsx +65 -0
- package/features/messaging/src/components/chat/MessageItem.tsx +352 -0
- package/features/messaging/src/components/chat/MessageList.tsx +248 -0
- package/features/messaging/src/components/chat/ReactionPicker.tsx +2 -0
- package/features/messaging/src/components/chat/ThreadPanel.tsx +107 -0
- package/features/messaging/src/components/chat/UserProfilePopover.tsx +158 -0
- package/features/messaging/src/components/chat-multi/AddMemberModal.tsx +188 -0
- package/features/messaging/src/components/chat-multi/BrowseGroupsModal.tsx +163 -0
- package/features/messaging/src/components/chat-multi/ChannelListItem.tsx +147 -0
- package/features/messaging/src/components/chat-multi/ChannelSettingsPanel.tsx +344 -0
- package/features/messaging/src/components/chat-multi/ChannelSidebar.tsx +460 -0
- package/features/messaging/src/components/chat-multi/CreateGroupModal.tsx +190 -0
- package/features/messaging/src/components/chat-multi/InvitationList.tsx +149 -0
- package/features/messaging/src/components/chat-multi/NewMessageModal.tsx +159 -0
- package/features/messaging/src/hooks/useChatChannel.ts +64 -0
- package/features/messaging/src/hooks/useLongPress.ts +74 -0
- package/features/messaging/src/hooks/useMultiChannel.ts +142 -0
- package/features/messaging/src/messaging-schema.ts +25 -0
- package/features/presence-test/feature.json +21 -0
- package/features/presence-test/src/PresenceTestPage.tsx +120 -0
- package/features/sidebar/feature.json +19 -0
- package/features/sidebar/src/AppSidebar.tsx +558 -0
- package/features/tasks/feature.json +33 -0
- package/features/tasks/src/TasksPage.tsx +598 -0
- package/features/tasks/src/tasks-constants.ts +43 -0
- package/features/tasks/src/tasks-schema.ts +56 -0
- package/features/testing/feature.json +49 -0
- package/features/testing/src/TestPage.tsx +145 -0
- package/features/testing/src/cpu-burn-action.ts +51 -0
- package/features/testing/src/subrequest-burn-action.ts +53 -0
- package/features/testing/src/test-schema.ts +23 -0
- package/features/topbar/feature.json +18 -0
- package/features/topbar/src/Topbar.tsx +353 -0
- package/features/tree/feature.json +26 -0
- package/features/tree/src/TreeSidebar.tsx +607 -0
- package/features/tree/src/tree-constants.ts +68 -0
- package/features/tree/src/tree-hooks.ts +258 -0
- package/package.json +4 -2
- package/scripts/add-feature.cjs +654 -0
package/dist/worker.js
CHANGED
|
@@ -239,6 +239,14 @@ var MSG = {
|
|
|
239
239
|
CRON_PAUSE: "cron.pause",
|
|
240
240
|
CRON_RESUME: "cron.resume",
|
|
241
241
|
CRON_STATUS: "cron.status",
|
|
242
|
+
// ---- JobRoom ---------------------------------------------------------
|
|
243
|
+
// Background-job queue with durable, alarm-driven execution. See
|
|
244
|
+
// server/rooms/job-room.ts. Clients enqueue and subscribe to updates;
|
|
245
|
+
// the room broadcasts JOB_UPDATE for every status transition.
|
|
246
|
+
JOB_ENQUEUE: "job.enqueue",
|
|
247
|
+
JOB_CANCEL: "job.cancel",
|
|
248
|
+
JOB_RETRY: "job.retry",
|
|
249
|
+
JOB_UPDATE: "job.update",
|
|
242
250
|
// ---- PresenceRoom ----------------------------------------------------
|
|
243
251
|
PRESENCE_SYNC: "presence.sync",
|
|
244
252
|
PRESENCE_JOIN: "presence.join",
|
|
@@ -934,6 +942,13 @@ var clientBuild = {
|
|
|
934
942
|
cronTrigger: (taskName) => ({ type: MSG.CRON_TRIGGER, payload: { taskName } }),
|
|
935
943
|
cronPause: (taskName) => ({ type: MSG.CRON_PAUSE, payload: { taskName } }),
|
|
936
944
|
cronResume: (taskName) => ({ type: MSG.CRON_RESUME, payload: { taskName } }),
|
|
945
|
+
// Jobs
|
|
946
|
+
jobEnqueue: (requestId, type, payload, maxAttempts) => ({
|
|
947
|
+
type: MSG.JOB_ENQUEUE,
|
|
948
|
+
payload: { requestId, type, payload, maxAttempts }
|
|
949
|
+
}),
|
|
950
|
+
jobCancel: (jobId) => ({ type: MSG.JOB_CANCEL, payload: { jobId } }),
|
|
951
|
+
jobRetry: (jobId) => ({ type: MSG.JOB_RETRY, payload: { jobId } }),
|
|
937
952
|
// Presence
|
|
938
953
|
presenceUpdate: (state) => ({ type: MSG.PRESENCE_UPDATE, payload: state }),
|
|
939
954
|
// Gateway
|
|
@@ -988,6 +1003,17 @@ var serverBuild = {
|
|
|
988
1003
|
cronTasks: (tasks) => ({ type: MSG.CRON_TASKS, payload: { tasks } }),
|
|
989
1004
|
cronHistory: (history) => ({ type: MSG.CRON_HISTORY, payload: { history } }),
|
|
990
1005
|
cronStatus: (tasks, recentHistory) => ({ type: MSG.CRON_STATUS, payload: { tasks, recentHistory } }),
|
|
1006
|
+
// Jobs
|
|
1007
|
+
jobSnapshot: (jobs) => ({ type: MSG.JOB_UPDATE, payload: { kind: "snapshot", jobs } }),
|
|
1008
|
+
jobEnqueued: (job, requestId) => ({
|
|
1009
|
+
type: MSG.JOB_UPDATE,
|
|
1010
|
+
payload: { kind: "enqueued", job, requestId }
|
|
1011
|
+
}),
|
|
1012
|
+
jobProgress: (job) => ({ type: MSG.JOB_UPDATE, payload: { kind: "progress", job } }),
|
|
1013
|
+
jobSucceeded: (job) => ({ type: MSG.JOB_UPDATE, payload: { kind: "succeeded", job } }),
|
|
1014
|
+
jobFailed: (job) => ({ type: MSG.JOB_UPDATE, payload: { kind: "failed", job } }),
|
|
1015
|
+
jobCanceled: (job) => ({ type: MSG.JOB_UPDATE, payload: { kind: "canceled", job } }),
|
|
1016
|
+
jobRetried: (job) => ({ type: MSG.JOB_UPDATE, payload: { kind: "retried", job } }),
|
|
991
1017
|
// Presence
|
|
992
1018
|
presenceSync: (peers) => ({ type: MSG.PRESENCE_SYNC, payload: { peers } }),
|
|
993
1019
|
presenceJoin: (peer) => ({ type: MSG.PRESENCE_JOIN, payload: { peer } }),
|
|
@@ -4275,6 +4301,543 @@ function cronMatches(parsed, timezone, date) {
|
|
|
4275
4301
|
return parsed.minute.has(minute) && parsed.hour.has(hour) && parsed.dayOfMonth.has(dayOfMonth) && parsed.month.has(month) && parsed.dayOfWeek.has(dayOfWeek);
|
|
4276
4302
|
}
|
|
4277
4303
|
|
|
4304
|
+
// src/server/rooms/job-room.ts
|
|
4305
|
+
var RUNNING_STALE_MS = 16 * 60 * 1e3;
|
|
4306
|
+
var DEFAULT_RETENTION_MS = 24 * 60 * 60 * 1e3;
|
|
4307
|
+
var DEFAULT_SNAPSHOT_LIMIT = 100;
|
|
4308
|
+
var JobRoom = class extends BaseRoom {
|
|
4309
|
+
initialized = false;
|
|
4310
|
+
defaultMaxAttempts;
|
|
4311
|
+
retentionMs;
|
|
4312
|
+
snapshotLimit;
|
|
4313
|
+
retryBackoffMs;
|
|
4314
|
+
/** In-flight jobs in this isolate; lets same-isolate JOB_CANCEL fire the signal. */
|
|
4315
|
+
inFlight = /* @__PURE__ */ new Map();
|
|
4316
|
+
/** Set by `ctx.continue` inside `executeJob`; reset before every run. */
|
|
4317
|
+
continueState = null;
|
|
4318
|
+
constructor(state, env, config = {}) {
|
|
4319
|
+
super(state, env);
|
|
4320
|
+
this.defaultMaxAttempts = Math.max(1, config.defaultMaxAttempts ?? 1);
|
|
4321
|
+
this.retentionMs = Math.max(0, config.retentionMs ?? DEFAULT_RETENTION_MS);
|
|
4322
|
+
this.snapshotLimit = Math.max(1, config.snapshotLimit ?? DEFAULT_SNAPSHOT_LIMIT);
|
|
4323
|
+
this.retryBackoffMs = Math.max(0, config.retryBackoffMs ?? 1e3);
|
|
4324
|
+
}
|
|
4325
|
+
ensureInitialized() {
|
|
4326
|
+
if (this.initialized) return;
|
|
4327
|
+
this.initialized = true;
|
|
4328
|
+
this.sql.exec(`
|
|
4329
|
+
CREATE TABLE IF NOT EXISTS jobs (
|
|
4330
|
+
id TEXT PRIMARY KEY,
|
|
4331
|
+
type TEXT NOT NULL,
|
|
4332
|
+
status TEXT NOT NULL,
|
|
4333
|
+
payload TEXT,
|
|
4334
|
+
result TEXT,
|
|
4335
|
+
error TEXT,
|
|
4336
|
+
progress REAL,
|
|
4337
|
+
progress_message TEXT,
|
|
4338
|
+
checkpoint TEXT,
|
|
4339
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
4340
|
+
max_attempts INTEGER NOT NULL DEFAULT 1,
|
|
4341
|
+
enqueued_at TEXT NOT NULL,
|
|
4342
|
+
started_at TEXT,
|
|
4343
|
+
completed_at TEXT,
|
|
4344
|
+
enqueued_by TEXT,
|
|
4345
|
+
next_run_at TEXT
|
|
4346
|
+
)
|
|
4347
|
+
`);
|
|
4348
|
+
this.sql.exec(
|
|
4349
|
+
`CREATE INDEX IF NOT EXISTS jobs_status_next_run ON jobs (status, next_run_at)`
|
|
4350
|
+
);
|
|
4351
|
+
this.sql.exec(
|
|
4352
|
+
`CREATE INDEX IF NOT EXISTS jobs_completed_at ON jobs (completed_at)`
|
|
4353
|
+
);
|
|
4354
|
+
this.recoverStuckRunning();
|
|
4355
|
+
this.scheduleNextAlarm();
|
|
4356
|
+
}
|
|
4357
|
+
// ==========================================================================
|
|
4358
|
+
// BaseRoom Lifecycle
|
|
4359
|
+
// ==========================================================================
|
|
4360
|
+
async fetch(request) {
|
|
4361
|
+
this.ensureInitialized();
|
|
4362
|
+
return super.fetch(request);
|
|
4363
|
+
}
|
|
4364
|
+
onConnect(ws, user) {
|
|
4365
|
+
this.ensureInitialized();
|
|
4366
|
+
this.sendTo(ws, serverBuild.jobSnapshot(this.getRecentJobs(this.snapshotLimit)));
|
|
4367
|
+
return user;
|
|
4368
|
+
}
|
|
4369
|
+
async onMessage(ws, user, message) {
|
|
4370
|
+
this.ensureInitialized();
|
|
4371
|
+
const { type, payload } = message;
|
|
4372
|
+
switch (type) {
|
|
4373
|
+
case MSG.JOB_ENQUEUE: {
|
|
4374
|
+
const requestId = String(payload.requestId ?? "");
|
|
4375
|
+
const jobType = String(payload.type ?? "");
|
|
4376
|
+
if (!jobType) {
|
|
4377
|
+
this.sendTo(ws, serverBuild.error("JOB_ENQUEUE missing type"));
|
|
4378
|
+
return;
|
|
4379
|
+
}
|
|
4380
|
+
const job = this.insertJob({
|
|
4381
|
+
type: jobType,
|
|
4382
|
+
payload: payload.payload,
|
|
4383
|
+
maxAttempts: typeof payload.maxAttempts === "number" ? Math.max(1, Math.floor(payload.maxAttempts)) : void 0,
|
|
4384
|
+
enqueuedBy: user.userId
|
|
4385
|
+
});
|
|
4386
|
+
this.sendTo(ws, serverBuild.jobEnqueued(job, requestId));
|
|
4387
|
+
this.broadcast(serverBuild.jobEnqueued(job), ws);
|
|
4388
|
+
this.scheduleNextAlarm();
|
|
4389
|
+
break;
|
|
4390
|
+
}
|
|
4391
|
+
case MSG.JOB_CANCEL: {
|
|
4392
|
+
const jobId = String(payload.jobId ?? "");
|
|
4393
|
+
if (!jobId) {
|
|
4394
|
+
this.sendTo(ws, serverBuild.error("JOB_CANCEL missing jobId"));
|
|
4395
|
+
return;
|
|
4396
|
+
}
|
|
4397
|
+
this.cancelJob(jobId);
|
|
4398
|
+
break;
|
|
4399
|
+
}
|
|
4400
|
+
case MSG.JOB_RETRY: {
|
|
4401
|
+
const jobId = String(payload.jobId ?? "");
|
|
4402
|
+
if (!jobId) {
|
|
4403
|
+
this.sendTo(ws, serverBuild.error("JOB_RETRY missing jobId"));
|
|
4404
|
+
return;
|
|
4405
|
+
}
|
|
4406
|
+
this.retryJob(jobId);
|
|
4407
|
+
break;
|
|
4408
|
+
}
|
|
4409
|
+
default:
|
|
4410
|
+
this.sendTo(ws, serverBuild.error(`Unknown job message type: ${type}`));
|
|
4411
|
+
}
|
|
4412
|
+
}
|
|
4413
|
+
async onAlarm() {
|
|
4414
|
+
this.ensureInitialized();
|
|
4415
|
+
await this.drainDueJobs();
|
|
4416
|
+
this.pruneExpired();
|
|
4417
|
+
this.scheduleNextAlarm();
|
|
4418
|
+
}
|
|
4419
|
+
// ==========================================================================
|
|
4420
|
+
// HTTP — internal enqueue path for cross-isolate callers
|
|
4421
|
+
// ==========================================================================
|
|
4422
|
+
/**
|
|
4423
|
+
* One internal endpoint, `POST /enqueue`, used by the `enqueueJob`
|
|
4424
|
+
* helper at the bottom of this file. The room is not meant to be
|
|
4425
|
+
* reachable from the public internet; callers route through the app
|
|
4426
|
+
* worker (or call `this.enqueue(...)` directly from a subclass).
|
|
4427
|
+
*
|
|
4428
|
+
* Subclasses overriding `onRequest` should call `super.onRequest(req)`
|
|
4429
|
+
* for paths they don't handle.
|
|
4430
|
+
*/
|
|
4431
|
+
async onRequest(request) {
|
|
4432
|
+
this.ensureInitialized();
|
|
4433
|
+
const url = new URL(request.url);
|
|
4434
|
+
if (request.method !== "POST" || !url.pathname.endsWith("/enqueue")) {
|
|
4435
|
+
return new Response("Not Found", { status: 404 });
|
|
4436
|
+
}
|
|
4437
|
+
const body = await request.json().catch(() => null);
|
|
4438
|
+
if (!body || typeof body.type !== "string" || !body.type) {
|
|
4439
|
+
return new Response(JSON.stringify({ error: "type required" }), {
|
|
4440
|
+
status: 400,
|
|
4441
|
+
headers: { "Content-Type": "application/json" }
|
|
4442
|
+
});
|
|
4443
|
+
}
|
|
4444
|
+
const job = this.insertJob({
|
|
4445
|
+
type: body.type,
|
|
4446
|
+
payload: body.payload,
|
|
4447
|
+
maxAttempts: typeof body.maxAttempts === "number" ? Math.max(1, Math.floor(body.maxAttempts)) : void 0,
|
|
4448
|
+
enqueuedBy: typeof body.enqueuedBy === "string" ? body.enqueuedBy : void 0
|
|
4449
|
+
});
|
|
4450
|
+
this.broadcast(serverBuild.jobEnqueued(job));
|
|
4451
|
+
this.scheduleNextAlarm();
|
|
4452
|
+
return new Response(JSON.stringify({ job }), {
|
|
4453
|
+
status: 200,
|
|
4454
|
+
headers: { "Content-Type": "application/json" }
|
|
4455
|
+
});
|
|
4456
|
+
}
|
|
4457
|
+
// ==========================================================================
|
|
4458
|
+
// Public enqueue surface (server-side callers in the same isolate)
|
|
4459
|
+
// ==========================================================================
|
|
4460
|
+
/**
|
|
4461
|
+
* Synchronously persist a new job and arm the alarm. Returns the new
|
|
4462
|
+
* job. This is the in-isolate path — useful for a subclass `onJob`
|
|
4463
|
+
* that wants to chain follow-up work without paying a fetch hop:
|
|
4464
|
+
*
|
|
4465
|
+
* protected async onJob(job: Job, ctx: JobContext) {
|
|
4466
|
+
* // ... do work ...
|
|
4467
|
+
* if (needsFollowup) this.enqueue('followup', { parent: job.id })
|
|
4468
|
+
* }
|
|
4469
|
+
*
|
|
4470
|
+
* Workers code that lives outside the DO isolate (HTTP routes, cron
|
|
4471
|
+
* tasks, server actions) cannot call this directly — they must go
|
|
4472
|
+
* through the `enqueueJob` helper at the bottom of this file, which
|
|
4473
|
+
* routes via the DO's `/enqueue` HTTP endpoint.
|
|
4474
|
+
*/
|
|
4475
|
+
enqueue(type, payload, options) {
|
|
4476
|
+
this.ensureInitialized();
|
|
4477
|
+
const job = this.insertJob({
|
|
4478
|
+
type,
|
|
4479
|
+
payload,
|
|
4480
|
+
maxAttempts: options?.maxAttempts,
|
|
4481
|
+
enqueuedBy: options?.enqueuedBy
|
|
4482
|
+
});
|
|
4483
|
+
this.broadcast(serverBuild.jobEnqueued(job));
|
|
4484
|
+
this.scheduleNextAlarm();
|
|
4485
|
+
return job;
|
|
4486
|
+
}
|
|
4487
|
+
// ==========================================================================
|
|
4488
|
+
// Mutation helpers
|
|
4489
|
+
// ==========================================================================
|
|
4490
|
+
insertJob(input) {
|
|
4491
|
+
const id = crypto.randomUUID();
|
|
4492
|
+
const enqueuedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4493
|
+
const maxAttempts = Math.max(1, input.maxAttempts ?? this.defaultMaxAttempts);
|
|
4494
|
+
const payloadJson = input.payload === void 0 ? null : safeStringify(input.payload);
|
|
4495
|
+
this.sql.exec(
|
|
4496
|
+
`INSERT INTO jobs (id, type, status, payload, attempts, max_attempts, enqueued_at, enqueued_by, next_run_at)
|
|
4497
|
+
VALUES (?, ?, 'queued', ?, 0, ?, ?, ?, ?)`,
|
|
4498
|
+
id,
|
|
4499
|
+
input.type,
|
|
4500
|
+
payloadJson,
|
|
4501
|
+
maxAttempts,
|
|
4502
|
+
enqueuedAt,
|
|
4503
|
+
input.enqueuedBy ?? null,
|
|
4504
|
+
enqueuedAt
|
|
4505
|
+
);
|
|
4506
|
+
return this.getJobById(id);
|
|
4507
|
+
}
|
|
4508
|
+
cancelJob(jobId) {
|
|
4509
|
+
const row = this.sql.exec(`SELECT status FROM jobs WHERE id = ?`, jobId).toArray()[0];
|
|
4510
|
+
if (!row) return;
|
|
4511
|
+
if (row.status === "succeeded" || row.status === "failed" || row.status === "canceled") {
|
|
4512
|
+
return;
|
|
4513
|
+
}
|
|
4514
|
+
const completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4515
|
+
this.sql.exec(
|
|
4516
|
+
`UPDATE jobs
|
|
4517
|
+
SET status = 'canceled',
|
|
4518
|
+
completed_at = ?,
|
|
4519
|
+
checkpoint = NULL,
|
|
4520
|
+
next_run_at = NULL
|
|
4521
|
+
WHERE id = ?`,
|
|
4522
|
+
completedAt,
|
|
4523
|
+
jobId
|
|
4524
|
+
);
|
|
4525
|
+
const controller = this.inFlight.get(jobId);
|
|
4526
|
+
if (controller) controller.abort();
|
|
4527
|
+
const job = this.getJobById(jobId);
|
|
4528
|
+
if (job) this.broadcast(serverBuild.jobCanceled(job));
|
|
4529
|
+
}
|
|
4530
|
+
retryJob(jobId) {
|
|
4531
|
+
const row = this.sql.exec(`SELECT status, attempts, max_attempts FROM jobs WHERE id = ?`, jobId).toArray()[0];
|
|
4532
|
+
if (!row) return;
|
|
4533
|
+
if (row.status !== "failed" && row.status !== "canceled") return;
|
|
4534
|
+
const newMax = Math.max(row.max_attempts, row.attempts + 1);
|
|
4535
|
+
this.sql.exec(
|
|
4536
|
+
`UPDATE jobs
|
|
4537
|
+
SET status = 'queued',
|
|
4538
|
+
max_attempts = ?,
|
|
4539
|
+
error = NULL,
|
|
4540
|
+
result = NULL,
|
|
4541
|
+
completed_at = NULL,
|
|
4542
|
+
next_run_at = ?
|
|
4543
|
+
WHERE id = ?`,
|
|
4544
|
+
newMax,
|
|
4545
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
4546
|
+
jobId
|
|
4547
|
+
);
|
|
4548
|
+
const job = this.getJobById(jobId);
|
|
4549
|
+
if (job) this.broadcast(serverBuild.jobRetried(job));
|
|
4550
|
+
this.scheduleNextAlarm();
|
|
4551
|
+
}
|
|
4552
|
+
// ==========================================================================
|
|
4553
|
+
// Alarm / execution
|
|
4554
|
+
// ==========================================================================
|
|
4555
|
+
/**
|
|
4556
|
+
* Drain due jobs in FIFO order. All due jobs share the alarm's
|
|
4557
|
+
* 15-min wall budget (a CF property, not a per-job allocation).
|
|
4558
|
+
*/
|
|
4559
|
+
async drainDueJobs() {
|
|
4560
|
+
const cutoff = (/* @__PURE__ */ new Date()).toISOString();
|
|
4561
|
+
while (true) {
|
|
4562
|
+
const row = this.sql.exec(
|
|
4563
|
+
`SELECT * FROM jobs
|
|
4564
|
+
WHERE status = 'queued'
|
|
4565
|
+
AND (next_run_at IS NULL OR next_run_at <= ?)
|
|
4566
|
+
ORDER BY enqueued_at ASC
|
|
4567
|
+
LIMIT 1`,
|
|
4568
|
+
cutoff
|
|
4569
|
+
).toArray()[0];
|
|
4570
|
+
if (!row) return;
|
|
4571
|
+
await this.executeJob(row);
|
|
4572
|
+
}
|
|
4573
|
+
}
|
|
4574
|
+
async executeJob(row) {
|
|
4575
|
+
const startedAt = row.started_at ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
4576
|
+
const attemptsBefore = row.attempts;
|
|
4577
|
+
const attempt = attemptsBefore + 1;
|
|
4578
|
+
this.sql.exec(
|
|
4579
|
+
`UPDATE jobs SET status = 'running', started_at = ?, attempts = ? WHERE id = ?`,
|
|
4580
|
+
startedAt,
|
|
4581
|
+
attempt,
|
|
4582
|
+
row.id
|
|
4583
|
+
);
|
|
4584
|
+
const controller = new AbortController();
|
|
4585
|
+
this.inFlight.set(row.id, controller);
|
|
4586
|
+
this.continueState = null;
|
|
4587
|
+
const job = {
|
|
4588
|
+
...rowToJobView(row),
|
|
4589
|
+
status: "running",
|
|
4590
|
+
attempts: attempt,
|
|
4591
|
+
startedAt
|
|
4592
|
+
};
|
|
4593
|
+
let outcome = "succeeded";
|
|
4594
|
+
let result;
|
|
4595
|
+
let errorMessage;
|
|
4596
|
+
try {
|
|
4597
|
+
const ctx = this.makeContext(row.id, controller.signal);
|
|
4598
|
+
result = await this.onJob(job, ctx);
|
|
4599
|
+
if (this.continueState) outcome = "continued";
|
|
4600
|
+
} catch (e) {
|
|
4601
|
+
outcome = "failed";
|
|
4602
|
+
errorMessage = e instanceof Error ? e.message : String(e);
|
|
4603
|
+
console.error(`[JobRoom] Job "${row.type}" (${row.id}) failed:`, e);
|
|
4604
|
+
} finally {
|
|
4605
|
+
this.inFlight.delete(row.id);
|
|
4606
|
+
}
|
|
4607
|
+
const currentStatus = this.readStatus(row.id);
|
|
4608
|
+
if (currentStatus === "canceled") {
|
|
4609
|
+
this.continueState = null;
|
|
4610
|
+
return;
|
|
4611
|
+
}
|
|
4612
|
+
if (outcome === "continued") {
|
|
4613
|
+
const state = this.continueState;
|
|
4614
|
+
this.continueState = null;
|
|
4615
|
+
const nextRunAt = new Date(Date.now() + Math.max(0, state.afterMs)).toISOString();
|
|
4616
|
+
this.sql.exec(
|
|
4617
|
+
`UPDATE jobs
|
|
4618
|
+
SET status = 'queued',
|
|
4619
|
+
checkpoint = ?,
|
|
4620
|
+
next_run_at = ?
|
|
4621
|
+
WHERE id = ?`,
|
|
4622
|
+
safeStringify(state.checkpoint),
|
|
4623
|
+
nextRunAt,
|
|
4624
|
+
row.id
|
|
4625
|
+
);
|
|
4626
|
+
const updated2 = this.getJobById(row.id);
|
|
4627
|
+
if (updated2) this.broadcast(serverBuild.jobProgress(updated2));
|
|
4628
|
+
return;
|
|
4629
|
+
}
|
|
4630
|
+
const completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4631
|
+
if (outcome === "succeeded") {
|
|
4632
|
+
this.sql.exec(
|
|
4633
|
+
`UPDATE jobs
|
|
4634
|
+
SET status = 'succeeded',
|
|
4635
|
+
result = ?,
|
|
4636
|
+
completed_at = ?,
|
|
4637
|
+
error = NULL,
|
|
4638
|
+
checkpoint = NULL,
|
|
4639
|
+
next_run_at = NULL,
|
|
4640
|
+
progress = NULL,
|
|
4641
|
+
progress_message = NULL
|
|
4642
|
+
WHERE id = ?`,
|
|
4643
|
+
result === void 0 ? null : safeStringify(result),
|
|
4644
|
+
completedAt,
|
|
4645
|
+
row.id
|
|
4646
|
+
);
|
|
4647
|
+
const updated2 = this.getJobById(row.id);
|
|
4648
|
+
if (updated2) this.broadcast(serverBuild.jobSucceeded(updated2));
|
|
4649
|
+
return;
|
|
4650
|
+
}
|
|
4651
|
+
if (attempt < row.max_attempts) {
|
|
4652
|
+
const nextRunAt = new Date(Date.now() + this.retryBackoffMs).toISOString();
|
|
4653
|
+
this.sql.exec(
|
|
4654
|
+
`UPDATE jobs
|
|
4655
|
+
SET status = 'queued',
|
|
4656
|
+
error = ?,
|
|
4657
|
+
next_run_at = ?,
|
|
4658
|
+
started_at = NULL,
|
|
4659
|
+
progress = NULL,
|
|
4660
|
+
progress_message = NULL
|
|
4661
|
+
WHERE id = ?`,
|
|
4662
|
+
errorMessage ?? "unknown error",
|
|
4663
|
+
nextRunAt,
|
|
4664
|
+
row.id
|
|
4665
|
+
);
|
|
4666
|
+
const updated2 = this.getJobById(row.id);
|
|
4667
|
+
if (updated2) this.broadcast(serverBuild.jobRetried(updated2));
|
|
4668
|
+
return;
|
|
4669
|
+
}
|
|
4670
|
+
this.sql.exec(
|
|
4671
|
+
`UPDATE jobs
|
|
4672
|
+
SET status = 'failed',
|
|
4673
|
+
error = ?,
|
|
4674
|
+
completed_at = ?,
|
|
4675
|
+
checkpoint = NULL,
|
|
4676
|
+
next_run_at = NULL,
|
|
4677
|
+
progress = NULL,
|
|
4678
|
+
progress_message = NULL
|
|
4679
|
+
WHERE id = ?`,
|
|
4680
|
+
errorMessage ?? "unknown error",
|
|
4681
|
+
completedAt,
|
|
4682
|
+
row.id
|
|
4683
|
+
);
|
|
4684
|
+
const updated = this.getJobById(row.id);
|
|
4685
|
+
if (updated) this.broadcast(serverBuild.jobFailed(updated));
|
|
4686
|
+
}
|
|
4687
|
+
makeContext(jobId, signal) {
|
|
4688
|
+
return {
|
|
4689
|
+
progress: (value, message) => {
|
|
4690
|
+
const clamped = Math.max(0, Math.min(1, value));
|
|
4691
|
+
this.sql.exec(
|
|
4692
|
+
`UPDATE jobs SET progress = ?, progress_message = ? WHERE id = ?`,
|
|
4693
|
+
clamped,
|
|
4694
|
+
message ?? null,
|
|
4695
|
+
jobId
|
|
4696
|
+
);
|
|
4697
|
+
const updated = this.getJobById(jobId);
|
|
4698
|
+
if (updated) this.broadcast(serverBuild.jobProgress(updated));
|
|
4699
|
+
},
|
|
4700
|
+
continue: (state, options) => {
|
|
4701
|
+
this.continueState = {
|
|
4702
|
+
checkpoint: state,
|
|
4703
|
+
afterMs: Math.max(0, options?.afterMs ?? 0)
|
|
4704
|
+
};
|
|
4705
|
+
},
|
|
4706
|
+
signal
|
|
4707
|
+
};
|
|
4708
|
+
}
|
|
4709
|
+
readStatus(jobId) {
|
|
4710
|
+
const row = this.sql.exec(`SELECT status FROM jobs WHERE id = ?`, jobId).toArray()[0];
|
|
4711
|
+
return row?.status ?? null;
|
|
4712
|
+
}
|
|
4713
|
+
// ==========================================================================
|
|
4714
|
+
// Scheduling / recovery / pruning
|
|
4715
|
+
// ==========================================================================
|
|
4716
|
+
scheduleNextAlarm() {
|
|
4717
|
+
const row = this.sql.exec(
|
|
4718
|
+
`SELECT next_run_at FROM jobs
|
|
4719
|
+
WHERE status = 'queued'
|
|
4720
|
+
ORDER BY (next_run_at IS NULL) ASC, next_run_at ASC
|
|
4721
|
+
LIMIT 1`
|
|
4722
|
+
).toArray()[0];
|
|
4723
|
+
if (!row) return;
|
|
4724
|
+
const now = Date.now();
|
|
4725
|
+
const target = row.next_run_at ? Math.max(now, new Date(row.next_run_at).getTime()) : now;
|
|
4726
|
+
this.state.storage.setAlarm(Math.max(target, now + 50));
|
|
4727
|
+
}
|
|
4728
|
+
recoverStuckRunning() {
|
|
4729
|
+
const cutoff = new Date(Date.now() - RUNNING_STALE_MS).toISOString();
|
|
4730
|
+
const stuck = this.sql.exec(
|
|
4731
|
+
`SELECT id, attempts, max_attempts FROM jobs
|
|
4732
|
+
WHERE status = 'running' AND started_at < ?`,
|
|
4733
|
+
cutoff
|
|
4734
|
+
).toArray();
|
|
4735
|
+
for (const r of stuck) {
|
|
4736
|
+
if (r.attempts >= r.max_attempts) {
|
|
4737
|
+
this.sql.exec(
|
|
4738
|
+
`UPDATE jobs
|
|
4739
|
+
SET status = 'failed',
|
|
4740
|
+
error = 'Isolate recycled or wall time exceeded',
|
|
4741
|
+
completed_at = ?,
|
|
4742
|
+
checkpoint = NULL,
|
|
4743
|
+
next_run_at = NULL
|
|
4744
|
+
WHERE id = ?`,
|
|
4745
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
4746
|
+
r.id
|
|
4747
|
+
);
|
|
4748
|
+
} else {
|
|
4749
|
+
this.sql.exec(
|
|
4750
|
+
`UPDATE jobs
|
|
4751
|
+
SET status = 'queued',
|
|
4752
|
+
started_at = NULL,
|
|
4753
|
+
next_run_at = ?,
|
|
4754
|
+
error = 'Previous attempt killed mid-run; retrying',
|
|
4755
|
+
progress = NULL,
|
|
4756
|
+
progress_message = NULL
|
|
4757
|
+
WHERE id = ?`,
|
|
4758
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
4759
|
+
r.id
|
|
4760
|
+
);
|
|
4761
|
+
}
|
|
4762
|
+
}
|
|
4763
|
+
}
|
|
4764
|
+
pruneExpired() {
|
|
4765
|
+
if (this.retentionMs === 0) return;
|
|
4766
|
+
const cutoff = new Date(Date.now() - this.retentionMs).toISOString();
|
|
4767
|
+
this.sql.exec(
|
|
4768
|
+
`DELETE FROM jobs
|
|
4769
|
+
WHERE status IN ('succeeded', 'failed', 'canceled')
|
|
4770
|
+
AND completed_at IS NOT NULL
|
|
4771
|
+
AND completed_at < ?`,
|
|
4772
|
+
cutoff
|
|
4773
|
+
);
|
|
4774
|
+
}
|
|
4775
|
+
// ==========================================================================
|
|
4776
|
+
// Read helpers
|
|
4777
|
+
// ==========================================================================
|
|
4778
|
+
getRecentJobs(limit) {
|
|
4779
|
+
const rows = this.sql.exec(
|
|
4780
|
+
`SELECT * FROM jobs
|
|
4781
|
+
ORDER BY (status IN ('queued','running')) DESC, enqueued_at DESC
|
|
4782
|
+
LIMIT ?`,
|
|
4783
|
+
limit
|
|
4784
|
+
).toArray();
|
|
4785
|
+
return rows.map(rowToJobView);
|
|
4786
|
+
}
|
|
4787
|
+
getJobById(id) {
|
|
4788
|
+
const row = this.sql.exec(`SELECT * FROM jobs WHERE id = ?`, id).toArray()[0];
|
|
4789
|
+
return row ? rowToJobView(row) : null;
|
|
4790
|
+
}
|
|
4791
|
+
};
|
|
4792
|
+
async function enqueueJob(namespace, roomId, type, payload, options) {
|
|
4793
|
+
const stub = namespace.get(namespace.idFromName(roomId));
|
|
4794
|
+
const res = await stub.fetch(
|
|
4795
|
+
new Request("https://job-room.internal/enqueue", {
|
|
4796
|
+
method: "POST",
|
|
4797
|
+
headers: { "Content-Type": "application/json" },
|
|
4798
|
+
body: JSON.stringify({
|
|
4799
|
+
type,
|
|
4800
|
+
payload,
|
|
4801
|
+
maxAttempts: options?.maxAttempts,
|
|
4802
|
+
enqueuedBy: options?.enqueuedBy
|
|
4803
|
+
})
|
|
4804
|
+
})
|
|
4805
|
+
);
|
|
4806
|
+
if (!res.ok) {
|
|
4807
|
+
const text = await res.text().catch(() => "");
|
|
4808
|
+
throw new Error(`enqueueJob failed: ${res.status} ${text}`);
|
|
4809
|
+
}
|
|
4810
|
+
const data = await res.json();
|
|
4811
|
+
if (!data.job?.id) throw new Error("enqueueJob: malformed response");
|
|
4812
|
+
return data.job.id;
|
|
4813
|
+
}
|
|
4814
|
+
function rowToJobView(row) {
|
|
4815
|
+
return {
|
|
4816
|
+
id: row.id,
|
|
4817
|
+
type: row.type,
|
|
4818
|
+
status: row.status,
|
|
4819
|
+
payload: row.payload == null ? void 0 : JSON.parse(row.payload),
|
|
4820
|
+
result: row.result == null ? void 0 : JSON.parse(row.result),
|
|
4821
|
+
error: row.error ?? void 0,
|
|
4822
|
+
progress: row.progress ?? void 0,
|
|
4823
|
+
progressMessage: row.progress_message ?? void 0,
|
|
4824
|
+
attempts: row.attempts,
|
|
4825
|
+
maxAttempts: row.max_attempts,
|
|
4826
|
+
enqueuedAt: row.enqueued_at,
|
|
4827
|
+
startedAt: row.started_at,
|
|
4828
|
+
completedAt: row.completed_at,
|
|
4829
|
+
enqueuedBy: row.enqueued_by,
|
|
4830
|
+
resumeFrom: row.checkpoint == null ? void 0 : JSON.parse(row.checkpoint)
|
|
4831
|
+
};
|
|
4832
|
+
}
|
|
4833
|
+
function safeStringify(value) {
|
|
4834
|
+
try {
|
|
4835
|
+
return JSON.stringify(value);
|
|
4836
|
+
} catch {
|
|
4837
|
+
return JSON.stringify({ __unserializable__: String(value) });
|
|
4838
|
+
}
|
|
4839
|
+
}
|
|
4840
|
+
|
|
4278
4841
|
// src/server/rooms/do-manifest.ts
|
|
4279
4842
|
var DEFAULT_DO_MANIFEST = [
|
|
4280
4843
|
{ binding: "RECORD_ROOMS", className: "AppRecordRoom", sqlite: true },
|
|
@@ -5198,6 +5761,20 @@ function sanitizeSubpath(raw) {
|
|
|
5198
5761
|
function isKeyWithinPrefix(key, prefix) {
|
|
5199
5762
|
return key.startsWith(prefix);
|
|
5200
5763
|
}
|
|
5764
|
+
var DANGEROUS_MIME_TYPES = /* @__PURE__ */ new Set([
|
|
5765
|
+
"text/html",
|
|
5766
|
+
"application/xhtml+xml",
|
|
5767
|
+
"image/svg+xml",
|
|
5768
|
+
"application/xml",
|
|
5769
|
+
"text/xml",
|
|
5770
|
+
"application/javascript",
|
|
5771
|
+
"text/javascript",
|
|
5772
|
+
"application/ecmascript",
|
|
5773
|
+
"text/ecmascript"
|
|
5774
|
+
]);
|
|
5775
|
+
function normalizeMimeType(raw) {
|
|
5776
|
+
return raw.split(";")[0].trim().toLowerCase();
|
|
5777
|
+
}
|
|
5201
5778
|
function createScopedR2Handler(config) {
|
|
5202
5779
|
const requireAuthForMutations = config.requireAuthForMutations ?? true;
|
|
5203
5780
|
return async (request, url, bucket, auth) => {
|
|
@@ -5287,6 +5864,13 @@ async function handleUpload(request, url, bucket, prefix, userId) {
|
|
|
5287
5864
|
fileName = body.name;
|
|
5288
5865
|
mimeType = body.mimeType || "application/octet-stream";
|
|
5289
5866
|
}
|
|
5867
|
+
mimeType = normalizeMimeType(mimeType) || "application/octet-stream";
|
|
5868
|
+
if (DANGEROUS_MIME_TYPES.has(mimeType)) {
|
|
5869
|
+
return Response.json(
|
|
5870
|
+
{ error: `Unsupported media type: ${mimeType}` },
|
|
5871
|
+
{ status: 415, headers: CORS_HEADERS3 }
|
|
5872
|
+
);
|
|
5873
|
+
}
|
|
5290
5874
|
const requestedKey = url.searchParams.get("key");
|
|
5291
5875
|
let key;
|
|
5292
5876
|
if (requestedKey !== null) {
|
|
@@ -5351,6 +5935,7 @@ async function handleDownload(bucket, key) {
|
|
|
5351
5935
|
}
|
|
5352
5936
|
const headers = new Headers();
|
|
5353
5937
|
headers.set("Content-Type", object.httpMetadata?.contentType || "application/octet-stream");
|
|
5938
|
+
headers.set("X-Content-Type-Options", "nosniff");
|
|
5354
5939
|
headers.set("Cache-Control", "public, max-age=31536000, immutable");
|
|
5355
5940
|
headers.set("Access-Control-Allow-Origin", "*");
|
|
5356
5941
|
if (object.customMetadata?.originalName) {
|
|
@@ -5612,7 +6197,7 @@ function createDeepSpaceAuth(config) {
|
|
|
5612
6197
|
}
|
|
5613
6198
|
|
|
5614
6199
|
// src/server/utils/cron.ts
|
|
5615
|
-
var DEFAULT_API_BASE_URL = "https://
|
|
6200
|
+
var DEFAULT_API_BASE_URL = "https://api-worker.deep.space";
|
|
5616
6201
|
function buildCronContext(env, ownerUserId, roomId = "default") {
|
|
5617
6202
|
const hmacSecret = env.INTERNAL_STORAGE_HMAC_SECRET;
|
|
5618
6203
|
const apiBase = env.API_BASE_URL || DEFAULT_API_BASE_URL;
|
|
@@ -6384,6 +6969,7 @@ export {
|
|
|
6384
6969
|
GLOBAL_DO_TYPES,
|
|
6385
6970
|
GLOBAL_DO_TYPE_NAMES,
|
|
6386
6971
|
GameRoom,
|
|
6972
|
+
JobRoom,
|
|
6387
6973
|
MESSAGES_SCHEMA,
|
|
6388
6974
|
MSG,
|
|
6389
6975
|
MSG_AWARENESS,
|
|
@@ -6447,6 +7033,7 @@ export {
|
|
|
6447
7033
|
encodeSyncStep1,
|
|
6448
7034
|
encodeSyncStep2,
|
|
6449
7035
|
encodeUpdate,
|
|
7036
|
+
enqueueJob,
|
|
6450
7037
|
getChat,
|
|
6451
7038
|
getGlobalDOSchemas,
|
|
6452
7039
|
getGlobalDOType,
|