deepspace 0.17.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +169 -1
- package/README.md +39 -41
- package/dist/cli.js +1585 -1403
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +18 -8
- package/dist/index.js +148 -25
- package/dist/index.js.map +1 -1
- package/dist/schema.js.map +1 -1
- package/dist/server.d.ts +40 -9
- package/dist/server.js +151 -44
- package/dist/server.js.map +1 -1
- package/dist/testing.d.ts +2 -2
- package/dist/testing.js +28 -25
- package/dist/testing.js.map +1 -1
- package/dist/worker.d.ts +61 -9
- package/dist/worker.js +190 -44
- package/dist/worker.js.map +1 -1
- package/features/documentation/feature.json +2 -1
- package/features/documents/feature.json +1 -1
- package/features/documents/src/DocumentsEditorPage.tsx +16 -52
- package/features/documents/src/LibrarySidebar.tsx +6 -8
- package/features/documents/src/editor/useDocEditor.ts +4 -9
- package/features/documents/src/use-documents-presence-access.ts +1 -3
- package/features/landing/feature.json +1 -1
- package/features/messaging/src/components/chat/MessageItem.tsx +83 -34
- package/features/messaging/src/hooks/useChatChannel.ts +2 -8
- package/features/tasks/src/TasksPage.tsx +6 -3
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -1361,7 +1361,7 @@ declare function encode<M extends BaseMessage<string, unknown>>(message: M): str
|
|
|
1361
1361
|
* Provides:
|
|
1362
1362
|
* - WebSocket upgrade with Cloudflare hibernation API
|
|
1363
1363
|
* - Connection tracking (WebSocket -> UserAttachment)
|
|
1364
|
-
* - Auth: parse JWT-verified user info from
|
|
1364
|
+
* - Auth: parse JWT-verified user info from internal request headers
|
|
1365
1365
|
* - Presence: connected users list, awareness on connect/disconnect
|
|
1366
1366
|
* - Message routing: JSON parse -> dispatch by `type` field, binary hook
|
|
1367
1367
|
* - Raw SQLite access via this.sql
|
|
@@ -1396,8 +1396,8 @@ interface ConnectionAttachment extends UserAttachment {
|
|
|
1396
1396
|
subscriptions: Subscription[];
|
|
1397
1397
|
/** Yjs docs this connection is editing */
|
|
1398
1398
|
yjsSubscriptions: YjsSubscription[];
|
|
1399
|
-
/** Client-side Yjs awareness clientId
|
|
1400
|
-
|
|
1399
|
+
/** Client-side Yjs awareness clientId for each subscribed document. */
|
|
1400
|
+
awarenessClientIds?: Partial<Record<YjsDocKey, number>>;
|
|
1401
1401
|
}
|
|
1402
1402
|
/**
|
|
1403
1403
|
* Context passed to handlers for accessing shared resources.
|
|
@@ -1418,6 +1418,8 @@ interface HandlerContext {
|
|
|
1418
1418
|
/** Asset-layer misses became real 404s, and the worker owns the fallback. The
|
|
1419
1419
|
* config and the worker's fallback path have to move together. */
|
|
1420
1420
|
declare const WORKER_OWNED_NOT_FOUND_MIGRATION_ID = "2026-08-worker-owned-not-found";
|
|
1421
|
+
/** Room identity moved to internal headers and privileged room routes gained app-role checks. */
|
|
1422
|
+
declare const SECURE_ROOM_BOUNDARIES_MIGRATION_ID = "2026-08-secure-room-boundaries";
|
|
1421
1423
|
type AppMigrationIdsValidation = {
|
|
1422
1424
|
valid: true;
|
|
1423
1425
|
ids: string[];
|
|
@@ -3065,6 +3067,8 @@ declare function useQuery<T = unknown>(collection: string, options?: {
|
|
|
3065
3067
|
* ```
|
|
3066
3068
|
*/
|
|
3067
3069
|
declare function useMutations<T = unknown>(collection: string): {
|
|
3070
|
+
/** True once the collection's RecordRoom can accept writes. */
|
|
3071
|
+
ready: boolean;
|
|
3068
3072
|
create: (data: T) => Promise<string>;
|
|
3069
3073
|
put: (recordId: string, data: Partial<T>) => Promise<void>;
|
|
3070
3074
|
remove: (recordId: string) => Promise<void>;
|
|
@@ -3073,6 +3077,12 @@ declare function useMutations<T = unknown>(collection: string): {
|
|
|
3073
3077
|
removeConfirmed: (recordId: string) => Promise<void>;
|
|
3074
3078
|
};
|
|
3075
3079
|
|
|
3080
|
+
/** A write was attempted before its RecordRoom connection became ready. */
|
|
3081
|
+
declare class RecordRoomNotReadyError extends Error {
|
|
3082
|
+
readonly code = "not_ready";
|
|
3083
|
+
constructor(collection?: string);
|
|
3084
|
+
}
|
|
3085
|
+
|
|
3076
3086
|
/**
|
|
3077
3087
|
* useUsers Hook
|
|
3078
3088
|
*
|
|
@@ -3220,7 +3230,7 @@ declare function usePresence(opts?: UsePresenceOptions): {
|
|
|
3220
3230
|
* Uses the shared yjs-protocol.ts encoding — no duplication.
|
|
3221
3231
|
*
|
|
3222
3232
|
* @example
|
|
3223
|
-
* const { doc, text, setText, synced, canWrite } = useYjsRoom(docId, 'content')
|
|
3233
|
+
* const { doc, text, setText, connected, synced, canWrite } = useYjsRoom(docId, 'content')
|
|
3224
3234
|
*/
|
|
3225
3235
|
|
|
3226
3236
|
interface UseYjsRoomResult {
|
|
@@ -3232,6 +3242,8 @@ interface UseYjsRoomResult {
|
|
|
3232
3242
|
text: string;
|
|
3233
3243
|
/** Set text (replaces full content) */
|
|
3234
3244
|
setText: (value: string) => void;
|
|
3245
|
+
/** Whether the current WebSocket transport is open */
|
|
3246
|
+
connected: boolean;
|
|
3235
3247
|
/** Whether initial sync is complete */
|
|
3236
3248
|
synced: boolean;
|
|
3237
3249
|
/** Whether user has write access */
|
|
@@ -3432,7 +3444,7 @@ declare function useJobs<P = unknown, R = unknown>(roomId: string): UseJobsResul
|
|
|
3432
3444
|
* Opens a WebSocket to /ws/presence/:scopeId for real-time presence tracking.
|
|
3433
3445
|
* Use with any scope: canvas, doc, thread, page, etc.
|
|
3434
3446
|
*
|
|
3435
|
-
* Peers can share
|
|
3447
|
+
* Peers can share small ephemeral state (cursor position, typing indicator,
|
|
3436
3448
|
* viewport, selection) via updateState().
|
|
3437
3449
|
*
|
|
3438
3450
|
* @example
|
|
@@ -3451,8 +3463,6 @@ declare function useJobs<P = unknown, R = unknown>(roomId: string): UseJobsResul
|
|
|
3451
3463
|
interface PresencePeerClient {
|
|
3452
3464
|
userId: string;
|
|
3453
3465
|
userName: string;
|
|
3454
|
-
userEmail: string;
|
|
3455
|
-
userImageUrl?: string;
|
|
3456
3466
|
joinedAt: string;
|
|
3457
3467
|
state: Record<string, unknown>;
|
|
3458
3468
|
}
|
|
@@ -4839,4 +4849,4 @@ declare const ACCOUNT_STORAGE_LIMIT_BYTES: {
|
|
|
4839
4849
|
declare function storageLimitForTier(tier: string | null | undefined): number;
|
|
4840
4850
|
declare function formatBytes(bytes: number): string;
|
|
4841
4851
|
|
|
4842
|
-
export { ACCOUNT_STORAGE_LIMIT_BYTES, APP_LOG_EVENT_KEYS, ASYNC_RESOURCE_DEFAULTS, type AdHocChargeOpts, type AiStreamAction, type AiStreamChunk, type AppLogEvent, type AppLogEventKey, type AppLogsResponse, type AppMigrationIdsValidation, type AsyncResourceDefaults, type AsyncResourceState, type AsyncResourceStatus, AuthGate, type AuthGateProps, AuthOverlay, Awareness, type AwarenessState, type AwarenessStates, type BaseMessage, type CanvasShapeClient, type Channel, type ChannelInvitation, type ChannelMember, type ChargeOnceOpts, type ChargeOnceResult, type ClientErrorKind, type ClientErrorReport, type ClientErrorReporterOptions, type ClientMessage, type CollectionSchema, type ConnectionAttachment, type ConnectionStatus, type ContentSegment, type ConversationObject, type CronHistoryEntry, type CronTaskState, DEEPSPACE_AGENT_PROFILES, DEEPSPACE_AI_DEFAULTS, DEEPSPACE_AI_MODELS, DEEPSPACE_MODEL_CATALOG_PROVENANCE, DEEPSPACE_THEME_PROPERTIES, DEFAULT_USER_COLORS, type DeepSpaceAIModel, type DeepSpaceAIModelId, type DeepSpaceAIProvider, type DeepSpaceAgentProfile, type DeepSpaceAgentProfileId, type DeepSpaceAgentSupport, DeepSpaceAuthProvider, type DeepSpaceThemeConfig, DeepSpaceThemeProvider, type DeepSpaceThemeProviderProps, type DeriveLoadStateInput, ENV, type Environment, type FileAttachment, type GroupedReaction, GuestBanner, type HandlerContext, type InboxEntry, type IntegrationResponse, type JobStatusView, type JobView, LOG_LEVELS, type LinkPreviewData, type LoadState, type LoadStateAuthInput, type LoadStateStatusInput, type LogLevel, MSG, MSG_AWARENESS, MSG_SYNC, MSG_SYNC_STEP1, MSG_SYNC_STEP2, MSG_SYNC_UPDATE, MSG_YJS_AWARENESS, MSG_YJS_SYNC, type MemberRecord, type Message, type MessageHandlers, type MessageRecord, type MsgType, type PagedResourceFetchArgs, type PagedResourcePage, type PagedResourceState, type PagedResourceStatus, type PlanInfo, PlatformContext, type PlatformContextValue, PlatformProvider, type PlatformWSOptions, type PlatformWSResult, type PresencePeerClient, PricingTable, type PricingTablePlan, type PricingTablePrice, type ProductChargeOpts, type ProtocolMessage, type Purchase, type Query, type R2FileInfo, type R2Scope, type R2UploadResult, ROLES, ROLE_ADMIN, ROLE_ANONYMOUS, ROLE_CONFIG, ROLE_DEFAULT, type Reaction, type ReactionRecord, type ReadCursorRecord, type ReadReceipt, type RecordData, RecordProvider, type RecordProviderProps, RecordScope, type RequestOptions, type ResolvedDeepSpaceAgentModel, type Role, type RoomUser, type ScopeEntry, ScopeRegistryProvider, type ServerMessage, SignedIn, SignedOut, type SubscribeOpts, type SubscribeResult, type SubscriptionState, type SubscriptionStatus, type SyncResult, type UseAsyncResourceOptions, type UseAuthProfileReadyOptions, type UseAuthStatusOptions, type UseCanvasResult, type UseCheckoutOpts, type UseCronMonitorResult, type UseJobsResult, type UsePagedResourceOptions, type UsePresenceRoomResult, type UseR2FilesReturn, type UseVoiceAgentOptions, type UseVoiceAgentResult, type UseYjsFieldResult, type UseYjsRoomResult, type UseYjsTextResult, type User, type UserCredits, type UserInfo, type UserKarma, type UserProfile, type ViewportClient, type VoiceStatus, type VoiceTranscriptEntry, WORKER_OWNED_NOT_FOUND_MIGRATION_ID, type WriteError, applyDeepSpaceTheme, applyUIThemeTokens, authClient, canAutoLoadPagedResource, clampPagedResourceItems, clearAuthToken, clearDeepSpaceTheme, clientBuild, createDecoder, createEncoder, decodeAiStreamChunk, deriveLoadState, detectEnvironment, dispatch, encode, encodeAwarenessMessage, encodeSyncStep1, encodeSyncStep2, encodeUpdate, formatBytes, formatFileSize, formatFullTimestamp, formatMessageTime, getApiUrl, getAuthToken, getAuthUrl, getConversationDisplayName, getConversationParticipantIds, getDeepSpaceAIModel, getEnvironmentConfig, getMessageType, getPagedResourceCooldownMs, getPlatformWorkerUrl, getThreadCounts, getUserColor, groupReactionsForMessage, handleAwarenessMessage, handleSyncMessage, hasContent, installClientErrorReporter, integration, isAsyncResourceSlow, isDMConversation, isDarkColor, isImageFile, isLocalDev, isProduction, listDeepSpaceAgentModels, parseMessageMetadata, parseSseLine, readThemeFromDOM, readVarUint, readVarUint8Array, reportClientError, resetEnvironmentCache, resolveDeepSpaceAgentModel, serverBuild, shouldGroupMessages, signIn, signOut, storageLimitForTier, toConnectionStatus, toUint8Array, useAsyncResource, useAuth, useAuthProfileReady, useAuthStatus, useAuthUser, useCanvas, useChannelMembers, useChannels, useCheckout, useCommunities, useConversation, useConversations, useCronMonitor, useDisplayName, useInbox, useIsDarkTheme, useJobs, useMessages, useMutations, usePagedResource, usePlatform, usePlatformWS, usePosts, usePresence, usePresenceRoom, useQuery, useR2Files, useReactions, useReadReceipts, useRecordContext, useSession, useSubscription, useUser, useUserLookup, useUsers, useVoiceAgent, useYjsField, useYjsRoom, useYjsText, validateAppMigrationIds, writeVarUint, writeVarUint8Array };
|
|
4852
|
+
export { ACCOUNT_STORAGE_LIMIT_BYTES, APP_LOG_EVENT_KEYS, ASYNC_RESOURCE_DEFAULTS, type AdHocChargeOpts, type AiStreamAction, type AiStreamChunk, type AppLogEvent, type AppLogEventKey, type AppLogsResponse, type AppMigrationIdsValidation, type AsyncResourceDefaults, type AsyncResourceState, type AsyncResourceStatus, AuthGate, type AuthGateProps, AuthOverlay, Awareness, type AwarenessState, type AwarenessStates, type BaseMessage, type CanvasShapeClient, type Channel, type ChannelInvitation, type ChannelMember, type ChargeOnceOpts, type ChargeOnceResult, type ClientErrorKind, type ClientErrorReport, type ClientErrorReporterOptions, type ClientMessage, type CollectionSchema, type ConnectionAttachment, type ConnectionStatus, type ContentSegment, type ConversationObject, type CronHistoryEntry, type CronTaskState, DEEPSPACE_AGENT_PROFILES, DEEPSPACE_AI_DEFAULTS, DEEPSPACE_AI_MODELS, DEEPSPACE_MODEL_CATALOG_PROVENANCE, DEEPSPACE_THEME_PROPERTIES, DEFAULT_USER_COLORS, type DeepSpaceAIModel, type DeepSpaceAIModelId, type DeepSpaceAIProvider, type DeepSpaceAgentProfile, type DeepSpaceAgentProfileId, type DeepSpaceAgentSupport, DeepSpaceAuthProvider, type DeepSpaceThemeConfig, DeepSpaceThemeProvider, type DeepSpaceThemeProviderProps, type DeriveLoadStateInput, ENV, type Environment, type FileAttachment, type GroupedReaction, GuestBanner, type HandlerContext, type InboxEntry, type IntegrationResponse, type JobStatusView, type JobView, LOG_LEVELS, type LinkPreviewData, type LoadState, type LoadStateAuthInput, type LoadStateStatusInput, type LogLevel, MSG, MSG_AWARENESS, MSG_SYNC, MSG_SYNC_STEP1, MSG_SYNC_STEP2, MSG_SYNC_UPDATE, MSG_YJS_AWARENESS, MSG_YJS_SYNC, type MemberRecord, type Message, type MessageHandlers, type MessageRecord, type MsgType, type PagedResourceFetchArgs, type PagedResourcePage, type PagedResourceState, type PagedResourceStatus, type PlanInfo, PlatformContext, type PlatformContextValue, PlatformProvider, type PlatformWSOptions, type PlatformWSResult, type PresencePeerClient, PricingTable, type PricingTablePlan, type PricingTablePrice, type ProductChargeOpts, type ProtocolMessage, type Purchase, type Query, type R2FileInfo, type R2Scope, type R2UploadResult, ROLES, ROLE_ADMIN, ROLE_ANONYMOUS, ROLE_CONFIG, ROLE_DEFAULT, type Reaction, type ReactionRecord, type ReadCursorRecord, type ReadReceipt, type RecordData, RecordProvider, type RecordProviderProps, RecordRoomNotReadyError, RecordScope, type RequestOptions, type ResolvedDeepSpaceAgentModel, type Role, type RoomUser, SECURE_ROOM_BOUNDARIES_MIGRATION_ID, type ScopeEntry, ScopeRegistryProvider, type ServerMessage, SignedIn, SignedOut, type SubscribeOpts, type SubscribeResult, type SubscriptionState, type SubscriptionStatus, type SyncResult, type UseAsyncResourceOptions, type UseAuthProfileReadyOptions, type UseAuthStatusOptions, type UseCanvasResult, type UseCheckoutOpts, type UseCronMonitorResult, type UseJobsResult, type UsePagedResourceOptions, type UsePresenceRoomResult, type UseR2FilesReturn, type UseVoiceAgentOptions, type UseVoiceAgentResult, type UseYjsFieldResult, type UseYjsRoomResult, type UseYjsTextResult, type User, type UserCredits, type UserInfo, type UserKarma, type UserProfile, type ViewportClient, type VoiceStatus, type VoiceTranscriptEntry, WORKER_OWNED_NOT_FOUND_MIGRATION_ID, type WriteError, applyDeepSpaceTheme, applyUIThemeTokens, authClient, canAutoLoadPagedResource, clampPagedResourceItems, clearAuthToken, clearDeepSpaceTheme, clientBuild, createDecoder, createEncoder, decodeAiStreamChunk, deriveLoadState, detectEnvironment, dispatch, encode, encodeAwarenessMessage, encodeSyncStep1, encodeSyncStep2, encodeUpdate, formatBytes, formatFileSize, formatFullTimestamp, formatMessageTime, getApiUrl, getAuthToken, getAuthUrl, getConversationDisplayName, getConversationParticipantIds, getDeepSpaceAIModel, getEnvironmentConfig, getMessageType, getPagedResourceCooldownMs, getPlatformWorkerUrl, getThreadCounts, getUserColor, groupReactionsForMessage, handleAwarenessMessage, handleSyncMessage, hasContent, installClientErrorReporter, integration, isAsyncResourceSlow, isDMConversation, isDarkColor, isImageFile, isLocalDev, isProduction, listDeepSpaceAgentModels, parseMessageMetadata, parseSseLine, readThemeFromDOM, readVarUint, readVarUint8Array, reportClientError, resetEnvironmentCache, resolveDeepSpaceAgentModel, serverBuild, shouldGroupMessages, signIn, signOut, storageLimitForTier, toConnectionStatus, toUint8Array, useAsyncResource, useAuth, useAuthProfileReady, useAuthStatus, useAuthUser, useCanvas, useChannelMembers, useChannels, useCheckout, useCommunities, useConversation, useConversations, useCronMonitor, useDisplayName, useInbox, useIsDarkTheme, useJobs, useMessages, useMutations, usePagedResource, usePlatform, usePlatformWS, usePosts, usePresence, usePresenceRoom, useQuery, useR2Files, useReactions, useReadReceipts, useRecordContext, useSession, useSubscription, useUser, useUserLookup, useUsers, useVoiceAgent, useYjsField, useYjsRoom, useYjsText, validateAppMigrationIds, writeVarUint, writeVarUint8Array };
|
package/dist/index.js
CHANGED
|
@@ -749,6 +749,7 @@ function encode(message) {
|
|
|
749
749
|
var APP_MIGRATION_ID_RE = /^[a-z0-9][a-z0-9._-]{0,127}$/;
|
|
750
750
|
var MAX_APP_MIGRATION_IDS = 256;
|
|
751
751
|
var WORKER_OWNED_NOT_FOUND_MIGRATION_ID = "2026-08-worker-owned-not-found";
|
|
752
|
+
var SECURE_ROOM_BOUNDARIES_MIGRATION_ID = "2026-08-secure-room-boundaries";
|
|
752
753
|
function validateAppMigrationIds(value) {
|
|
753
754
|
if (!Array.isArray(value)) {
|
|
754
755
|
return { valid: false, reason: "app migrations must be a JSON string array" };
|
|
@@ -1688,6 +1689,17 @@ function reconnectDelayMs(attempt) {
|
|
|
1688
1689
|
return Math.min(1e3 * Math.pow(2, attempt), 3e4);
|
|
1689
1690
|
}
|
|
1690
1691
|
|
|
1692
|
+
// src/client/storage/errors.ts
|
|
1693
|
+
var RecordRoomNotReadyError = class extends Error {
|
|
1694
|
+
code = "not_ready";
|
|
1695
|
+
constructor(collection) {
|
|
1696
|
+
super(
|
|
1697
|
+
collection ? `RecordRoom is not ready for "${collection}" writes.` : "RecordRoom is not ready for writes."
|
|
1698
|
+
);
|
|
1699
|
+
this.name = "RecordRoomNotReadyError";
|
|
1700
|
+
}
|
|
1701
|
+
};
|
|
1702
|
+
|
|
1691
1703
|
// src/client/storage/serverErrors.ts
|
|
1692
1704
|
function humanizeCollection(slug) {
|
|
1693
1705
|
return slug.replace(/^rbac-/, "").split(/[-_]/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
@@ -1786,6 +1798,8 @@ function parseServerError(error) {
|
|
|
1786
1798
|
// src/client/storage/record-socket.ts
|
|
1787
1799
|
var WS_CONNECTING = 0;
|
|
1788
1800
|
var WS_OPEN = 1;
|
|
1801
|
+
var PING_EVERY_MS = 15e3;
|
|
1802
|
+
var PONG_TIMEOUT_MS = 3e4;
|
|
1789
1803
|
var RecordSocket = class {
|
|
1790
1804
|
constructor(config) {
|
|
1791
1805
|
this.config = config;
|
|
@@ -1801,6 +1815,14 @@ var RecordSocket = class {
|
|
|
1801
1815
|
pendingRequests = /* @__PURE__ */ new Map();
|
|
1802
1816
|
reconnectAttempt = 0;
|
|
1803
1817
|
reconnectTimeout = null;
|
|
1818
|
+
heartbeat = null;
|
|
1819
|
+
/** Any inbound frame proves the peer is answering. */
|
|
1820
|
+
lastInboundAt = 0;
|
|
1821
|
+
/** Monotonic time of the one probe awaiting an inbound answer. */
|
|
1822
|
+
outstandingPingAt = null;
|
|
1823
|
+
/** Monotonic time of the prior heartbeat callback. A long gap means the
|
|
1824
|
+
* scheduler was suspended, so a pre-suspension probe cannot be judged. */
|
|
1825
|
+
lastHeartbeatCheckAt = 0;
|
|
1804
1826
|
// Bumped by connect() (on entry) and disconnect()/destroy(). connect()
|
|
1805
1827
|
// captures it before awaiting the auth token and re-checks after; a
|
|
1806
1828
|
// mismatch means a teardown (or a newer connect) raced in during the
|
|
@@ -1849,10 +1871,12 @@ var RecordSocket = class {
|
|
|
1849
1871
|
ws.onopen = () => {
|
|
1850
1872
|
config.log?.("connected", config.roomId);
|
|
1851
1873
|
config.listeners.onStatus("connected");
|
|
1874
|
+
this.startHeartbeat(ws);
|
|
1852
1875
|
this.resubscribeAll(ws);
|
|
1853
1876
|
};
|
|
1854
1877
|
ws.onmessage = (event) => this.handleMessage(event);
|
|
1855
1878
|
ws.onclose = () => {
|
|
1879
|
+
this.stopHeartbeat();
|
|
1856
1880
|
config.log?.("disconnected", config.roomId);
|
|
1857
1881
|
config.listeners.onStatus("disconnected");
|
|
1858
1882
|
config.listeners.onReady(false);
|
|
@@ -1907,8 +1931,48 @@ var RecordSocket = class {
|
|
|
1907
1931
|
this.reconnectTimeout = null;
|
|
1908
1932
|
}
|
|
1909
1933
|
}
|
|
1934
|
+
/**
|
|
1935
|
+
* Turn a peer that stopped answering into a `close` event. That is the whole
|
|
1936
|
+
* job — `onclose` already reports 'disconnected', marks every query stale,
|
|
1937
|
+
* and reconnects with backoff. Without this it is never called, because
|
|
1938
|
+
* nothing about the connection is actually broken.
|
|
1939
|
+
*/
|
|
1940
|
+
startHeartbeat(ws) {
|
|
1941
|
+
this.stopHeartbeat();
|
|
1942
|
+
this.lastInboundAt = performance.now();
|
|
1943
|
+
this.lastHeartbeatCheckAt = this.lastInboundAt;
|
|
1944
|
+
this.outstandingPingAt = null;
|
|
1945
|
+
this.heartbeat = setInterval(() => {
|
|
1946
|
+
if (ws.readyState !== WS_OPEN) return;
|
|
1947
|
+
const now = performance.now();
|
|
1948
|
+
const callbackGap = now - this.lastHeartbeatCheckAt;
|
|
1949
|
+
this.lastHeartbeatCheckAt = now;
|
|
1950
|
+
if (this.outstandingPingAt !== null) {
|
|
1951
|
+
if (now - this.outstandingPingAt >= PONG_TIMEOUT_MS) {
|
|
1952
|
+
if (callbackGap >= PONG_TIMEOUT_MS) {
|
|
1953
|
+
ws.send("ping");
|
|
1954
|
+
this.outstandingPingAt = now;
|
|
1955
|
+
} else {
|
|
1956
|
+
ws.close();
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
return;
|
|
1960
|
+
}
|
|
1961
|
+
if (now - this.lastInboundAt >= PING_EVERY_MS) {
|
|
1962
|
+
ws.send("ping");
|
|
1963
|
+
this.outstandingPingAt = now;
|
|
1964
|
+
}
|
|
1965
|
+
}, PING_EVERY_MS);
|
|
1966
|
+
}
|
|
1967
|
+
stopHeartbeat() {
|
|
1968
|
+
if (this.heartbeat) clearInterval(this.heartbeat);
|
|
1969
|
+
this.heartbeat = null;
|
|
1970
|
+
this.outstandingPingAt = null;
|
|
1971
|
+
this.lastHeartbeatCheckAt = 0;
|
|
1972
|
+
}
|
|
1910
1973
|
/** Close without firing onclose — prevents the zombie-reconnect. */
|
|
1911
1974
|
teardownSocket() {
|
|
1975
|
+
this.stopHeartbeat();
|
|
1912
1976
|
const ws = this.ws;
|
|
1913
1977
|
if (!ws) return;
|
|
1914
1978
|
this.config.log?.("closing", this.config.roomId);
|
|
@@ -1930,7 +1994,7 @@ var RecordSocket = class {
|
|
|
1930
1994
|
sendConfirmed(message, timeoutMs = 1e4) {
|
|
1931
1995
|
const ws = this.ws;
|
|
1932
1996
|
if (!ws || ws.readyState !== WS_OPEN) {
|
|
1933
|
-
return Promise.reject(new
|
|
1997
|
+
return Promise.reject(new RecordRoomNotReadyError());
|
|
1934
1998
|
}
|
|
1935
1999
|
const requestId = `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
1936
2000
|
return new Promise((resolve, reject) => {
|
|
@@ -1977,6 +2041,9 @@ var RecordSocket = class {
|
|
|
1977
2041
|
}
|
|
1978
2042
|
}
|
|
1979
2043
|
handleMessage(event) {
|
|
2044
|
+
this.lastInboundAt = performance.now();
|
|
2045
|
+
this.outstandingPingAt = null;
|
|
2046
|
+
if (event.data === "pong") return;
|
|
1980
2047
|
if (event.data instanceof ArrayBuffer) {
|
|
1981
2048
|
this.binaryHandlers.forEach((h) => h(event.data));
|
|
1982
2049
|
return;
|
|
@@ -2010,10 +2077,17 @@ var RecordSocket = class {
|
|
|
2010
2077
|
}
|
|
2011
2078
|
case MSG.RECORD_CHANGE: {
|
|
2012
2079
|
const { collection, record, changeType } = payload;
|
|
2013
|
-
for (const queryKey of this.subscriptions
|
|
2080
|
+
for (const [subscriptionId, queryKey] of this.subscriptions) {
|
|
2014
2081
|
try {
|
|
2015
2082
|
const query = JSON.parse(queryKey);
|
|
2016
2083
|
if (query.collection !== collection) continue;
|
|
2084
|
+
if (query.orderBy !== void 0 || query.limit !== void 0) {
|
|
2085
|
+
this.sendMessage({
|
|
2086
|
+
type: MSG.SUBSCRIBE,
|
|
2087
|
+
payload: { subscriptionId, query }
|
|
2088
|
+
});
|
|
2089
|
+
continue;
|
|
2090
|
+
}
|
|
2017
2091
|
const matches = recordMatchesWhere(record, query.where);
|
|
2018
2092
|
const exists = store.hasRecord(queryKey, record.recordId);
|
|
2019
2093
|
if (changeType === "delete") {
|
|
@@ -2201,7 +2275,7 @@ function ScopeConnection({
|
|
|
2201
2275
|
const sendConfirmed = useCallback3(
|
|
2202
2276
|
(message, timeoutMs = 1e4) => {
|
|
2203
2277
|
const socket = socketRef.current;
|
|
2204
|
-
if (!socket) return Promise.reject(new
|
|
2278
|
+
if (!socket) return Promise.reject(new RecordRoomNotReadyError());
|
|
2205
2279
|
return socket.sendConfirmed(message, timeoutMs);
|
|
2206
2280
|
},
|
|
2207
2281
|
[]
|
|
@@ -2434,33 +2508,41 @@ function useMutations(collection) {
|
|
|
2434
2508
|
const preferLocal = recordCtx?.registeredCollections?.has(collection) ?? false;
|
|
2435
2509
|
const sendMessage = preferLocal ? recordCtx.sendMessage : scopeEntry?.sendMessage ?? recordCtx?.sendMessage;
|
|
2436
2510
|
const sendConfirmed = preferLocal ? recordCtx.sendConfirmed : scopeEntry?.sendConfirmed ?? recordCtx?.sendConfirmed;
|
|
2511
|
+
const ready = preferLocal ? recordCtx.ready : scopeEntry?.ready ?? recordCtx?.ready ?? false;
|
|
2437
2512
|
if (!sendMessage || !sendConfirmed) {
|
|
2438
2513
|
throw new Error(
|
|
2439
2514
|
`useMutations('${collection}'): No scope found. Wrap this hook in a RecordScope that registers the collection.`
|
|
2440
2515
|
);
|
|
2441
2516
|
}
|
|
2517
|
+
const assertReady = useCallback5(() => {
|
|
2518
|
+
if (!ready) throw new RecordRoomNotReadyError(collection);
|
|
2519
|
+
}, [ready, collection]);
|
|
2442
2520
|
const create = useCallback5(
|
|
2443
2521
|
async (data) => {
|
|
2522
|
+
assertReady();
|
|
2444
2523
|
const recordId = `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
|
|
2445
2524
|
sendMessage({ type: MSG.PUT, payload: { collection, recordId, data } });
|
|
2446
2525
|
return recordId;
|
|
2447
2526
|
},
|
|
2448
|
-
[sendMessage, collection]
|
|
2527
|
+
[assertReady, sendMessage, collection]
|
|
2449
2528
|
);
|
|
2450
2529
|
const put = useCallback5(
|
|
2451
2530
|
async (recordId, data) => {
|
|
2531
|
+
assertReady();
|
|
2452
2532
|
sendMessage({ type: MSG.PUT, payload: { collection, recordId, data } });
|
|
2453
2533
|
},
|
|
2454
|
-
[sendMessage, collection]
|
|
2534
|
+
[assertReady, sendMessage, collection]
|
|
2455
2535
|
);
|
|
2456
2536
|
const remove = useCallback5(
|
|
2457
2537
|
async (recordId) => {
|
|
2538
|
+
assertReady();
|
|
2458
2539
|
sendMessage({ type: MSG.DELETE, payload: { collection, recordId } });
|
|
2459
2540
|
},
|
|
2460
|
-
[sendMessage, collection]
|
|
2541
|
+
[assertReady, sendMessage, collection]
|
|
2461
2542
|
);
|
|
2462
2543
|
const createConfirmed = useCallback5(
|
|
2463
2544
|
async (data) => {
|
|
2545
|
+
assertReady();
|
|
2464
2546
|
const recordId = `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
|
|
2465
2547
|
await sendConfirmed({
|
|
2466
2548
|
type: MSG.PUT,
|
|
@@ -2468,26 +2550,28 @@ function useMutations(collection) {
|
|
|
2468
2550
|
});
|
|
2469
2551
|
return recordId;
|
|
2470
2552
|
},
|
|
2471
|
-
[sendConfirmed, collection]
|
|
2553
|
+
[assertReady, sendConfirmed, collection]
|
|
2472
2554
|
);
|
|
2473
2555
|
const putConfirmed = useCallback5(
|
|
2474
2556
|
async (recordId, data) => {
|
|
2557
|
+
assertReady();
|
|
2475
2558
|
await sendConfirmed({
|
|
2476
2559
|
type: MSG.PUT,
|
|
2477
2560
|
payload: { collection, recordId, data }
|
|
2478
2561
|
});
|
|
2479
2562
|
},
|
|
2480
|
-
[sendConfirmed, collection]
|
|
2563
|
+
[assertReady, sendConfirmed, collection]
|
|
2481
2564
|
);
|
|
2482
2565
|
const removeConfirmed = useCallback5(
|
|
2483
2566
|
async (recordId) => {
|
|
2567
|
+
assertReady();
|
|
2484
2568
|
await sendConfirmed({ type: MSG.DELETE, payload: { collection, recordId } });
|
|
2485
2569
|
},
|
|
2486
|
-
[sendConfirmed, collection]
|
|
2570
|
+
[assertReady, sendConfirmed, collection]
|
|
2487
2571
|
);
|
|
2488
2572
|
return useMemo6(
|
|
2489
|
-
() => ({ create, put, remove, createConfirmed, putConfirmed, removeConfirmed }),
|
|
2490
|
-
[create, put, remove, createConfirmed, putConfirmed, removeConfirmed]
|
|
2573
|
+
() => ({ ready, create, put, remove, createConfirmed, putConfirmed, removeConfirmed }),
|
|
2574
|
+
[ready, create, put, remove, createConfirmed, putConfirmed, removeConfirmed]
|
|
2491
2575
|
);
|
|
2492
2576
|
}
|
|
2493
2577
|
|
|
@@ -2747,6 +2831,7 @@ import { useState as useState7, useEffect as useEffect10, useLayoutEffect as use
|
|
|
2747
2831
|
import * as Y3 from "yjs";
|
|
2748
2832
|
var UPDATE_BATCH_DELAY_MS = 16;
|
|
2749
2833
|
function useYjsRoom(docId, fieldName) {
|
|
2834
|
+
const [connected, setConnected] = useState7(false);
|
|
2750
2835
|
const [syncedDocId, setSyncedDocId] = useState7(null);
|
|
2751
2836
|
const [canWrite, setCanWrite] = useState7(false);
|
|
2752
2837
|
const [writeAuthResolved, setWriteAuthResolved] = useState7(false);
|
|
@@ -2782,6 +2867,7 @@ function useYjsRoom(docId, fieldName) {
|
|
|
2782
2867
|
};
|
|
2783
2868
|
}, [awareness, doc]);
|
|
2784
2869
|
useLayoutEffect2(() => {
|
|
2870
|
+
setConnected(false);
|
|
2785
2871
|
setSyncedDocId(null);
|
|
2786
2872
|
setCanWrite(false);
|
|
2787
2873
|
setWriteAuthResolved(false);
|
|
@@ -2799,31 +2885,44 @@ function useYjsRoom(docId, fieldName) {
|
|
|
2799
2885
|
let ws = null;
|
|
2800
2886
|
let reconnectTimer = null;
|
|
2801
2887
|
let alive = true;
|
|
2888
|
+
let browserOnline = typeof navigator === "undefined" || navigator.onLine;
|
|
2889
|
+
let connecting = false;
|
|
2802
2890
|
const renewLocalAwareness = () => {
|
|
2803
2891
|
const localState = awareness.getLocalState();
|
|
2804
2892
|
if (localState) awareness.setLocalState(localState);
|
|
2805
2893
|
};
|
|
2806
2894
|
const connect = async () => {
|
|
2807
|
-
if (!alive) return;
|
|
2895
|
+
if (!alive || !browserOnline) return;
|
|
2896
|
+
if (connecting) return;
|
|
2897
|
+
if (ws?.readyState === WebSocket.CONNECTING || ws?.readyState === WebSocket.OPEN) return;
|
|
2898
|
+
connecting = true;
|
|
2808
2899
|
setCanWrite(false);
|
|
2809
2900
|
setWriteAuthResolved(false);
|
|
2810
2901
|
renewLocalAwareness();
|
|
2811
|
-
|
|
2812
|
-
|
|
2902
|
+
let token;
|
|
2903
|
+
try {
|
|
2904
|
+
token = await getAuthToken();
|
|
2905
|
+
} finally {
|
|
2906
|
+
connecting = false;
|
|
2907
|
+
}
|
|
2908
|
+
if (!alive || !browserOnline) return;
|
|
2813
2909
|
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
|
2814
2910
|
const baseUrl = `${protocol}//${window.location.host}`;
|
|
2815
2911
|
const url = new URL(`/ws/yjs/${encodeURIComponent(docId)}`, baseUrl);
|
|
2816
2912
|
if (token) url.searchParams.set("token", token);
|
|
2817
2913
|
wsLog("connecting", `yjs:${docId}`);
|
|
2818
|
-
|
|
2819
|
-
ws
|
|
2820
|
-
|
|
2821
|
-
|
|
2914
|
+
const socket = new WebSocket(url.toString());
|
|
2915
|
+
ws = socket;
|
|
2916
|
+
socket.binaryType = "arraybuffer";
|
|
2917
|
+
wsRef.current = socket;
|
|
2918
|
+
socket.onopen = () => {
|
|
2919
|
+
if (ws !== socket) return socket.close();
|
|
2822
2920
|
wsLog("connected", `yjs:${docId}`);
|
|
2921
|
+
setConnected(true);
|
|
2823
2922
|
setSyncedDocId(null);
|
|
2824
2923
|
renewLocalAwareness();
|
|
2825
2924
|
};
|
|
2826
|
-
|
|
2925
|
+
socket.onmessage = (event) => {
|
|
2827
2926
|
if (typeof event.data === "string") {
|
|
2828
2927
|
try {
|
|
2829
2928
|
const msg = JSON.parse(event.data);
|
|
@@ -2848,7 +2947,7 @@ function useYjsRoom(docId, fieldName) {
|
|
|
2848
2947
|
writeVarUint(enc, MSG_SYNC);
|
|
2849
2948
|
writeVarUint(enc, MSG_SYNC_STEP2);
|
|
2850
2949
|
writeVarUint8Array(enc, diff);
|
|
2851
|
-
|
|
2950
|
+
socket.send(toUint8Array(enc).buffer);
|
|
2852
2951
|
setSyncedDocId(docId);
|
|
2853
2952
|
break;
|
|
2854
2953
|
}
|
|
@@ -2876,21 +2975,40 @@ function useYjsRoom(docId, fieldName) {
|
|
|
2876
2975
|
}
|
|
2877
2976
|
}
|
|
2878
2977
|
};
|
|
2879
|
-
|
|
2978
|
+
socket.onclose = () => {
|
|
2979
|
+
if (ws !== socket) return;
|
|
2880
2980
|
wsLog("disconnected", `yjs:${docId}`);
|
|
2981
|
+
ws = null;
|
|
2881
2982
|
wsRef.current = null;
|
|
2983
|
+
setConnected(false);
|
|
2882
2984
|
setSyncedDocId(null);
|
|
2883
2985
|
setCanWrite(false);
|
|
2884
2986
|
setWriteAuthResolved(false);
|
|
2885
|
-
if (alive) reconnectTimer = setTimeout(connect, 1e3);
|
|
2987
|
+
if (alive && browserOnline) reconnectTimer = setTimeout(connect, 1e3);
|
|
2886
2988
|
};
|
|
2887
|
-
|
|
2989
|
+
socket.onerror = () => socket.close();
|
|
2888
2990
|
};
|
|
2889
|
-
|
|
2991
|
+
const handleOffline = () => {
|
|
2992
|
+
browserOnline = false;
|
|
2993
|
+
setConnected(false);
|
|
2994
|
+
setSyncedDocId(null);
|
|
2995
|
+
setCanWrite(false);
|
|
2996
|
+
setWriteAuthResolved(false);
|
|
2997
|
+
ws?.close();
|
|
2998
|
+
};
|
|
2999
|
+
const handleOnline = () => {
|
|
3000
|
+
browserOnline = true;
|
|
3001
|
+
void connect();
|
|
3002
|
+
};
|
|
3003
|
+
window.addEventListener("offline", handleOffline);
|
|
3004
|
+
window.addEventListener("online", handleOnline);
|
|
3005
|
+
void connect();
|
|
2890
3006
|
return () => {
|
|
2891
3007
|
wsLog("closing", `yjs:${docId}`);
|
|
2892
3008
|
alive = false;
|
|
2893
3009
|
if (reconnectTimer) clearTimeout(reconnectTimer);
|
|
3010
|
+
window.removeEventListener("offline", handleOffline);
|
|
3011
|
+
window.removeEventListener("online", handleOnline);
|
|
2894
3012
|
flushPendingUpdate(ws);
|
|
2895
3013
|
if (ws) {
|
|
2896
3014
|
ws.onclose = null;
|
|
@@ -2947,7 +3065,7 @@ function useYjsRoom(docId, fieldName) {
|
|
|
2947
3065
|
},
|
|
2948
3066
|
[doc, yText, canWrite]
|
|
2949
3067
|
);
|
|
2950
|
-
return { doc, awareness, text, setText, synced, canWrite, writeAuthResolved };
|
|
3068
|
+
return { doc, awareness, text, setText, connected, synced, canWrite, writeAuthResolved };
|
|
2951
3069
|
}
|
|
2952
3070
|
|
|
2953
3071
|
// src/client/storage/hooks/useCanvas.ts
|
|
@@ -3215,6 +3333,9 @@ function useJobs(roomId) {
|
|
|
3215
3333
|
ws.onopen = () => setConnected(true);
|
|
3216
3334
|
ws.onmessage = (event) => {
|
|
3217
3335
|
dispatch(event.data, {
|
|
3336
|
+
[MSG.AUTH]: (p) => {
|
|
3337
|
+
if (!p.canWrite) rejectAllPending(new Error("JobRoom write access denied"));
|
|
3338
|
+
},
|
|
3218
3339
|
[MSG.JOB_UPDATE]: (p) => {
|
|
3219
3340
|
if (p.kind === "snapshot" && Array.isArray(p.jobs)) {
|
|
3220
3341
|
setJobs(p.jobs);
|
|
@@ -6208,7 +6329,9 @@ export {
|
|
|
6208
6329
|
ROLE_CONFIG,
|
|
6209
6330
|
ROLE_DEFAULT,
|
|
6210
6331
|
RecordProvider,
|
|
6332
|
+
RecordRoomNotReadyError,
|
|
6211
6333
|
RecordScope,
|
|
6334
|
+
SECURE_ROOM_BOUNDARIES_MIGRATION_ID,
|
|
6212
6335
|
ScopeRegistryProvider,
|
|
6213
6336
|
SignedIn,
|
|
6214
6337
|
SignedOut,
|