deepspace 0.3.10 → 0.4.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.
Files changed (120) hide show
  1. package/dist/cli.js +331 -115
  2. package/dist/cli.js.map +1 -1
  3. package/dist/index.d.ts +199 -16
  4. package/dist/index.js +354 -166
  5. package/dist/index.js.map +1 -1
  6. package/dist/server.d.ts +242 -73
  7. package/dist/server.js +985 -374
  8. package/dist/server.js.map +1 -1
  9. package/dist/worker.d.ts +322 -73
  10. package/dist/worker.js +961 -343
  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/index.js CHANGED
@@ -407,6 +407,13 @@ var MSG = {
407
407
  USER_LIST: "user.list",
408
408
  SET_ROLE: "user.set_role",
409
409
  USER_UPDATE: "user.update",
410
+ // ---- Auth signal -----------------------------------------------------
411
+ // Server → client one-shot on connect from rooms that enforce role-based
412
+ // writes (Canvas, Game, Cron). Tells the client whether this connection
413
+ // can write so its hook can disable local write APIs and the UI can
414
+ // render controls accordingly. Without this, a viewer's click would
415
+ // round-trip to the server and silently come back as ERROR.
416
+ AUTH: "auth",
410
417
  // ---- Yjs JSON control frames (binary sync lives in ./yjs.ts) ---------
411
418
  YJS_JOIN: "yjs.join",
412
419
  YJS_LEAVE: "yjs.leave",
@@ -440,6 +447,14 @@ var MSG = {
440
447
  CRON_PAUSE: "cron.pause",
441
448
  CRON_RESUME: "cron.resume",
442
449
  CRON_STATUS: "cron.status",
450
+ // ---- JobRoom ---------------------------------------------------------
451
+ // Background-job queue with durable, alarm-driven execution. See
452
+ // server/rooms/job-room.ts. Clients enqueue and subscribe to updates;
453
+ // the room broadcasts JOB_UPDATE for every status transition.
454
+ JOB_ENQUEUE: "job.enqueue",
455
+ JOB_CANCEL: "job.cancel",
456
+ JOB_RETRY: "job.retry",
457
+ JOB_UPDATE: "job.update",
443
458
  // ---- PresenceRoom ----------------------------------------------------
444
459
  PRESENCE_SYNC: "presence.sync",
445
460
  PRESENCE_JOIN: "presence.join",
@@ -496,6 +511,13 @@ var clientBuild = {
496
511
  cronTrigger: (taskName) => ({ type: MSG.CRON_TRIGGER, payload: { taskName } }),
497
512
  cronPause: (taskName) => ({ type: MSG.CRON_PAUSE, payload: { taskName } }),
498
513
  cronResume: (taskName) => ({ type: MSG.CRON_RESUME, payload: { taskName } }),
514
+ // Jobs
515
+ jobEnqueue: (requestId, type, payload, maxAttempts) => ({
516
+ type: MSG.JOB_ENQUEUE,
517
+ payload: { requestId, type, payload, maxAttempts }
518
+ }),
519
+ jobCancel: (jobId) => ({ type: MSG.JOB_CANCEL, payload: { jobId } }),
520
+ jobRetry: (jobId) => ({ type: MSG.JOB_RETRY, payload: { jobId } }),
499
521
  // Presence
500
522
  presenceUpdate: (state) => ({ type: MSG.PRESENCE_UPDATE, payload: state }),
501
523
  // Gateway
@@ -550,6 +572,17 @@ var serverBuild = {
550
572
  cronTasks: (tasks) => ({ type: MSG.CRON_TASKS, payload: { tasks } }),
551
573
  cronHistory: (history) => ({ type: MSG.CRON_HISTORY, payload: { history } }),
552
574
  cronStatus: (tasks, recentHistory) => ({ type: MSG.CRON_STATUS, payload: { tasks, recentHistory } }),
575
+ // Jobs
576
+ jobSnapshot: (jobs) => ({ type: MSG.JOB_UPDATE, payload: { kind: "snapshot", jobs } }),
577
+ jobEnqueued: (job, requestId) => ({
578
+ type: MSG.JOB_UPDATE,
579
+ payload: { kind: "enqueued", job, requestId }
580
+ }),
581
+ jobProgress: (job) => ({ type: MSG.JOB_UPDATE, payload: { kind: "progress", job } }),
582
+ jobSucceeded: (job) => ({ type: MSG.JOB_UPDATE, payload: { kind: "succeeded", job } }),
583
+ jobFailed: (job) => ({ type: MSG.JOB_UPDATE, payload: { kind: "failed", job } }),
584
+ jobCanceled: (job) => ({ type: MSG.JOB_UPDATE, payload: { kind: "canceled", job } }),
585
+ jobRetried: (job) => ({ type: MSG.JOB_UPDATE, payload: { kind: "retried", job } }),
553
586
  // Presence
554
587
  presenceSync: (peers) => ({ type: MSG.PRESENCE_SYNC, payload: { peers } }),
555
588
  presenceJoin: (peer) => ({ type: MSG.PRESENCE_JOIN, payload: { peer } }),
@@ -691,12 +724,17 @@ function DeepSpaceAuthProvider({ children }) {
691
724
  }
692
725
 
693
726
  // src/client/auth/hooks.ts
727
+ var sessionHasResolved = false;
728
+ function stickyIsLoaded(isPending) {
729
+ if (!isPending) sessionHasResolved = true;
730
+ return sessionHasResolved;
731
+ }
694
732
  function useAuth() {
695
733
  const session = useSession();
696
734
  const isSignedIn = !!session.data?.user;
697
735
  const userId = session.data?.user?.id ?? null;
698
736
  return {
699
- isLoaded: !session.isPending,
737
+ isLoaded: stickyIsLoaded(session.isPending),
700
738
  isSignedIn,
701
739
  userId,
702
740
  sessionId: session.data?.session?.id ?? null
@@ -706,7 +744,7 @@ function useAuthUser() {
706
744
  const session = useSession();
707
745
  const user = session.data?.user ?? null;
708
746
  return {
709
- isLoaded: !session.isPending,
747
+ isLoaded: stickyIsLoaded(session.isPending),
710
748
  isSignedIn: !!user,
711
749
  user: user ? {
712
750
  id: user.id,
@@ -2822,6 +2860,7 @@ function useGameRoom(roomId) {
2822
2860
  const [players, setPlayers] = useState8([]);
2823
2861
  const [running, setRunning] = useState8(false);
2824
2862
  const [connected, setConnected] = useState8(false);
2863
+ const [canWrite, setCanWrite] = useState8(false);
2825
2864
  const wsRef = useRef9(null);
2826
2865
  useEffect11(() => {
2827
2866
  let ws = null;
@@ -2843,6 +2882,9 @@ function useGameRoom(roomId) {
2843
2882
  };
2844
2883
  ws.onmessage = (event) => {
2845
2884
  dispatch(event.data, {
2885
+ [MSG.AUTH]: (p) => {
2886
+ setCanWrite(p.canWrite);
2887
+ },
2846
2888
  [MSG.GAME_STATE]: (p) => {
2847
2889
  setGameState(p.state);
2848
2890
  setTick(p.tick);
@@ -2880,6 +2922,7 @@ function useGameRoom(roomId) {
2880
2922
  wsLog("disconnected", `game:${roomId}`);
2881
2923
  wsRef.current = null;
2882
2924
  setConnected(false);
2925
+ setCanWrite(false);
2883
2926
  if (alive) reconnectTimer = setTimeout(connect, 1e3);
2884
2927
  };
2885
2928
  ws.onerror = () => ws?.close();
@@ -2898,27 +2941,23 @@ function useGameRoom(roomId) {
2898
2941
  wsRef.current = null;
2899
2942
  };
2900
2943
  }, [roomId]);
2901
- const sendInput = useCallback10((action, data = {}) => {
2902
- const ws = wsRef.current;
2903
- if (!ws || ws.readyState !== WebSocket.OPEN) return;
2904
- ws.send(encode(clientBuild.gameInput(action, data)));
2905
- }, []);
2906
- const setReady = useCallback10(() => {
2907
- const ws = wsRef.current;
2908
- if (!ws || ws.readyState !== WebSocket.OPEN) return;
2909
- ws.send(encode(clientBuild.gamePlayerReady()));
2910
- }, []);
2911
- const startGame = useCallback10(() => {
2912
- const ws = wsRef.current;
2913
- if (!ws || ws.readyState !== WebSocket.OPEN) return;
2914
- ws.send(encode(clientBuild.gameStart()));
2915
- }, []);
2916
- const endGame = useCallback10(() => {
2917
- const ws = wsRef.current;
2918
- if (!ws || ws.readyState !== WebSocket.OPEN) return;
2919
- ws.send(encode(clientBuild.gameEnd()));
2920
- }, []);
2921
- return { state: gameState, tick, players, running, connected, sendInput, setReady, startGame, endGame };
2944
+ const send = useCallback10(
2945
+ (message) => {
2946
+ if (!canWrite) return;
2947
+ const ws = wsRef.current;
2948
+ if (!ws || ws.readyState !== WebSocket.OPEN) return;
2949
+ ws.send(encode(message));
2950
+ },
2951
+ [canWrite]
2952
+ );
2953
+ const sendInput = useCallback10(
2954
+ (action, data = {}) => send(clientBuild.gameInput(action, data)),
2955
+ [send]
2956
+ );
2957
+ const setReady = useCallback10(() => send(clientBuild.gamePlayerReady()), [send]);
2958
+ const startGame = useCallback10(() => send(clientBuild.gameStart()), [send]);
2959
+ const endGame = useCallback10(() => send(clientBuild.gameEnd()), [send]);
2960
+ return { state: gameState, tick, players, running, connected, canWrite, sendInput, setReady, startGame, endGame };
2922
2961
  }
2923
2962
 
2924
2963
  // src/client/storage/hooks/useCanvas.ts
@@ -2927,6 +2966,7 @@ function useCanvas(roomId) {
2927
2966
  const [shapes, setShapes] = useState9([]);
2928
2967
  const [viewports, setViewports] = useState9([]);
2929
2968
  const [connected, setConnected] = useState9(false);
2969
+ const [canWrite, setCanWrite] = useState9(false);
2930
2970
  const wsRef = useRef10(null);
2931
2971
  useEffect12(() => {
2932
2972
  let ws = null;
@@ -2948,6 +2988,9 @@ function useCanvas(roomId) {
2948
2988
  };
2949
2989
  ws.onmessage = (event) => {
2950
2990
  dispatch(event.data, {
2991
+ [MSG.AUTH]: (p) => {
2992
+ setCanWrite(p.canWrite);
2993
+ },
2951
2994
  [MSG.CANVAS_SHAPES]: (p) => {
2952
2995
  setShapes(p.shapes);
2953
2996
  setViewports(p.viewports);
@@ -3000,6 +3043,7 @@ function useCanvas(roomId) {
3000
3043
  wsLog("disconnected", `canvas:${roomId}`);
3001
3044
  wsRef.current = null;
3002
3045
  setConnected(false);
3046
+ setCanWrite(false);
3003
3047
  if (alive) reconnectTimer = setTimeout(connect, 1e3);
3004
3048
  };
3005
3049
  ws.onerror = () => ws?.close();
@@ -3026,33 +3070,40 @@ function useCanvas(roomId) {
3026
3070
  },
3027
3071
  []
3028
3072
  );
3073
+ const sendWrite = useCallback11(
3074
+ (message) => {
3075
+ if (!canWrite) return;
3076
+ sendBuilt(message);
3077
+ },
3078
+ [sendBuilt, canWrite]
3079
+ );
3029
3080
  const addShape = useCallback11(
3030
- (shape) => sendBuilt(clientBuild.canvasAdd(shape)),
3031
- [sendBuilt]
3081
+ (shape) => sendWrite(clientBuild.canvasAdd(shape)),
3082
+ [sendWrite]
3032
3083
  );
3033
3084
  const moveShape = useCallback11(
3034
- (shapeId, x, y) => sendBuilt(clientBuild.canvasMove(shapeId, x, y)),
3035
- [sendBuilt]
3085
+ (shapeId, x, y) => sendWrite(clientBuild.canvasMove(shapeId, x, y)),
3086
+ [sendWrite]
3036
3087
  );
3037
3088
  const resizeShape = useCallback11(
3038
- (shapeId, width, height, x, y) => sendBuilt(clientBuild.canvasResize(shapeId, width, height, x, y)),
3039
- [sendBuilt]
3089
+ (shapeId, width, height, x, y) => sendWrite(clientBuild.canvasResize(shapeId, width, height, x, y)),
3090
+ [sendWrite]
3040
3091
  );
3041
3092
  const deleteShape = useCallback11(
3042
- (shapeId) => sendBuilt(clientBuild.canvasDelete(shapeId)),
3043
- [sendBuilt]
3093
+ (shapeId) => sendWrite(clientBuild.canvasDelete(shapeId)),
3094
+ [sendWrite]
3044
3095
  );
3045
3096
  const updateShape = useCallback11(
3046
- (shapeId, props) => sendBuilt(clientBuild.canvasUpdate(shapeId, props)),
3047
- [sendBuilt]
3097
+ (shapeId, props) => sendWrite(clientBuild.canvasUpdate(shapeId, props)),
3098
+ [sendWrite]
3048
3099
  );
3049
3100
  const setViewport = useCallback11(
3050
3101
  (viewport) => sendBuilt(clientBuild.canvasViewport(viewport)),
3051
3102
  [sendBuilt]
3052
3103
  );
3053
- const undo = useCallback11(() => sendBuilt(clientBuild.canvasUndo()), [sendBuilt]);
3054
- const redo = useCallback11(() => sendBuilt(clientBuild.canvasRedo()), [sendBuilt]);
3055
- return { shapes, viewports, connected, addShape, moveShape, resizeShape, deleteShape, updateShape, setViewport, undo, redo };
3104
+ const undo = useCallback11(() => sendWrite(clientBuild.canvasUndo()), [sendWrite]);
3105
+ const redo = useCallback11(() => sendWrite(clientBuild.canvasRedo()), [sendWrite]);
3106
+ return { shapes, viewports, connected, canWrite, addShape, moveShape, resizeShape, deleteShape, updateShape, setViewport, undo, redo };
3056
3107
  }
3057
3108
 
3058
3109
  // src/client/storage/hooks/useCronMonitor.ts
@@ -3061,6 +3112,7 @@ function useCronMonitor(roomId) {
3061
3112
  const [tasks, setTasks] = useState10([]);
3062
3113
  const [history, setHistory] = useState10([]);
3063
3114
  const [connected, setConnected] = useState10(false);
3115
+ const [canWrite, setCanWrite] = useState10(false);
3064
3116
  const wsRef = useRef11(null);
3065
3117
  useEffect13(() => {
3066
3118
  let ws = null;
@@ -3078,6 +3130,9 @@ function useCronMonitor(roomId) {
3078
3130
  ws.onopen = () => setConnected(true);
3079
3131
  ws.onmessage = (event) => {
3080
3132
  dispatch(event.data, {
3133
+ [MSG.AUTH]: (p) => {
3134
+ setCanWrite(p.canWrite);
3135
+ },
3081
3136
  [MSG.CRON_TASKS]: (p) => {
3082
3137
  setTasks(p.tasks);
3083
3138
  },
@@ -3093,6 +3148,7 @@ function useCronMonitor(roomId) {
3093
3148
  ws.onclose = () => {
3094
3149
  wsRef.current = null;
3095
3150
  setConnected(false);
3151
+ setCanWrite(false);
3096
3152
  if (alive) reconnectTimer = setTimeout(connect, 1e3);
3097
3153
  };
3098
3154
  ws.onerror = () => ws?.close();
@@ -3105,36 +3161,167 @@ function useCronMonitor(roomId) {
3105
3161
  wsRef.current = null;
3106
3162
  };
3107
3163
  }, [roomId]);
3108
- const sendBuilt = useCallback12(
3164
+ const sendWrite = useCallback12(
3109
3165
  (message) => {
3166
+ if (!canWrite) return;
3110
3167
  const ws = wsRef.current;
3111
3168
  if (!ws || ws.readyState !== WebSocket.OPEN) return;
3112
3169
  ws.send(encode(message));
3113
3170
  },
3114
- []
3171
+ [canWrite]
3115
3172
  );
3116
3173
  const trigger = useCallback12(
3117
- (taskName) => sendBuilt(clientBuild.cronTrigger(taskName)),
3118
- [sendBuilt]
3174
+ (taskName) => sendWrite(clientBuild.cronTrigger(taskName)),
3175
+ [sendWrite]
3119
3176
  );
3120
3177
  const pause = useCallback12(
3121
- (taskName) => sendBuilt(clientBuild.cronPause(taskName)),
3122
- [sendBuilt]
3178
+ (taskName) => sendWrite(clientBuild.cronPause(taskName)),
3179
+ [sendWrite]
3123
3180
  );
3124
3181
  const resume = useCallback12(
3125
- (taskName) => sendBuilt(clientBuild.cronResume(taskName)),
3126
- [sendBuilt]
3182
+ (taskName) => sendWrite(clientBuild.cronResume(taskName)),
3183
+ [sendWrite]
3127
3184
  );
3128
- return { tasks, history, connected, trigger, pause, resume };
3185
+ return { tasks, history, connected, canWrite, trigger, pause, resume };
3129
3186
  }
3130
3187
 
3131
- // src/client/storage/hooks/usePresenceRoom.ts
3188
+ // src/client/storage/hooks/useJobs.ts
3132
3189
  import { useState as useState11, useEffect as useEffect14, useCallback as useCallback13, useRef as useRef12 } from "react";
3133
- function usePresenceRoom(scopeId) {
3134
- const [peers, setPeers] = useState11([]);
3190
+ var ENQUEUE_TIMEOUT_MS = 1e4;
3191
+ function useJobs(roomId) {
3192
+ const [jobs, setJobs] = useState11([]);
3135
3193
  const [connected, setConnected] = useState11(false);
3136
3194
  const wsRef = useRef12(null);
3195
+ const pendingEnqueueRef = useRef12(/* @__PURE__ */ new Map());
3137
3196
  useEffect14(() => {
3197
+ let ws = null;
3198
+ let reconnectTimer = null;
3199
+ let alive = true;
3200
+ const upsert = (next) => {
3201
+ setJobs((prev) => {
3202
+ const idx = prev.findIndex((j) => j.id === next.id);
3203
+ if (idx === -1) return [next, ...prev];
3204
+ const copy = prev.slice();
3205
+ copy[idx] = next;
3206
+ return copy;
3207
+ });
3208
+ };
3209
+ const rejectAllPending = (err) => {
3210
+ for (const [id, pending] of pendingEnqueueRef.current.entries()) {
3211
+ clearTimeout(pending.timer);
3212
+ pending.reject(err);
3213
+ pendingEnqueueRef.current.delete(id);
3214
+ }
3215
+ };
3216
+ const connect = async () => {
3217
+ if (!alive) return;
3218
+ const token = await getAuthToken();
3219
+ const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
3220
+ const baseUrl = `${protocol}//${window.location.host}`;
3221
+ const url = new URL(`/ws/jobs/${encodeURIComponent(roomId)}`, baseUrl);
3222
+ if (token) url.searchParams.set("token", token);
3223
+ ws = new WebSocket(url.toString());
3224
+ wsRef.current = ws;
3225
+ ws.onopen = () => setConnected(true);
3226
+ ws.onmessage = (event) => {
3227
+ dispatch(event.data, {
3228
+ [MSG.JOB_UPDATE]: (p) => {
3229
+ if (p.kind === "snapshot" && Array.isArray(p.jobs)) {
3230
+ setJobs(p.jobs);
3231
+ return;
3232
+ }
3233
+ const job = p.job;
3234
+ if (!job) return;
3235
+ upsert(job);
3236
+ if (p.kind === "enqueued" && typeof p.requestId === "string") {
3237
+ const pending = pendingEnqueueRef.current.get(p.requestId);
3238
+ if (pending) {
3239
+ clearTimeout(pending.timer);
3240
+ pendingEnqueueRef.current.delete(p.requestId);
3241
+ pending.resolve(job.id);
3242
+ }
3243
+ }
3244
+ }
3245
+ });
3246
+ };
3247
+ ws.onclose = () => {
3248
+ wsRef.current = null;
3249
+ setConnected(false);
3250
+ rejectAllPending(new Error("JobRoom WebSocket closed"));
3251
+ if (alive) reconnectTimer = setTimeout(connect, 1e3);
3252
+ };
3253
+ ws.onerror = () => ws?.close();
3254
+ };
3255
+ connect();
3256
+ return () => {
3257
+ alive = false;
3258
+ if (reconnectTimer) clearTimeout(reconnectTimer);
3259
+ rejectAllPending(new Error("useJobs unmounted"));
3260
+ ws?.close();
3261
+ wsRef.current = null;
3262
+ };
3263
+ }, [roomId]);
3264
+ const enqueue = useCallback13(
3265
+ (type, payload, options) => {
3266
+ return new Promise((resolve, reject) => {
3267
+ const ws = wsRef.current;
3268
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
3269
+ reject(new Error("JobRoom WebSocket not connected"));
3270
+ return;
3271
+ }
3272
+ const requestId = crypto.randomUUID();
3273
+ const timer = setTimeout(() => {
3274
+ pendingEnqueueRef.current.delete(requestId);
3275
+ reject(
3276
+ new Error(
3277
+ `JobRoom enqueue timed out (no server ack within ${ENQUEUE_TIMEOUT_MS}ms)`
3278
+ )
3279
+ );
3280
+ }, ENQUEUE_TIMEOUT_MS);
3281
+ pendingEnqueueRef.current.set(requestId, { resolve, reject, timer });
3282
+ try {
3283
+ ws.send(
3284
+ encode(clientBuild.jobEnqueue(requestId, type, payload, options?.maxAttempts))
3285
+ );
3286
+ } catch (e) {
3287
+ clearTimeout(timer);
3288
+ pendingEnqueueRef.current.delete(requestId);
3289
+ reject(e instanceof Error ? e : new Error(String(e)));
3290
+ }
3291
+ });
3292
+ },
3293
+ []
3294
+ );
3295
+ const sendBuilt = useCallback13(
3296
+ (message) => {
3297
+ const ws = wsRef.current;
3298
+ if (!ws || ws.readyState !== WebSocket.OPEN) return;
3299
+ ws.send(encode(message));
3300
+ },
3301
+ []
3302
+ );
3303
+ const getJob = useCallback13(
3304
+ (jobId) => jobs.find((j) => j.id === jobId),
3305
+ [jobs]
3306
+ );
3307
+ const cancel = useCallback13(
3308
+ (jobId) => sendBuilt(clientBuild.jobCancel(jobId)),
3309
+ [sendBuilt]
3310
+ );
3311
+ const retry = useCallback13(
3312
+ (jobId) => sendBuilt(clientBuild.jobRetry(jobId)),
3313
+ [sendBuilt]
3314
+ );
3315
+ return { jobs, connected, enqueue, getJob, cancel, retry };
3316
+ }
3317
+
3318
+ // src/client/storage/hooks/usePresenceRoom.ts
3319
+ import { useState as useState12, useEffect as useEffect15, useCallback as useCallback14, useRef as useRef13 } from "react";
3320
+ function usePresenceRoom(scopeId) {
3321
+ const [peers, setPeers] = useState12([]);
3322
+ const [connected, setConnected] = useState12(false);
3323
+ const wsRef = useRef13(null);
3324
+ useEffect15(() => {
3138
3325
  let ws = null;
3139
3326
  let reconnectTimer = null;
3140
3327
  let alive = true;
@@ -3195,7 +3382,7 @@ function usePresenceRoom(scopeId) {
3195
3382
  wsRef.current = null;
3196
3383
  };
3197
3384
  }, [scopeId]);
3198
- const updateState = useCallback13((state) => {
3385
+ const updateState = useCallback14((state) => {
3199
3386
  const ws = wsRef.current;
3200
3387
  if (!ws || ws.readyState !== WebSocket.OPEN) return;
3201
3388
  ws.send(encode(clientBuild.presenceUpdate(state)));
@@ -3211,7 +3398,7 @@ function toConnectionStatus(queryStatus) {
3211
3398
  }
3212
3399
 
3213
3400
  // src/client/storage/useR2Files.ts
3214
- import { useCallback as useCallback14, useState as useState12, useMemo as useMemo10 } from "react";
3401
+ import { useCallback as useCallback15, useState as useState13, useMemo as useMemo10 } from "react";
3215
3402
  function resolveFile(fileOrKey) {
3216
3403
  if (typeof fileOrKey === "string") {
3217
3404
  return { key: fileOrKey, name: void 0 };
@@ -3219,20 +3406,20 @@ function resolveFile(fileOrKey) {
3219
3406
  return { key: fileOrKey.key, name: fileOrKey.originalName };
3220
3407
  }
3221
3408
  function useR2Files(options) {
3222
- const [isUploading, setIsUploading] = useState12(false);
3409
+ const [isUploading, setIsUploading] = useState13(false);
3223
3410
  const scopeParams = useMemo10(() => {
3224
3411
  const params = new URLSearchParams();
3225
3412
  params.set("scope", "self");
3226
3413
  return params.toString();
3227
3414
  }, []);
3228
- const authHeaders = useCallback14(async () => {
3415
+ const authHeaders = useCallback15(async () => {
3229
3416
  const token = await getAuthToken();
3230
3417
  if (token) {
3231
3418
  return { Authorization: `Bearer ${token}` };
3232
3419
  }
3233
3420
  return {};
3234
3421
  }, []);
3235
- const upload = useCallback14(async (file, name) => {
3422
+ const upload = useCallback15(async (file, name) => {
3236
3423
  setIsUploading(true);
3237
3424
  try {
3238
3425
  const headers = await authHeaders();
@@ -3251,7 +3438,7 @@ function useR2Files(options) {
3251
3438
  setIsUploading(false);
3252
3439
  }
3253
3440
  }, [scopeParams, authHeaders]);
3254
- const uploadBase64 = useCallback14(async (base64Data, name, mimeType) => {
3441
+ const uploadBase64 = useCallback15(async (base64Data, name, mimeType) => {
3255
3442
  setIsUploading(true);
3256
3443
  try {
3257
3444
  const headers = await authHeaders();
@@ -3267,7 +3454,7 @@ function useR2Files(options) {
3267
3454
  setIsUploading(false);
3268
3455
  }
3269
3456
  }, [scopeParams, authHeaders]);
3270
- const deleteFile = useCallback14(async (fileOrKey) => {
3457
+ const deleteFile = useCallback15(async (fileOrKey) => {
3271
3458
  const { key } = resolveFile(fileOrKey);
3272
3459
  try {
3273
3460
  const headers = await authHeaders();
@@ -3277,7 +3464,7 @@ function useR2Files(options) {
3277
3464
  return { success: false, error: err instanceof Error ? err.message : "Delete failed" };
3278
3465
  }
3279
3466
  }, [scopeParams, authHeaders]);
3280
- const list = useCallback14(async (prefix) => {
3467
+ const list = useCallback15(async (prefix) => {
3281
3468
  try {
3282
3469
  const headers = await authHeaders();
3283
3470
  const params = new URLSearchParams(scopeParams);
@@ -3289,7 +3476,7 @@ function useR2Files(options) {
3289
3476
  return [];
3290
3477
  }
3291
3478
  }, [scopeParams, authHeaders]);
3292
- const downloadFile = useCallback14(async (fileOrKey, fileName) => {
3479
+ const downloadFile = useCallback15(async (fileOrKey, fileName) => {
3293
3480
  const { key, name: infoName } = resolveFile(fileOrKey);
3294
3481
  try {
3295
3482
  const headers = await authHeaders();
@@ -3316,7 +3503,7 @@ function useR2Files(options) {
3316
3503
  return { success: false, error: err instanceof Error ? err.message : "Download failed" };
3317
3504
  }
3318
3505
  }, [scopeParams, authHeaders]);
3319
- const readFile = useCallback14(async (fileOrKey) => {
3506
+ const readFile = useCallback15(async (fileOrKey) => {
3320
3507
  const { key } = resolveFile(fileOrKey);
3321
3508
  const headers = await authHeaders();
3322
3509
  const response = await fetch(`/api/files/${key}?${scopeParams}`, { headers });
@@ -3326,7 +3513,7 @@ function useR2Files(options) {
3326
3513
  }
3327
3514
  return response;
3328
3515
  }, [scopeParams, authHeaders]);
3329
- const getUrl = useCallback14((fileOrKey) => {
3516
+ const getUrl = useCallback15((fileOrKey) => {
3330
3517
  const { key } = resolveFile(fileOrKey);
3331
3518
  return `/api/files/${key}?${scopeParams}`;
3332
3519
  }, [scopeParams]);
@@ -3369,7 +3556,7 @@ function getUserColor(userId, palette = DEFAULT_USER_COLORS) {
3369
3556
  }
3370
3557
 
3371
3558
  // src/client/messaging/useMessages.ts
3372
- import { useCallback as useCallback15, useMemo as useMemo11 } from "react";
3559
+ import { useCallback as useCallback16, useMemo as useMemo11 } from "react";
3373
3560
  function useMessages(channelId, options) {
3374
3561
  const where = useMemo11(() => {
3375
3562
  if (!channelId) return { channelId: "__none__" };
@@ -3386,7 +3573,7 @@ function useMessages(channelId, options) {
3386
3573
  orderDir: "asc"
3387
3574
  });
3388
3575
  const { create, put, remove: removeMutation } = useMutations("messages");
3389
- const send = useCallback15(
3576
+ const send = useCallback16(
3390
3577
  (content, parentMessageId) => {
3391
3578
  if (!channelId || !user) return;
3392
3579
  const data = { channelId, content, authorId: user.id, edited: false };
@@ -3395,19 +3582,19 @@ function useMessages(channelId, options) {
3395
3582
  },
3396
3583
  [channelId, create]
3397
3584
  );
3398
- const edit = useCallback15(
3585
+ const edit = useCallback16(
3399
3586
  (messageId, newContent) => {
3400
3587
  put(messageId, { content: newContent, edited: true });
3401
3588
  },
3402
3589
  [put]
3403
3590
  );
3404
- const remove = useCallback15(
3591
+ const remove = useCallback16(
3405
3592
  (messageId) => {
3406
3593
  removeMutation(messageId);
3407
3594
  },
3408
3595
  [removeMutation]
3409
3596
  );
3410
- const softDelete = useCallback15(
3597
+ const softDelete = useCallback16(
3411
3598
  (messageId) => {
3412
3599
  put(messageId, { deleted: true });
3413
3600
  },
@@ -3417,7 +3604,7 @@ function useMessages(channelId, options) {
3417
3604
  }
3418
3605
 
3419
3606
  // src/client/messaging/useChannels.ts
3420
- import { useCallback as useCallback16 } from "react";
3607
+ import { useCallback as useCallback17 } from "react";
3421
3608
  function useChannels() {
3422
3609
  const { user } = useUser();
3423
3610
  const { records, status, error } = useQuery("channels", {
@@ -3425,7 +3612,7 @@ function useChannels() {
3425
3612
  orderDir: "asc"
3426
3613
  });
3427
3614
  const { create: createMutation, put, remove } = useMutations("channels");
3428
- const create = useCallback16(
3615
+ const create = useCallback17(
3429
3616
  (data) => {
3430
3617
  return createMutation({
3431
3618
  name: data.name,
@@ -3437,13 +3624,13 @@ function useChannels() {
3437
3624
  },
3438
3625
  [createMutation]
3439
3626
  );
3440
- const archive = useCallback16(
3627
+ const archive = useCallback17(
3441
3628
  (channelId) => {
3442
3629
  put(channelId, { archived: true });
3443
3630
  },
3444
3631
  [put]
3445
3632
  );
3446
- const update = useCallback16(
3633
+ const update = useCallback17(
3447
3634
  (channelId, data) => {
3448
3635
  put(channelId, data);
3449
3636
  },
@@ -3453,14 +3640,14 @@ function useChannels() {
3453
3640
  }
3454
3641
 
3455
3642
  // src/client/messaging/useReactions.ts
3456
- import { useCallback as useCallback17 } from "react";
3643
+ import { useCallback as useCallback18 } from "react";
3457
3644
  function useReactions(channelId) {
3458
3645
  const { user } = useUser();
3459
3646
  const { records, status, error } = useQuery("reactions", {
3460
3647
  where: channelId ? { channelId } : { channelId: "__none__" }
3461
3648
  });
3462
3649
  const { create, remove: removeMutation } = useMutations("reactions");
3463
- const getReactionsForMessage = useCallback17(
3650
+ const getReactionsForMessage = useCallback18(
3464
3651
  (messageId) => {
3465
3652
  const messageReactions = records.filter((r) => r.data.messageId === messageId);
3466
3653
  const grouped = /* @__PURE__ */ new Map();
@@ -3477,7 +3664,7 @@ function useReactions(channelId) {
3477
3664
  },
3478
3665
  [records, user]
3479
3666
  );
3480
- const toggle = useCallback17(
3667
+ const toggle = useCallback18(
3481
3668
  (messageId, emoji) => {
3482
3669
  if (!channelId || !user) return;
3483
3670
  const existing = records.find(
@@ -3495,20 +3682,20 @@ function useReactions(channelId) {
3495
3682
  }
3496
3683
 
3497
3684
  // src/client/messaging/useChannelMembers.ts
3498
- import { useCallback as useCallback18 } from "react";
3685
+ import { useCallback as useCallback19 } from "react";
3499
3686
  function useChannelMembers(channelId) {
3500
3687
  const { user } = useUser();
3501
3688
  const { records, status, error } = useQuery("channel-members", {
3502
3689
  where: channelId ? { channelId } : { channelId: "__none__" }
3503
3690
  });
3504
3691
  const { create, remove: removeMutation } = useMutations("channel-members");
3505
- const join = useCallback18(() => {
3692
+ const join = useCallback19(() => {
3506
3693
  if (!channelId || !user) return;
3507
3694
  const existing = records.find((r) => r.data.userId === user.id);
3508
3695
  if (existing) return;
3509
3696
  create({ channelId, userId: user.id, joinedAt: (/* @__PURE__ */ new Date()).toISOString() });
3510
3697
  }, [channelId, user, records, create]);
3511
- const leave = useCallback18(() => {
3698
+ const leave = useCallback19(() => {
3512
3699
  if (!channelId || !user) return;
3513
3700
  const membership = records.find((r) => r.data.userId === user.id);
3514
3701
  if (membership) removeMutation(membership.recordId);
@@ -3518,14 +3705,14 @@ function useChannelMembers(channelId) {
3518
3705
  }
3519
3706
 
3520
3707
  // src/client/messaging/useReadReceipts.ts
3521
- import { useCallback as useCallback19 } from "react";
3708
+ import { useCallback as useCallback20 } from "react";
3522
3709
  function useReadReceipts() {
3523
3710
  const { user } = useUser();
3524
3711
  const { records, status, error } = useQuery("read-receipts", {
3525
3712
  where: user ? { userId: user.id } : { userId: "__none__" }
3526
3713
  });
3527
3714
  const { create, put } = useMutations("read-receipts");
3528
- const markAsRead = useCallback19(
3715
+ const markAsRead = useCallback20(
3529
3716
  (channelId) => {
3530
3717
  if (!user) return;
3531
3718
  const existing = records.find((r) => r.data.channelId === channelId);
@@ -3538,7 +3725,7 @@ function useReadReceipts() {
3538
3725
  },
3539
3726
  [user, records, create, put]
3540
3727
  );
3541
- const getUnreadCount = useCallback19(
3728
+ const getUnreadCount = useCallback20(
3542
3729
  (channelId, messages) => {
3543
3730
  const receipt = records.find((r) => r.data.channelId === channelId);
3544
3731
  if (!receipt) return messages.length;
@@ -3551,7 +3738,7 @@ function useReadReceipts() {
3551
3738
  }
3552
3739
 
3553
3740
  // src/client/messaging/useConversation.ts
3554
- import { useCallback as useCallback20, useMemo as useMemo12 } from "react";
3741
+ import { useCallback as useCallback21, useMemo as useMemo12 } from "react";
3555
3742
  function useConversation(opts) {
3556
3743
  const { user } = useUser();
3557
3744
  const { ready } = useRecordContext();
@@ -3560,7 +3747,7 @@ function useConversation(opts) {
3560
3747
  const { records: members } = useQuery("conv_members");
3561
3748
  const messageMut = useMutations("conv_messages");
3562
3749
  const reactionMut = useMutations("conv_reactions");
3563
- const send = useCallback20(
3750
+ const send = useCallback21(
3564
3751
  (content, parentMessageId, messageType, metadata) => {
3565
3752
  messageMut.create({
3566
3753
  Content: content,
@@ -3574,7 +3761,7 @@ function useConversation(opts) {
3574
3761
  },
3575
3762
  [messageMut, user?.id, opts?.onMessageSent]
3576
3763
  );
3577
- const edit = useCallback20(
3764
+ const edit = useCallback21(
3578
3765
  (recordId, content) => {
3579
3766
  const existing = allMessages.find((m) => m.recordId === recordId);
3580
3767
  if (!existing) return;
@@ -3582,7 +3769,7 @@ function useConversation(opts) {
3582
3769
  },
3583
3770
  [messageMut, allMessages]
3584
3771
  );
3585
- const remove = useCallback20(
3772
+ const remove = useCallback21(
3586
3773
  (recordId) => {
3587
3774
  const existing = allMessages.find((m) => m.recordId === recordId);
3588
3775
  if (!existing) return;
@@ -3595,7 +3782,7 @@ function useConversation(opts) {
3595
3782
  },
3596
3783
  [messageMut, allMessages]
3597
3784
  );
3598
- const toggleReaction = useCallback20(
3785
+ const toggleReaction = useCallback21(
3599
3786
  (messageId, emoji) => {
3600
3787
  const existing = allReactions.find(
3601
3788
  (r) => r.data.MessageId === messageId && r.data.Emoji === emoji && r.data.UserId === user?.id
@@ -3728,7 +3915,7 @@ function parseMessageMetadata(raw) {
3728
3915
  }
3729
3916
 
3730
3917
  // src/client/directory/useConversations.ts
3731
- import { useCallback as useCallback21, useMemo as useMemo13, useRef as useRef13 } from "react";
3918
+ import { useCallback as useCallback22, useMemo as useMemo13, useRef as useRef14 } from "react";
3732
3919
  function useConversations() {
3733
3920
  const { records: conversations, status } = useQuery("conversations");
3734
3921
  const { create, put } = useMutations("conversations");
@@ -3736,7 +3923,7 @@ function useConversations() {
3736
3923
  const stateMut = useMutations("conversation_state");
3737
3924
  const { user } = useUser();
3738
3925
  const ready = status === "ready";
3739
- const recentCreationsRef = useRef13(/* @__PURE__ */ new Map());
3926
+ const recentCreationsRef = useRef14(/* @__PURE__ */ new Map());
3740
3927
  for (const [id] of recentCreationsRef.current) {
3741
3928
  if (conversations.find((c) => c.recordId === id)) {
3742
3929
  recentCreationsRef.current.delete(id);
@@ -3758,7 +3945,7 @@ function useConversations() {
3758
3945
  () => new Set(stateRecords.filter((r) => r.data.Archived).map((r) => r.data.ConversationId)),
3759
3946
  [stateRecords]
3760
3947
  );
3761
- const getConversationState = useCallback21(
3948
+ const getConversationState = useCallback22(
3762
3949
  (conversationId) => {
3763
3950
  return stateRecords.find((r) => r.data.ConversationId === conversationId)?.data;
3764
3951
  },
@@ -3771,7 +3958,7 @@ function useConversations() {
3771
3958
  LinkedRef: "",
3772
3959
  MessageCount: 0
3773
3960
  };
3774
- const createChannel = useCallback21(
3961
+ const createChannel = useCallback22(
3775
3962
  async (name, description) => {
3776
3963
  const existing = conversations.find((r) => r.data.Name === name && r.data.Type !== "dm");
3777
3964
  if (existing) return existing.recordId;
@@ -3791,7 +3978,7 @@ function useConversations() {
3791
3978
  },
3792
3979
  [create, user?.id, conversations]
3793
3980
  );
3794
- const createDM = useCallback21(
3981
+ const createDM = useCallback22(
3795
3982
  async (otherUserId) => {
3796
3983
  if (!user?.id) return null;
3797
3984
  const hash = [user.id, otherUserId].sort().join(":");
@@ -3819,7 +4006,7 @@ function useConversations() {
3819
4006
  },
3820
4007
  [create, user?.id, conversations]
3821
4008
  );
3822
- const createGroupDM = useCallback21(
4009
+ const createGroupDM = useCallback22(
3823
4010
  async (participantIds) => {
3824
4011
  if (!user?.id) return null;
3825
4012
  const allIds = [.../* @__PURE__ */ new Set([user.id, ...participantIds])];
@@ -3849,14 +4036,14 @@ function useConversations() {
3849
4036
  },
3850
4037
  [create, user?.id, conversations]
3851
4038
  );
3852
- const lookupByName = useCallback21(
4039
+ const lookupByName = useCallback22(
3853
4040
  (name) => {
3854
4041
  const found = conversations.find((r) => r.data.Name === name);
3855
4042
  return found?.recordId ?? null;
3856
4043
  },
3857
4044
  [conversations]
3858
4045
  );
3859
- const updateLastMessage = useCallback21(
4046
+ const updateLastMessage = useCallback22(
3860
4047
  (conversationId, preview, authorId) => {
3861
4048
  const conv = conversations.find((c) => c.recordId === conversationId);
3862
4049
  const baseData = conv?.data ?? recentCreationsRef.current.get(conversationId);
@@ -3875,7 +4062,7 @@ function useConversations() {
3875
4062
  },
3876
4063
  [put, conversations]
3877
4064
  );
3878
- const upsertState = useCallback21(
4065
+ const upsertState = useCallback22(
3879
4066
  (conversationId, partial) => {
3880
4067
  if (!user?.id) return;
3881
4068
  const existing = stateRecords.find(
@@ -3897,7 +4084,7 @@ function useConversations() {
3897
4084
  },
3898
4085
  [stateMut, stateRecords, user?.id]
3899
4086
  );
3900
- const markRead = useCallback21(
4087
+ const markRead = useCallback22(
3901
4088
  (conversationId) => {
3902
4089
  const conv = conversations.find((c) => c.recordId === conversationId);
3903
4090
  const msgCount = conv?.data.MessageCount ?? recentCreationsRef.current.get(conversationId)?.MessageCount ?? 0;
@@ -3908,32 +4095,32 @@ function useConversations() {
3908
4095
  },
3909
4096
  [upsertState, conversations]
3910
4097
  );
3911
- const toggleStar = useCallback21(
4098
+ const toggleStar = useCallback22(
3912
4099
  (conversationId) => {
3913
4100
  const isStarred = starredSet.has(conversationId);
3914
4101
  upsertState(conversationId, { Starred: isStarred ? 0 : 1 });
3915
4102
  },
3916
4103
  [upsertState, starredSet]
3917
4104
  );
3918
- const setArchived = useCallback21(
4105
+ const setArchived = useCallback22(
3919
4106
  (conversationId, archived) => {
3920
4107
  upsertState(conversationId, { Archived: archived ? 1 : 0 });
3921
4108
  },
3922
4109
  [upsertState]
3923
4110
  );
3924
- const setTrashed = useCallback21(
4111
+ const setTrashed = useCallback22(
3925
4112
  (conversationId, trashed) => {
3926
4113
  upsertState(conversationId, { Trashed: trashed ? 1 : 0 });
3927
4114
  },
3928
4115
  [upsertState]
3929
4116
  );
3930
- const setLabels = useCallback21(
4117
+ const setLabels = useCallback22(
3931
4118
  (conversationId, labels) => {
3932
4119
  upsertState(conversationId, { Labels: JSON.stringify(labels) });
3933
4120
  },
3934
4121
  [upsertState]
3935
4122
  );
3936
- const setFolder = useCallback21(
4123
+ const setFolder = useCallback22(
3937
4124
  (conversationId, folder) => {
3938
4125
  upsertState(conversationId, { Folder: folder });
3939
4126
  },
@@ -3963,7 +4150,7 @@ function useConversations() {
3963
4150
  }
3964
4151
 
3965
4152
  // src/client/directory/useCommunities.ts
3966
- import { useCallback as useCallback22, useMemo as useMemo14 } from "react";
4153
+ import { useCallback as useCallback23, useMemo as useMemo14 } from "react";
3967
4154
  function useCommunities() {
3968
4155
  const { records: communities, status } = useQuery("communities");
3969
4156
  const communityMut = useMutations("communities");
@@ -3986,7 +4173,7 @@ function useCommunities() {
3986
4173
  ),
3987
4174
  [memberships, user?.id]
3988
4175
  );
3989
- const createCommunity = useCallback22(
4176
+ const createCommunity = useCallback23(
3990
4177
  async (name, opts) => {
3991
4178
  const existing = communities.find((c) => c.data.Name === name);
3992
4179
  if (existing) return existing.recordId;
@@ -4005,7 +4192,7 @@ function useCommunities() {
4005
4192
  },
4006
4193
  [communityMut, user?.id, communities]
4007
4194
  );
4008
- const updateCommunity = useCallback22(
4195
+ const updateCommunity = useCallback23(
4009
4196
  (communityId, updates) => {
4010
4197
  const existing = communities.find((c) => c.recordId === communityId);
4011
4198
  if (!existing) return;
@@ -4013,7 +4200,7 @@ function useCommunities() {
4013
4200
  },
4014
4201
  [communityMut, communities]
4015
4202
  );
4016
- const joinCommunity = useCallback22(
4203
+ const joinCommunity = useCallback23(
4017
4204
  async (communityId, userName) => {
4018
4205
  if (!user?.id) return null;
4019
4206
  const existing = memberships.find(
@@ -4031,7 +4218,7 @@ function useCommunities() {
4031
4218
  },
4032
4219
  [membershipMut, memberships, user?.id]
4033
4220
  );
4034
- const leaveCommunity = useCallback22(
4221
+ const leaveCommunity = useCallback23(
4035
4222
  (communityId) => {
4036
4223
  if (!user?.id) return;
4037
4224
  const membership = memberships.find(
@@ -4043,13 +4230,13 @@ function useCommunities() {
4043
4230
  },
4044
4231
  [membershipMut, memberships, user?.id]
4045
4232
  );
4046
- const getMembersOf = useCallback22(
4233
+ const getMembersOf = useCallback23(
4047
4234
  (communityId) => {
4048
4235
  return membershipMap.get(communityId) ?? [];
4049
4236
  },
4050
4237
  [membershipMap]
4051
4238
  );
4052
- const lookupByName = useCallback22(
4239
+ const lookupByName = useCallback23(
4053
4240
  (name) => {
4054
4241
  const found = communities.find((c) => c.data.Name === name);
4055
4242
  return found?.recordId ?? null;
@@ -4071,14 +4258,14 @@ function useCommunities() {
4071
4258
  }
4072
4259
 
4073
4260
  // src/client/directory/usePosts.ts
4074
- import { useCallback as useCallback23 } from "react";
4261
+ import { useCallback as useCallback24 } from "react";
4075
4262
  function usePosts(opts) {
4076
4263
  const { records: allPosts, status } = useQuery("posts");
4077
4264
  const postMut = useMutations("posts");
4078
4265
  const { user } = useUser();
4079
4266
  const ready = status === "ready";
4080
4267
  const posts = opts?.communityId ? allPosts.filter((p) => p.data.CommunityId === opts.communityId) : allPosts;
4081
- const createPost = useCallback23(
4268
+ const createPost = useCallback24(
4082
4269
  async (data) => {
4083
4270
  const recordId = await postMut.create({
4084
4271
  Title: data.title,
@@ -4096,7 +4283,7 @@ function usePosts(opts) {
4096
4283
  },
4097
4284
  [postMut, user?.id]
4098
4285
  );
4099
- const updatePost = useCallback23(
4286
+ const updatePost = useCallback24(
4100
4287
  (postId, updates) => {
4101
4288
  const existing = allPosts.find((p) => p.recordId === postId);
4102
4289
  if (!existing) return;
@@ -4104,13 +4291,13 @@ function usePosts(opts) {
4104
4291
  },
4105
4292
  [postMut, allPosts]
4106
4293
  );
4107
- const deletePost = useCallback23(
4294
+ const deletePost = useCallback24(
4108
4295
  (postId) => {
4109
4296
  postMut.remove(postId);
4110
4297
  },
4111
4298
  [postMut]
4112
4299
  );
4113
- const setConversationId = useCallback23(
4300
+ const setConversationId = useCallback24(
4114
4301
  (postId, conversationId) => {
4115
4302
  updatePost(postId, { ConversationId: conversationId });
4116
4303
  },
@@ -4127,7 +4314,7 @@ function usePosts(opts) {
4127
4314
  }
4128
4315
 
4129
4316
  // src/client/theme/DeepSpaceThemeProvider.tsx
4130
- import { useEffect as useEffect15 } from "react";
4317
+ import { useEffect as useEffect16 } from "react";
4131
4318
 
4132
4319
  // src/client/theme/applyTheme.ts
4133
4320
  function mix(color, pct, base = "transparent") {
@@ -4440,7 +4627,7 @@ function DeepSpaceThemeProvider({
4440
4627
  children,
4441
4628
  theme
4442
4629
  }) {
4443
- useEffect15(() => {
4630
+ useEffect16(() => {
4444
4631
  const root = document.documentElement;
4445
4632
  const config = theme ?? readThemeFromDOM(root);
4446
4633
  applyDeepSpaceTheme(config, root);
@@ -4449,12 +4636,12 @@ function DeepSpaceThemeProvider({
4449
4636
  }
4450
4637
 
4451
4638
  // src/client/theme/useIsDarkTheme.ts
4452
- import { useState as useState13, useEffect as useEffect16 } from "react";
4639
+ import { useState as useState14, useEffect as useEffect17 } from "react";
4453
4640
  function useIsDarkTheme() {
4454
- const [isDark, setIsDark] = useState13(
4641
+ const [isDark, setIsDark] = useState14(
4455
4642
  () => typeof document === "undefined" || document.documentElement.dataset.uiTheme !== "light"
4456
4643
  );
4457
- useEffect16(() => {
4644
+ useEffect17(() => {
4458
4645
  const update = () => {
4459
4646
  setIsDark(document.documentElement.dataset.uiTheme !== "light");
4460
4647
  };
@@ -4466,7 +4653,7 @@ function useIsDarkTheme() {
4466
4653
  }
4467
4654
 
4468
4655
  // src/client/platform/PlatformProvider.tsx
4469
- import { createContext as createContext3, useContext as useContext5, useState as useState14, useEffect as useEffect17, useRef as useRef14, useCallback as useCallback24 } from "react";
4656
+ import { createContext as createContext3, useContext as useContext5, useState as useState15, useEffect as useEffect18, useRef as useRef15, useCallback as useCallback25 } from "react";
4470
4657
  import { jsx as jsx9 } from "react/jsx-runtime";
4471
4658
  var PlatformContext = createContext3(null);
4472
4659
  function usePlatform() {
@@ -4476,14 +4663,14 @@ function usePlatform() {
4476
4663
  }
4477
4664
  function useInbox() {
4478
4665
  const { inbox, subscribeInbox } = usePlatform();
4479
- useEffect17(() => subscribeInbox(), [subscribeInbox]);
4666
+ useEffect18(() => subscribeInbox(), [subscribeInbox]);
4480
4667
  return inbox;
4481
4668
  }
4482
4669
  function PlatformProvider({ children }) {
4483
- const [inbox, setInbox] = useState14([]);
4484
- const inboxSubscribers = useRef14(0);
4485
- const [inboxActive, setInboxActive] = useState14(false);
4486
- const subscribeInbox = useCallback24(() => {
4670
+ const [inbox, setInbox] = useState15([]);
4671
+ const inboxSubscribers = useRef15(0);
4672
+ const [inboxActive, setInboxActive] = useState15(false);
4673
+ const subscribeInbox = useCallback25(() => {
4487
4674
  inboxSubscribers.current++;
4488
4675
  if (inboxSubscribers.current === 1) setInboxActive(true);
4489
4676
  return () => {
@@ -4491,7 +4678,7 @@ function PlatformProvider({ children }) {
4491
4678
  if (inboxSubscribers.current === 0) setInboxActive(false);
4492
4679
  };
4493
4680
  }, []);
4494
- useEffect17(() => {
4681
+ useEffect18(() => {
4495
4682
  if (!inboxActive) return;
4496
4683
  let mounted = true;
4497
4684
  let ws = null;
@@ -4549,7 +4736,7 @@ function PlatformProvider({ children }) {
4549
4736
  setInbox([]);
4550
4737
  };
4551
4738
  }, [inboxActive]);
4552
- const platformFetch = useCallback24(async (path, init) => {
4739
+ const platformFetch = useCallback25(async (path, init) => {
4553
4740
  const token = await getAuthToken();
4554
4741
  const headers = new Headers(init?.headers);
4555
4742
  if (token) headers.set("Authorization", `Bearer ${token}`);
@@ -4560,28 +4747,28 @@ function PlatformProvider({ children }) {
4560
4747
  }
4561
4748
 
4562
4749
  // src/client/platform/usePlatformWS.ts
4563
- import { useState as useState15, useEffect as useEffect18, useRef as useRef15 } from "react";
4750
+ import { useState as useState16, useEffect as useEffect19, useRef as useRef16 } from "react";
4564
4751
  var MAX_RECONNECT_DELAY = 1e4;
4565
4752
  var BASE_RECONNECT_DELAY = 500;
4566
4753
  function usePlatformWS(options) {
4567
4754
  const { path, scopeId, initialState, onMessage } = options;
4568
- const wsRef = useRef15(null);
4569
- const pingIntervalRef = useRef15(null);
4570
- const [state, setState] = useState15(initialState);
4571
- const reconnectAttemptRef = useRef15(0);
4572
- const reconnectTimerRef = useRef15(null);
4573
- const mountedRef = useRef15(true);
4574
- const onMessageRef = useRef15(onMessage);
4755
+ const wsRef = useRef16(null);
4756
+ const pingIntervalRef = useRef16(null);
4757
+ const [state, setState] = useState16(initialState);
4758
+ const reconnectAttemptRef = useRef16(0);
4759
+ const reconnectTimerRef = useRef16(null);
4760
+ const mountedRef = useRef16(true);
4761
+ const onMessageRef = useRef16(onMessage);
4575
4762
  onMessageRef.current = onMessage;
4576
- const initialStateRef = useRef15(initialState);
4763
+ const initialStateRef = useRef16(initialState);
4577
4764
  initialStateRef.current = initialState;
4578
- useEffect18(() => {
4765
+ useEffect19(() => {
4579
4766
  mountedRef.current = true;
4580
4767
  return () => {
4581
4768
  mountedRef.current = false;
4582
4769
  };
4583
4770
  }, []);
4584
- useEffect18(() => {
4771
+ useEffect19(() => {
4585
4772
  if (!scopeId) return;
4586
4773
  setState(initialStateRef.current);
4587
4774
  reconnectAttemptRef.current = 0;
@@ -4640,7 +4827,7 @@ function usePlatformWS(options) {
4640
4827
  wsRef.current = null;
4641
4828
  };
4642
4829
  }, [scopeId, path]);
4643
- const send = useRef15((msg) => {
4830
+ const send = useRef16((msg) => {
4644
4831
  wsRef.current?.send(JSON.stringify(msg));
4645
4832
  }).current;
4646
4833
  return { state, send };
@@ -4802,7 +4989,7 @@ function decodeAiStreamChunk(chunk) {
4802
4989
  }
4803
4990
 
4804
4991
  // src/client/subscriptions/useSubscription.ts
4805
- import { useCallback as useCallback25, useEffect as useEffect19, useState as useState16 } from "react";
4992
+ import { useCallback as useCallback26, useEffect as useEffect20, useState as useState17 } from "react";
4806
4993
 
4807
4994
  // src/shared/subscription.ts
4808
4995
  var ENTITLED_STATUSES = /* @__PURE__ */ new Set([
@@ -4833,7 +5020,7 @@ async function authedJson(path, init) {
4833
5020
  return data;
4834
5021
  }
4835
5022
  function useSubscription() {
4836
- const [state, setState] = useState16({
5023
+ const [state, setState] = useState17({
4837
5024
  tier: "free",
4838
5025
  status: "none",
4839
5026
  interval: null,
@@ -4842,9 +5029,9 @@ function useSubscription() {
4842
5029
  trialEndsAt: null,
4843
5030
  plans: []
4844
5031
  });
4845
- const [isLoading, setLoading] = useState16(true);
4846
- const [error, setError] = useState16(null);
4847
- const refresh = useCallback25(async () => {
5032
+ const [isLoading, setLoading] = useState17(true);
5033
+ const [error, setError] = useState17(null);
5034
+ const refresh = useCallback26(async () => {
4848
5035
  setLoading(true);
4849
5036
  try {
4850
5037
  const r = await authedJson("/_deepspace/subscriptions/me");
@@ -4864,15 +5051,15 @@ function useSubscription() {
4864
5051
  setLoading(false);
4865
5052
  }
4866
5053
  }, []);
4867
- useEffect19(() => {
5054
+ useEffect20(() => {
4868
5055
  refresh();
4869
5056
  }, [refresh]);
4870
5057
  const entitled = isEntitled(state.tier, state.status);
4871
- const hasTier = useCallback25(
5058
+ const hasTier = useCallback26(
4872
5059
  (slug) => state.tier === slug && isEntitled(state.tier, state.status),
4873
5060
  [state.tier, state.status]
4874
5061
  );
4875
- const isAtLeast = useCallback25(
5062
+ const isAtLeast = useCallback26(
4876
5063
  (slug) => {
4877
5064
  if (!isEntitled(state.tier, state.status)) return false;
4878
5065
  const targetRank = state.plans.find((p) => p.slug === slug)?.rank;
@@ -4882,7 +5069,7 @@ function useSubscription() {
4882
5069
  },
4883
5070
  [state.plans, state.tier, state.status]
4884
5071
  );
4885
- const subscribe = useCallback25(
5072
+ const subscribe = useCallback26(
4886
5073
  async (planSlug, opts) => {
4887
5074
  const result = await authedJson("/_deepspace/subscriptions/checkout", {
4888
5075
  method: "POST",
@@ -4902,7 +5089,7 @@ function useSubscription() {
4902
5089
  },
4903
5090
  [refresh]
4904
5091
  );
4905
- const openPortal = useCallback25(
5092
+ const openPortal = useCallback26(
4906
5093
  async (returnUrl) => {
4907
5094
  return authedJson("/_deepspace/subscriptions/portal", {
4908
5095
  method: "POST",
@@ -4971,7 +5158,7 @@ function PricingTable(props) {
4971
5158
  }
4972
5159
 
4973
5160
  // src/client/charges/useCheckout.ts
4974
- import { useCallback as useCallback26, useEffect as useEffect20, useMemo as useMemo15, useState as useState17 } from "react";
5161
+ import { useCallback as useCallback27, useEffect as useEffect21, useMemo as useMemo15, useState as useState18 } from "react";
4975
5162
  async function authedJson2(path, init) {
4976
5163
  const token = await getAuthToken();
4977
5164
  const res = await fetch(path, {
@@ -4990,10 +5177,10 @@ async function authedJson2(path, init) {
4990
5177
  return data;
4991
5178
  }
4992
5179
  function useCheckout(opts) {
4993
- const [purchases, setPurchases] = useState17([]);
4994
- const [isLoading, setLoading] = useState17(true);
4995
- const [error, setError] = useState17(null);
4996
- const refresh = useCallback26(async () => {
5180
+ const [purchases, setPurchases] = useState18([]);
5181
+ const [isLoading, setLoading] = useState18(true);
5182
+ const [error, setError] = useState18(null);
5183
+ const refresh = useCallback27(async () => {
4997
5184
  setLoading(true);
4998
5185
  try {
4999
5186
  const r = await authedJson2(
@@ -5007,10 +5194,10 @@ function useCheckout(opts) {
5007
5194
  setLoading(false);
5008
5195
  }
5009
5196
  }, []);
5010
- useEffect20(() => {
5197
+ useEffect21(() => {
5011
5198
  refresh();
5012
5199
  }, [refresh]);
5013
- const chargeOnce = useCallback26(
5200
+ const chargeOnce = useCallback27(
5014
5201
  async (chargeOpts) => {
5015
5202
  setError(null);
5016
5203
  const common = {
@@ -5044,7 +5231,7 @@ function useCheckout(opts) {
5044
5231
  },
5045
5232
  []
5046
5233
  );
5047
- const ownsProduct = useCallback26(
5234
+ const ownsProduct = useCallback27(
5048
5235
  (productId) => purchases.some((p) => p.productId === productId),
5049
5236
  [purchases]
5050
5237
  );
@@ -5895,9 +6082,9 @@ AlertDescription.displayName = "AlertDescription";
5895
6082
  import {
5896
6083
  createContext as createContext4,
5897
6084
  useContext as useContext6,
5898
- useState as useState18,
5899
- useCallback as useCallback27,
5900
- useEffect as useEffect21
6085
+ useState as useState19,
6086
+ useCallback as useCallback28,
6087
+ useEffect as useEffect22
5901
6088
  } from "react";
5902
6089
  import { jsx as jsx28, jsxs as jsxs10 } from "react/jsx-runtime";
5903
6090
  function CheckCircleIcon({ className }) {
@@ -5944,14 +6131,14 @@ function ToastProvider({
5944
6131
  position = "bottom-right",
5945
6132
  maxToasts = 5
5946
6133
  }) {
5947
- const [toasts, setToasts] = useState18([]);
5948
- const dismiss = useCallback27((id) => {
6134
+ const [toasts, setToasts] = useState19([]);
6135
+ const dismiss = useCallback28((id) => {
5949
6136
  setToasts((prev) => prev.filter((t) => t.id !== id));
5950
6137
  }, []);
5951
- const dismissAll = useCallback27(() => {
6138
+ const dismissAll = useCallback28(() => {
5952
6139
  setToasts([]);
5953
6140
  }, []);
5954
- const addToast = useCallback27(
6141
+ const addToast = useCallback28(
5955
6142
  (options) => {
5956
6143
  const id = Math.random().toString(36).slice(2);
5957
6144
  const duration = options.duration ?? 5e3;
@@ -5965,23 +6152,23 @@ function ToastProvider({
5965
6152
  },
5966
6153
  [dismiss, maxToasts]
5967
6154
  );
5968
- const toast = useCallback27(
6155
+ const toast = useCallback28(
5969
6156
  (options) => addToast(options),
5970
6157
  [addToast]
5971
6158
  );
5972
- const success = useCallback27(
6159
+ const success = useCallback28(
5973
6160
  (title, description) => addToast({ type: "success", title, description }),
5974
6161
  [addToast]
5975
6162
  );
5976
- const error = useCallback27(
6163
+ const error = useCallback28(
5977
6164
  (title, description) => addToast({ type: "error", title, description }),
5978
6165
  [addToast]
5979
6166
  );
5980
- const warning = useCallback27(
6167
+ const warning = useCallback28(
5981
6168
  (title, description) => addToast({ type: "warning", title, description }),
5982
6169
  [addToast]
5983
6170
  );
5984
- const info = useCallback27(
6171
+ const info = useCallback28(
5985
6172
  (title, description) => addToast({ type: "info", title, description }),
5986
6173
  [addToast]
5987
6174
  );
@@ -6018,8 +6205,8 @@ var TOAST_CONFIG = {
6018
6205
  };
6019
6206
  function ToastItem({ toast, onDismiss }) {
6020
6207
  const { Icon: Icon2, accent, icon } = TOAST_CONFIG[toast.type];
6021
- const [exiting, setExiting] = useState18(false);
6022
- useEffect21(() => {
6208
+ const [exiting, setExiting] = useState19(false);
6209
+ useEffect22(() => {
6023
6210
  if (exiting) {
6024
6211
  const timer = setTimeout(onDismiss, 150);
6025
6212
  return () => clearTimeout(timer);
@@ -6701,6 +6888,7 @@ export {
6701
6888
  useGameRoom,
6702
6889
  useInbox,
6703
6890
  useIsDarkTheme,
6891
+ useJobs,
6704
6892
  useMessages,
6705
6893
  useMutations,
6706
6894
  usePlatform,