@taskforcehq/taskforce 0.3.303 → 0.3.304
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Taskforce.module.css +102 -5
- package/dist/TaskforceCore.js +514 -212
- package/dist/TaskforceCore.test.js +1172 -133
- package/dist/annotatedAttachments/service.d.ts +1 -1
- package/dist/annotatedAttachments/types.d.ts +1 -1
- package/dist/components/features/AgentsModule.js +1 -1
- package/dist/components/features/AgentsModule.test.js +1 -1
- package/dist/components/features/TaskSettings.js +58 -38
- package/dist/components/features/TaskSettings.test.js +139 -40
- package/dist/components/task/TaskActionHeader.d.ts +2 -1
- package/dist/components/task/TaskActionHeader.js +3 -3
- package/dist/components/task/TaskActionHeader.test.js +8 -0
- package/dist/components/task/TaskCard.js +16 -1
- package/dist/components/task/TaskCard.test.js +31 -0
- package/dist/components/task/TaskForm.d.ts +2 -1
- package/dist/components/task/TaskForm.js +22 -4
- package/dist/components/task/TaskForm.test.js +46 -0
- package/dist/components/task/TaskKanban.d.ts +1 -1
- package/dist/components/task/TaskKanban.js +31 -160
- package/dist/components/task/TaskStatusActions.d.ts +6 -1
- package/dist/components/task/TaskStatusActions.js +8 -3
- package/dist/components/task/TaskStatusActions.test.js +9 -0
- package/dist/components/task/taskKanbanDropRules.d.ts +58 -0
- package/dist/components/task/taskKanbanDropRules.js +187 -0
- package/dist/components/task/taskKanbanDropRules.test.d.ts +1 -0
- package/dist/components/task/taskKanbanDropRules.test.js +114 -0
- package/dist/components/ui/Modal.d.ts +1 -1
- package/dist/components/ui/Modal.js +1 -0
- package/dist/components/views/AppShellHeader.js +5 -1
- package/dist/components/views/AppShellHeader.test.js +4 -0
- package/dist/components/views/PlanComparisonPage.d.ts +3 -2
- package/dist/components/views/PlanComparisonPage.js +4 -2
- package/dist/components/views/PlanComparisonPage.test.js +4 -0
- package/dist/components/views/PlansPage.d.ts +5 -1
- package/dist/components/views/PlansPage.js +39 -12
- package/dist/components/views/PlansPage.test.js +59 -2
- package/dist/components/views/StandaloneLayout.js +113 -38
- package/dist/components/views/WidgetView.js +11 -2
- package/dist/components/views/standalone/modals/SyncStatusModal.js +1 -1
- package/dist/core/DeletedTaskLifecycleService.d.ts +2 -1
- package/dist/core/PlanEntitlementService.d.ts +3 -1
- package/dist/core/PlanEntitlementService.js +61 -11
- package/dist/core/SignupMonetizationSettings.test.js +37 -2
- package/dist/core/TaskActivityService.d.ts +4 -1
- package/dist/core/TaskActivityService.js +45 -0
- package/dist/core/TaskAuditTimeline.test.js +52 -1
- package/dist/core/Taskforce.d.ts +15 -11
- package/dist/core/Taskforce.js +43 -2
- package/dist/core/WorkspacePlanMode.test.js +65 -0
- package/dist/documentReviews/service.d.ts +1 -1
- package/dist/documentReviews/types.d.ts +1 -1
- package/dist/hooks/auth/useTaskforceAuthBootstrap.d.ts +48 -0
- package/dist/hooks/auth/useTaskforceAuthBootstrap.js +219 -0
- package/dist/hooks/sync/auth.d.ts +37 -0
- package/dist/hooks/sync/auth.js +61 -0
- package/dist/hooks/sync/bootstrap.d.ts +34 -0
- package/dist/hooks/sync/bootstrap.js +52 -0
- package/dist/hooks/sync/orchestratorShared.d.ts +36 -0
- package/dist/hooks/sync/orchestratorShared.js +209 -0
- package/dist/hooks/sync/orchestratorShared.test.d.ts +1 -0
- package/dist/hooks/sync/orchestratorShared.test.js +49 -0
- package/dist/hooks/sync/realtime.d.ts +25 -0
- package/dist/hooks/sync/realtime.js +60 -0
- package/dist/hooks/sync/recovery.d.ts +69 -0
- package/dist/hooks/sync/recovery.js +118 -0
- package/dist/hooks/sync/transfers.d.ts +122 -0
- package/dist/hooks/sync/transfers.js +431 -0
- package/dist/hooks/sync/useDataVersionRefresh.d.ts +16 -0
- package/dist/hooks/sync/useDataVersionRefresh.js +73 -0
- package/dist/hooks/ui/useResourceExport.d.ts +3 -1
- package/dist/hooks/ui/useResourceExport.js +5 -2
- package/dist/hooks/ui/useWorkflowTemplates.js +11 -6
- package/dist/hooks/useRealtimeSync.js +2 -1
- package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +8 -0
- package/dist/hooks/useSyncOrchestrator.context-assets.test.d.ts +1 -0
- package/dist/hooks/useSyncOrchestrator.context-assets.test.js +228 -0
- package/dist/hooks/useSyncOrchestrator.d.ts +5 -4
- package/dist/hooks/useSyncOrchestrator.js +314 -873
- package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +568 -3
- package/dist/hooks/useTaskMutations.js +45 -6
- package/dist/hooks/useTaskMutations.test.js +147 -0
- package/dist/hooks/useTaskforce.d.ts +8 -2
- package/dist/hooks/useTaskforce.js +285 -812
- package/dist/hooks/useWorkspaceSyncController.d.ts +1 -0
- package/dist/hooks/useWorkspaceSyncController.js +22 -10
- package/dist/hooks/workspace/useTaskforceApiRouting.test.js +17 -0
- package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +166 -0
- package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +434 -0
- package/dist/hooks/workspace/workspaceLocalState.d.ts +19 -0
- package/dist/hooks/workspace/workspaceLocalState.js +100 -0
- package/dist/hooks/workspace/workspaceLocalState.test.d.ts +1 -0
- package/dist/hooks/workspace/workspaceLocalState.test.js +59 -0
- package/dist/localization/locales/en-US.js +2 -2
- package/dist/mcp/canonicalAssetHelpers.d.ts +70 -0
- package/dist/mcp/canonicalAssetHelpers.js +259 -0
- package/dist/mcp/clientIntegrations.d.ts +12 -1
- package/dist/mcp/clientIntegrations.js +161 -6
- package/dist/mcp/clientIntegrations.test.js +76 -4
- package/dist/mcp/documentHelpers.d.ts +100 -0
- package/dist/mcp/documentHelpers.js +161 -0
- package/dist/mcp/runtime.js +174 -476
- package/dist/mcp/runtime.test.js +81 -8
- package/dist/mcp/taskAttachmentHelpers.d.ts +41 -0
- package/dist/mcp/taskAttachmentHelpers.js +60 -0
- package/dist/mcp/taskAttachmentTypes.d.ts +37 -0
- package/dist/mcp/taskAttachmentTypes.js +1 -0
- package/dist/plugins/vite.js +1 -1
- package/dist/runtime/appGateDefinitions.d.ts +1 -1
- package/dist/runtime/appGateDefinitions.js +1 -1
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +46 -3
- package/dist/server/index.test.js +12 -1
- package/dist/server/routes/admin.js +13 -19
- package/dist/server/routes/auth.js +37 -2
- package/dist/server/routes/billing.js +18 -67
- package/dist/server/routes/billing.test.js +43 -0
- package/dist/server/routes/shared.d.ts +30 -0
- package/dist/server/routes/shared.js +26 -0
- package/dist/server/routes/sync.integration.test.js +20 -0
- package/dist/server/routes/sync.js +110 -237
- package/dist/server/routes/tasks.js +49 -7
- package/dist/server/routes.js +8 -19
- package/dist/server/routes.test.js +243 -5
- package/dist/shared/taskActor.d.ts +10 -0
- package/dist/shared/taskActor.js +1 -0
- package/dist/sync/collaborationSyncPayload.js +4 -0
- package/dist/sync/collaborationSyncPayload.test.js +8 -0
- package/dist/sync/collaborationSyncState.d.ts +3 -1
- package/dist/sync/collaborationSyncState.js +19 -0
- package/dist/sync/contentSyncState.js +12 -10
- package/dist/sync/syncApplyHandlers.d.ts +87 -1
- package/dist/sync/syncApplyHandlers.js +370 -8
- package/dist/sync/syncApplyHandlers.test.d.ts +1 -0
- package/dist/sync/syncApplyHandlers.test.js +377 -0
- package/dist/sync/syncService.d.ts +8 -1
- package/dist/sync/syncService.js +36 -2
- package/dist/sync/syncService.test.js +48 -0
- package/dist/sync/workspacePullFeed.d.ts +1 -0
- package/dist/sync/workspacePullFeed.js +66 -59
- package/dist/sync/workspacePullFeed.test.js +72 -4
- package/dist/sync/workspaceSyncModel.d.ts +22 -1
- package/dist/sync/workspaceSyncModel.js +65 -0
- package/dist/sync/workspaceSyncModel.test.js +119 -0
- package/dist/types.d.ts +2 -10
- package/dist/ui/assets/AgentsModule-BLWVbuKP.js +1 -0
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-C_TmXZwA.js → AnnotatedAttachmentWorkspace-BlS-cqnl.js} +1 -1
- package/dist/ui/assets/{DocumentWorkspace-C3GyzrWm.js → DocumentWorkspace-BH_6DF6x.js} +1 -1
- package/dist/ui/assets/{InitiativesModule-4-Rh2K2n.js → InitiativesModule-BjR6iBf9.js} +1 -1
- package/dist/ui/assets/{PlansPage-BlVC_lRq.css → PlansPage-C2dd2n1X.css} +1 -1
- package/dist/ui/assets/PlansPage-CSDQ4sLa.js +1 -0
- package/dist/ui/assets/TaskSettings-CEDy34Jo.js +9 -0
- package/dist/ui/assets/{WorkflowsModule-CGk9s3NJ.js → WorkflowsModule-DnFqdUME.js} +1 -1
- package/dist/ui/assets/index-DXUdtH1B.js +6 -0
- package/dist/ui/assets/index-Ii0B0rTO.css +1 -0
- package/dist/ui/assets/{vendor-icons-pWocEipT.js → vendor-icons-I7ZwG1z_.js} +1 -1
- package/dist/ui/index.html +4 -4
- package/dist/utils/taskActivity.js +4 -0
- package/dist/utils/taskActivity.test.js +25 -1
- package/package.json +3 -1
- package/scripts/playwright-auth.sh +4 -0
- package/dist/ui/assets/AgentsModule-CpsTmrIW.js +0 -1
- package/dist/ui/assets/PlansPage-BP7AYOqr.js +0 -1
- package/dist/ui/assets/TaskSettings-BOC5F6Ag.js +0 -8
- package/dist/ui/assets/index-CLIMgR6g.js +0 -6
- package/dist/ui/assets/index-DneETxcu.css +0 -1
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { isTransientWorkspaceSyncStatus } from '../../sync/syncService';
|
|
2
|
+
import { isValidWorkspaceKey } from './orchestratorShared';
|
|
3
|
+
export function createSyncAuthCoordinator({ currentWorkspaceId, workspaceSyncPhase, clearWorkspaceRetry, setWorkspaceSyncBusy, setUserGlobalSyncStatus, setUserGlobalSyncError, setWorkspaceLastErrorMessage, setWorkspaceLastErrorAt, reportSyncEvent, setAuthBlocked, setIsAuthenticated, checkAuthSession, ensureCloudWorkspaceReadyForSync, scheduleWorkspaceSyncRetry, persistWorkspaceSyncPatchSafely }) {
|
|
4
|
+
const handleSyncAuthFailure = async (source, statusCode) => {
|
|
5
|
+
clearWorkspaceRetry();
|
|
6
|
+
setWorkspaceSyncBusy(false);
|
|
7
|
+
setUserGlobalSyncStatus('error');
|
|
8
|
+
const message = 'Session expired. Sign in again to resume cloud sync.';
|
|
9
|
+
setUserGlobalSyncError(message);
|
|
10
|
+
setWorkspaceLastErrorMessage(message);
|
|
11
|
+
setWorkspaceLastErrorAt(new Date().toISOString());
|
|
12
|
+
reportSyncEvent(currentWorkspaceId, {
|
|
13
|
+
eventType: source === 'handshake' ? 'handshake' : source,
|
|
14
|
+
status: 'error',
|
|
15
|
+
statusCode,
|
|
16
|
+
errorMessage: message
|
|
17
|
+
});
|
|
18
|
+
setAuthBlocked(true);
|
|
19
|
+
setIsAuthenticated(false);
|
|
20
|
+
await checkAuthSession();
|
|
21
|
+
};
|
|
22
|
+
const ensureWorkspaceSyncReady = async () => {
|
|
23
|
+
if (!isValidWorkspaceKey(currentWorkspaceId)) {
|
|
24
|
+
const message = 'Workspace sync blocked: local workspace ID is unresolved.';
|
|
25
|
+
setWorkspaceLastErrorMessage(message);
|
|
26
|
+
setWorkspaceLastErrorAt(new Date().toISOString());
|
|
27
|
+
setUserGlobalSyncError(message);
|
|
28
|
+
setUserGlobalSyncStatus('error');
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
const ensured = await ensureCloudWorkspaceReadyForSync();
|
|
32
|
+
if (ensured.success)
|
|
33
|
+
return true;
|
|
34
|
+
if (ensured.statusCode === 401) {
|
|
35
|
+
await handleSyncAuthFailure('handshake', 401);
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
const message = ensured.error || 'Workspace sync handshake failed.';
|
|
39
|
+
setWorkspaceLastErrorMessage(message);
|
|
40
|
+
setWorkspaceLastErrorAt(new Date().toISOString());
|
|
41
|
+
setUserGlobalSyncError(message);
|
|
42
|
+
setUserGlobalSyncStatus('error');
|
|
43
|
+
reportSyncEvent(currentWorkspaceId, {
|
|
44
|
+
eventType: 'handshake',
|
|
45
|
+
status: 'error',
|
|
46
|
+
statusCode: ensured.statusCode,
|
|
47
|
+
errorMessage: message
|
|
48
|
+
});
|
|
49
|
+
if (ensured.transient || isTransientWorkspaceSyncStatus(ensured.statusCode)) {
|
|
50
|
+
scheduleWorkspaceSyncRetry(ensured.retryAfterMs);
|
|
51
|
+
}
|
|
52
|
+
else if (workspaceSyncPhase === 'active') {
|
|
53
|
+
persistWorkspaceSyncPatchSafely({ phase: 'error', lastErrorMessage: message });
|
|
54
|
+
}
|
|
55
|
+
return false;
|
|
56
|
+
};
|
|
57
|
+
return {
|
|
58
|
+
handleSyncAuthFailure,
|
|
59
|
+
ensureWorkspaceSyncReady
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { MutableRefObject } from 'react';
|
|
2
|
+
import type { WorkspaceSyncPhase } from '../../sync/workspaceSyncState';
|
|
3
|
+
interface BootstrapPhaseTrackingArgs {
|
|
4
|
+
workspaceSyncPhase: WorkspaceSyncPhase;
|
|
5
|
+
lastBootstrapPhaseRef: MutableRefObject<WorkspaceSyncPhase>;
|
|
6
|
+
bootstrapPhaseStartedAtRef: MutableRefObject<number | null>;
|
|
7
|
+
bootstrapLastProgressAtRef: MutableRefObject<number | null>;
|
|
8
|
+
}
|
|
9
|
+
export declare function syncBootstrapPhaseTracking({ workspaceSyncPhase, lastBootstrapPhaseRef, bootstrapPhaseStartedAtRef, bootstrapLastProgressAtRef }: BootstrapPhaseTrackingArgs): void;
|
|
10
|
+
interface BootstrapTimeoutMonitorArgs {
|
|
11
|
+
currentWorkspaceId: string;
|
|
12
|
+
workspaceCloudSyncEnabled: boolean;
|
|
13
|
+
workspaceSyncPhase: WorkspaceSyncPhase;
|
|
14
|
+
workspacePullInFlightRef: MutableRefObject<boolean>;
|
|
15
|
+
workspacePushInFlightRef: MutableRefObject<boolean>;
|
|
16
|
+
bootstrapPhaseStartedAtRef: MutableRefObject<number | null>;
|
|
17
|
+
bootstrapLastProgressAtRef: MutableRefObject<number | null>;
|
|
18
|
+
setWorkspaceLastErrorMessage: (value: string) => void;
|
|
19
|
+
setWorkspaceLastErrorAt: (value: string) => void;
|
|
20
|
+
setUserGlobalSyncStatus: (value: 'error') => void;
|
|
21
|
+
setUserGlobalSyncError: (value: string) => void;
|
|
22
|
+
setWorkspaceSyncBusy: (value: boolean) => void;
|
|
23
|
+
persistWorkspaceSyncPatchSafely: (patch: {
|
|
24
|
+
phase: 'error';
|
|
25
|
+
lastErrorMessage: string;
|
|
26
|
+
}) => void;
|
|
27
|
+
}
|
|
28
|
+
export declare function createBootstrapTimeoutMonitor({ currentWorkspaceId, workspaceCloudSyncEnabled, workspaceSyncPhase, workspacePullInFlightRef, workspacePushInFlightRef, bootstrapPhaseStartedAtRef, bootstrapLastProgressAtRef, setWorkspaceLastErrorMessage, setWorkspaceLastErrorAt, setUserGlobalSyncStatus, setUserGlobalSyncError, setWorkspaceSyncBusy, persistWorkspaceSyncPatchSafely }: BootstrapTimeoutMonitorArgs): (() => void) | undefined;
|
|
29
|
+
export declare function resolveRepairPhase(args: {
|
|
30
|
+
workspaceSyncPhase: WorkspaceSyncPhase;
|
|
31
|
+
lastBootstrapPhase: WorkspaceSyncPhase;
|
|
32
|
+
workspaceLastPullAt: string | null;
|
|
33
|
+
}): WorkspaceSyncPhase;
|
|
34
|
+
export {};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { WORKSPACE_BOOTSTRAP_TIMEOUT_MS, logSyncDebug } from './orchestratorShared';
|
|
2
|
+
export function syncBootstrapPhaseTracking({ workspaceSyncPhase, lastBootstrapPhaseRef, bootstrapPhaseStartedAtRef, bootstrapLastProgressAtRef }) {
|
|
3
|
+
if (workspaceSyncPhase === 'attach-cloud' || workspaceSyncPhase === 'provision-local') {
|
|
4
|
+
lastBootstrapPhaseRef.current = workspaceSyncPhase;
|
|
5
|
+
if (bootstrapPhaseStartedAtRef.current === null) {
|
|
6
|
+
bootstrapPhaseStartedAtRef.current = Date.now();
|
|
7
|
+
}
|
|
8
|
+
bootstrapLastProgressAtRef.current = null;
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
bootstrapPhaseStartedAtRef.current = null;
|
|
12
|
+
bootstrapLastProgressAtRef.current = null;
|
|
13
|
+
}
|
|
14
|
+
export function createBootstrapTimeoutMonitor({ currentWorkspaceId, workspaceCloudSyncEnabled, workspaceSyncPhase, workspacePullInFlightRef, workspacePushInFlightRef, bootstrapPhaseStartedAtRef, bootstrapLastProgressAtRef, setWorkspaceLastErrorMessage, setWorkspaceLastErrorAt, setUserGlobalSyncStatus, setUserGlobalSyncError, setWorkspaceSyncBusy, persistWorkspaceSyncPatchSafely }) {
|
|
15
|
+
if (!workspaceCloudSyncEnabled)
|
|
16
|
+
return undefined;
|
|
17
|
+
if (workspaceSyncPhase !== 'attach-cloud' && workspaceSyncPhase !== 'provision-local')
|
|
18
|
+
return undefined;
|
|
19
|
+
const intervalId = window.setInterval(() => {
|
|
20
|
+
if (workspaceSyncPhase !== 'attach-cloud' && workspaceSyncPhase !== 'provision-local')
|
|
21
|
+
return;
|
|
22
|
+
if (workspacePullInFlightRef.current || workspacePushInFlightRef.current)
|
|
23
|
+
return;
|
|
24
|
+
const lastActivity = Math.max(bootstrapPhaseStartedAtRef.current ?? 0, bootstrapLastProgressAtRef.current ?? 0);
|
|
25
|
+
if (lastActivity > 0 && (Date.now() - lastActivity) < WORKSPACE_BOOTSTRAP_TIMEOUT_MS)
|
|
26
|
+
return;
|
|
27
|
+
const phaseLabel = workspaceSyncPhase === 'attach-cloud' ? 'initial cloud pull' : 'initial cloud upload';
|
|
28
|
+
const message = `Workspace sync ${phaseLabel} stalled. Press Repair to restart sync for this workspace.`;
|
|
29
|
+
setWorkspaceLastErrorMessage(message);
|
|
30
|
+
setWorkspaceLastErrorAt(new Date().toISOString());
|
|
31
|
+
setUserGlobalSyncStatus('error');
|
|
32
|
+
setUserGlobalSyncError(message);
|
|
33
|
+
setWorkspaceSyncBusy(false);
|
|
34
|
+
persistWorkspaceSyncPatchSafely({ phase: 'error', lastErrorMessage: message });
|
|
35
|
+
logSyncDebug('workspace_sync_bootstrap_timeout', {
|
|
36
|
+
workspaceId: currentWorkspaceId,
|
|
37
|
+
phase: workspaceSyncPhase
|
|
38
|
+
});
|
|
39
|
+
}, 10_000);
|
|
40
|
+
return () => window.clearInterval(intervalId);
|
|
41
|
+
}
|
|
42
|
+
export function resolveRepairPhase(args) {
|
|
43
|
+
const { workspaceSyncPhase, lastBootstrapPhase, workspaceLastPullAt } = args;
|
|
44
|
+
if (workspaceSyncPhase === 'provision-local')
|
|
45
|
+
return 'provision-local';
|
|
46
|
+
if (workspaceSyncPhase === 'attach-cloud')
|
|
47
|
+
return 'attach-cloud';
|
|
48
|
+
if (lastBootstrapPhase === 'provision-local' || lastBootstrapPhase === 'attach-cloud') {
|
|
49
|
+
return lastBootstrapPhase;
|
|
50
|
+
}
|
|
51
|
+
return workspaceLastPullAt ? 'attach-cloud' : 'provision-local';
|
|
52
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { WorkspaceTaxonomyState } from '../../shared/taxonomyState.js';
|
|
2
|
+
import type { WorkspaceSyncAiProfileSnapshot, WorkspaceSyncDocumentReviewCommentSnapshot, WorkspaceSyncDocumentReviewSessionSnapshot, WorkspaceSyncTaskEventSnapshot } from '../../sync/workspaceSyncModel';
|
|
3
|
+
import type { TaskItem } from '../../types';
|
|
4
|
+
export declare const USER_GLOBAL_SYNC_META_KEY = "taskforce.userGlobalSyncMeta.v1";
|
|
5
|
+
export declare const SYNC_DEBUG_LOG_KEY = "taskforce.syncDebug.v1";
|
|
6
|
+
export declare const WORKSPACE_BOOTSTRAP_TIMEOUT_MS = 120000;
|
|
7
|
+
export declare const WORKSPACE_SYNC_WATERMARKS_KEY_PREFIX = "taskforce.syncWatermarks.v3";
|
|
8
|
+
export declare const WORKSPACE_FULL_RECONCILE_INTERVAL_MS: number;
|
|
9
|
+
export declare const WORKSPACE_CONCURRENT_WINDOW_MESSAGE = "Another local window is already syncing this workspace. Wait a few seconds, or press Repair in that window.";
|
|
10
|
+
export declare function isValidWorkspaceKey(workspaceId: string): boolean;
|
|
11
|
+
export declare function logSyncDebug(event: string, details?: Record<string, unknown>): void;
|
|
12
|
+
export declare function normalizeWorkspaceSyncAiProfileSnapshotEntry(entry: any): WorkspaceSyncAiProfileSnapshot;
|
|
13
|
+
export declare function readUserGlobalSyncMeta(): Record<string, {
|
|
14
|
+
updatedAt: string;
|
|
15
|
+
}>;
|
|
16
|
+
export declare function writeUserGlobalSyncMeta(next: Record<string, {
|
|
17
|
+
updatedAt: string;
|
|
18
|
+
}>): void;
|
|
19
|
+
export declare function buildTaxonomyStateFingerprint(taxonomyState?: WorkspaceTaxonomyState): string;
|
|
20
|
+
export declare function flattenDocumentReviewComments(sessions: WorkspaceSyncDocumentReviewSessionSnapshot[]): WorkspaceSyncDocumentReviewCommentSnapshot[];
|
|
21
|
+
export declare function flattenTaskEvents(tasks: TaskItem[], archivedTasks: TaskItem[]): WorkspaceSyncTaskEventSnapshot[];
|
|
22
|
+
export interface PersistedSyncWatermarks {
|
|
23
|
+
taskWatermarks: Map<string, string>;
|
|
24
|
+
initiativeWatermarks: Map<string, string>;
|
|
25
|
+
workstreamWatermarks: Map<string, string>;
|
|
26
|
+
documentWatermarks: Map<string, string>;
|
|
27
|
+
assetWatermarks: Map<string, string>;
|
|
28
|
+
documentReviewSessionWatermarks: Map<string, string>;
|
|
29
|
+
documentReviewCommentWatermarks: Map<string, string>;
|
|
30
|
+
annotatedAttachmentSessionWatermarks: Map<string, string>;
|
|
31
|
+
taskEventWatermarks: Map<string, string>;
|
|
32
|
+
}
|
|
33
|
+
export declare function readPersistedSyncWatermarks(workspaceId: string, taxonomyStateFingerprint?: string): PersistedSyncWatermarks | null;
|
|
34
|
+
export declare function savePersistedSyncWatermarks(workspaceId: string, data: PersistedSyncWatermarks & {
|
|
35
|
+
taxonomyStateFingerprint?: string;
|
|
36
|
+
}): void;
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { parseAiProfileSurfaceType } from '../../shared/aiProfileSurfaceType.js';
|
|
2
|
+
export const USER_GLOBAL_SYNC_META_KEY = 'taskforce.userGlobalSyncMeta.v1';
|
|
3
|
+
export const SYNC_DEBUG_LOG_KEY = 'taskforce.syncDebug.v1';
|
|
4
|
+
export const WORKSPACE_BOOTSTRAP_TIMEOUT_MS = 120_000;
|
|
5
|
+
export const WORKSPACE_SYNC_WATERMARKS_KEY_PREFIX = 'taskforce.syncWatermarks.v3';
|
|
6
|
+
export const WORKSPACE_FULL_RECONCILE_INTERVAL_MS = 5 * 60 * 1000;
|
|
7
|
+
export const WORKSPACE_CONCURRENT_WINDOW_MESSAGE = 'Another local window is already syncing this workspace. Wait a few seconds, or press Repair in that window.';
|
|
8
|
+
export function isValidWorkspaceKey(workspaceId) {
|
|
9
|
+
const workspaceKey = String(workspaceId || '').trim();
|
|
10
|
+
return workspaceKey.length > 0 && workspaceKey.toLowerCase() !== 'default';
|
|
11
|
+
}
|
|
12
|
+
function isSyncDebugLoggingEnabled() {
|
|
13
|
+
if (typeof window === 'undefined')
|
|
14
|
+
return false;
|
|
15
|
+
try {
|
|
16
|
+
const raw = String(window.localStorage.getItem(SYNC_DEBUG_LOG_KEY) || '').trim().toLowerCase();
|
|
17
|
+
return raw === '1' || raw === 'true' || raw === 'on' || raw === 'yes';
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export function logSyncDebug(event, details) {
|
|
24
|
+
if (!isSyncDebugLoggingEnabled())
|
|
25
|
+
return;
|
|
26
|
+
const payload = details && typeof details === 'object' ? details : {};
|
|
27
|
+
console.info('[Taskforce Sync]', event, payload);
|
|
28
|
+
}
|
|
29
|
+
export function normalizeWorkspaceSyncAiProfileSnapshotEntry(entry) {
|
|
30
|
+
const metadata = entry?.providerMetadata ?? entry?.provider_metadata ?? null;
|
|
31
|
+
return {
|
|
32
|
+
id: String(entry?.id || '').trim(),
|
|
33
|
+
workspaceId: String(entry?.workspaceId || entry?.workspace_id || '').trim(),
|
|
34
|
+
profileToken: String(entry?.profileToken || entry?.profile_token || '').trim(),
|
|
35
|
+
name: String(entry?.name || '').trim(),
|
|
36
|
+
username: String(entry?.username || '').trim(),
|
|
37
|
+
icon: String(entry?.icon || 'Bot').trim() || 'Bot',
|
|
38
|
+
color: String(entry?.color || '#8b5cf6').trim() || '#8b5cf6',
|
|
39
|
+
description: typeof entry?.description === 'string' && entry.description.trim().length > 0
|
|
40
|
+
? entry.description.trim()
|
|
41
|
+
: null,
|
|
42
|
+
role: typeof entry?.role === 'string' && entry.role.trim().length > 0
|
|
43
|
+
? entry.role.trim()
|
|
44
|
+
: null,
|
|
45
|
+
provider: typeof entry?.provider === 'string' && entry.provider.trim().length > 0
|
|
46
|
+
? entry.provider.trim()
|
|
47
|
+
: null,
|
|
48
|
+
model: typeof entry?.model === 'string' && entry.model.trim().length > 0
|
|
49
|
+
? entry.model.trim()
|
|
50
|
+
: null,
|
|
51
|
+
surfaceType: parseAiProfileSurfaceType(entry?.surfaceType ?? entry?.surface_type),
|
|
52
|
+
providerMetadata: metadata && typeof metadata === 'object' && !Array.isArray(metadata)
|
|
53
|
+
? metadata
|
|
54
|
+
: null,
|
|
55
|
+
createdAt: String(entry?.createdAt || entry?.created_at || '').trim(),
|
|
56
|
+
updatedAt: String(entry?.updatedAt || entry?.updated_at || entry?.createdAt || entry?.created_at || '').trim(),
|
|
57
|
+
lastActiveAt: typeof entry?.lastActiveAt === 'string' && entry.lastActiveAt.trim().length > 0
|
|
58
|
+
? entry.lastActiveAt.trim()
|
|
59
|
+
: typeof entry?.last_active_at === 'string' && entry.last_active_at.trim().length > 0
|
|
60
|
+
? entry.last_active_at.trim()
|
|
61
|
+
: null
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
export function readUserGlobalSyncMeta() {
|
|
65
|
+
if (typeof window === 'undefined')
|
|
66
|
+
return {};
|
|
67
|
+
try {
|
|
68
|
+
const raw = window.localStorage.getItem(USER_GLOBAL_SYNC_META_KEY);
|
|
69
|
+
if (!raw)
|
|
70
|
+
return {};
|
|
71
|
+
const parsed = JSON.parse(raw);
|
|
72
|
+
return parsed && typeof parsed === 'object'
|
|
73
|
+
? parsed
|
|
74
|
+
: {};
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return {};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
export function writeUserGlobalSyncMeta(next) {
|
|
81
|
+
if (typeof window === 'undefined')
|
|
82
|
+
return;
|
|
83
|
+
try {
|
|
84
|
+
window.localStorage.setItem(USER_GLOBAL_SYNC_META_KEY, JSON.stringify(next));
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// best-effort only
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
export function buildTaxonomyStateFingerprint(taxonomyState) {
|
|
91
|
+
try {
|
|
92
|
+
return JSON.stringify(taxonomyState && typeof taxonomyState === 'object' ? taxonomyState : null);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return 'null';
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
export function flattenDocumentReviewComments(sessions) {
|
|
99
|
+
return sessions.flatMap((session) => {
|
|
100
|
+
const sessionId = String(session?.id || '').trim();
|
|
101
|
+
if (!sessionId || !Array.isArray(session?.comments))
|
|
102
|
+
return [];
|
|
103
|
+
return session.comments
|
|
104
|
+
.map((comment) => ({
|
|
105
|
+
id: String(comment?.id || '').trim(),
|
|
106
|
+
sessionId,
|
|
107
|
+
body: String(comment?.body || ''),
|
|
108
|
+
anchor: comment?.anchor ?? null,
|
|
109
|
+
order: Number.isFinite(Number(comment?.order)) ? Math.max(0, Math.floor(Number(comment.order))) : 0,
|
|
110
|
+
authorActorId: typeof comment?.authorActorId === 'string' && comment.authorActorId.trim().length > 0 ? comment.authorActorId.trim() : null,
|
|
111
|
+
createdAt: String(comment?.createdAt || '').trim(),
|
|
112
|
+
updatedAt: String(comment?.updatedAt || comment?.createdAt || '').trim(),
|
|
113
|
+
deletedAt: typeof comment?.deletedAt === 'string' && comment.deletedAt.trim().length > 0 ? comment.deletedAt.trim() : null,
|
|
114
|
+
}))
|
|
115
|
+
.filter((comment) => comment.id.length > 0 && comment.sessionId.length > 0);
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
export function flattenTaskEvents(tasks, archivedTasks) {
|
|
119
|
+
const deduped = new Map();
|
|
120
|
+
for (const task of [...(tasks || []), ...(archivedTasks || [])]) {
|
|
121
|
+
const taskEvents = Array.isArray(task?.taskEvents) ? task.taskEvents : [];
|
|
122
|
+
for (const event of taskEvents) {
|
|
123
|
+
const id = String(event?.id || '').trim();
|
|
124
|
+
const taskId = String(event?.taskId || task?.id || '').trim();
|
|
125
|
+
const workspaceId = String(event?.workspaceId || '').trim();
|
|
126
|
+
const createdAt = String(event?.createdAt || '').trim();
|
|
127
|
+
if (!id || !taskId || !workspaceId || !createdAt)
|
|
128
|
+
continue;
|
|
129
|
+
const nextEvent = {
|
|
130
|
+
id,
|
|
131
|
+
taskId,
|
|
132
|
+
workspaceId,
|
|
133
|
+
action: String(event?.action || '').trim(),
|
|
134
|
+
actor: String(event?.actor || 'system').trim() || 'system',
|
|
135
|
+
actorType: event?.actorType === 'ai' || event?.actorType === 'human' || event?.actorType === 'system'
|
|
136
|
+
? event.actorType
|
|
137
|
+
: 'system',
|
|
138
|
+
details: event?.details,
|
|
139
|
+
createdAt,
|
|
140
|
+
};
|
|
141
|
+
const existing = deduped.get(id);
|
|
142
|
+
if (!existing || nextEvent.createdAt > existing.createdAt) {
|
|
143
|
+
deduped.set(id, nextEvent);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return Array.from(deduped.values()).sort((left, right) => {
|
|
148
|
+
if (left.createdAt !== right.createdAt)
|
|
149
|
+
return left.createdAt.localeCompare(right.createdAt);
|
|
150
|
+
return left.id.localeCompare(right.id);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
export function readPersistedSyncWatermarks(workspaceId, taxonomyStateFingerprint) {
|
|
154
|
+
if (typeof window === 'undefined')
|
|
155
|
+
return null;
|
|
156
|
+
try {
|
|
157
|
+
const key = `${WORKSPACE_SYNC_WATERMARKS_KEY_PREFIX}.${workspaceId}`;
|
|
158
|
+
const raw = window.localStorage.getItem(key);
|
|
159
|
+
if (!raw)
|
|
160
|
+
return null;
|
|
161
|
+
const parsed = JSON.parse(raw);
|
|
162
|
+
if (!parsed || (parsed.v !== 2 && parsed.v !== 3 && parsed.v !== 4 && parsed.v !== 5 && parsed.v !== 6 && parsed.v !== 7 && parsed.v !== 8))
|
|
163
|
+
return null;
|
|
164
|
+
const savedTaxonomyStateFingerprint = typeof parsed.taxonomyStateFingerprint === 'string'
|
|
165
|
+
? parsed.taxonomyStateFingerprint
|
|
166
|
+
: '';
|
|
167
|
+
const taskWatermarks = (!taxonomyStateFingerprint || !savedTaxonomyStateFingerprint || savedTaxonomyStateFingerprint === taxonomyStateFingerprint)
|
|
168
|
+
? new Map(Array.isArray(parsed.taskWatermarks) ? parsed.taskWatermarks : [])
|
|
169
|
+
: new Map();
|
|
170
|
+
return {
|
|
171
|
+
taskWatermarks,
|
|
172
|
+
initiativeWatermarks: new Map(Array.isArray(parsed.initiativeWatermarks) ? parsed.initiativeWatermarks : []),
|
|
173
|
+
workstreamWatermarks: new Map(Array.isArray(parsed.workstreamWatermarks) ? parsed.workstreamWatermarks : []),
|
|
174
|
+
documentWatermarks: new Map(Array.isArray(parsed.documentWatermarks) ? parsed.documentWatermarks : []),
|
|
175
|
+
assetWatermarks: new Map(Array.isArray(parsed.assetWatermarks) ? parsed.assetWatermarks : []),
|
|
176
|
+
documentReviewSessionWatermarks: new Map(Array.isArray(parsed.documentReviewSessionWatermarks) ? parsed.documentReviewSessionWatermarks : []),
|
|
177
|
+
documentReviewCommentWatermarks: new Map(Array.isArray(parsed.documentReviewCommentWatermarks) ? parsed.documentReviewCommentWatermarks : []),
|
|
178
|
+
annotatedAttachmentSessionWatermarks: new Map(Array.isArray(parsed.annotatedAttachmentSessionWatermarks) ? parsed.annotatedAttachmentSessionWatermarks : []),
|
|
179
|
+
taskEventWatermarks: new Map(Array.isArray(parsed.taskEventWatermarks) ? parsed.taskEventWatermarks : []),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
export function savePersistedSyncWatermarks(workspaceId, data) {
|
|
187
|
+
if (typeof window === 'undefined')
|
|
188
|
+
return;
|
|
189
|
+
try {
|
|
190
|
+
const key = `${WORKSPACE_SYNC_WATERMARKS_KEY_PREFIX}.${workspaceId}`;
|
|
191
|
+
window.localStorage.setItem(key, JSON.stringify({
|
|
192
|
+
v: 8,
|
|
193
|
+
savedAt: new Date().toISOString(),
|
|
194
|
+
taskWatermarks: Array.from(data.taskWatermarks.entries()),
|
|
195
|
+
initiativeWatermarks: Array.from(data.initiativeWatermarks.entries()),
|
|
196
|
+
workstreamWatermarks: Array.from(data.workstreamWatermarks.entries()),
|
|
197
|
+
documentWatermarks: Array.from(data.documentWatermarks.entries()),
|
|
198
|
+
assetWatermarks: Array.from(data.assetWatermarks.entries()),
|
|
199
|
+
documentReviewSessionWatermarks: Array.from(data.documentReviewSessionWatermarks.entries()),
|
|
200
|
+
documentReviewCommentWatermarks: Array.from(data.documentReviewCommentWatermarks.entries()),
|
|
201
|
+
annotatedAttachmentSessionWatermarks: Array.from(data.annotatedAttachmentSessionWatermarks.entries()),
|
|
202
|
+
taskEventWatermarks: Array.from(data.taskEventWatermarks.entries()),
|
|
203
|
+
taxonomyStateFingerprint: typeof data.taxonomyStateFingerprint === 'string' ? data.taxonomyStateFingerprint : ''
|
|
204
|
+
}));
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
// localStorage may be full or unavailable; non-fatal
|
|
208
|
+
}
|
|
209
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import { readPersistedSyncWatermarks, savePersistedSyncWatermarks, WORKSPACE_SYNC_WATERMARKS_KEY_PREFIX, } from './orchestratorShared';
|
|
3
|
+
describe('orchestratorShared sync watermarks', () => {
|
|
4
|
+
const workspaceId = 'workspace-local-active';
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
localStorage.clear();
|
|
7
|
+
});
|
|
8
|
+
it('keeps persisted sync watermarks available across long idle periods', () => {
|
|
9
|
+
const key = `${WORKSPACE_SYNC_WATERMARKS_KEY_PREFIX}.${workspaceId}`;
|
|
10
|
+
localStorage.setItem(key, JSON.stringify({
|
|
11
|
+
v: 8,
|
|
12
|
+
savedAt: '2026-04-14T10:00:00.000Z',
|
|
13
|
+
taskWatermarks: [['task-1', '2026-04-14T09:00:00.000Z']],
|
|
14
|
+
initiativeWatermarks: [['initiative-1', '2026-04-14T09:00:00.000Z']],
|
|
15
|
+
workstreamWatermarks: [],
|
|
16
|
+
documentWatermarks: [],
|
|
17
|
+
assetWatermarks: [],
|
|
18
|
+
documentReviewSessionWatermarks: [],
|
|
19
|
+
documentReviewCommentWatermarks: [],
|
|
20
|
+
annotatedAttachmentSessionWatermarks: [],
|
|
21
|
+
taskEventWatermarks: [['event-1', '2026-04-14T09:30:00.000Z']],
|
|
22
|
+
taxonomyStateFingerprint: ''
|
|
23
|
+
}));
|
|
24
|
+
const persisted = readPersistedSyncWatermarks(workspaceId, '');
|
|
25
|
+
expect(persisted).not.toBeNull();
|
|
26
|
+
expect(persisted?.taskWatermarks.get('task-1')).toBe('2026-04-14T09:00:00.000Z');
|
|
27
|
+
expect(persisted?.initiativeWatermarks.get('initiative-1')).toBe('2026-04-14T09:00:00.000Z');
|
|
28
|
+
expect(persisted?.taskEventWatermarks.get('event-1')).toBe('2026-04-14T09:30:00.000Z');
|
|
29
|
+
});
|
|
30
|
+
it('still clears task watermarks when the taxonomy fingerprint changes', () => {
|
|
31
|
+
savePersistedSyncWatermarks(workspaceId, {
|
|
32
|
+
taskWatermarks: new Map([['task-1', '2026-04-14T09:00:00.000Z']]),
|
|
33
|
+
initiativeWatermarks: new Map([['initiative-1', '2026-04-14T09:00:00.000Z']]),
|
|
34
|
+
workstreamWatermarks: new Map(),
|
|
35
|
+
documentWatermarks: new Map(),
|
|
36
|
+
assetWatermarks: new Map(),
|
|
37
|
+
documentReviewSessionWatermarks: new Map(),
|
|
38
|
+
documentReviewCommentWatermarks: new Map(),
|
|
39
|
+
annotatedAttachmentSessionWatermarks: new Map(),
|
|
40
|
+
taskEventWatermarks: new Map([['event-1', '2026-04-14T09:30:00.000Z']]),
|
|
41
|
+
taxonomyStateFingerprint: 'fingerprint-a'
|
|
42
|
+
});
|
|
43
|
+
const persisted = readPersistedSyncWatermarks(workspaceId, 'fingerprint-b');
|
|
44
|
+
expect(persisted).not.toBeNull();
|
|
45
|
+
expect(persisted?.taskWatermarks.size).toBe(0);
|
|
46
|
+
expect(persisted?.initiativeWatermarks.get('initiative-1')).toBe('2026-04-14T09:00:00.000Z');
|
|
47
|
+
expect(persisted?.taskEventWatermarks.get('event-1')).toBe('2026-04-14T09:30:00.000Z');
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { MutableRefObject } from 'react';
|
|
2
|
+
import type { WorkspaceSyncPhase } from '../../sync/workspaceSyncState';
|
|
3
|
+
import type { RealtimeSyncSignal } from '../useRealtimeSync';
|
|
4
|
+
interface CreateRealtimeSyncBridgeArgs {
|
|
5
|
+
cloudAuthConfigured: boolean;
|
|
6
|
+
runtimeMode: 'local' | 'cloud';
|
|
7
|
+
workspaceCloudSyncEnabled: boolean;
|
|
8
|
+
workspaceSyncPhase: WorkspaceSyncPhase;
|
|
9
|
+
workspacePullInFlightRef: MutableRefObject<boolean>;
|
|
10
|
+
realtimePullDebounceRef: MutableRefObject<number | null>;
|
|
11
|
+
realtimeSuppressedEventIdsRef: MutableRefObject<Set<string>>;
|
|
12
|
+
clearWorkspaceRetry: () => void;
|
|
13
|
+
isWorkspaceRetryPending: () => boolean;
|
|
14
|
+
pullWorkspaceChangesFromCloud: () => Promise<boolean>;
|
|
15
|
+
}
|
|
16
|
+
export declare function createRealtimeSyncBridge({ cloudAuthConfigured, runtimeMode, workspaceCloudSyncEnabled, workspaceSyncPhase, workspacePullInFlightRef, realtimePullDebounceRef, realtimeSuppressedEventIdsRef, clearWorkspaceRetry, isWorkspaceRetryPending, pullWorkspaceChangesFromCloud }: CreateRealtimeSyncBridgeArgs): {
|
|
17
|
+
handleRealtimeSignal: (signal: RealtimeSyncSignal) => void;
|
|
18
|
+
recordSuppressedEventIds: (eventIds: Iterable<string>, options?: {
|
|
19
|
+
queueRef?: MutableRefObject<string[]>;
|
|
20
|
+
maxQueueSize?: number;
|
|
21
|
+
ttlMs?: number;
|
|
22
|
+
}) => void;
|
|
23
|
+
clearRealtimePullDebounce: () => void;
|
|
24
|
+
};
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export function createRealtimeSyncBridge({ cloudAuthConfigured, runtimeMode, workspaceCloudSyncEnabled, workspaceSyncPhase, workspacePullInFlightRef, realtimePullDebounceRef, realtimeSuppressedEventIdsRef, clearWorkspaceRetry, isWorkspaceRetryPending, pullWorkspaceChangesFromCloud }) {
|
|
2
|
+
const handleRealtimeSignal = (signal) => {
|
|
3
|
+
if (!cloudAuthConfigured || runtimeMode !== 'local' || !workspaceCloudSyncEnabled)
|
|
4
|
+
return;
|
|
5
|
+
if (workspaceSyncPhase === 'provision-local')
|
|
6
|
+
return;
|
|
7
|
+
const eventId = String(signal?.eventId || '').trim();
|
|
8
|
+
if (eventId && realtimeSuppressedEventIdsRef.current.delete(eventId))
|
|
9
|
+
return;
|
|
10
|
+
if (workspacePullInFlightRef.current)
|
|
11
|
+
return;
|
|
12
|
+
if (isWorkspaceRetryPending()) {
|
|
13
|
+
clearWorkspaceRetry();
|
|
14
|
+
}
|
|
15
|
+
if (realtimePullDebounceRef.current !== null) {
|
|
16
|
+
window.clearTimeout(realtimePullDebounceRef.current);
|
|
17
|
+
}
|
|
18
|
+
realtimePullDebounceRef.current = window.setTimeout(() => {
|
|
19
|
+
realtimePullDebounceRef.current = null;
|
|
20
|
+
void pullWorkspaceChangesFromCloud();
|
|
21
|
+
}, 180);
|
|
22
|
+
};
|
|
23
|
+
const recordSuppressedEventIds = (eventIds, options) => {
|
|
24
|
+
const queueRef = options?.queueRef;
|
|
25
|
+
const maxQueueSize = Number.isFinite(options?.maxQueueSize) ? Math.max(1, Number(options?.maxQueueSize)) : 2048;
|
|
26
|
+
const ttlMs = Number.isFinite(options?.ttlMs) ? Math.max(0, Number(options?.ttlMs)) : null;
|
|
27
|
+
for (const rawEventId of eventIds) {
|
|
28
|
+
const eventId = String(rawEventId || '').trim();
|
|
29
|
+
if (!eventId || realtimeSuppressedEventIdsRef.current.has(eventId))
|
|
30
|
+
continue;
|
|
31
|
+
realtimeSuppressedEventIdsRef.current.add(eventId);
|
|
32
|
+
if (queueRef) {
|
|
33
|
+
queueRef.current.push(eventId);
|
|
34
|
+
}
|
|
35
|
+
if (ttlMs !== null) {
|
|
36
|
+
window.setTimeout(() => {
|
|
37
|
+
realtimeSuppressedEventIdsRef.current.delete(eventId);
|
|
38
|
+
}, ttlMs);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (queueRef) {
|
|
42
|
+
while (queueRef.current.length > maxQueueSize) {
|
|
43
|
+
const stale = queueRef.current.shift();
|
|
44
|
+
if (stale)
|
|
45
|
+
realtimeSuppressedEventIdsRef.current.delete(stale);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
const clearRealtimePullDebounce = () => {
|
|
50
|
+
if (realtimePullDebounceRef.current !== null) {
|
|
51
|
+
window.clearTimeout(realtimePullDebounceRef.current);
|
|
52
|
+
realtimePullDebounceRef.current = null;
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
return {
|
|
56
|
+
handleRealtimeSignal,
|
|
57
|
+
recordSuppressedEventIds,
|
|
58
|
+
clearRealtimePullDebounce
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { MutableRefObject } from 'react';
|
|
2
|
+
import type { WorkspaceSyncPhase } from '../../sync/workspaceSyncState';
|
|
3
|
+
interface CreateSyncRecoveryCoordinatorArgs {
|
|
4
|
+
currentWorkspaceId: string;
|
|
5
|
+
taxonomyStateFingerprint: string;
|
|
6
|
+
workspaceCloudSyncEnabled: boolean;
|
|
7
|
+
workspaceSyncPhase: WorkspaceSyncPhase;
|
|
8
|
+
workspaceLastPullAt: string | null;
|
|
9
|
+
lastBootstrapPhaseRef: MutableRefObject<WorkspaceSyncPhase>;
|
|
10
|
+
workspaceRepairModeRef: MutableRefObject<boolean>;
|
|
11
|
+
workspaceForceFullAiProfilePushRef: MutableRefObject<boolean>;
|
|
12
|
+
workspaceStartupRepairRanRef: MutableRefObject<boolean>;
|
|
13
|
+
workspaceStartupFullReconcileRanRef: MutableRefObject<boolean>;
|
|
14
|
+
workspaceFullReconcileInFlightRef: MutableRefObject<boolean>;
|
|
15
|
+
workspacePullCursorRef: MutableRefObject<string | null>;
|
|
16
|
+
workspaceAttachBootstrapBaselineSignatureRef: MutableRefObject<string>;
|
|
17
|
+
workspaceCaptureAttachBootstrapBaselineRef: MutableRefObject<boolean>;
|
|
18
|
+
workspacePendingSignatureRef: MutableRefObject<string>;
|
|
19
|
+
workspaceLastPushedSignatureRef: MutableRefObject<string>;
|
|
20
|
+
workspaceLastPushedWatermarksRef: MutableRefObject<Map<string, string>>;
|
|
21
|
+
workspaceLastPushedInitiativeIdsRef: MutableRefObject<Set<string>>;
|
|
22
|
+
workspaceLastPushedInitiativeWatermarksRef: MutableRefObject<Map<string, string>>;
|
|
23
|
+
workspaceLastPushedWorkstreamIdsRef: MutableRefObject<Set<string>>;
|
|
24
|
+
workspaceLastPushedWorkstreamWatermarksRef: MutableRefObject<Map<string, string>>;
|
|
25
|
+
workspaceLastPushedAiProfileIdsRef: MutableRefObject<Set<string>>;
|
|
26
|
+
workspaceLastPushedAiProfileWatermarksRef: MutableRefObject<Map<string, string>>;
|
|
27
|
+
workspaceLastPushedDocumentPathsRef: MutableRefObject<Set<string>>;
|
|
28
|
+
workspaceLastPushedDocumentWatermarksRef: MutableRefObject<Map<string, string>>;
|
|
29
|
+
workspaceLastPushedAssetPathsRef: MutableRefObject<Set<string>>;
|
|
30
|
+
workspaceLastPushedAssetWatermarksRef: MutableRefObject<Map<string, string>>;
|
|
31
|
+
workspaceLastPushedDocumentReviewSessionIdsRef: MutableRefObject<Set<string>>;
|
|
32
|
+
workspaceLastPushedDocumentReviewSessionWatermarksRef: MutableRefObject<Map<string, string>>;
|
|
33
|
+
workspaceLastPushedDocumentReviewCommentIdsRef: MutableRefObject<Set<string>>;
|
|
34
|
+
workspaceLastPushedDocumentReviewCommentWatermarksRef: MutableRefObject<Map<string, string>>;
|
|
35
|
+
workspaceLastPushedAnnotatedAttachmentSessionIdsRef: MutableRefObject<Set<string>>;
|
|
36
|
+
workspaceLastPushedAnnotatedAttachmentSessionWatermarksRef: MutableRefObject<Map<string, string>>;
|
|
37
|
+
workspaceLastPushedTaskEventWatermarksRef: MutableRefObject<Map<string, string>>;
|
|
38
|
+
bootstrapPhaseStartedAtRef: MutableRefObject<number | null>;
|
|
39
|
+
bootstrapLastProgressAtRef: MutableRefObject<number | null>;
|
|
40
|
+
acquireWorkspaceSyncLease: (workspaceId: string, owner: 'pull' | 'push' | 'repair') => Promise<boolean>;
|
|
41
|
+
releaseWorkspaceSyncLease: (workspaceId: string) => void;
|
|
42
|
+
forceClearWorkspaceSyncLease: (workspaceId: string) => void;
|
|
43
|
+
clearWorkspaceRetry: () => void;
|
|
44
|
+
clearWorkspaceIssue: () => void;
|
|
45
|
+
reportWorkspaceWindowContention: (operation: 'pull' | 'push' | 'repair') => void;
|
|
46
|
+
persistWorkspaceSyncPatch: (patch: {
|
|
47
|
+
phase: WorkspaceSyncPhase;
|
|
48
|
+
pullCursor: null;
|
|
49
|
+
lastErrorMessage: null;
|
|
50
|
+
}) => Promise<unknown>;
|
|
51
|
+
buildWorkspaceSyncSignature: () => string;
|
|
52
|
+
pullWorkspaceChangesFromCloud: (options?: {
|
|
53
|
+
repairMode?: boolean;
|
|
54
|
+
forceCursorNull?: boolean;
|
|
55
|
+
}) => Promise<boolean>;
|
|
56
|
+
pushWorkspaceChangesToCloud: (signature: string, options?: {
|
|
57
|
+
forceAllAiProfiles?: boolean;
|
|
58
|
+
repairMode?: boolean;
|
|
59
|
+
}) => Promise<boolean>;
|
|
60
|
+
cloudAuthConfigured: boolean;
|
|
61
|
+
runtimeMode: 'local' | 'cloud';
|
|
62
|
+
isAuthenticated: boolean;
|
|
63
|
+
checkAuthSession: () => Promise<boolean>;
|
|
64
|
+
}
|
|
65
|
+
export declare function createSyncRecoveryCoordinator({ currentWorkspaceId, taxonomyStateFingerprint, workspaceCloudSyncEnabled, workspaceSyncPhase, workspaceLastPullAt, lastBootstrapPhaseRef, workspaceRepairModeRef, workspaceForceFullAiProfilePushRef, workspaceStartupRepairRanRef, workspaceStartupFullReconcileRanRef, workspaceFullReconcileInFlightRef, workspacePullCursorRef, workspaceAttachBootstrapBaselineSignatureRef, workspaceCaptureAttachBootstrapBaselineRef, workspacePendingSignatureRef, workspaceLastPushedSignatureRef, workspaceLastPushedWatermarksRef, workspaceLastPushedInitiativeIdsRef, workspaceLastPushedInitiativeWatermarksRef, workspaceLastPushedWorkstreamIdsRef, workspaceLastPushedWorkstreamWatermarksRef, workspaceLastPushedAiProfileIdsRef, workspaceLastPushedAiProfileWatermarksRef, workspaceLastPushedDocumentPathsRef, workspaceLastPushedDocumentWatermarksRef, workspaceLastPushedAssetPathsRef, workspaceLastPushedAssetWatermarksRef, workspaceLastPushedDocumentReviewSessionIdsRef, workspaceLastPushedDocumentReviewSessionWatermarksRef, workspaceLastPushedDocumentReviewCommentIdsRef, workspaceLastPushedDocumentReviewCommentWatermarksRef, workspaceLastPushedAnnotatedAttachmentSessionIdsRef, workspaceLastPushedAnnotatedAttachmentSessionWatermarksRef, workspaceLastPushedTaskEventWatermarksRef, bootstrapPhaseStartedAtRef, bootstrapLastProgressAtRef, acquireWorkspaceSyncLease, releaseWorkspaceSyncLease, forceClearWorkspaceSyncLease, clearWorkspaceRetry, clearWorkspaceIssue, reportWorkspaceWindowContention, persistWorkspaceSyncPatch, buildWorkspaceSyncSignature, pullWorkspaceChangesFromCloud, pushWorkspaceChangesToCloud, cloudAuthConfigured, runtimeMode, isAuthenticated, checkAuthSession }: CreateSyncRecoveryCoordinatorArgs): {
|
|
66
|
+
retryWorkspaceCloudSync: () => Promise<boolean>;
|
|
67
|
+
resetWorkspaceSyncCursorAndPull: () => Promise<void>;
|
|
68
|
+
};
|
|
69
|
+
export {};
|