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.
Files changed (120) hide show
  1. package/dist/cli.js +96 -18
  2. package/dist/cli.js.map +1 -1
  3. package/dist/index.d.ts +160 -1
  4. package/dist/index.js +283 -126
  5. package/dist/index.js.map +1 -1
  6. package/dist/server.d.ts +172 -2
  7. package/dist/server.js +581 -1
  8. package/dist/server.js.map +1 -1
  9. package/dist/worker.d.ts +252 -2
  10. package/dist/worker.js +588 -1
  11. package/dist/worker.js.map +1 -1
  12. package/features/admin-page/feature.json +29 -0
  13. package/features/admin-page/src/AdminPage.tsx +292 -0
  14. package/features/admin-page/src/admin-schema.ts +20 -0
  15. package/features/ai-chat/feature.json +41 -0
  16. package/features/ai-chat/src/AiChatPage.tsx +692 -0
  17. package/features/ai-chat/src/ChatPanel.tsx +1098 -0
  18. package/features/ai-chat/src/ai-chat-schema.ts +11 -0
  19. package/features/canvas/feature.json +45 -0
  20. package/features/canvas/src/CanvasEditorPage.tsx +87 -0
  21. package/features/canvas/src/CanvasListPage.tsx +202 -0
  22. package/features/canvas/src/canvas-schema.ts +33 -0
  23. package/features/canvas/src/components/CanvasToolbar.tsx +170 -0
  24. package/features/canvas/src/components/CanvasView.tsx +369 -0
  25. package/features/canvas/src/components/ShapeRenderer.tsx +114 -0
  26. package/features/cron/feature.json +33 -0
  27. package/features/cron/src/CronLogPage.tsx +100 -0
  28. package/features/cron/src/cron.ts +25 -0
  29. package/features/docs/feature.json +96 -0
  30. package/features/docs/src/DocsEditorPage.tsx +1012 -0
  31. package/features/docs/src/DocsListPage.tsx +951 -0
  32. package/features/docs/src/DocsOutlinePanel.tsx +68 -0
  33. package/features/docs/src/DocsPresence.tsx +138 -0
  34. package/features/docs/src/InviteDialog.tsx +336 -0
  35. package/features/docs/src/LibrarySidebar.tsx +464 -0
  36. package/features/docs/src/docs-favorites.ts +18 -0
  37. package/features/docs/src/docs-library-types.ts +15 -0
  38. package/features/docs/src/docs-schema.ts +80 -0
  39. package/features/docs/src/docs-ui.css +309 -0
  40. package/features/docs/src/editor/DocEditorSurface.tsx +257 -0
  41. package/features/docs/src/editor/DocsTiptapToolbar.tsx +560 -0
  42. package/features/docs/src/editor/useDocEditor.ts +206 -0
  43. package/features/docs/src/use-yjs-room-with-awareness.ts +285 -0
  44. package/features/file-attachments/feature.json +42 -0
  45. package/features/file-attachments/src/AttachmentsPage.tsx +864 -0
  46. package/features/file-attachments/src/attachments-constants.ts +9 -0
  47. package/features/file-attachments/src/attachments-schema.ts +45 -0
  48. package/features/file-attachments/src/useFilePreview.ts +471 -0
  49. package/features/file-attachments/src/useMimeTypeDetection.ts +138 -0
  50. package/features/file-manager/feature.json +24 -0
  51. package/features/file-manager/src/FileManagerPage.tsx +330 -0
  52. package/features/integration-test/feature.json +18 -0
  53. package/features/integration-test/src/IntegrationTestPage.tsx +423 -0
  54. package/features/integration-test/src/integration.spec.ts +42 -0
  55. package/features/items/feature.json +33 -0
  56. package/features/items/src/ItemsPage.tsx +325 -0
  57. package/features/items/src/items-constants.ts +10 -0
  58. package/features/items/src/items-schema.ts +27 -0
  59. package/features/kanban/feature.json +18 -0
  60. package/features/kanban/src/Kanban.tsx +283 -0
  61. package/features/landing/feature.json +62 -0
  62. package/features/landing/src/LandingPage.tsx +627 -0
  63. package/features/landing/src/primitives.tsx +389 -0
  64. package/features/landing/src/sections/FAQSection.tsx +110 -0
  65. package/features/landing/src/sections/FeaturesGridSection.tsx +311 -0
  66. package/features/landing/src/sections/HowItWorksSection.tsx +62 -0
  67. package/features/landing/src/sections/LogoCloudSection.tsx +58 -0
  68. package/features/landing/src/sections/ShowcaseSection.tsx +63 -0
  69. package/features/landing/src/sections/StatsSection.tsx +31 -0
  70. package/features/landing/src/sections/TeamSection.tsx +86 -0
  71. package/features/landing/src/sections/TestimonialsSection.tsx +68 -0
  72. package/features/landing/src/sections/VideoSection.tsx +71 -0
  73. package/features/leaderboard/feature.json +33 -0
  74. package/features/leaderboard/src/LeaderboardPage.tsx +372 -0
  75. package/features/leaderboard/src/leaderboard-constants.ts +21 -0
  76. package/features/leaderboard/src/leaderboard-schema.ts +36 -0
  77. package/features/messaging/feature.json +111 -0
  78. package/features/messaging/src/ChatMultiPage.tsx +300 -0
  79. package/features/messaging/src/ChatPage.tsx +75 -0
  80. package/features/messaging/src/components/chat/ChatHeader.tsx +76 -0
  81. package/features/messaging/src/components/chat/MessageActionSheet.tsx +168 -0
  82. package/features/messaging/src/components/chat/MessageInput.tsx +65 -0
  83. package/features/messaging/src/components/chat/MessageItem.tsx +352 -0
  84. package/features/messaging/src/components/chat/MessageList.tsx +248 -0
  85. package/features/messaging/src/components/chat/ReactionPicker.tsx +2 -0
  86. package/features/messaging/src/components/chat/ThreadPanel.tsx +107 -0
  87. package/features/messaging/src/components/chat/UserProfilePopover.tsx +158 -0
  88. package/features/messaging/src/components/chat-multi/AddMemberModal.tsx +188 -0
  89. package/features/messaging/src/components/chat-multi/BrowseGroupsModal.tsx +163 -0
  90. package/features/messaging/src/components/chat-multi/ChannelListItem.tsx +147 -0
  91. package/features/messaging/src/components/chat-multi/ChannelSettingsPanel.tsx +344 -0
  92. package/features/messaging/src/components/chat-multi/ChannelSidebar.tsx +460 -0
  93. package/features/messaging/src/components/chat-multi/CreateGroupModal.tsx +190 -0
  94. package/features/messaging/src/components/chat-multi/InvitationList.tsx +149 -0
  95. package/features/messaging/src/components/chat-multi/NewMessageModal.tsx +159 -0
  96. package/features/messaging/src/hooks/useChatChannel.ts +64 -0
  97. package/features/messaging/src/hooks/useLongPress.ts +74 -0
  98. package/features/messaging/src/hooks/useMultiChannel.ts +142 -0
  99. package/features/messaging/src/messaging-schema.ts +25 -0
  100. package/features/presence-test/feature.json +21 -0
  101. package/features/presence-test/src/PresenceTestPage.tsx +120 -0
  102. package/features/sidebar/feature.json +19 -0
  103. package/features/sidebar/src/AppSidebar.tsx +558 -0
  104. package/features/tasks/feature.json +33 -0
  105. package/features/tasks/src/TasksPage.tsx +598 -0
  106. package/features/tasks/src/tasks-constants.ts +43 -0
  107. package/features/tasks/src/tasks-schema.ts +56 -0
  108. package/features/testing/feature.json +49 -0
  109. package/features/testing/src/TestPage.tsx +145 -0
  110. package/features/testing/src/cpu-burn-action.ts +51 -0
  111. package/features/testing/src/subrequest-burn-action.ts +53 -0
  112. package/features/testing/src/test-schema.ts +23 -0
  113. package/features/topbar/feature.json +18 -0
  114. package/features/topbar/src/Topbar.tsx +353 -0
  115. package/features/tree/feature.json +26 -0
  116. package/features/tree/src/TreeSidebar.tsx +607 -0
  117. package/features/tree/src/tree-constants.ts +68 -0
  118. package/features/tree/src/tree-hooks.ts +258 -0
  119. package/package.json +4 -2
  120. 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://deepspace-api.eudaimonicincorporated.workers.dev";
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,