@ai-matrx/media 0.10.1 → 0.10.2
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/files/engine/cache/register-service-worker.js +1 -1
- package/dist/files/engine/cache/register-service-worker.js.map +1 -1
- package/dist/files/engine/redux/thunks.js +1 -1
- package/dist/files/engine/redux/thunks.js.map +1 -1
- package/dist/files/engine/support/logger.js +1 -1
- package/dist/files/engine/support/logger.js.map +1 -1
- package/dist/files/engine/upload/tusUpload.js +1 -1
- package/dist/files/engine/upload/tusUpload.js.map +1 -1
- package/dist/files/engine/upload/uploadGuardOpeners.js +1 -1
- package/dist/files/engine/upload/uploadGuardOpeners.js.map +1 -1
- package/package.json +4 -4
|
@@ -9,7 +9,7 @@ function registerBlobCacheServiceWorker(args) {
|
|
|
9
9
|
if (typeof window === "undefined" || !("serviceWorker" in navigator)) {
|
|
10
10
|
return null;
|
|
11
11
|
}
|
|
12
|
-
const isProd =
|
|
12
|
+
const isProd = process.env.NODE_ENV === "production";
|
|
13
13
|
const devOptIn = typeof window !== "undefined" && window.localStorage?.getItem("matrx_dev_sw") === "1";
|
|
14
14
|
if (!isProd && !devOptIn && !args.force) {
|
|
15
15
|
return null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/files/engine/cache/register-service-worker.ts"],"sourcesContent":["/**\n * features/files/cache/register-service-worker.ts\n *\n * Register the blob-cache Service Worker (`/blob-sw.js`) and push the\n * current backend URL + signed-in user id into it. The SW intercepts\n * cloud-files byte fetches (and registered CDN / share-link URLs) and\n * serves them from IndexedDB when present, falling through to the\n * network otherwise.\n *\n * Called from `app/DeferredSingletons.tsx` (a Client Component) on idle\n * after auth boot, so SSR is unaffected and registration doesn't block\n * the first paint.\n *\n * Dev-mode guards:\n * - Disabled by default in development (`NODE_ENV !== 'production'`)\n * to avoid HMR interference with Next's RSC cache.\n * - Opt-in via `localStorage.matrx_dev_sw = '1'` for local testing.\n *\n * iOS Safari / private-browsing failure modes are tolerated silently —\n * if `navigator.serviceWorker` is undefined or `register()` rejects,\n * the app continues to function with just the in-memory LRU + IDB tiers\n * (no transparent `<img src>` interception).\n */\n\nconst SW_URL = \"/blob-sw.js\";\n\nlet registrationPromise: Promise<ServiceWorkerRegistration | null> | null = null;\n\n/**\n * Register the SW and (when ready) send the initial `set-config` message\n * with the backend URL and signed-in user id. Returns the registration\n * (or null when SW isn't supported / registration failed).\n *\n * Idempotent — subsequent calls reuse the in-flight promise.\n */\nexport function registerBlobCacheServiceWorker(args: {\n backendUrl: string;\n userId: string | null;\n /** When true, force-register even in dev. */\n force?: boolean;\n}): Promise<ServiceWorkerRegistration | null> {\n if (registrationPromise) {\n void postSetConfig(args).catch(() => undefined);\n return registrationPromise;\n }\n registrationPromise = (async () => {\n if (typeof window === \"undefined\" || !(\"serviceWorker\" in navigator)) {\n return null;\n }\n const isProd = process.env.NODE_ENV === \"production\";\n const devOptIn =\n typeof window !== \"undefined\" &&\n window.localStorage?.getItem(\"matrx_dev_sw\") === \"1\";\n if (!isProd && !devOptIn && !args.force) {\n return null;\n }\n try {\n const reg = await navigator.serviceWorker.register(SW_URL, {\n scope: \"/\",\n });\n // Push config as soon as we have a controller — covers fresh\n // registrations (where the controller may not be set yet) by also\n // listening on `controllerchange`.\n void postSetConfig(args);\n navigator.serviceWorker.addEventListener(\"controllerchange\", () => {\n void postSetConfig(args);\n });\n return reg;\n } catch (err) {\n // eslint-disable-next-line no-console\n console.warn(\"[blob-cache-sw] registration failed\", err);\n return null;\n }\n })();\n return registrationPromise;\n}\n\nasync function postSetConfig(args: {\n backendUrl: string;\n userId: string | null;\n}): Promise<void> {\n const target =\n navigator.serviceWorker.controller ??\n (await navigator.serviceWorker.ready.then((r) => r.active));\n if (!target) return;\n // NEXT_PUBLIC_FILES_URL: when set, file/share downloads go to the standalone\n // matrx-files service; tell the SW so it caches those the same as backend\n // downloads. Read from env here so no call site needs to thread it through.\n const filesUrl = (process.env.NEXT_PUBLIC_FILES_URL as string | undefined) || undefined;\n target.postMessage({\n kind: \"set-config\",\n backendUrl: args.backendUrl.replace(/\\/$/, \"\"),\n filesUrl: filesUrl ? filesUrl.replace(/\\/$/, \"\") : undefined,\n userId: args.userId,\n });\n}\n\n/**\n * Imperative invalidation. Drops every cached version of a fileId in the\n * SW's IDB tier. Called from invalidate() in features/files/hooks/blob-cache.ts.\n */\nexport async function postBlobCacheInvalidate(fileId: string): Promise<void> {\n if (typeof navigator === \"undefined\" || !(\"serviceWorker\" in navigator)) {\n return;\n }\n const ctrl = navigator.serviceWorker.controller;\n if (!ctrl) return;\n ctrl.postMessage({ kind: \"invalidate\", fileId });\n}\n\n/**\n * Per-user wipe. Called from sign-out.\n */\nexport async function postBlobCacheClearUser(userId: string): Promise<void> {\n if (typeof navigator === \"undefined\" || !(\"serviceWorker\" in navigator)) {\n return;\n }\n const ctrl = navigator.serviceWorker.controller;\n if (!ctrl) return;\n ctrl.postMessage({ kind: \"clear-user\", userId });\n}\n\n/**\n * Register a (canonical-URL → fileId+version+checksum) mapping so the SW\n * can recognize a public CDN URL or other byte URL the page has issued\n * and serve it from the same cache entry as the canonical\n * `/files/{id}/download` URL.\n */\nexport async function postBlobCacheRegisterUrlMapping(args: {\n url: string;\n fileId: string;\n version: number | null;\n checksum: string | null;\n}): Promise<void> {\n if (typeof navigator === \"undefined\" || !(\"serviceWorker\" in navigator)) {\n return;\n }\n const ctrl = navigator.serviceWorker.controller;\n if (!ctrl) return;\n ctrl.postMessage({\n kind: \"register-url-mapping\",\n url: args.url,\n fileId: args.fileId,\n version: args.version,\n checksum: args.checksum,\n });\n}\n"],"mappings":"AAwBA,MAAM,SAAS;AAEf,IAAI,sBAAwE;AASrE,SAAS,+BAA+B,MAKD;AAC5C,MAAI,qBAAqB;AACvB,SAAK,cAAc,IAAI,EAAE,MAAM,MAAM,MAAS;AAC9C,WAAO;AAAA,EACT;AACA,yBAAuB,YAAY;AACjC,QAAI,OAAO,WAAW,eAAe,EAAE,mBAAmB,YAAY;AACpE,aAAO;AAAA,IACT;AACA,UAAM,SAAS;
|
|
1
|
+
{"version":3,"sources":["../../../../src/files/engine/cache/register-service-worker.ts"],"sourcesContent":["/**\n * features/files/cache/register-service-worker.ts\n *\n * Register the blob-cache Service Worker (`/blob-sw.js`) and push the\n * current backend URL + signed-in user id into it. The SW intercepts\n * cloud-files byte fetches (and registered CDN / share-link URLs) and\n * serves them from IndexedDB when present, falling through to the\n * network otherwise.\n *\n * Called from `app/DeferredSingletons.tsx` (a Client Component) on idle\n * after auth boot, so SSR is unaffected and registration doesn't block\n * the first paint.\n *\n * Dev-mode guards:\n * - Disabled by default in development (`NODE_ENV !== 'production'`)\n * to avoid HMR interference with Next's RSC cache.\n * - Opt-in via `localStorage.matrx_dev_sw = '1'` for local testing.\n *\n * iOS Safari / private-browsing failure modes are tolerated silently —\n * if `navigator.serviceWorker` is undefined or `register()` rejects,\n * the app continues to function with just the in-memory LRU + IDB tiers\n * (no transparent `<img src>` interception).\n */\n\nconst SW_URL = \"/blob-sw.js\";\n\nlet registrationPromise: Promise<ServiceWorkerRegistration | null> | null = null;\n\n/**\n * Register the SW and (when ready) send the initial `set-config` message\n * with the backend URL and signed-in user id. Returns the registration\n * (or null when SW isn't supported / registration failed).\n *\n * Idempotent — subsequent calls reuse the in-flight promise.\n */\nexport function registerBlobCacheServiceWorker(args: {\n backendUrl: string;\n userId: string | null;\n /** When true, force-register even in dev. */\n force?: boolean;\n}): Promise<ServiceWorkerRegistration | null> {\n if (registrationPromise) {\n void postSetConfig(args).catch(() => undefined);\n return registrationPromise;\n }\n registrationPromise = (async () => {\n if (typeof window === \"undefined\" || !(\"serviceWorker\" in navigator)) {\n return null;\n }\n const isProd = process.env.NODE_ENV === \"production\";\n const devOptIn =\n typeof window !== \"undefined\" &&\n window.localStorage?.getItem(\"matrx_dev_sw\") === \"1\";\n if (!isProd && !devOptIn && !args.force) {\n return null;\n }\n try {\n const reg = await navigator.serviceWorker.register(SW_URL, {\n scope: \"/\",\n });\n // Push config as soon as we have a controller — covers fresh\n // registrations (where the controller may not be set yet) by also\n // listening on `controllerchange`.\n void postSetConfig(args);\n navigator.serviceWorker.addEventListener(\"controllerchange\", () => {\n void postSetConfig(args);\n });\n return reg;\n } catch (err) {\n // eslint-disable-next-line no-console\n console.warn(\"[blob-cache-sw] registration failed\", err);\n return null;\n }\n })();\n return registrationPromise;\n}\n\nasync function postSetConfig(args: {\n backendUrl: string;\n userId: string | null;\n}): Promise<void> {\n const target =\n navigator.serviceWorker.controller ??\n (await navigator.serviceWorker.ready.then((r) => r.active));\n if (!target) return;\n // NEXT_PUBLIC_FILES_URL: when set, file/share downloads go to the standalone\n // matrx-files service; tell the SW so it caches those the same as backend\n // downloads. Read from env here so no call site needs to thread it through.\n const filesUrl = (process.env.NEXT_PUBLIC_FILES_URL as string | undefined) || undefined;\n target.postMessage({\n kind: \"set-config\",\n backendUrl: args.backendUrl.replace(/\\/$/, \"\"),\n filesUrl: filesUrl ? filesUrl.replace(/\\/$/, \"\") : undefined,\n userId: args.userId,\n });\n}\n\n/**\n * Imperative invalidation. Drops every cached version of a fileId in the\n * SW's IDB tier. Called from invalidate() in features/files/hooks/blob-cache.ts.\n */\nexport async function postBlobCacheInvalidate(fileId: string): Promise<void> {\n if (typeof navigator === \"undefined\" || !(\"serviceWorker\" in navigator)) {\n return;\n }\n const ctrl = navigator.serviceWorker.controller;\n if (!ctrl) return;\n ctrl.postMessage({ kind: \"invalidate\", fileId });\n}\n\n/**\n * Per-user wipe. Called from sign-out.\n */\nexport async function postBlobCacheClearUser(userId: string): Promise<void> {\n if (typeof navigator === \"undefined\" || !(\"serviceWorker\" in navigator)) {\n return;\n }\n const ctrl = navigator.serviceWorker.controller;\n if (!ctrl) return;\n ctrl.postMessage({ kind: \"clear-user\", userId });\n}\n\n/**\n * Register a (canonical-URL → fileId+version+checksum) mapping so the SW\n * can recognize a public CDN URL or other byte URL the page has issued\n * and serve it from the same cache entry as the canonical\n * `/files/{id}/download` URL.\n */\nexport async function postBlobCacheRegisterUrlMapping(args: {\n url: string;\n fileId: string;\n version: number | null;\n checksum: string | null;\n}): Promise<void> {\n if (typeof navigator === \"undefined\" || !(\"serviceWorker\" in navigator)) {\n return;\n }\n const ctrl = navigator.serviceWorker.controller;\n if (!ctrl) return;\n ctrl.postMessage({\n kind: \"register-url-mapping\",\n url: args.url,\n fileId: args.fileId,\n version: args.version,\n checksum: args.checksum,\n });\n}\n"],"mappings":"AAwBA,MAAM,SAAS;AAEf,IAAI,sBAAwE;AASrE,SAAS,+BAA+B,MAKD;AAC5C,MAAI,qBAAqB;AACvB,SAAK,cAAc,IAAI,EAAE,MAAM,MAAM,MAAS;AAC9C,WAAO;AAAA,EACT;AACA,yBAAuB,YAAY;AACjC,QAAI,OAAO,WAAW,eAAe,EAAE,mBAAmB,YAAY;AACpE,aAAO;AAAA,IACT;AACA,UAAM,SAAS,yBAAyB;AACxC,UAAM,WACJ,OAAO,WAAW,eAClB,OAAO,cAAc,QAAQ,cAAc,MAAM;AACnD,QAAI,CAAC,UAAU,CAAC,YAAY,CAAC,KAAK,OAAO;AACvC,aAAO;AAAA,IACT;AACA,QAAI;AACF,YAAM,MAAM,MAAM,UAAU,cAAc,SAAS,QAAQ;AAAA,QACzD,OAAO;AAAA,MACT,CAAC;AAID,WAAK,cAAc,IAAI;AACvB,gBAAU,cAAc,iBAAiB,oBAAoB,MAAM;AACjE,aAAK,cAAc,IAAI;AAAA,MACzB,CAAC;AACD,aAAO;AAAA,IACT,SAAS,KAAK;AAEZ,cAAQ,KAAK,uCAAuC,GAAG;AACvD,aAAO;AAAA,IACT;AAAA,EACF,GAAG;AACH,SAAO;AACT;AAEA,eAAe,cAAc,MAGX;AAChB,QAAM,SACJ,UAAU,cAAc,cACvB,MAAM,UAAU,cAAc,MAAM,KAAK,CAAC,MAAM,EAAE,MAAM;AAC3D,MAAI,CAAC,OAAQ;AAIb,QAAM,WAAY,QAAQ,IAAI,yBAAgD;AAC9E,SAAO,YAAY;AAAA,IACjB,MAAM;AAAA,IACN,YAAY,KAAK,WAAW,QAAQ,OAAO,EAAE;AAAA,IAC7C,UAAU,WAAW,SAAS,QAAQ,OAAO,EAAE,IAAI;AAAA,IACnD,QAAQ,KAAK;AAAA,EACf,CAAC;AACH;AAMA,eAAsB,wBAAwB,QAA+B;AAC3E,MAAI,OAAO,cAAc,eAAe,EAAE,mBAAmB,YAAY;AACvE;AAAA,EACF;AACA,QAAM,OAAO,UAAU,cAAc;AACrC,MAAI,CAAC,KAAM;AACX,OAAK,YAAY,EAAE,MAAM,cAAc,OAAO,CAAC;AACjD;AAKA,eAAsB,uBAAuB,QAA+B;AAC1E,MAAI,OAAO,cAAc,eAAe,EAAE,mBAAmB,YAAY;AACvE;AAAA,EACF;AACA,QAAM,OAAO,UAAU,cAAc;AACrC,MAAI,CAAC,KAAM;AACX,OAAK,YAAY,EAAE,MAAM,cAAc,OAAO,CAAC;AACjD;AAQA,eAAsB,gCAAgC,MAKpC;AAChB,MAAI,OAAO,cAAc,eAAe,EAAE,mBAAmB,YAAY;AACvE;AAAA,EACF;AACA,QAAM,OAAO,UAAU,cAAc;AACrC,MAAI,CAAC,KAAM;AACX,OAAK,YAAY;AAAA,IACf,MAAM;AAAA,IACN,KAAK,KAAK;AAAA,IACV,QAAQ,KAAK;AAAA,IACb,SAAS,KAAK;AAAA,IACd,UAAU,KAAK;AAAA,EACjB,CAAC;AACH;","names":[]}
|
|
@@ -443,7 +443,7 @@ const createFolder = createAsyncThunk("cloudFiles/createFolder", async (arg, { d
|
|
|
443
443
|
);
|
|
444
444
|
return folder.id;
|
|
445
445
|
} catch (err) {
|
|
446
|
-
if (
|
|
446
|
+
if (process.env.NODE_ENV !== "production") {
|
|
447
447
|
const e = err;
|
|
448
448
|
console.error("[createFolder] backend error", {
|
|
449
449
|
status: e.status ?? null,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/files/engine/redux/thunks.ts"],"sourcesContent":["/**\n * features/files/redux/thunks.ts\n *\n * Async orchestration for cloud files: reads via supabase-js (RLS), writes via\n * the REST API client. Every mutation follows the pattern:\n *\n * 1. Snapshot current state for rollback.\n * 2. Dispatch optimistic reducer immediately.\n * 3. Register the requestId in the ledger (for realtime echo dedup).\n * 4. Call the REST API.\n * 5. On success: markSaved, release ledger entry.\n * 6. On error: dispatch rollback, set error, release ledger entry, rethrow.\n */\n\n\"use client\";\n\nimport { createAsyncThunk } from \"@reduxjs/toolkit\";\nimport type { ThunkDispatch, UnknownAction } from \"@reduxjs/toolkit\";\nimport type { CloudFilesState } from \"../types\";\n\n// Minimal local types — avoids importing from store.ts (which imports this\n// module via rootReducer → middleware chain), breaking the type-level cycle.\ntype StateWithCloudFiles = { cloudFiles: CloudFilesState };\ntype AppDispatch = ThunkDispatch<StateWithCloudFiles, unknown, UnknownAction>;\nimport { supabase } from \"../host/supabase\";\nimport { filesDb, FILE_VERSIONS_TABLE_COLUMNS } from \"../filesDb\";\nimport { pgErrorToError } from \"@ai-matrx/data\";\n\nimport * as Files from \"../api/files\";\nimport {\n claimUpload,\n uploadDedupKey,\n type UploadClaim,\n} from \"../upload/uploadDedupGuard\";\nimport * as Folders from \"../api/folders\";\nimport * as Permissions from \"../api/permissions\";\nimport * as Versions from \"../api/versions\";\nimport {\n createShareLink as createCanonicalShareLink,\n listShareLinks as listCanonicalShareLinks,\n revokeShareLink as revokeCanonicalShareLink,\n} from \"../host/share-links\";\nimport {\n restoreFileDirect,\n restoreFolderDirect,\n softDeleteFileDirect,\n softDeleteFolderDirect,\n} from \"../api/direct\";\nimport { fileHandler } from \"../handler/handler\";\nimport { newRequestId } from \"../host/python-client\";\nimport { extractErrorMessage } from \"@ai-matrx/data/net\";\nimport {\n apiFileRecordToCloudFile,\n dbRowToCloudFilePermission,\n dbRowToCloudFileVersion,\n dbRowToCloudFolder,\n canonicalShareLinkToCloudShareLink,\n parseCloudTreeRows,\n rpcPermissionRowToCloudFilePermission,\n} from \"./converters\";\nimport {\n areCloudFileFieldsLoaded,\n FILE_DB_RECORD_FIELDS,\n fileHintToCloudFilePartial,\n needsOnlyRenderFields,\n type CloudFileHydrationField,\n} from \"./file-hydration\";\nimport {\n registerRequest,\n releaseRequest,\n beginResourceOp,\n isLatestResourceOp,\n hasInFlight,\n} from \"./request-ledger\";\nimport { toast } from \"../host/toast\";\nimport { buildTreeState } from \"./tree-utils\";\nimport {\n isUserVisibleFilePath,\n isUserVisibleFolderPath,\n} from \"../utils/user-visible\";\nimport {\n hasMatchingFileTreeSession,\n runFileTreeSessionOperation,\n} from \"./file-tree-auth-boundary\";\nimport {\n isFileTreeLoadDeferred,\n runWithFileTreeLoadTimeout,\n} from \"./file-tree-timeout\";\nimport {\n invalidate as invalidateBlobCache,\n replaceWithSavedBytes,\n} from \"../hooks/blob-cache\";\nimport { invalidateOfficeExtraction } from \"../hooks/office-extraction-cache\";\nimport {\n addFilePendingRequest,\n attachChildToFolder,\n clearUpload,\n detachChildFromFolder,\n markFileSaved,\n markFolderFullyLoaded,\n removeFile,\n removeFilePendingRequest,\n removeFolder,\n removePermissionForResource,\n removeShareLink,\n replaceTree,\n rollbackFileOptimisticUpdate,\n setFileError,\n setFileField,\n setFileLoading,\n setTreeStatus,\n trackUploadStart,\n updateUploadProgress,\n updateUploadStatus,\n upsertFile,\n upsertFiles,\n upsertFolder,\n upsertFolders,\n upsertPermissionsForResource,\n upsertShareLinksForResource,\n upsertVersionsForFile,\n} from \"./slice\";\nimport { getFileFromState } from \"./selectors\";\n\nimport type {\n BulkDeleteFilesArg,\n BulkMoveFilesArg,\n BulkMoveFoldersArg,\n BulkResponse,\n CloudFile,\n CloudFileFieldSnapshot,\n CloudFileRecord,\n CloudFolderRecord,\n CloudFilePermission,\n CloudFileVersion,\n CloudFolder,\n CloudShareLink,\n CreateShareLinkArg,\n RevokeShareLinkArg,\n DeleteFileArg,\n GranteeType,\n GrantPermissionArg,\n MoveFileArg,\n RenameFileArg,\n ResourceType,\n RestoreVersionArg,\n SaveFileNewVersionArg,\n SaveFileNewVersionResult,\n RevokePermissionArg,\n UpdateFileMetadataArg,\n UploadFilesArg,\n FileIdentityHint,\n} from \"../types\";\n\nimport { getClaimsUser } from \"../host/supabase\";\ntype ThunkApi = { dispatch: AppDispatch; state: StateWithCloudFiles };\n\n// ---------------------------------------------------------------------------\n// Reads\n// ---------------------------------------------------------------------------\n\nexport interface EnsureCloudFileFieldsArg {\n fileId: string;\n fields?: readonly CloudFileHydrationField[];\n hint?: FileIdentityHint;\n}\n\nexport type EnsureCloudFileFieldsOutcome = \"ready\" | \"missing\";\n\nconst fileFieldHydrationInFlight = new Map<\n string,\n Promise<(Partial<CloudFile> & Pick<CloudFile, \"id\">) | null>\n>();\n\nasync function fetchCloudFileFields(\n fileId: string,\n fields: readonly CloudFileHydrationField[],\n): Promise<(Partial<CloudFile> & Pick<CloudFile, \"id\">) | null> {\n const key = `${fileId}:${[...fields].sort().join(\",\")}`;\n const existing = fileFieldHydrationInFlight.get(key);\n if (existing) return existing;\n\n const request = (async () => {\n // A direct PostgREST exact-id read still compiles the entire files.files\n // RLS access expression. On a PDF surface many file chips hydrate at\n // once, so planning alone can cross its statement deadline before the\n // primary-key lookup begins. The server's `/files/{id}` boundary checks\n // the same canonical access rule once and reads by primary key without\n // browser RLS planning. Keep its URL/thumbnail work off this metadata\n // path; consumers that need it use the regular getFile endpoint.\n const { data } = await Files.getFileMetadata(fileId);\n const record = apiFileRecordToCloudFile(data);\n if (record.deletedAt) return null;\n\n if (needsOnlyRenderFields(fields)) {\n return {\n id: record.id,\n fileName: record.fileName,\n mimeType: record.mimeType,\n fileSize: record.fileSize,\n visibility: record.visibility,\n };\n }\n return record;\n })().finally(() => fileFieldHydrationInFlight.delete(key));\n\n fileFieldHydrationInFlight.set(key, request);\n return request;\n}\n\n/**\n * Ensure a durable file id has the requested canonical fields in Redux.\n * Known hints merge first, so the network reads only what is actually absent.\n */\nexport const ensureCloudFileFields = createAsyncThunk<\n EnsureCloudFileFieldsOutcome,\n EnsureCloudFileFieldsArg,\n ThunkApi\n>(\n \"cloudFiles/ensureCloudFileFields\",\n async (\n { fileId, fields = FILE_DB_RECORD_FIELDS, hint },\n { dispatch, getState },\n ) => {\n dispatch(upsertFile(fileHintToCloudFilePartial(fileId, hint)));\n\n if (\n areCloudFileFieldsLoaded(getFileFromState(getState(), fileId), fields)\n ) {\n return \"ready\";\n }\n\n dispatch(setFileLoading({ id: fileId, loading: true }));\n dispatch(setFileError({ id: fileId, error: null }));\n try {\n const partial = await fetchCloudFileFields(fileId, fields);\n if (!partial) return \"missing\";\n dispatch(upsertFile(partial));\n return \"ready\";\n } catch (error) {\n const normalized = pgErrorToError(error);\n dispatch(setFileError({ id: fileId, error: normalized.message }));\n throw normalized;\n } finally {\n dispatch(setFileLoading({ id: fileId, loading: false }));\n }\n },\n);\n\n/**\n * Loads the full tree for the current user via the get_user_file_tree\n * RPC. Normalizes into filesById / foldersById / tree.\n */\n// `get_user_file_tree` server-side cap (`p_limit := LEAST(GREATEST(p_limit, 1), 5000);`).\n// Passing anything > 5000 silently clamps. The FE pages via `p_offset`\n// to recover the full tree for power users (users with >5k files).\nconst TREE_PAGE_SIZE = 5000;\n\n// Sanity cap on the pagination loop — prevents an infinite loop if the\n// RPC ever returns a full page on the page that should be the last.\n// 20 * 5000 = 100,000 rows, well above any realistic user.\nconst TREE_MAX_PAGES = 20;\n\n/**\n * Shared promise for an in-progress full-tree load, so concurrent mounts\n * collapse onto one `get_user_file_tree` RPC. Session-local, not Redux state.\n */\nlet _treeInFlight: Promise<void> | null = null;\nlet _treeInFlightUserId: string | null = null;\n\nexport const loadUserFileTree = createAsyncThunk<\n void,\n { userId: string },\n ThunkApi\n>(\"cloudFiles/loadUserFileTree\", async ({ userId }, { dispatch, getState }) => {\n // Redux is server-seeded before client effects run, so during signed-out\n // hydration or session teardown it can briefly retain a user UUID while\n // supabase-js is already anonymous. Isolate that race at this canonical\n // producer boundary instead of sending an authenticated-only RPC as anon.\n if (!(await hasMatchingFileTreeSession(userId))) return;\n\n // In-flight dedupe. Several surfaces hydrate the tree on mount\n // (CloudFilesRealtimeProvider + every useCloudTree consumer), and their\n // effects all run in the same commit, so a status check in the component\n // can't see a sibling's fetch. Sharing the promise collapses them to one\n // RPC while keeping `.unwrap()` working for every caller (unlike a\n // `condition` guard, which rejects the losers).\n if (_treeInFlight && _treeInFlightUserId === userId) return _treeInFlight;\n\n const run = (async () => {\n dispatch(setTreeStatus({ status: \"loading\" }));\n\n // RPC contract (migration 014, 2026-05-17): identity-locked to\n // `auth.uid()`, returns owner OR explicit-grant rows only (no public\n // leak), excludes `parent_file_id IS NOT NULL` + `system-files/%`\n // paths. So we can consume the response raw — no FE-side ownership\n // or system-path filtering needed. See common-docs systems/files/file-service/WIRE_CONTRACT.md\n // (2026-05-17 \"Phase 1d.5\" entry).\n //\n // Pagination: server caps p_limit at 5000. We loop on `p_offset`\n // until a partial page comes back. Sequential pages — Postgres\n // handles them fast and parallelism would just contend for the\n // same connection.\n try {\n // Bounded per attempt; a timeout in a hidden tab waits for the person to\n // return and retries instead of failing (see file-tree-timeout.ts).\n const rows = await runWithFileTreeLoadTimeout(async (signal) => {\n const rows: ReturnType<typeof parseCloudTreeRows> = [];\n for (let page = 0; page < TREE_MAX_PAGES; page += 1) {\n const { data, error } = await runFileTreeSessionOperation(() =>\n supabase\n .rpc(\"get_user_file_tree\", {\n p_user_id: userId,\n p_limit: TREE_PAGE_SIZE,\n p_offset: page * TREE_PAGE_SIZE,\n p_include_folders: true,\n p_include_deleted: false,\n })\n .abortSignal(signal),\n );\n\n if (error) {\n // An aborted page is the timeout's call, not a server failure:\n // the runner decides (retry on return, or TimeoutError).\n if (!signal.aborted) {\n dispatch(\n setTreeStatus({ status: \"error\", error: error.message }),\n );\n }\n throw error;\n }\n\n const pageRows = parseCloudTreeRows(data);\n rows.push(...pageRows);\n if (pageRows.length < TREE_PAGE_SIZE) break;\n }\n return rows;\n });\n\n // Every field pushed below is always set from the non-optional\n // `CloudTreeRow` columns — narrowed here so the tree-spine reconstruction\n // (below) doesn't need a non-null assertion on `f.id` or a nullish-default\n // fallback on `f.ownerId`, which would otherwise silently paper over a\n // real parse gap.\n type PartialCloudFileWithId = Partial<CloudFile> &\n Pick<\n CloudFile,\n | \"id\"\n | \"ownerId\"\n | \"filePath\"\n | \"fileName\"\n | \"visibility\"\n | \"currentVersion\"\n | \"createdAt\"\n | \"updatedAt\"\n | \"deletedAt\"\n >;\n type PartialCloudFolderWithId = Partial<CloudFolder> &\n Pick<\n CloudFolder,\n | \"id\"\n | \"ownerId\"\n | \"folderPath\"\n | \"folderName\"\n | \"visibility\"\n | \"createdAt\"\n | \"updatedAt\"\n | \"deletedAt\"\n >;\n const files: PartialCloudFileWithId[] = [];\n const folders: PartialCloudFolderWithId[] = [];\n for (const row of rows) {\n if (row.kind === \"file\") {\n // No client-side visibility filter: `get_user_file_tree` already\n // applied `files.is_user_visible_path`. A second rule here is the\n // D16 disagreement this cutover deleted.\n files.push({\n id: row.id,\n ownerId: row.owner_id,\n filePath: row.file_path,\n fileName: row.file_name,\n parentFolderId: row.parent_folder_id,\n mimeType: row.mime_type,\n // Phase 0 rename: `file_size` → `size_bytes`. The\n // `CloudTreeFileRow` shape and Supabase row both carry the new\n // name now. See docs/PYTHON_UPDATES.md §3.\n fileSize: row.size_bytes,\n visibility: row.visibility,\n currentVersion: row.current_version,\n createdAt: row.created_at,\n updatedAt: row.updated_at,\n deletedAt: row.deleted_at,\n originDeviceId: row.origin_device_id,\n metadata: row.metadata,\n });\n } else {\n folders.push({\n id: row.id,\n ownerId: row.owner_id,\n folderPath: row.folder_path,\n folderName: row.folder_name,\n parentId: row.parent_id,\n visibility: row.visibility,\n createdAt: row.created_at,\n updatedAt: row.updated_at,\n deletedAt: row.deleted_at,\n metadata: row.metadata,\n });\n }\n }\n\n dispatch(upsertFiles(files));\n dispatch(upsertFolders(folders));\n\n // Drop rows the DATABASE's predicate rejects that slipped into the\n // store through realtime before the mirror was applied. FastFire rows\n // are user-visible now and are deliberately NOT purged.\n const prior = getState().cloudFiles;\n for (const [id, rec] of Object.entries(prior.filesById)) {\n if (rec && !isUserVisibleFilePath(rec.filePath)) {\n dispatch(removeFile({ id }));\n }\n }\n for (const [id, rec] of Object.entries(prior.foldersById)) {\n if (\n rec &&\n rec.source.kind === \"real\" &&\n !isUserVisibleFolderPath(rec.folderPath)\n ) {\n dispatch(removeFolder({ id }));\n }\n }\n\n // Build tree spine directly from the just-parsed rows.\n // the normalized slice back out, and avoids any race with batched dispatch.\n const fileIds = files.map((f) => f.id).filter(Boolean);\n const folderIds = folders.map((f) => f.id).filter(Boolean);\n const tree = buildTreeState({\n fileIds,\n folderIds,\n filesById: Object.fromEntries(\n files.map((f) => [\n f.id,\n {\n id: f.id,\n ownerId: f.ownerId,\n filePath: f.filePath,\n fileName: f.fileName,\n mimeType: f.mimeType ?? null,\n fileSize: f.fileSize ?? null,\n checksum: null,\n visibility: f.visibility,\n currentVersion: f.currentVersion,\n parentFolderId: f.parentFolderId ?? null,\n metadata: f.metadata ?? {},\n createdAt: f.createdAt,\n updatedAt: f.updatedAt,\n deletedAt: f.deletedAt,\n // Tree-spine reconstruction is internal — the computed URL fields\n // and the Phase-1b backend thumbnail_url are only populated when\n // records arrive via the REST API. Default all to null here; the\n // file grid (`MediaThumbnail`) falls through to `useFileAsset` →\n // `Asset.variants[\"thumbnail_url\"]` when these are absent.\n publicUrl: null,\n url: null,\n cdnUrl: null,\n downloadUrl: null,\n thumbnailUrl: null,\n source: { kind: \"real\" },\n _dirty: false,\n _dirtyFields: {},\n _fieldHistory: {},\n _loadedFields: {},\n _loading: false,\n _error: null,\n _pendingRequestIds: [],\n },\n ]),\n ),\n foldersById: Object.fromEntries(\n folders.map((f) => [\n f.id,\n {\n id: f.id,\n ownerId: f.ownerId,\n folderPath: f.folderPath,\n folderName: f.folderName,\n parentId: f.parentId ?? null,\n visibility: f.visibility,\n metadata: f.metadata ?? {},\n createdAt: f.createdAt,\n updatedAt: f.updatedAt,\n deletedAt: f.deletedAt,\n // Tree-spine reconstruction is internal — public_url is only\n // populated when records arrive via the API. Default to null\n // here; surfaces that need a CDN URL fetch via useFileSrc.\n publicUrl: null,\n source: { kind: \"real\" },\n _dirty: false,\n _dirtyFields: {},\n _fieldHistory: {},\n _loadedFields: {},\n _loading: false,\n _error: null,\n _pendingRequestIds: [],\n },\n ]),\n ),\n });\n dispatch(\n replaceTree({\n rootFolderIds: tree.rootFolderIds,\n rootFileIds: tree.rootFileIds,\n childrenByFolderId: tree.childrenByFolderId,\n // This RPC returns the complete discoverable tree, not one lazy level.\n // Mark every real folder so navigation never re-downloads up to 100k\n // rows merely because the user opened a different folder.\n fullyLoadedFolderIds: Object.fromEntries(\n folderIds.map((id) => [id, true] as const),\n ),\n }),\n );\n } catch (error) {\n // A load deferred by a hidden tab is not a failure: back to idle, and\n // the next consumer reloads. A visible timeout arrives as TimeoutError.\n if (isFileTreeLoadDeferred(error)) {\n dispatch(setTreeStatus({ status: \"idle\" }));\n throw error;\n }\n const message =\n error instanceof Error\n ? error.message\n : \"Could not load your file library.\";\n dispatch(setTreeStatus({ status: \"error\", error: message }));\n throw error;\n }\n })();\n\n _treeInFlight = run;\n _treeInFlightUserId = userId;\n try {\n await run;\n } finally {\n if (_treeInFlight === run) {\n _treeInFlight = null;\n _treeInFlightUserId = null;\n }\n }\n});\n\n/**\n * Re-run the tree load. Fired by the realtime middleware when the subscription\n * reconnects after an outage — guarantees we don't miss events.\n */\nexport const reconcileTree = createAsyncThunk<\n void,\n { userId: string },\n ThunkApi\n>(\"cloudFiles/reconcileTree\", async ({ userId }, { dispatch }) => {\n await dispatch(loadUserFileTree({ userId })).unwrap();\n});\n\nexport const loadFolderContents = createAsyncThunk<\n void,\n { folderId: string },\n ThunkApi\n>(\"cloudFiles/loadFolderContents\", async ({ folderId }, { dispatch }) => {\n // Virtual folders use their adapter's `list()` via `loadVirtualChildren`\n // — `cld_*` tables don't know about them and would 22P02 on the synthetic id.\n if (folderId.startsWith(\"vfs:\")) return;\n // RLS intentionally permits contextual reads for files conveyed through a\n // shared conversation, so a raw table enumeration here leaks those rows into\n // the global Files UI. Refresh through the discoverability-gated tree RPC —\n // one canonical list contract for roots and nested folders alike.\n const { data, error } = await getClaimsUser(supabase);\n if (error) throw error;\n if (!data.user?.id)\n throw new Error(\"Cannot load folder contents without a user\");\n await dispatch(loadUserFileTree({ userId: data.user.id })).unwrap();\n dispatch(markFolderFullyLoaded({ folderId }));\n});\n\n// Virtual ids look like `vfs:<adapter>:<vid>` and can't be queried against\n// the real `cld_*` tables — Postgres rejects them with `22P02 invalid input\n// syntax for type uuid`. The version / permission / share-link surfaces\n// aren't mapped to virtual sources in v1, so these thunks short-circuit\n// for synthetic ids. Consumers (FileVersionsList, useSharing) also render\n// a \"not supported here\" empty state so the UX is clear, not silent.\nfunction isVirtualResourceId(id: string): boolean {\n return id.startsWith(\"vfs:\");\n}\n\nexport const loadFileVersions = createAsyncThunk<\n void,\n { fileId: string },\n ThunkApi\n>(\"cloudFiles/loadFileVersions\", async ({ fileId }, { dispatch }) => {\n if (isVirtualResourceId(fileId)) return;\n const { data, error } = await filesDb(supabase)\n .from(\"file_versions\")\n .select(FILE_VERSIONS_TABLE_COLUMNS)\n .eq(\"file_id\", fileId)\n .order(\"version_number\", { ascending: false });\n if (error) throw pgErrorToError(error);\n const versions: CloudFileVersion[] = (data ?? []).map(\n dbRowToCloudFileVersion,\n );\n dispatch(upsertVersionsForFile({ fileId, versions }));\n});\n\nexport const loadPermissions = createAsyncThunk<\n void,\n { resourceId: string },\n ThunkApi\n>(\"cloudFiles/loadPermissions\", async ({ resourceId }, { dispatch }) => {\n if (isVirtualResourceId(resourceId)) return;\n // Canonical grant store is `public.permissions` (resource_type='file'),\n // NOT the legacy cld_ file-permission duplicate. RLS returns the rows the\n // caller is allowed to see (own grants / grants to them / org / public).\n const { data, error } = await supabase\n .schema(\"iam\")\n .from(\"permissions\")\n .select(\"*\")\n .eq(\"resource_type\", \"file\")\n .eq(\"resource_id\", resourceId);\n if (error) throw pgErrorToError(error);\n const permissions: CloudFilePermission[] = (data ?? []).map(\n dbRowToCloudFilePermission,\n );\n dispatch(upsertPermissionsForResource({ resourceId, permissions }));\n});\n\nexport const loadShareLinks = createAsyncThunk<\n void,\n { resourceId: string; resourceType?: ResourceType },\n ThunkApi\n>(\n \"cloudFiles/loadShareLinks\",\n async ({ resourceId, resourceType = \"file\" }, { dispatch }) => {\n if (isVirtualResourceId(resourceId)) return;\n // Canonical owner-gated list over platform.share_links.\n const links = await listCanonicalShareLinks(resourceType, resourceId);\n const shareLinks: CloudShareLink[] = links\n .filter((l) => l.isActive)\n .map((l) =>\n canonicalShareLinkToCloudShareLink(l, resourceType, resourceId),\n );\n dispatch(upsertShareLinksForResource({ resourceId, shareLinks }));\n },\n);\n\n// ---------------------------------------------------------------------------\n// Writes — folders\n// ---------------------------------------------------------------------------\n//\n// Folder-CRUD endpoints (`POST /folders`, `PATCH /folders/{id}`,\n// `DELETE /folders/{id}`) shipped 2026-04-26. These thunks now hit\n// the REST surface; legacy direct supabase-js writes were retired.\n\n/**\n * Create a folder via the Python REST contract. We hit `POST /folders` with\n * the explicit `{ folder_name, parent_id }` form because callers already\n * resolve the parent id from the tree. The path-style form (`folder_path`)\n * is preferred for upload-time auto-create, which `uploadFiles` handles.\n *\n * Architecturally important: this thunk no longer touches `cld_folders` from\n * the browser. Folder writes were the single biggest source of RLS recursion\n * regressions; routing them through the backend (which uses SECURITY DEFINER\n * helpers) makes the path uniform with file uploads.\n */\nexport const createFolder = createAsyncThunk<\n string,\n import(\"../types\").CreateFolderArg,\n ThunkApi\n>(\"cloudFiles/createFolder\", async (arg, { dispatch, getState }) => {\n const folderName = arg.folderName.trim();\n if (!folderName) {\n throw new Error(\"Folder name cannot be empty.\");\n }\n if (/[/\\\\]/.test(folderName)) {\n throw new Error(\"Folder names cannot contain '/' or '\\\\'.\");\n }\n // Reject NUL + ASCII control characters. These can be paste-injected\n // and confuse downstream object stores or any layer that materialises\n // the path on disk. Mirrors the rule in `validateRenameInput`.\n if (/[\\x00-\\x1f\\x7f]/.test(folderName)) {\n throw new Error(\"Folder names cannot contain control characters.\");\n }\n if (folderName.endsWith(\".\")) {\n throw new Error(\"Folder names cannot end with '.'.\");\n }\n // Sibling collision check — case-insensitive. Without this the thunk\n // happily issues `POST /folders` with a duplicate name and the backend\n // either returns a confusing UNIQUE-constraint error or (worse) silently\n // creates a sibling that's indistinguishable from the existing one.\n {\n const state = getState().cloudFiles;\n const lowered = folderName.toLowerCase();\n const collision = Object.values(state.foldersById).some(\n (f) =>\n !!f &&\n !f.deletedAt &&\n (f.parentId ?? null) === (arg.parentId ?? null) &&\n f.folderName.toLowerCase() === lowered,\n );\n if (collision) {\n throw new Error(\n `A folder named '${folderName}' already exists in this location.`,\n );\n }\n }\n\n const requestId = newRequestId();\n registerRequest({\n requestId,\n kind: \"folder-create\",\n resourceId: null,\n resourceType: \"folder\",\n });\n\n // Always use path-style: the backend rejects `{folder_name, parent_id}`\n // with `validation_error` (both at root with parent_id=null AND nested\n // with a parent uuid). Path-style creation is idempotent and creates\n // any missing intermediate segments. For nested creation we resolve the\n // parent's path from local state and append the new segment.\n let folderPath = folderName;\n if (arg.parentId !== null) {\n const parent = getState().cloudFiles.foldersById[arg.parentId];\n if (!parent) {\n // The parent isn't in this view's slice — a stale tree, not a missing\n // folder. Say what the user can do; the id stays out of the sentence\n // and in the rejected-thunk capture.\n throw new Error(\n \"We couldn't create the folder here — this view is out of date. Refresh and try again.\",\n );\n }\n folderPath = `${parent.folderPath}/${folderName}`;\n }\n const body = {\n folder_path: folderPath,\n visibility: arg.visibility ?? \"personal\",\n metadata: arg.metadata ?? null,\n };\n\n try {\n const { data: row } = await Folders.createFolder(body, { requestId });\n\n const folder = dbRowToCloudFolder(row);\n dispatch(upsertFolder(folder));\n dispatch(\n attachChildToFolder({\n parentFolderId: folder.parentId,\n kind: \"folder\",\n id: folder.id,\n }),\n );\n return folder.id;\n } catch (err) {\n if (process.env.NODE_ENV !== \"production\") {\n const e = err as {\n status?: number | null;\n code?: string;\n detail?: string;\n userMessage?: string;\n details?: unknown;\n };\n // Surface the full backend response so we can fix the contract drift\n // rather than show a generic \"request failed\" toast.\n console.error(\"[createFolder] backend error\", {\n status: e.status ?? null,\n code: e.code,\n detail: e.detail,\n userMessage: e.userMessage,\n details: e.details,\n sentBody: body,\n });\n }\n throw err;\n } finally {\n releaseRequest(requestId);\n }\n});\n\n/**\n * Update folder properties — rename, move (`parentId`), change visibility,\n * patch metadata. Optimistic: applies the patch locally before the REST call\n * and rolls back on failure.\n */\nexport const updateFolder = createAsyncThunk<\n void,\n import(\"../types\").UpdateFolderArg,\n ThunkApi\n>(\"cloudFiles/updateFolder\", async (arg, { dispatch, getState }) => {\n const state = getState();\n const folder = state.cloudFiles.foldersById[arg.folderId];\n if (!folder)\n throw new Error(\n \"We couldn't update that folder — this view is out of date. Refresh and try again.\",\n );\n\n const requestId = newRequestId();\n registerRequest({\n requestId,\n kind: \"folder-update\",\n resourceId: arg.folderId,\n resourceType: \"folder\",\n });\n\n // Optimistic: apply the patch locally.\n const patch = arg.patch;\n const optimistic: CloudFolder = {\n ...folder,\n ...(patch.folderName !== undefined ? { folderName: patch.folderName } : {}),\n ...(patch.parentId !== undefined ? { parentId: patch.parentId } : {}),\n ...(patch.visibility !== undefined ? { visibility: patch.visibility } : {}),\n ...(patch.metadata !== undefined ? { metadata: patch.metadata } : {}),\n };\n dispatch(upsertFolder(optimistic));\n\n // Track move in tree state — detach from old parent, attach under new.\n if (patch.parentId !== undefined && patch.parentId !== folder.parentId) {\n dispatch(\n detachChildFromFolder({\n parentFolderId: folder.parentId,\n kind: \"folder\",\n id: folder.id,\n }),\n );\n dispatch(\n attachChildToFolder({\n parentFolderId: patch.parentId,\n kind: \"folder\",\n id: folder.id,\n }),\n );\n }\n\n // The backend PATCH /folders honors folder_path / visibility / metadata ONLY;\n // it silently ignores folder_name / parent_id (same as POST /folders, which\n // rejects them). Rename AND move must therefore be expressed as the TARGET\n // folder_path — the server renames, reparents, and cascades descendants from\n // it. Sending folder_name/parent_id no-op'd server-side (rename/move reverted\n // on refresh). Compute the target path from the new name + new parent's path.\n let targetFolderPath: string | undefined;\n if (patch.folderName !== undefined || patch.parentId !== undefined) {\n const newName = patch.folderName ?? folder.folderName;\n const newParentId =\n patch.parentId !== undefined ? patch.parentId : folder.parentId;\n if (newParentId) {\n const newParent = state.cloudFiles.foldersById[newParentId];\n if (!newParent) {\n throw new Error(\n \"We couldn't move the folder there — this view is out of date. Refresh and try again.\",\n );\n }\n targetFolderPath = `${newParent.folderPath}/${newName}`;\n } else {\n targetFolderPath = newName;\n }\n }\n\n try {\n const { data: row } = await Folders.patchFolder(\n arg.folderId,\n {\n folder_path: targetFolderPath,\n visibility: patch.visibility,\n metadata: patch.metadata ?? null,\n },\n { requestId },\n );\n dispatch(upsertFolder(dbRowToCloudFolder(row)));\n } catch (err) {\n // Roll back to the pre-edit folder state and tree links (strip frozen\n // runtime fields — re-upserting a store record crashes the merge).\n dispatch(upsertFolder(toCloudFolderPartial(folder)));\n if (patch.parentId !== undefined && patch.parentId !== folder.parentId) {\n dispatch(\n detachChildFromFolder({\n parentFolderId: patch.parentId,\n kind: \"folder\",\n id: folder.id,\n }),\n );\n dispatch(\n attachChildToFolder({\n parentFolderId: folder.parentId,\n kind: \"folder\",\n id: folder.id,\n }),\n );\n }\n throw err;\n } finally {\n releaseRequest(requestId);\n }\n});\n\nexport const deleteFolder = createAsyncThunk<\n void,\n import(\"../types\").DeleteFolderArg,\n ThunkApi\n>(\"cloudFiles/deleteFolder\", async (arg, { dispatch, getState }) => {\n const state = getState();\n const folder = state.cloudFiles.foldersById[arg.folderId];\n if (!folder) return;\n\n const requestId = newRequestId();\n registerRequest({\n requestId,\n kind: \"folder-delete\",\n resourceId: arg.folderId,\n resourceType: \"folder\",\n });\n\n try {\n // Soft delete (Trash) cascades the subtree in SQL (soft_delete_folder) →\n // direct. There is no hard delete.\n await softDeleteFolderDirect(arg.folderId);\n dispatch(removeFolder({ id: arg.folderId }));\n } finally {\n releaseRequest(requestId);\n }\n});\n\n/**\n * Ensure every segment of `folderPath` exists; create any that don't.\n * Returns the leaf folder's id. Used for convention-based uploads like\n * \"save all pasted images under /Images\".\n */\nexport const ensureFolderPath = createAsyncThunk<\n string,\n import(\"../types\").EnsureFolderPathArg,\n ThunkApi\n>(\"cloudFiles/ensureFolderPath\", async (arg, { dispatch, getState }) => {\n const normalizedPath = arg.folderPath\n .split(\"/\")\n .map((s) => s.trim())\n .filter(Boolean)\n .join(\"/\");\n if (!normalizedPath) {\n throw new Error(\"folderPath cannot be empty.\");\n }\n\n // Realtime/tree state is the zero-request fast path.\n const existing = Object.values(getState().cloudFiles.foldersById).find(\n (folder) => folder.folderPath === normalizedPath && !folder.deletedAt,\n );\n if (existing) {\n return existing.id;\n }\n\n // The backend owns path traversal and race handling. One atomic request\n // replaces the obsolete per-segment files.folders PostgREST reads.\n const requestId = newRequestId();\n registerRequest({\n requestId,\n kind: \"folder-create\",\n resourceId: null,\n resourceType: \"folder\",\n });\n try {\n const { data: row } = await Folders.ensureFolderPath(\n normalizedPath,\n arg.visibility ?? \"personal\",\n { requestId },\n );\n const folder = dbRowToCloudFolder(row);\n dispatch(upsertFolder(folder));\n dispatch(\n attachChildToFolder({\n parentFolderId: folder.parentId,\n kind: \"folder\",\n id: folder.id,\n }),\n );\n return folder.id;\n } finally {\n releaseRequest(requestId);\n }\n});\n\n// ---------------------------------------------------------------------------\n// Writes — uploads (multi-file with progress)\n// ---------------------------------------------------------------------------\n\n/**\n * Say, on the screen the person is actually looking at, that an upload failed —\n * in the SERVER'S words.\n *\n * 🚨 THE 2026-09-19 SILENT FAILURE (`common-docs/systems/knowledge/ingestion/projects/acquisition-frontier/\n * own-files/VERIFICATION.md` §15/§16). A 21-file drop on a fresh Rulebook fired\n * 21 uploads and every one came back `400 matrx-files: this write carries no\n * organization`. Every sentence was captured correctly — into\n * `failed[].error` and into the upload slice — and then shown NOWHERE, because:\n *\n * - `mutation-toast-middleware` deliberately skips `uploadFiles`, on the\n * reasoning that \"uploads surface per-file errors inline in the\n * UploadProgressList\". That is true on the Files surface and FALSE\n * everywhere else — the Rulebook sources panel never renders that list.\n * - the capture toolbar that started the drop collapses N failures into\n * \"Failed to upload N files\", dropping every reason.\n *\n * So the person saw a card that still read \"Nothing attached yet\" and no reason\n * anywhere. An upload that failed must never be indistinguishable from an\n * upload nobody started.\n *\n * This announces from the THUNK, which every caller shares, so no surface can\n * forget. Reasons are deduped (21 identical 400s are one sentence, not 21\n * toasts), and the toast carries a stable id so the surrounding UploadProgressList\n * or capture toolbar cannot stack a second copy of the same news.\n */\nexport function announceUploadFailures(\n failed: ReadonlyArray<{ name: string; error: string }>,\n): void {\n if (failed.length === 0) return;\n const reasons = Array.from(\n new Set(failed.map((f) => f.error).filter((e) => e && e.trim().length > 0)),\n );\n const title =\n failed.length === 1\n ? `Couldn't upload ${failed[0].name}`\n : `Couldn't upload ${failed.length} files`;\n toast.error(title, {\n // The server's own sentence, never a sentence of ours. When several\n // distinct things went wrong, each one gets said.\n description:\n reasons.length > 0\n ? reasons.join(\" · \")\n : \"The server refused the upload and gave no reason.\",\n id: `upload-failed:${failed.map((f) => f.name).join(\"|\")}`,\n duration: 12000,\n });\n}\n\nexport const uploadFiles = createAsyncThunk<\n { uploaded: string[]; failed: Array<{ name: string; error: string }> },\n UploadFilesArg,\n ThunkApi\n>(\"cloudFiles/uploadFiles\", async (arg, { dispatch, getState }) => {\n const concurrency = Math.max(1, arg.concurrency ?? 3);\n const uploaded: string[] = [];\n // Track REAL error per file (not just filename). Without this, every\n // upload failure surfaces to callers as the file's name rather than the\n // backend's actual error code/message — which is what made the Phase\n // 11 migration look \"broken\" when really we just couldn't see why.\n const failed: Array<{ name: string; error: string }> = [];\n\n // Resolve logical path prefix.\n //\n // Order of preference:\n // 1. `folderPath` arg — passed directly (the Python backend\n // auto-creates the hierarchy server-side; the browser never has to\n // query `cld_folders` via supabase-js, which avoids the\n // well-known RLS recursion bug on the legacy file-permission table).\n // 2. `parentFolderId` — look up the folder in slice state (works when\n // the folder is already loaded from the tree RPC or realtime).\n // 3. Empty prefix — file lands at root.\n const state = getState();\n let prefix = \"\";\n if (arg.folderPath) {\n prefix = `${arg.folderPath.replace(/^\\/+|\\/+$/g, \"\")}/`;\n } else if (arg.parentFolderId) {\n const parentFolder = state.cloudFiles.foldersById[arg.parentFolderId];\n if (parentFolder) {\n prefix = `${parentFolder.folderPath}/`;\n }\n }\n\n // ─── Collision handling ───────────────────────────────────────────────\n //\n // Build a case-insensitive set of file names already present in the\n // destination. Used by `uniqueName` below to auto-rename uploads that\n // collide (\"report.pdf\" → \"report (1).pdf\"). Without this, the backend\n // either silently overwrites or version-bumps the existing file —\n // neither matches user expectations from Drive / Dropbox where a\n // duplicate upload becomes \"report (1).pdf\" by default.\n //\n // We only consider files in the SAME parent folder. The lookup is by\n // path-prefix because virtual folders may not have stable ids in\n // local state at upload time (e.g. `folderPath` arg with no\n // pre-loaded folder row).\n const existingNames = (() => {\n const out = new Set<string>();\n const targetPrefix = prefix; // captured outside; matches \"<path>/<name>\"\n for (const f of Object.values(state.cloudFiles.filesById)) {\n if (!f) continue;\n if (f.deletedAt) continue;\n // Match by storage path under the prefix. `f.filePath` is the\n // backend's canonical path including the file name.\n if (!f.filePath) continue;\n const normalized = f.filePath.replace(/^\\/+/, \"\");\n if (!normalized.startsWith(targetPrefix)) continue;\n const tail = normalized.slice(targetPrefix.length);\n // Skip files nested deeper than this folder.\n if (tail.includes(\"/\")) continue;\n if (tail) out.add(tail.toLowerCase());\n }\n return out;\n })();\n\n /**\n * Pick the next non-colliding name, mutating the queue:\n * \"report.pdf\" → \"report.pdf\" (no collision)\n * \"report.pdf\" → \"report (1).pdf\" (one collision)\n * \"report.pdf\" → \"report (2).pdf\" (two collisions)\n * \".env\" → \".env (1)\" (hidden file: suffix only)\n *\n * Reserves the chosen name in `existingNames` so back-to-back uploads\n * of the same source file (drop the same File twice) don't collide\n * with each other.\n */\n function uniqueName(originalName: string): string {\n const lowered = originalName.toLowerCase();\n if (!existingNames.has(lowered)) {\n existingNames.add(lowered);\n return originalName;\n }\n const dot = originalName.lastIndexOf(\".\");\n const isHidden = originalName.startsWith(\".\");\n const stem =\n dot > 0 && !isHidden ? originalName.slice(0, dot) : originalName;\n const ext = dot > 0 && !isHidden ? originalName.slice(dot) : \"\";\n for (let i = 1; i < 1000; i++) {\n const candidate = `${stem} (${i})${ext}`;\n if (!existingNames.has(candidate.toLowerCase())) {\n existingNames.add(candidate.toLowerCase());\n return candidate;\n }\n }\n // 1000 collisions — fall back to original name and let the backend\n // version-bump. Practically never reached.\n return originalName;\n }\n\n // ─── Per-file overrides from the duplicate-upload dialog ────────────\n //\n // When a user picks \"Overwrite\" on a duplicate, the dialog passes the\n // existing file's exact name back as a `filenameOverrides[index]`\n // entry. We honour the override verbatim — the auto \" (1)\" rename\n // would defeat the point. When they pick \"Skip\", the dialog adds the\n // index to `skipIndices` and we drop that file from the queue.\n const overrides = arg.filenameOverrides ?? {};\n const skipSet = new Set(arg.skipIndices ?? []);\n const forceNewCopySet = new Set(arg.forceNewCopyIndices ?? []);\n\n // Reserve override target names in `existingNames` BEFORE the loop\n // runs, so two overrides into the same path can't both win and so\n // a file with no override doesn't accidentally collide-rename onto\n // an override target.\n for (const [, name] of Object.entries(overrides)) {\n if (typeof name === \"string\") existingNames.add(name.toLowerCase());\n }\n\n // Build the queue as [originalIndex, file] pairs so workers can\n // look up overrides by index regardless of dequeue order.\n const queue: Array<{ index: number; file: File }> = arg.files\n .map((file, index) => ({ index, file }))\n .filter(({ index }) => !skipSet.has(index));\n\n async function worker(): Promise<void> {\n while (queue.length) {\n const next = queue.shift();\n if (!next) return;\n const { file, index } = next;\n const overrideName = overrides[index];\n const forcedCopy = forceNewCopySet.has(index);\n\n // Dedup guard (VERIFICATION.md §12, 2026-09-18): a caller-level retry\n // after a transient failure (dev-server connection reset, etc.) can\n // re-dispatch `uploadFiles` for a file whose FIRST attempt already\n // landed — the client has no proof it didn't, and the backend's own\n // `X-Idempotency-Key` only replays a 412, never a success (see\n // `features/files/upload/uploadDedupGuard.ts` for the full trace).\n // Only the plain path participates: an explicit \"Overwrite\" override\n // or \"force new copy\" means the caller WANTS a fresh write.\n const dedupKey =\n !overrideName && !forcedCopy\n ? uploadDedupKey(prefix, file.name, file.size)\n : null;\n let dedupClaim: UploadClaim | null = null;\n if (dedupKey) {\n const claim = claimUpload(dedupKey);\n if (claim.shared) {\n try {\n const outcome = await claim.promise;\n // Reuse the earlier attempt's outcome — no bytes sent again.\n uploaded.push(outcome.fileId);\n continue;\n } catch {\n // The earlier attempt genuinely failed, and the guard already\n // cleared its slot — fall through and upload for real below.\n }\n } else {\n dedupClaim = claim;\n }\n }\n\n const requestId = newRequestId();\n // Override (from duplicate dialog \"Overwrite\") wins. Otherwise\n // the auto \" (1)\" rename runs. We track the display name\n // separately from the original file.name so progress + telemetry\n // continue to use what the user dragged in.\n const targetName =\n typeof overrideName === \"string\" && overrideName\n ? overrideName\n : uniqueName(file.name);\n dispatch(\n trackUploadStart({\n requestId,\n fileName: targetName,\n fileSize: file.size,\n parentFolderId: arg.parentFolderId ?? null,\n folderPath: arg.folderPath ?? null,\n }),\n );\n registerRequest({\n requestId,\n kind: \"upload\",\n resourceId: null,\n resourceType: \"file\",\n });\n try {\n const { data } = await Files.uploadFileWithProgress(\n {\n file,\n filePath: `${prefix}${targetName}`,\n visibility: arg.visibility ?? \"personal\",\n shareWith: arg.shareWith,\n shareLevel: arg.shareLevel,\n changeSummary: arg.changeSummary,\n metadata: arg.metadata,\n options: arg.options,\n ...(forceNewCopySet.has(index)\n ? {\n intent: \"force_new_copy\" as const,\n reason:\n \"User selected Make a copy in the duplicate upload dialog\",\n }\n : {}),\n },\n (ev) =>\n dispatch(\n updateUploadProgress({\n requestId,\n bytesUploaded: ev.loaded,\n }),\n ),\n {\n requestId,\n // Idempotency key — same value across automatic retries of the\n // same intended upload. The backend stores it in\n // `metadata._idempotency_key` so retries don't create duplicate\n // version rows. We reuse `requestId` because it's generated\n // once per intended upload, before any retry.\n idempotencyKey: requestId,\n },\n );\n // Upsert file into slice from response.\n dispatch(\n upsertFile(\n apiFileRecordToCloudFile({\n id: data.file_id,\n owner_id: \"\",\n file_path: data.file_path,\n file_name: data.file_path.split(\"/\").pop() ?? targetName,\n mime_type: file.type || null,\n // Phase 0 rename — see docs/PYTHON_UPDATES.md §3.\n size_bytes: data.size_bytes,\n checksum: data.checksum,\n visibility: arg.visibility ?? \"personal\",\n current_version: data.version_number,\n parent_folder_id: arg.parentFolderId ?? null,\n metadata: arg.metadata ?? {},\n // P1-7: stamp the optimistic row with the current time instead of\n // null. The upload just happened, so \"now\" is accurate for the\n // recents filter / \"sort by modified\" until the realtime echo\n // delivers the authoritative server timestamp. Null sorted the\n // freshly-uploaded file to the wrong end of the list.\n created_at: new Date().toISOString(),\n updated_at: new Date().toISOString(),\n deleted_at: null,\n }),\n ),\n );\n // If this upload replaced an existing file (same logical path,\n // backend bumped current_version), the cached blob for the\n // original version is now stale. Invalidate so the next preview\n // open re-fetches the latest bytes.\n invalidateBlobCache(data.file_id);\n invalidateOfficeExtraction(data.file_id);\n dispatch(\n attachChildToFolder({\n parentFolderId: arg.parentFolderId ?? null,\n kind: \"file\",\n id: data.file_id,\n }),\n );\n dispatch(\n updateUploadStatus({\n requestId,\n status: \"success\",\n fileId: data.file_id,\n }),\n );\n uploaded.push(data.file_id);\n dedupClaim?.settle(\n Promise.resolve({ fileId: data.file_id, filePath: data.file_path }),\n );\n } catch (err) {\n const message = extractErrorMessage(err);\n dispatch(\n updateUploadStatus({\n requestId,\n status: \"error\",\n error: message,\n }),\n );\n failed.push({ name: file.name, error: message });\n dedupClaim?.settle(Promise.reject(err));\n } finally {\n releaseRequest(requestId);\n }\n }\n }\n\n await Promise.all(\n Array.from({ length: Math.min(concurrency, arg.files.length) }, () =>\n worker(),\n ),\n );\n\n announceUploadFailures(failed);\n return { uploaded, failed };\n});\n\nexport const clearUploadEntry = createAsyncThunk<\n void,\n { requestId: string },\n ThunkApi\n>(\"cloudFiles/clearUpload\", async ({ requestId }, { dispatch }) => {\n dispatch(clearUpload({ requestId }));\n});\n\n// ---------------------------------------------------------------------------\n// Writes — optimistic metadata updates\n// ---------------------------------------------------------------------------\n\nexport const renameFile = createAsyncThunk<void, RenameFileArg, ThunkApi>(\n \"cloudFiles/rename\",\n async ({ fileId, newName }, { dispatch, getState }) => {\n const record = getFileFromState(getState(), fileId);\n if (!record)\n throw new Error(\n \"We couldn't find that file in this view. Refresh and try again.\",\n );\n\n const requestId = newRequestId();\n // Per-resource op sequence — guards against an older response clobbering a\n // newer optimistic state (rename A→B→C out of order, or a double-click).\n const seq = beginResourceOp(fileId);\n const snapshot: CloudFileFieldSnapshot = {\n fileName: record.fileName,\n filePath: record.filePath,\n };\n\n // Compute new file path (replace final segment).\n const pathParts = record.filePath.split(\"/\");\n pathParts[pathParts.length - 1] = newName;\n const newPath = pathParts.join(\"/\");\n\n dispatch(setFileField({ id: fileId, field: \"fileName\", value: newName }));\n dispatch(setFileField({ id: fileId, field: \"filePath\", value: newPath }));\n dispatch(addFilePendingRequest({ id: fileId, requestId }));\n registerRequest({\n requestId,\n kind: \"rename\",\n resourceId: fileId,\n resourceType: \"file\",\n });\n\n try {\n // Use the dedicated rename endpoint — it accepts a full new path,\n // auto-creates parent folders if any segment is missing, and lets\n // the backend handle the file_path / storage-location update atomically.\n const { data } = await Files.renameFile(\n fileId,\n { new_path: newPath },\n { requestId },\n );\n // Only apply if no newer op for this file has begun since (out-of-order\n // guard); otherwise the newer op owns the state.\n if (isLatestResourceOp(fileId, seq)) {\n dispatch(upsertFile(apiFileRecordToCloudFile(data)));\n dispatch(markFileSaved({ id: fileId }));\n }\n } catch (err) {\n // Don't roll back if a newer op has superseded this one.\n if (isLatestResourceOp(fileId, seq)) {\n dispatch(rollbackFileOptimisticUpdate({ id: fileId, snapshot }));\n const msg = extractErrorMessage(err);\n dispatch(setFileError({ id: fileId, error: msg }));\n }\n throw err;\n } finally {\n dispatch(removeFilePendingRequest({ id: fileId, requestId }));\n releaseRequest(requestId);\n }\n },\n);\n\nexport const moveFile = createAsyncThunk<void, MoveFileArg, ThunkApi>(\n \"cloudFiles/move\",\n async ({ fileId, newParentFolderId }, { dispatch, getState }) => {\n const record = getFileFromState(getState(), fileId);\n if (!record)\n throw new Error(\n \"We couldn't find that file in this view. Refresh and try again.\",\n );\n const oldParentId = record.parentFolderId;\n const requestId = newRequestId();\n const seq = beginResourceOp(fileId);\n const snapshot: CloudFileFieldSnapshot = {\n parentFolderId: oldParentId,\n };\n\n dispatch(\n setFileField({\n id: fileId,\n field: \"parentFolderId\",\n value: newParentFolderId,\n }),\n );\n dispatch(\n detachChildFromFolder({\n parentFolderId: oldParentId,\n kind: \"file\",\n id: fileId,\n }),\n );\n dispatch(\n attachChildToFolder({\n parentFolderId: newParentFolderId,\n kind: \"file\",\n id: fileId,\n }),\n );\n dispatch(addFilePendingRequest({ id: fileId, requestId }));\n registerRequest({\n requestId,\n kind: \"move\",\n resourceId: fileId,\n resourceType: \"file\",\n });\n\n try {\n // Move = rename to a new full logical path. The backend's rename\n // endpoint auto-creates missing parent folders, so we just compute\n // `<targetFolderPath>/<filename>` (or `<filename>` for root) and\n // hand it over.\n const targetFolder =\n newParentFolderId === null\n ? null\n : (getState().cloudFiles.foldersById[newParentFolderId] ?? null);\n const targetPrefix = targetFolder ? `${targetFolder.folderPath}/` : \"\";\n const newPath = `${targetPrefix}${record.fileName}`;\n\n const { data } = await Files.renameFile(\n fileId,\n { new_path: newPath },\n { requestId },\n );\n if (isLatestResourceOp(fileId, seq)) {\n dispatch(upsertFile(apiFileRecordToCloudFile(data)));\n dispatch(markFileSaved({ id: fileId }));\n }\n } catch (err) {\n if (isLatestResourceOp(fileId, seq)) {\n // Rollback tree membership + field.\n dispatch(\n detachChildFromFolder({\n parentFolderId: newParentFolderId,\n kind: \"file\",\n id: fileId,\n }),\n );\n dispatch(\n attachChildToFolder({\n parentFolderId: oldParentId,\n kind: \"file\",\n id: fileId,\n }),\n );\n dispatch(rollbackFileOptimisticUpdate({ id: fileId, snapshot }));\n const msg = extractErrorMessage(err);\n dispatch(setFileError({ id: fileId, error: msg }));\n }\n throw err;\n } finally {\n dispatch(removeFilePendingRequest({ id: fileId, requestId }));\n releaseRequest(requestId);\n }\n },\n);\n\nexport const updateFileMetadata = createAsyncThunk<\n void,\n UpdateFileMetadataArg,\n ThunkApi\n>(\n \"cloudFiles/updateMetadata\",\n async ({ fileId, patch }, { dispatch, getState }) => {\n const record = getFileFromState(getState(), fileId);\n if (!record)\n throw new Error(\n \"We couldn't find that file in this view. Refresh and try again.\",\n );\n\n const requestId = newRequestId();\n const seq = beginResourceOp(fileId);\n const snapshot: CloudFileFieldSnapshot = {\n visibility: record.visibility,\n metadata: record.metadata,\n };\n\n if (patch.visibility !== undefined) {\n dispatch(\n setFileField({\n id: fileId,\n field: \"visibility\",\n value: patch.visibility,\n }),\n );\n }\n if (patch.metadata !== undefined) {\n dispatch(\n setFileField({\n id: fileId,\n field: \"metadata\",\n value: patch.metadata,\n }),\n );\n }\n dispatch(addFilePendingRequest({ id: fileId, requestId }));\n registerRequest({\n requestId,\n kind: \"update\",\n resourceId: fileId,\n resourceType: \"file\",\n });\n\n try {\n const { data } = await Files.patchFile(\n fileId,\n {\n visibility: patch.visibility,\n metadata: patch.metadata,\n },\n { requestId },\n );\n if (isLatestResourceOp(fileId, seq)) {\n dispatch(upsertFile(apiFileRecordToCloudFile(data)));\n dispatch(markFileSaved({ id: fileId }));\n }\n } catch (err) {\n if (isLatestResourceOp(fileId, seq)) {\n dispatch(rollbackFileOptimisticUpdate({ id: fileId, snapshot }));\n const msg = extractErrorMessage(err);\n dispatch(setFileError({ id: fileId, error: msg }));\n }\n throw err;\n } finally {\n dispatch(removeFilePendingRequest({ id: fileId, requestId }));\n releaseRequest(requestId);\n }\n },\n);\n\n// ---------------------------------------------------------------------------\n// Writes — delete\n// ---------------------------------------------------------------------------\n\nexport const deleteFile = createAsyncThunk<void, DeleteFileArg, ThunkApi>(\n \"cloudFiles/delete\",\n async ({ fileId }, { dispatch, getState }) => {\n const record = getFileFromState(getState(), fileId);\n if (!record) return; // nothing to do\n const parentFolderId = record.parentFolderId;\n const requestId = newRequestId();\n\n // Optimistic remove.\n dispatch(removeFile({ id: fileId }));\n dispatch(\n detachChildFromFolder({\n parentFolderId,\n kind: \"file\",\n id: fileId,\n }),\n );\n // Drop the cached blob bytes — the file is gone, no point holding\n // memory for something the user can't open anymore (and if the\n // delete is rolled back on error, the next open will re-fetch).\n invalidateBlobCache(fileId);\n invalidateOfficeExtraction(fileId);\n registerRequest({\n requestId,\n kind: \"delete\",\n resourceId: fileId,\n resourceType: \"file\",\n });\n\n try {\n // Soft delete (Trash) is a pure metadata write → direct (canonical).\n // There is no hard delete; the file stays restorable from Trash.\n await softDeleteFileDirect(fileId);\n } catch (err) {\n // Rollback — reinsert the record and reattach to its parent.\n dispatch(upsertFile(toCloudFilePartial(record)));\n dispatch(\n attachChildToFolder({\n parentFolderId,\n kind: \"file\",\n id: fileId,\n }),\n );\n throw err;\n } finally {\n releaseRequest(requestId);\n }\n },\n);\n\n// ---------------------------------------------------------------------------\n// Trash — hydrate, restore (delete means archive: no purge from the client)\n// ---------------------------------------------------------------------------\n\n/**\n * Hydrate the trash section. The main tree load runs with\n * `p_include_deleted: false` and every delete/echo REMOVES rows from the\n * store, so soft-deleted records never reach Redux on their own — this thunk\n * pages the same RPC with deleted rows included and upserts ONLY the trashed\n * ones (`deletedAt` set). It never touches the folder tree spine, so trashed\n * rows can't leak into live folder listings; the section filters in\n * PageShell/row-data pick them up by `deletedAt`.\n */\n\n/**\n * A store record carries immer-frozen runtime bookkeeping (`_loadedFields`,\n * `_dirtyFields`, ...). Re-dispatching it into `upsertFile` on the INSERT path\n * would adopt those frozen objects into the new draft and crash the next\n * merge (\"object is not extensible\") — strip to domain fields first. Used by\n * every optimistic-rollback site.\n */\nfunction toCloudFilePartial(record: CloudFileRecord): Partial<CloudFile> {\n const {\n _dirty,\n _dirtyFields,\n _fieldHistory,\n _loadedFields,\n _loading,\n _error,\n _pendingRequestIds,\n ...domain\n } = record;\n return domain;\n}\n\n/** Folder twin of `toCloudFilePartial` — same frozen-runtime-fields hazard. */\nfunction toCloudFolderPartial(record: CloudFolderRecord): Partial<CloudFolder> {\n const {\n _dirty,\n _dirtyFields,\n _fieldHistory,\n _loadedFields,\n _loading,\n _error,\n _pendingRequestIds,\n ...domain\n } = record;\n return domain;\n}\n\nexport const loadTrash = createAsyncThunk<void, { userId: string }, ThunkApi>(\n \"cloudFiles/loadTrash\",\n async ({ userId }, { dispatch }) => {\n const rows: ReturnType<typeof parseCloudTreeRows> = [];\n for (let page = 0; page < TREE_MAX_PAGES; page += 1) {\n const { data, error } = await supabase.rpc(\"get_user_file_tree\", {\n p_user_id: userId,\n p_limit: TREE_PAGE_SIZE,\n p_offset: page * TREE_PAGE_SIZE,\n p_include_folders: true,\n p_include_deleted: true,\n });\n if (error) throw error;\n const pageRows = parseCloudTreeRows(data);\n rows.push(...pageRows);\n if (pageRows.length < TREE_PAGE_SIZE) break;\n }\n\n const files: Partial<CloudFile>[] = [];\n const folders: Partial<CloudFolder>[] = [];\n for (const row of rows) {\n if (!row.deleted_at) continue;\n if (row.kind === \"file\") {\n // Already filtered by the RPC's predicate — see above.\n files.push({\n id: row.id,\n ownerId: row.owner_id,\n filePath: row.file_path,\n fileName: row.file_name,\n parentFolderId: row.parent_folder_id,\n mimeType: row.mime_type,\n fileSize: row.size_bytes,\n visibility: row.visibility,\n currentVersion: row.current_version,\n createdAt: row.created_at,\n updatedAt: row.updated_at,\n deletedAt: row.deleted_at,\n originDeviceId: row.origin_device_id,\n });\n } else {\n folders.push({\n id: row.id,\n ownerId: row.owner_id,\n folderPath: row.folder_path,\n folderName: row.folder_name,\n parentId: row.parent_id,\n visibility: row.visibility,\n createdAt: row.created_at,\n updatedAt: row.updated_at,\n deletedAt: row.deleted_at,\n });\n }\n }\n dispatch(upsertFiles(files));\n dispatch(upsertFolders(folders));\n },\n);\n\n/** Restore a trashed file: flip `deletedAt` off and reattach to its folder. */\nexport const restoreFile = createAsyncThunk<void, { fileId: string }, ThunkApi>(\n \"cloudFiles/restoreFile\",\n async ({ fileId }, { dispatch, getState }) => {\n const record = getFileFromState(getState(), fileId);\n if (!record?.deletedAt) return;\n const requestId = newRequestId();\n\n // Optimistic: back to live, back under its parent.\n dispatch(upsertFile({ ...toCloudFilePartial(record), deletedAt: null }));\n dispatch(\n attachChildToFolder({\n parentFolderId: record.parentFolderId,\n kind: \"file\",\n id: fileId,\n }),\n );\n registerRequest({\n requestId,\n kind: \"file-restore\",\n resourceId: fileId,\n resourceType: \"file\",\n });\n try {\n await restoreFileDirect(fileId);\n } catch (err) {\n dispatch(upsertFile(toCloudFilePartial(record))); // rollback to trashed (clone — state objects are frozen)\n dispatch(\n detachChildFromFolder({\n parentFolderId: record.parentFolderId,\n kind: \"file\",\n id: fileId,\n }),\n );\n throw err;\n } finally {\n releaseRequest(requestId);\n }\n },\n);\n\n/**\n * Restore a trashed folder + its subtree. The RPC un-deletes descendants in\n * SQL; rather than mirroring that cascade optimistically we reconcile by\n * reloading the live tree afterwards (folder restore is rare — correctness\n * over cleverness).\n */\nexport const restoreFolder = createAsyncThunk<\n void,\n { folderId: string },\n ThunkApi\n>(\"cloudFiles/restoreFolder\", async ({ folderId }, { dispatch, getState }) => {\n const folder = getState().cloudFiles.foldersById[folderId];\n if (!folder?.deletedAt) return;\n await restoreFolderDirect(folderId);\n dispatch(upsertFolder({ ...toCloudFolderPartial(folder), deletedAt: null }));\n const userId = folder.ownerId;\n await dispatch(loadUserFileTree({ userId }));\n await dispatch(loadTrash({ userId }));\n});\n\n// ---------------------------------------------------------------------------\n// Writes — versions\n// ---------------------------------------------------------------------------\n\nexport const restoreVersion = createAsyncThunk<\n void,\n RestoreVersionArg,\n ThunkApi\n>(\n \"cloudFiles/restoreVersion\",\n async ({ fileId, versionNumber }, { dispatch }) => {\n if (isVirtualResourceId(fileId)) return;\n dispatch(setFileLoading({ id: fileId, loading: true }));\n const requestId = newRequestId();\n registerRequest({\n requestId,\n kind: \"restore-version\",\n resourceId: fileId,\n resourceType: \"file\",\n });\n try {\n const { data } = await Versions.restoreVersion(fileId, versionNumber, {\n requestId,\n });\n dispatch(upsertFile(apiFileRecordToCloudFile(data)));\n // The current bytes just changed — drop any cached blob so the\n // next preview reads the restored version, not the in-memory\n // copy of the version that was active before restore.\n invalidateBlobCache(fileId);\n invalidateOfficeExtraction(fileId);\n // Reload version list to pick up the new synthetic version row.\n await dispatch(loadFileVersions({ fileId })).unwrap();\n } finally {\n dispatch(setFileLoading({ id: fileId, loading: false }));\n releaseRequest(requestId);\n }\n },\n);\n\n/**\n * THE edit-save path: write new content as the next version of the SAME file.\n *\n * Every editor that saves a file's text goes through here — never `uploadFiles`.\n * `uploadFiles` is the NEW-file path: it auto-renames a colliding name to\n * \"name (1).ext\" (Drive semantics for a dropped duplicate), so an editor that\n * re-uploaded its own file created a second file and left the original at\n * version 1 while the UI said \"Saved\" (2026-09-30).\n *\n * ONE path for everyone: `POST /files/{id}/versions` writes by file ID, so the\n * owner and an editor holding an edit grant on someone else's file save the\n * same way (the server checks the caller's write right, keeps the file's owner\n * and path, and records the caller + `changeSummary` on the version). The\n * answer is still REFUSED if it is not that same row (a different `file_id` or\n * `is_new: true`), so a save can never silently become a copy.\n */\nexport const saveFileNewVersion = createAsyncThunk<\n SaveFileNewVersionResult,\n SaveFileNewVersionArg,\n ThunkApi\n>(\n \"cloudFiles/saveFileNewVersion\",\n async ({ fileId, content, changeSummary }, { dispatch, getState }) => {\n if (isVirtualResourceId(fileId)) {\n throw new Error(\n \"saveFileNewVersion is for stored files; virtual files save through writeAny.\",\n );\n }\n let record: CloudFile | undefined = getState().cloudFiles.filesById[fileId];\n // A row hydrated for rendering only may not carry its name/mime — read the\n // file's own metadata rather than guessing.\n if (!record?.fileName) {\n const { data } = await Files.getFileMetadata(fileId);\n const fresh = apiFileRecordToCloudFile(data);\n dispatch(upsertFile(fresh));\n record = fresh;\n }\n const mimeType = record.mimeType ?? \"text/plain\";\n const body =\n typeof content === \"string\"\n ? new Blob([content], { type: mimeType })\n : content;\n const upload = new File([body], record.fileName, { type: mimeType });\n\n dispatch(setFileLoading({ id: fileId, loading: true }));\n const requestId = newRequestId();\n registerRequest({\n requestId,\n kind: \"upload\",\n resourceId: fileId,\n resourceType: \"file\",\n });\n try {\n const reqOpts = { requestId, idempotencyKey: requestId };\n const summary = changeSummary ?? \"Edited in place\";\n const { data } = await Files.uploadNewVersion(\n fileId,\n { file: upload, changeSummary: summary },\n reqOpts,\n );\n if (data.file_id !== fileId || data.is_new) {\n throw new Error(\n `Saving \"${record.fileName}\" did not update it — the files service wrote ` +\n `a separate file (${data.file_path}, id ${data.file_id}) instead of ` +\n `version ${record.currentVersion + 1}. Your original is unchanged; ` +\n `remove the extra file from the folder.`,\n );\n }\n dispatch(\n upsertFile({\n id: fileId,\n currentVersion: data.version_number,\n fileSize: data.size_bytes,\n checksum: data.checksum,\n updatedAt: new Date().toISOString(),\n }),\n );\n // The current bytes are the ones just uploaded — cache them as the new\n // version so every viewer shows it without downloading them back.\n replaceWithSavedBytes(fileId, body, data.version_number);\n invalidateOfficeExtraction(fileId);\n // Refresh the history without holding the save on it — the save has\n // landed, and \"Saving…\" must not wait on a list read.\n void dispatch(loadFileVersions({ fileId }))\n .unwrap()\n .catch((err: unknown) => {\n // The save landed; only the history refresh failed. Say so.\n console.error(\n \"[saveFileNewVersion] saved, but the version list did not refresh:\",\n err,\n );\n });\n return { fileId, versionNumber: data.version_number };\n } finally {\n dispatch(setFileLoading({ id: fileId, loading: false }));\n releaseRequest(requestId);\n }\n },\n);\n\n// ---------------------------------------------------------------------------\n// Writes — permissions\n// ---------------------------------------------------------------------------\n\n/**\n * The by-grantee REST grant/revoke endpoints (`GrantPermissionRequest`,\n * `?grantee_type=`) accept only `user | group`. PUBLIC access is not a\n * by-grantee grant — it is toggled on the resource's visibility. Reaching the\n * grant/revoke thunks with `granteeType: \"public\"` is a programming error, so\n * fail loudly (repo principle) rather than silently coercing it to a bogus\n * \"user\"/\"group\" call against an empty/row-id grantee.\n */\nfunction requireByGranteeType(\n granteeType: GranteeType | undefined,\n): \"user\" | \"group\" {\n if (granteeType === \"public\") {\n throw new Error(\n \"Public access is toggled via the resource's visibility, not the by-grantee grant/revoke path.\",\n );\n }\n return granteeType ?? \"user\";\n}\n\nexport const grantPermission = createAsyncThunk<\n void,\n GrantPermissionArg,\n ThunkApi\n>(\"cloudFiles/grantPermission\", async (arg, { dispatch }) => {\n if (isVirtualResourceId(arg.resourceId)) {\n throw new Error(\"Sharing isn't available for this source yet.\");\n }\n const requestId = newRequestId();\n registerRequest({\n requestId,\n kind: \"grant-permission\",\n resourceId: arg.resourceId,\n resourceType: arg.resourceType,\n });\n try {\n const body = {\n grantee_id: arg.granteeId,\n level: arg.level,\n grantee_type: requireByGranteeType(arg.granteeType),\n expires_at: arg.expiresAt ?? null,\n };\n const { data } =\n arg.resourceType === \"folder\"\n ? await Permissions.grantFolderPermission(arg.resourceId, body, {\n requestId,\n })\n : await Permissions.grantFilePermission(arg.resourceId, body, {\n requestId,\n });\n dispatch(\n upsertPermissionsForResource({\n resourceId: arg.resourceId,\n permissions: [rpcPermissionRowToCloudFilePermission(data)],\n }),\n );\n // P1-6: the returned row already makes the UI correct. Reconcile the full\n // list in the BACKGROUND (don't await) so the dialog doesn't stall on a\n // second round-trip after every grant.\n void dispatch(loadPermissions({ resourceId: arg.resourceId }));\n } finally {\n releaseRequest(requestId);\n }\n});\n\nexport const revokePermission = createAsyncThunk<\n void,\n RevokePermissionArg,\n ThunkApi\n>(\"cloudFiles/revokePermission\", async (arg, { dispatch }) => {\n if (isVirtualResourceId(arg.resourceId)) {\n throw new Error(\"Sharing isn't available for this source yet.\");\n }\n const requestId = newRequestId();\n registerRequest({\n requestId,\n kind: \"revoke-permission\",\n resourceId: arg.resourceId,\n resourceType: arg.resourceType,\n });\n try {\n const granteeType = requireByGranteeType(arg.granteeType);\n if (arg.resourceType === \"folder\") {\n await Permissions.revokeFolderPermission(\n arg.resourceId,\n arg.granteeId,\n { granteeType },\n { requestId },\n );\n } else {\n await Permissions.revokeFilePermission(\n arg.resourceId,\n arg.granteeId,\n { granteeType },\n { requestId },\n );\n }\n dispatch(\n removePermissionForResource({\n resourceId: arg.resourceId,\n granteeId: arg.granteeId,\n granteeType,\n }),\n );\n } finally {\n releaseRequest(requestId);\n }\n});\n\n// ---------------------------------------------------------------------------\n// Writes — share links\n// ---------------------------------------------------------------------------\n\nexport const createShareLink = createAsyncThunk<\n CloudShareLink,\n CreateShareLinkArg,\n ThunkApi\n>(\"cloudFiles/createShareLink\", async (arg, { dispatch, getState }) => {\n if (isVirtualResourceId(arg.resourceId)) {\n throw new Error(\"Share links aren't available for this source yet.\");\n }\n // P1-1: a second click while the first create is in flight would mint a\n // second token for the same resource. Suppress the duplicate submit.\n if (hasInFlight(arg.resourceId, \"create-share-link\")) {\n throw new Error(\"A share link is already being created for this item.\");\n }\n const requestId = newRequestId();\n registerRequest({\n requestId,\n kind: \"create-share-link\",\n resourceId: arg.resourceId,\n resourceType: arg.resourceType,\n });\n try {\n // Canonical owner-gated mint over platform.share_links.\n const created = await createCanonicalShareLink({\n resourceType: arg.resourceType,\n resourceId: arg.resourceId,\n permissionLevel: arg.permissionLevel,\n expiresAt: arg.expiresAt ?? null,\n maxUses: arg.maxUses ?? null,\n });\n if (!created.success || !created.token) {\n throw new Error(created.error ?? \"Failed to create share link\");\n }\n // Reload the full list — the create RPC returns only { token, id }.\n await dispatch(\n loadShareLinks({\n resourceId: arg.resourceId,\n resourceType: arg.resourceType,\n }),\n ).unwrap();\n const link = (\n getState().cloudFiles.shareLinksByResourceId[arg.resourceId] ?? []\n ).find((l) => l.shareToken === created.token);\n if (!link) {\n throw new Error(\"Share link was created but could not be loaded back\");\n }\n return link;\n } finally {\n releaseRequest(requestId);\n }\n});\n\nexport const revokeShareLink = createAsyncThunk<\n void,\n RevokeShareLinkArg,\n ThunkApi\n>(\"cloudFiles/revokeShareLink\", async ({ linkId }, { dispatch }) => {\n const requestId = newRequestId();\n registerRequest({\n requestId,\n kind: \"revoke-share-link\",\n resourceId: null,\n resourceType: null,\n });\n try {\n const result = await revokeCanonicalShareLink(linkId);\n if (!result.success) {\n throw new Error(result.error ?? \"Failed to revoke share link\");\n }\n dispatch(removeShareLink({ linkId }));\n } finally {\n releaseRequest(requestId);\n }\n});\n\n// ---------------------------------------------------------------------------\n// Utility — getFileUrl (no slice state change).\n//\n// Routes through the universal handler and returns the durable renderable\n// URL (permanent CDN for public files, the durable authenticated download\n// route otherwise). Durable URLs never expire.\n// ---------------------------------------------------------------------------\n\nexport const getFileUrl = createAsyncThunk<\n { url: string },\n { fileId: string },\n ThunkApi\n>(\"cloudFiles/getFileUrl\", async ({ fileId }) => {\n if (isVirtualResourceId(fileId)) {\n // Virtual files don't have stored bytes — there's no byte URL. Callers\n // already hide the buttons that surface URLs (Download, Copy link,\n // Open in new tab) for virtual rows; this guards the API path for any\n // remaining callers.\n throw new Error(\"Byte URLs aren't available for virtual sources\");\n }\n const url = await fileHandler\n .use({ kind: \"file_id\", fileId })\n .as({ kind: \"html_src\" });\n if (!url) {\n throw new Error(`Could not resolve a URL for file ${fileId}`);\n }\n return { url };\n});\n\n// ---------------------------------------------------------------------------\n// Bulk operations (Python P-7)\n// ---------------------------------------------------------------------------\n//\n// Each bulk thunk applies the optimistic local change up front, then makes a\n// single REST round-trip. The backend returns a per-item succeeded/failed\n// envelope; we re-apply the failed entries by re-upserting their pre-change\n// state from a snapshot taken before the optimistic update.\n\n/**\n * Move many files to Trash (soft delete) in one round-trip.\n *\n * Returns the standard `BulkResponse` envelope so the caller can decide\n * how to surface partial failures (toast vs. row-level error chips).\n * Successful ids are removed from the local store immediately for\n * snappy UI; failed ids are restored from the pre-change snapshot.\n */\n/**\n * P1-5: bulk ops resolve successfully even when some items fail (per-item\n * results in the envelope), so the rejected-thunk toast middleware never sees\n * them. Surface partial failures here so the user isn't left thinking every\n * item succeeded. Full success stays silent (the rows visibly changed).\n */\nfunction toastBulkPartialFailure(result: BulkResponse, verb: string): void {\n if (result.failed > 0) {\n const total = result.succeeded + result.failed;\n toast.error(`Couldn't ${verb} ${result.failed} of ${total} items`, {\n description:\n result.succeeded > 0\n ? `${result.succeeded} succeeded. The rest were restored.`\n : \"All items were restored.\",\n });\n }\n}\n\nexport const bulkDeleteFiles = createAsyncThunk<\n BulkResponse,\n BulkDeleteFilesArg,\n ThunkApi\n>(\"cloudFiles/bulkDeleteFiles\", async (arg, { dispatch, getState }) => {\n if (arg.fileIds.length === 0) {\n return { results: [], succeeded: 0, failed: 0 };\n }\n\n // Snapshot pre-change records for rollback on per-item failures.\n const state = getState();\n const snapshots = new Map<string, CloudFile>();\n for (const id of arg.fileIds) {\n const file = state.cloudFiles.filesById[id];\n if (file) snapshots.set(id, file);\n }\n\n const requestId = newRequestId();\n registerRequest({\n requestId,\n kind: \"bulk-delete-files\",\n resourceId: null,\n resourceType: \"file\",\n });\n\n // Optimistic: remove all targets from the local store and drop their\n // cached blobs (deletion makes the cache entry useless and keeping\n // bytes around for soft-deleted items wastes the LRU budget).\n for (const id of arg.fileIds) {\n dispatch(removeFile({ id }));\n invalidateBlobCache(id);\n invalidateOfficeExtraction(id);\n }\n\n try {\n const { data } = await Files.bulkDeleteFiles(\n { file_ids: arg.fileIds },\n { requestId },\n );\n const result: BulkResponse = data ?? {\n results: arg.fileIds.map((id) => ({ id, ok: true, error: null })),\n succeeded: arg.fileIds.length,\n failed: 0,\n };\n\n // Roll back any items the backend reports as failed.\n for (const r of result.results) {\n if (r.ok) continue;\n const snap = snapshots.get(r.id);\n if (snap) dispatch(upsertFile(snap));\n }\n toastBulkPartialFailure(result, \"delete\");\n return result;\n } catch (err) {\n // Whole-call failure — restore everything we removed.\n for (const snap of snapshots.values()) dispatch(upsertFile(snap));\n throw err;\n } finally {\n releaseRequest(requestId);\n }\n});\n\n/**\n * Move many files to a new parent folder in one round-trip. Pass `null` to\n * move to root.\n */\nexport const bulkMoveFiles = createAsyncThunk<\n BulkResponse,\n BulkMoveFilesArg,\n ThunkApi\n>(\"cloudFiles/bulkMoveFiles\", async (arg, { dispatch, getState }) => {\n if (arg.fileIds.length === 0) {\n return { results: [], succeeded: 0, failed: 0 };\n }\n\n const state = getState();\n const snapshots = new Map<string, CloudFile>();\n for (const id of arg.fileIds) {\n const file = state.cloudFiles.filesById[id];\n if (file) snapshots.set(id, file);\n }\n\n const requestId = newRequestId();\n registerRequest({\n requestId,\n kind: \"bulk-move-files\",\n resourceId: null,\n resourceType: \"file\",\n });\n\n // Optimistic: re-parent + retract from old parent / attach to new in tree.\n for (const [id, file] of snapshots) {\n if (file.parentFolderId === arg.newParentFolderId) continue;\n dispatch(\n detachChildFromFolder({\n parentFolderId: file.parentFolderId,\n kind: \"file\",\n id,\n }),\n );\n dispatch(\n attachChildToFolder({\n parentFolderId: arg.newParentFolderId,\n kind: \"file\",\n id,\n }),\n );\n dispatch(upsertFile({ ...file, parentFolderId: arg.newParentFolderId }));\n }\n\n try {\n const { data } = await Files.bulkMoveFiles(\n {\n file_ids: arg.fileIds,\n new_parent_folder_id: arg.newParentFolderId,\n },\n { requestId },\n );\n\n // Roll back per-item failures.\n for (const r of data.results) {\n if (r.ok) continue;\n const snap = snapshots.get(r.id);\n if (!snap || snap.parentFolderId === arg.newParentFolderId) continue;\n dispatch(\n detachChildFromFolder({\n parentFolderId: arg.newParentFolderId,\n kind: \"file\",\n id: snap.id,\n }),\n );\n dispatch(\n attachChildToFolder({\n parentFolderId: snap.parentFolderId,\n kind: \"file\",\n id: snap.id,\n }),\n );\n dispatch(upsertFile(snap));\n }\n toastBulkPartialFailure(data, \"move\");\n return data;\n } catch (err) {\n // Whole-call failure — restore everything.\n for (const snap of snapshots.values()) {\n if (snap.parentFolderId === arg.newParentFolderId) continue;\n dispatch(\n detachChildFromFolder({\n parentFolderId: arg.newParentFolderId,\n kind: \"file\",\n id: snap.id,\n }),\n );\n dispatch(\n attachChildToFolder({\n parentFolderId: snap.parentFolderId,\n kind: \"file\",\n id: snap.id,\n }),\n );\n dispatch(upsertFile(snap));\n }\n throw err;\n } finally {\n releaseRequest(requestId);\n }\n});\n\n/**\n * Move many folders to a new parent in one round-trip. Pass `null` to move\n * to root. The backend cascades `folder_path` updates to descendants.\n */\nexport const bulkMoveFolders = createAsyncThunk<\n BulkResponse,\n BulkMoveFoldersArg,\n ThunkApi\n>(\"cloudFiles/bulkMoveFolders\", async (arg, { dispatch, getState }) => {\n if (arg.folderIds.length === 0) {\n return { results: [], succeeded: 0, failed: 0 };\n }\n\n const state = getState();\n const snapshots = new Map<string, CloudFolder>();\n for (const id of arg.folderIds) {\n const folder = state.cloudFiles.foldersById[id];\n if (folder) snapshots.set(id, folder);\n }\n\n const requestId = newRequestId();\n registerRequest({\n requestId,\n kind: \"bulk-move-folders\",\n resourceId: null,\n resourceType: \"folder\",\n });\n\n // Optimistic: rewire each folder under the new parent.\n for (const [id, folder] of snapshots) {\n if (folder.parentId === arg.newParentId) continue;\n dispatch(\n detachChildFromFolder({\n parentFolderId: folder.parentId,\n kind: \"folder\",\n id,\n }),\n );\n dispatch(\n attachChildToFolder({\n parentFolderId: arg.newParentId,\n kind: \"folder\",\n id,\n }),\n );\n dispatch(upsertFolder({ ...folder, parentId: arg.newParentId }));\n }\n\n try {\n const { data } = await Folders.bulkMoveFolders(\n { folder_ids: arg.folderIds, new_parent_id: arg.newParentId },\n { requestId },\n );\n\n // Roll back per-item failures.\n for (const r of data.results) {\n if (r.ok) continue;\n const snap = snapshots.get(r.id);\n if (!snap || snap.parentId === arg.newParentId) continue;\n dispatch(\n detachChildFromFolder({\n parentFolderId: arg.newParentId,\n kind: \"folder\",\n id: snap.id,\n }),\n );\n dispatch(\n attachChildToFolder({\n parentFolderId: snap.parentId,\n kind: \"folder\",\n id: snap.id,\n }),\n );\n dispatch(upsertFolder(snap));\n }\n toastBulkPartialFailure(data, \"move\");\n return data;\n } catch (err) {\n for (const snap of snapshots.values()) {\n if (snap.parentId === arg.newParentId) continue;\n dispatch(\n detachChildFromFolder({\n parentFolderId: arg.newParentId,\n kind: \"folder\",\n id: snap.id,\n }),\n );\n dispatch(\n attachChildToFolder({\n parentFolderId: snap.parentId,\n kind: \"folder\",\n id: snap.id,\n }),\n );\n dispatch(upsertFolder(snap));\n }\n throw err;\n } finally {\n releaseRequest(requestId);\n }\n});\n"],"mappings":"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","names":["rows"]}
|
|
1
|
+
{"version":3,"sources":["../../../../src/files/engine/redux/thunks.ts"],"sourcesContent":["/**\n * features/files/redux/thunks.ts\n *\n * Async orchestration for cloud files: reads via supabase-js (RLS), writes via\n * the REST API client. Every mutation follows the pattern:\n *\n * 1. Snapshot current state for rollback.\n * 2. Dispatch optimistic reducer immediately.\n * 3. Register the requestId in the ledger (for realtime echo dedup).\n * 4. Call the REST API.\n * 5. On success: markSaved, release ledger entry.\n * 6. On error: dispatch rollback, set error, release ledger entry, rethrow.\n */\n\n\"use client\";\n\nimport { createAsyncThunk } from \"@reduxjs/toolkit\";\nimport type { ThunkDispatch, UnknownAction } from \"@reduxjs/toolkit\";\nimport type { CloudFilesState } from \"../types\";\n\n// Minimal local types — avoids importing from store.ts (which imports this\n// module via rootReducer → middleware chain), breaking the type-level cycle.\ntype StateWithCloudFiles = { cloudFiles: CloudFilesState };\ntype AppDispatch = ThunkDispatch<StateWithCloudFiles, unknown, UnknownAction>;\nimport { supabase } from \"../host/supabase\";\nimport { filesDb, FILE_VERSIONS_TABLE_COLUMNS } from \"../filesDb\";\nimport { pgErrorToError } from \"@ai-matrx/data\";\n\nimport * as Files from \"../api/files\";\nimport {\n claimUpload,\n uploadDedupKey,\n type UploadClaim,\n} from \"../upload/uploadDedupGuard\";\nimport * as Folders from \"../api/folders\";\nimport * as Permissions from \"../api/permissions\";\nimport * as Versions from \"../api/versions\";\nimport {\n createShareLink as createCanonicalShareLink,\n listShareLinks as listCanonicalShareLinks,\n revokeShareLink as revokeCanonicalShareLink,\n} from \"../host/share-links\";\nimport {\n restoreFileDirect,\n restoreFolderDirect,\n softDeleteFileDirect,\n softDeleteFolderDirect,\n} from \"../api/direct\";\nimport { fileHandler } from \"../handler/handler\";\nimport { newRequestId } from \"../host/python-client\";\nimport { extractErrorMessage } from \"@ai-matrx/data/net\";\nimport {\n apiFileRecordToCloudFile,\n dbRowToCloudFilePermission,\n dbRowToCloudFileVersion,\n dbRowToCloudFolder,\n canonicalShareLinkToCloudShareLink,\n parseCloudTreeRows,\n rpcPermissionRowToCloudFilePermission,\n} from \"./converters\";\nimport {\n areCloudFileFieldsLoaded,\n FILE_DB_RECORD_FIELDS,\n fileHintToCloudFilePartial,\n needsOnlyRenderFields,\n type CloudFileHydrationField,\n} from \"./file-hydration\";\nimport {\n registerRequest,\n releaseRequest,\n beginResourceOp,\n isLatestResourceOp,\n hasInFlight,\n} from \"./request-ledger\";\nimport { toast } from \"../host/toast\";\nimport { buildTreeState } from \"./tree-utils\";\nimport {\n isUserVisibleFilePath,\n isUserVisibleFolderPath,\n} from \"../utils/user-visible\";\nimport {\n hasMatchingFileTreeSession,\n runFileTreeSessionOperation,\n} from \"./file-tree-auth-boundary\";\nimport {\n isFileTreeLoadDeferred,\n runWithFileTreeLoadTimeout,\n} from \"./file-tree-timeout\";\nimport {\n invalidate as invalidateBlobCache,\n replaceWithSavedBytes,\n} from \"../hooks/blob-cache\";\nimport { invalidateOfficeExtraction } from \"../hooks/office-extraction-cache\";\nimport {\n addFilePendingRequest,\n attachChildToFolder,\n clearUpload,\n detachChildFromFolder,\n markFileSaved,\n markFolderFullyLoaded,\n removeFile,\n removeFilePendingRequest,\n removeFolder,\n removePermissionForResource,\n removeShareLink,\n replaceTree,\n rollbackFileOptimisticUpdate,\n setFileError,\n setFileField,\n setFileLoading,\n setTreeStatus,\n trackUploadStart,\n updateUploadProgress,\n updateUploadStatus,\n upsertFile,\n upsertFiles,\n upsertFolder,\n upsertFolders,\n upsertPermissionsForResource,\n upsertShareLinksForResource,\n upsertVersionsForFile,\n} from \"./slice\";\nimport { getFileFromState } from \"./selectors\";\n\nimport type {\n BulkDeleteFilesArg,\n BulkMoveFilesArg,\n BulkMoveFoldersArg,\n BulkResponse,\n CloudFile,\n CloudFileFieldSnapshot,\n CloudFileRecord,\n CloudFolderRecord,\n CloudFilePermission,\n CloudFileVersion,\n CloudFolder,\n CloudShareLink,\n CreateShareLinkArg,\n RevokeShareLinkArg,\n DeleteFileArg,\n GranteeType,\n GrantPermissionArg,\n MoveFileArg,\n RenameFileArg,\n ResourceType,\n RestoreVersionArg,\n SaveFileNewVersionArg,\n SaveFileNewVersionResult,\n RevokePermissionArg,\n UpdateFileMetadataArg,\n UploadFilesArg,\n FileIdentityHint,\n} from \"../types\";\n\nimport { getClaimsUser } from \"../host/supabase\";\ntype ThunkApi = { dispatch: AppDispatch; state: StateWithCloudFiles };\n\n// ---------------------------------------------------------------------------\n// Reads\n// ---------------------------------------------------------------------------\n\nexport interface EnsureCloudFileFieldsArg {\n fileId: string;\n fields?: readonly CloudFileHydrationField[];\n hint?: FileIdentityHint;\n}\n\nexport type EnsureCloudFileFieldsOutcome = \"ready\" | \"missing\";\n\nconst fileFieldHydrationInFlight = new Map<\n string,\n Promise<(Partial<CloudFile> & Pick<CloudFile, \"id\">) | null>\n>();\n\nasync function fetchCloudFileFields(\n fileId: string,\n fields: readonly CloudFileHydrationField[],\n): Promise<(Partial<CloudFile> & Pick<CloudFile, \"id\">) | null> {\n const key = `${fileId}:${[...fields].sort().join(\",\")}`;\n const existing = fileFieldHydrationInFlight.get(key);\n if (existing) return existing;\n\n const request = (async () => {\n // A direct PostgREST exact-id read still compiles the entire files.files\n // RLS access expression. On a PDF surface many file chips hydrate at\n // once, so planning alone can cross its statement deadline before the\n // primary-key lookup begins. The server's `/files/{id}` boundary checks\n // the same canonical access rule once and reads by primary key without\n // browser RLS planning. Keep its URL/thumbnail work off this metadata\n // path; consumers that need it use the regular getFile endpoint.\n const { data } = await Files.getFileMetadata(fileId);\n const record = apiFileRecordToCloudFile(data);\n if (record.deletedAt) return null;\n\n if (needsOnlyRenderFields(fields)) {\n return {\n id: record.id,\n fileName: record.fileName,\n mimeType: record.mimeType,\n fileSize: record.fileSize,\n visibility: record.visibility,\n };\n }\n return record;\n })().finally(() => fileFieldHydrationInFlight.delete(key));\n\n fileFieldHydrationInFlight.set(key, request);\n return request;\n}\n\n/**\n * Ensure a durable file id has the requested canonical fields in Redux.\n * Known hints merge first, so the network reads only what is actually absent.\n */\nexport const ensureCloudFileFields = createAsyncThunk<\n EnsureCloudFileFieldsOutcome,\n EnsureCloudFileFieldsArg,\n ThunkApi\n>(\n \"cloudFiles/ensureCloudFileFields\",\n async (\n { fileId, fields = FILE_DB_RECORD_FIELDS, hint },\n { dispatch, getState },\n ) => {\n dispatch(upsertFile(fileHintToCloudFilePartial(fileId, hint)));\n\n if (\n areCloudFileFieldsLoaded(getFileFromState(getState(), fileId), fields)\n ) {\n return \"ready\";\n }\n\n dispatch(setFileLoading({ id: fileId, loading: true }));\n dispatch(setFileError({ id: fileId, error: null }));\n try {\n const partial = await fetchCloudFileFields(fileId, fields);\n if (!partial) return \"missing\";\n dispatch(upsertFile(partial));\n return \"ready\";\n } catch (error) {\n const normalized = pgErrorToError(error);\n dispatch(setFileError({ id: fileId, error: normalized.message }));\n throw normalized;\n } finally {\n dispatch(setFileLoading({ id: fileId, loading: false }));\n }\n },\n);\n\n/**\n * Loads the full tree for the current user via the get_user_file_tree\n * RPC. Normalizes into filesById / foldersById / tree.\n */\n// `get_user_file_tree` server-side cap (`p_limit := LEAST(GREATEST(p_limit, 1), 5000);`).\n// Passing anything > 5000 silently clamps. The FE pages via `p_offset`\n// to recover the full tree for power users (users with >5k files).\nconst TREE_PAGE_SIZE = 5000;\n\n// Sanity cap on the pagination loop — prevents an infinite loop if the\n// RPC ever returns a full page on the page that should be the last.\n// 20 * 5000 = 100,000 rows, well above any realistic user.\nconst TREE_MAX_PAGES = 20;\n\n/**\n * Shared promise for an in-progress full-tree load, so concurrent mounts\n * collapse onto one `get_user_file_tree` RPC. Session-local, not Redux state.\n */\nlet _treeInFlight: Promise<void> | null = null;\nlet _treeInFlightUserId: string | null = null;\n\nexport const loadUserFileTree = createAsyncThunk<\n void,\n { userId: string },\n ThunkApi\n>(\"cloudFiles/loadUserFileTree\", async ({ userId }, { dispatch, getState }) => {\n // Redux is server-seeded before client effects run, so during signed-out\n // hydration or session teardown it can briefly retain a user UUID while\n // supabase-js is already anonymous. Isolate that race at this canonical\n // producer boundary instead of sending an authenticated-only RPC as anon.\n if (!(await hasMatchingFileTreeSession(userId))) return;\n\n // In-flight dedupe. Several surfaces hydrate the tree on mount\n // (CloudFilesRealtimeProvider + every useCloudTree consumer), and their\n // effects all run in the same commit, so a status check in the component\n // can't see a sibling's fetch. Sharing the promise collapses them to one\n // RPC while keeping `.unwrap()` working for every caller (unlike a\n // `condition` guard, which rejects the losers).\n if (_treeInFlight && _treeInFlightUserId === userId) return _treeInFlight;\n\n const run = (async () => {\n dispatch(setTreeStatus({ status: \"loading\" }));\n\n // RPC contract (migration 014, 2026-05-17): identity-locked to\n // `auth.uid()`, returns owner OR explicit-grant rows only (no public\n // leak), excludes `parent_file_id IS NOT NULL` + `system-files/%`\n // paths. So we can consume the response raw — no FE-side ownership\n // or system-path filtering needed. See common-docs systems/files/file-service/WIRE_CONTRACT.md\n // (2026-05-17 \"Phase 1d.5\" entry).\n //\n // Pagination: server caps p_limit at 5000. We loop on `p_offset`\n // until a partial page comes back. Sequential pages — Postgres\n // handles them fast and parallelism would just contend for the\n // same connection.\n try {\n // Bounded per attempt; a timeout in a hidden tab waits for the person to\n // return and retries instead of failing (see file-tree-timeout.ts).\n const rows = await runWithFileTreeLoadTimeout(async (signal) => {\n const rows: ReturnType<typeof parseCloudTreeRows> = [];\n for (let page = 0; page < TREE_MAX_PAGES; page += 1) {\n const { data, error } = await runFileTreeSessionOperation(() =>\n supabase\n .rpc(\"get_user_file_tree\", {\n p_user_id: userId,\n p_limit: TREE_PAGE_SIZE,\n p_offset: page * TREE_PAGE_SIZE,\n p_include_folders: true,\n p_include_deleted: false,\n })\n .abortSignal(signal),\n );\n\n if (error) {\n // An aborted page is the timeout's call, not a server failure:\n // the runner decides (retry on return, or TimeoutError).\n if (!signal.aborted) {\n dispatch(\n setTreeStatus({ status: \"error\", error: error.message }),\n );\n }\n throw error;\n }\n\n const pageRows = parseCloudTreeRows(data);\n rows.push(...pageRows);\n if (pageRows.length < TREE_PAGE_SIZE) break;\n }\n return rows;\n });\n\n // Every field pushed below is always set from the non-optional\n // `CloudTreeRow` columns — narrowed here so the tree-spine reconstruction\n // (below) doesn't need a non-null assertion on `f.id` or a nullish-default\n // fallback on `f.ownerId`, which would otherwise silently paper over a\n // real parse gap.\n type PartialCloudFileWithId = Partial<CloudFile> &\n Pick<\n CloudFile,\n | \"id\"\n | \"ownerId\"\n | \"filePath\"\n | \"fileName\"\n | \"visibility\"\n | \"currentVersion\"\n | \"createdAt\"\n | \"updatedAt\"\n | \"deletedAt\"\n >;\n type PartialCloudFolderWithId = Partial<CloudFolder> &\n Pick<\n CloudFolder,\n | \"id\"\n | \"ownerId\"\n | \"folderPath\"\n | \"folderName\"\n | \"visibility\"\n | \"createdAt\"\n | \"updatedAt\"\n | \"deletedAt\"\n >;\n const files: PartialCloudFileWithId[] = [];\n const folders: PartialCloudFolderWithId[] = [];\n for (const row of rows) {\n if (row.kind === \"file\") {\n // No client-side visibility filter: `get_user_file_tree` already\n // applied `files.is_user_visible_path`. A second rule here is the\n // D16 disagreement this cutover deleted.\n files.push({\n id: row.id,\n ownerId: row.owner_id,\n filePath: row.file_path,\n fileName: row.file_name,\n parentFolderId: row.parent_folder_id,\n mimeType: row.mime_type,\n // Phase 0 rename: `file_size` → `size_bytes`. The\n // `CloudTreeFileRow` shape and Supabase row both carry the new\n // name now. See docs/PYTHON_UPDATES.md §3.\n fileSize: row.size_bytes,\n visibility: row.visibility,\n currentVersion: row.current_version,\n createdAt: row.created_at,\n updatedAt: row.updated_at,\n deletedAt: row.deleted_at,\n originDeviceId: row.origin_device_id,\n metadata: row.metadata,\n });\n } else {\n folders.push({\n id: row.id,\n ownerId: row.owner_id,\n folderPath: row.folder_path,\n folderName: row.folder_name,\n parentId: row.parent_id,\n visibility: row.visibility,\n createdAt: row.created_at,\n updatedAt: row.updated_at,\n deletedAt: row.deleted_at,\n metadata: row.metadata,\n });\n }\n }\n\n dispatch(upsertFiles(files));\n dispatch(upsertFolders(folders));\n\n // Drop rows the DATABASE's predicate rejects that slipped into the\n // store through realtime before the mirror was applied. FastFire rows\n // are user-visible now and are deliberately NOT purged.\n const prior = getState().cloudFiles;\n for (const [id, rec] of Object.entries(prior.filesById)) {\n if (rec && !isUserVisibleFilePath(rec.filePath)) {\n dispatch(removeFile({ id }));\n }\n }\n for (const [id, rec] of Object.entries(prior.foldersById)) {\n if (\n rec &&\n rec.source.kind === \"real\" &&\n !isUserVisibleFolderPath(rec.folderPath)\n ) {\n dispatch(removeFolder({ id }));\n }\n }\n\n // Build tree spine directly from the just-parsed rows.\n // the normalized slice back out, and avoids any race with batched dispatch.\n const fileIds = files.map((f) => f.id).filter(Boolean);\n const folderIds = folders.map((f) => f.id).filter(Boolean);\n const tree = buildTreeState({\n fileIds,\n folderIds,\n filesById: Object.fromEntries(\n files.map((f) => [\n f.id,\n {\n id: f.id,\n ownerId: f.ownerId,\n filePath: f.filePath,\n fileName: f.fileName,\n mimeType: f.mimeType ?? null,\n fileSize: f.fileSize ?? null,\n checksum: null,\n visibility: f.visibility,\n currentVersion: f.currentVersion,\n parentFolderId: f.parentFolderId ?? null,\n metadata: f.metadata ?? {},\n createdAt: f.createdAt,\n updatedAt: f.updatedAt,\n deletedAt: f.deletedAt,\n // Tree-spine reconstruction is internal — the computed URL fields\n // and the Phase-1b backend thumbnail_url are only populated when\n // records arrive via the REST API. Default all to null here; the\n // file grid (`MediaThumbnail`) falls through to `useFileAsset` →\n // `Asset.variants[\"thumbnail_url\"]` when these are absent.\n publicUrl: null,\n url: null,\n cdnUrl: null,\n downloadUrl: null,\n thumbnailUrl: null,\n source: { kind: \"real\" },\n _dirty: false,\n _dirtyFields: {},\n _fieldHistory: {},\n _loadedFields: {},\n _loading: false,\n _error: null,\n _pendingRequestIds: [],\n },\n ]),\n ),\n foldersById: Object.fromEntries(\n folders.map((f) => [\n f.id,\n {\n id: f.id,\n ownerId: f.ownerId,\n folderPath: f.folderPath,\n folderName: f.folderName,\n parentId: f.parentId ?? null,\n visibility: f.visibility,\n metadata: f.metadata ?? {},\n createdAt: f.createdAt,\n updatedAt: f.updatedAt,\n deletedAt: f.deletedAt,\n // Tree-spine reconstruction is internal — public_url is only\n // populated when records arrive via the API. Default to null\n // here; surfaces that need a CDN URL fetch via useFileSrc.\n publicUrl: null,\n source: { kind: \"real\" },\n _dirty: false,\n _dirtyFields: {},\n _fieldHistory: {},\n _loadedFields: {},\n _loading: false,\n _error: null,\n _pendingRequestIds: [],\n },\n ]),\n ),\n });\n dispatch(\n replaceTree({\n rootFolderIds: tree.rootFolderIds,\n rootFileIds: tree.rootFileIds,\n childrenByFolderId: tree.childrenByFolderId,\n // This RPC returns the complete discoverable tree, not one lazy level.\n // Mark every real folder so navigation never re-downloads up to 100k\n // rows merely because the user opened a different folder.\n fullyLoadedFolderIds: Object.fromEntries(\n folderIds.map((id) => [id, true] as const),\n ),\n }),\n );\n } catch (error) {\n // A load deferred by a hidden tab is not a failure: back to idle, and\n // the next consumer reloads. A visible timeout arrives as TimeoutError.\n if (isFileTreeLoadDeferred(error)) {\n dispatch(setTreeStatus({ status: \"idle\" }));\n throw error;\n }\n const message =\n error instanceof Error\n ? error.message\n : \"Could not load your file library.\";\n dispatch(setTreeStatus({ status: \"error\", error: message }));\n throw error;\n }\n })();\n\n _treeInFlight = run;\n _treeInFlightUserId = userId;\n try {\n await run;\n } finally {\n if (_treeInFlight === run) {\n _treeInFlight = null;\n _treeInFlightUserId = null;\n }\n }\n});\n\n/**\n * Re-run the tree load. Fired by the realtime middleware when the subscription\n * reconnects after an outage — guarantees we don't miss events.\n */\nexport const reconcileTree = createAsyncThunk<\n void,\n { userId: string },\n ThunkApi\n>(\"cloudFiles/reconcileTree\", async ({ userId }, { dispatch }) => {\n await dispatch(loadUserFileTree({ userId })).unwrap();\n});\n\nexport const loadFolderContents = createAsyncThunk<\n void,\n { folderId: string },\n ThunkApi\n>(\"cloudFiles/loadFolderContents\", async ({ folderId }, { dispatch }) => {\n // Virtual folders use their adapter's `list()` via `loadVirtualChildren`\n // — `cld_*` tables don't know about them and would 22P02 on the synthetic id.\n if (folderId.startsWith(\"vfs:\")) return;\n // RLS intentionally permits contextual reads for files conveyed through a\n // shared conversation, so a raw table enumeration here leaks those rows into\n // the global Files UI. Refresh through the discoverability-gated tree RPC —\n // one canonical list contract for roots and nested folders alike.\n const { data, error } = await getClaimsUser(supabase);\n if (error) throw error;\n if (!data.user?.id)\n throw new Error(\"Cannot load folder contents without a user\");\n await dispatch(loadUserFileTree({ userId: data.user.id })).unwrap();\n dispatch(markFolderFullyLoaded({ folderId }));\n});\n\n// Virtual ids look like `vfs:<adapter>:<vid>` and can't be queried against\n// the real `cld_*` tables — Postgres rejects them with `22P02 invalid input\n// syntax for type uuid`. The version / permission / share-link surfaces\n// aren't mapped to virtual sources in v1, so these thunks short-circuit\n// for synthetic ids. Consumers (FileVersionsList, useSharing) also render\n// a \"not supported here\" empty state so the UX is clear, not silent.\nfunction isVirtualResourceId(id: string): boolean {\n return id.startsWith(\"vfs:\");\n}\n\nexport const loadFileVersions = createAsyncThunk<\n void,\n { fileId: string },\n ThunkApi\n>(\"cloudFiles/loadFileVersions\", async ({ fileId }, { dispatch }) => {\n if (isVirtualResourceId(fileId)) return;\n const { data, error } = await filesDb(supabase)\n .from(\"file_versions\")\n .select(FILE_VERSIONS_TABLE_COLUMNS)\n .eq(\"file_id\", fileId)\n .order(\"version_number\", { ascending: false });\n if (error) throw pgErrorToError(error);\n const versions: CloudFileVersion[] = (data ?? []).map(\n dbRowToCloudFileVersion,\n );\n dispatch(upsertVersionsForFile({ fileId, versions }));\n});\n\nexport const loadPermissions = createAsyncThunk<\n void,\n { resourceId: string },\n ThunkApi\n>(\"cloudFiles/loadPermissions\", async ({ resourceId }, { dispatch }) => {\n if (isVirtualResourceId(resourceId)) return;\n // Canonical grant store is `public.permissions` (resource_type='file'),\n // NOT the legacy cld_ file-permission duplicate. RLS returns the rows the\n // caller is allowed to see (own grants / grants to them / org / public).\n const { data, error } = await supabase\n .schema(\"iam\")\n .from(\"permissions\")\n .select(\"*\")\n .eq(\"resource_type\", \"file\")\n .eq(\"resource_id\", resourceId);\n if (error) throw pgErrorToError(error);\n const permissions: CloudFilePermission[] = (data ?? []).map(\n dbRowToCloudFilePermission,\n );\n dispatch(upsertPermissionsForResource({ resourceId, permissions }));\n});\n\nexport const loadShareLinks = createAsyncThunk<\n void,\n { resourceId: string; resourceType?: ResourceType },\n ThunkApi\n>(\n \"cloudFiles/loadShareLinks\",\n async ({ resourceId, resourceType = \"file\" }, { dispatch }) => {\n if (isVirtualResourceId(resourceId)) return;\n // Canonical owner-gated list over platform.share_links.\n const links = await listCanonicalShareLinks(resourceType, resourceId);\n const shareLinks: CloudShareLink[] = links\n .filter((l) => l.isActive)\n .map((l) =>\n canonicalShareLinkToCloudShareLink(l, resourceType, resourceId),\n );\n dispatch(upsertShareLinksForResource({ resourceId, shareLinks }));\n },\n);\n\n// ---------------------------------------------------------------------------\n// Writes — folders\n// ---------------------------------------------------------------------------\n//\n// Folder-CRUD endpoints (`POST /folders`, `PATCH /folders/{id}`,\n// `DELETE /folders/{id}`) shipped 2026-04-26. These thunks now hit\n// the REST surface; legacy direct supabase-js writes were retired.\n\n/**\n * Create a folder via the Python REST contract. We hit `POST /folders` with\n * the explicit `{ folder_name, parent_id }` form because callers already\n * resolve the parent id from the tree. The path-style form (`folder_path`)\n * is preferred for upload-time auto-create, which `uploadFiles` handles.\n *\n * Architecturally important: this thunk no longer touches `cld_folders` from\n * the browser. Folder writes were the single biggest source of RLS recursion\n * regressions; routing them through the backend (which uses SECURITY DEFINER\n * helpers) makes the path uniform with file uploads.\n */\nexport const createFolder = createAsyncThunk<\n string,\n import(\"../types\").CreateFolderArg,\n ThunkApi\n>(\"cloudFiles/createFolder\", async (arg, { dispatch, getState }) => {\n const folderName = arg.folderName.trim();\n if (!folderName) {\n throw new Error(\"Folder name cannot be empty.\");\n }\n if (/[/\\\\]/.test(folderName)) {\n throw new Error(\"Folder names cannot contain '/' or '\\\\'.\");\n }\n // Reject NUL + ASCII control characters. These can be paste-injected\n // and confuse downstream object stores or any layer that materialises\n // the path on disk. Mirrors the rule in `validateRenameInput`.\n if (/[\\x00-\\x1f\\x7f]/.test(folderName)) {\n throw new Error(\"Folder names cannot contain control characters.\");\n }\n if (folderName.endsWith(\".\")) {\n throw new Error(\"Folder names cannot end with '.'.\");\n }\n // Sibling collision check — case-insensitive. Without this the thunk\n // happily issues `POST /folders` with a duplicate name and the backend\n // either returns a confusing UNIQUE-constraint error or (worse) silently\n // creates a sibling that's indistinguishable from the existing one.\n {\n const state = getState().cloudFiles;\n const lowered = folderName.toLowerCase();\n const collision = Object.values(state.foldersById).some(\n (f) =>\n !!f &&\n !f.deletedAt &&\n (f.parentId ?? null) === (arg.parentId ?? null) &&\n f.folderName.toLowerCase() === lowered,\n );\n if (collision) {\n throw new Error(\n `A folder named '${folderName}' already exists in this location.`,\n );\n }\n }\n\n const requestId = newRequestId();\n registerRequest({\n requestId,\n kind: \"folder-create\",\n resourceId: null,\n resourceType: \"folder\",\n });\n\n // Always use path-style: the backend rejects `{folder_name, parent_id}`\n // with `validation_error` (both at root with parent_id=null AND nested\n // with a parent uuid). Path-style creation is idempotent and creates\n // any missing intermediate segments. For nested creation we resolve the\n // parent's path from local state and append the new segment.\n let folderPath = folderName;\n if (arg.parentId !== null) {\n const parent = getState().cloudFiles.foldersById[arg.parentId];\n if (!parent) {\n // The parent isn't in this view's slice — a stale tree, not a missing\n // folder. Say what the user can do; the id stays out of the sentence\n // and in the rejected-thunk capture.\n throw new Error(\n \"We couldn't create the folder here — this view is out of date. Refresh and try again.\",\n );\n }\n folderPath = `${parent.folderPath}/${folderName}`;\n }\n const body = {\n folder_path: folderPath,\n visibility: arg.visibility ?? \"personal\",\n metadata: arg.metadata ?? null,\n };\n\n try {\n const { data: row } = await Folders.createFolder(body, { requestId });\n\n const folder = dbRowToCloudFolder(row);\n dispatch(upsertFolder(folder));\n dispatch(\n attachChildToFolder({\n parentFolderId: folder.parentId,\n kind: \"folder\",\n id: folder.id,\n }),\n );\n return folder.id;\n } catch (err) {\n if (process.env.NODE_ENV !== \"production\") {\n const e = err as {\n status?: number | null;\n code?: string;\n detail?: string;\n userMessage?: string;\n details?: unknown;\n };\n // Surface the full backend response so we can fix the contract drift\n // rather than show a generic \"request failed\" toast.\n console.error(\"[createFolder] backend error\", {\n status: e.status ?? null,\n code: e.code,\n detail: e.detail,\n userMessage: e.userMessage,\n details: e.details,\n sentBody: body,\n });\n }\n throw err;\n } finally {\n releaseRequest(requestId);\n }\n});\n\n/**\n * Update folder properties — rename, move (`parentId`), change visibility,\n * patch metadata. Optimistic: applies the patch locally before the REST call\n * and rolls back on failure.\n */\nexport const updateFolder = createAsyncThunk<\n void,\n import(\"../types\").UpdateFolderArg,\n ThunkApi\n>(\"cloudFiles/updateFolder\", async (arg, { dispatch, getState }) => {\n const state = getState();\n const folder = state.cloudFiles.foldersById[arg.folderId];\n if (!folder)\n throw new Error(\n \"We couldn't update that folder — this view is out of date. Refresh and try again.\",\n );\n\n const requestId = newRequestId();\n registerRequest({\n requestId,\n kind: \"folder-update\",\n resourceId: arg.folderId,\n resourceType: \"folder\",\n });\n\n // Optimistic: apply the patch locally.\n const patch = arg.patch;\n const optimistic: CloudFolder = {\n ...folder,\n ...(patch.folderName !== undefined ? { folderName: patch.folderName } : {}),\n ...(patch.parentId !== undefined ? { parentId: patch.parentId } : {}),\n ...(patch.visibility !== undefined ? { visibility: patch.visibility } : {}),\n ...(patch.metadata !== undefined ? { metadata: patch.metadata } : {}),\n };\n dispatch(upsertFolder(optimistic));\n\n // Track move in tree state — detach from old parent, attach under new.\n if (patch.parentId !== undefined && patch.parentId !== folder.parentId) {\n dispatch(\n detachChildFromFolder({\n parentFolderId: folder.parentId,\n kind: \"folder\",\n id: folder.id,\n }),\n );\n dispatch(\n attachChildToFolder({\n parentFolderId: patch.parentId,\n kind: \"folder\",\n id: folder.id,\n }),\n );\n }\n\n // The backend PATCH /folders honors folder_path / visibility / metadata ONLY;\n // it silently ignores folder_name / parent_id (same as POST /folders, which\n // rejects them). Rename AND move must therefore be expressed as the TARGET\n // folder_path — the server renames, reparents, and cascades descendants from\n // it. Sending folder_name/parent_id no-op'd server-side (rename/move reverted\n // on refresh). Compute the target path from the new name + new parent's path.\n let targetFolderPath: string | undefined;\n if (patch.folderName !== undefined || patch.parentId !== undefined) {\n const newName = patch.folderName ?? folder.folderName;\n const newParentId =\n patch.parentId !== undefined ? patch.parentId : folder.parentId;\n if (newParentId) {\n const newParent = state.cloudFiles.foldersById[newParentId];\n if (!newParent) {\n throw new Error(\n \"We couldn't move the folder there — this view is out of date. Refresh and try again.\",\n );\n }\n targetFolderPath = `${newParent.folderPath}/${newName}`;\n } else {\n targetFolderPath = newName;\n }\n }\n\n try {\n const { data: row } = await Folders.patchFolder(\n arg.folderId,\n {\n folder_path: targetFolderPath,\n visibility: patch.visibility,\n metadata: patch.metadata ?? null,\n },\n { requestId },\n );\n dispatch(upsertFolder(dbRowToCloudFolder(row)));\n } catch (err) {\n // Roll back to the pre-edit folder state and tree links (strip frozen\n // runtime fields — re-upserting a store record crashes the merge).\n dispatch(upsertFolder(toCloudFolderPartial(folder)));\n if (patch.parentId !== undefined && patch.parentId !== folder.parentId) {\n dispatch(\n detachChildFromFolder({\n parentFolderId: patch.parentId,\n kind: \"folder\",\n id: folder.id,\n }),\n );\n dispatch(\n attachChildToFolder({\n parentFolderId: folder.parentId,\n kind: \"folder\",\n id: folder.id,\n }),\n );\n }\n throw err;\n } finally {\n releaseRequest(requestId);\n }\n});\n\nexport const deleteFolder = createAsyncThunk<\n void,\n import(\"../types\").DeleteFolderArg,\n ThunkApi\n>(\"cloudFiles/deleteFolder\", async (arg, { dispatch, getState }) => {\n const state = getState();\n const folder = state.cloudFiles.foldersById[arg.folderId];\n if (!folder) return;\n\n const requestId = newRequestId();\n registerRequest({\n requestId,\n kind: \"folder-delete\",\n resourceId: arg.folderId,\n resourceType: \"folder\",\n });\n\n try {\n // Soft delete (Trash) cascades the subtree in SQL (soft_delete_folder) →\n // direct. There is no hard delete.\n await softDeleteFolderDirect(arg.folderId);\n dispatch(removeFolder({ id: arg.folderId }));\n } finally {\n releaseRequest(requestId);\n }\n});\n\n/**\n * Ensure every segment of `folderPath` exists; create any that don't.\n * Returns the leaf folder's id. Used for convention-based uploads like\n * \"save all pasted images under /Images\".\n */\nexport const ensureFolderPath = createAsyncThunk<\n string,\n import(\"../types\").EnsureFolderPathArg,\n ThunkApi\n>(\"cloudFiles/ensureFolderPath\", async (arg, { dispatch, getState }) => {\n const normalizedPath = arg.folderPath\n .split(\"/\")\n .map((s) => s.trim())\n .filter(Boolean)\n .join(\"/\");\n if (!normalizedPath) {\n throw new Error(\"folderPath cannot be empty.\");\n }\n\n // Realtime/tree state is the zero-request fast path.\n const existing = Object.values(getState().cloudFiles.foldersById).find(\n (folder) => folder.folderPath === normalizedPath && !folder.deletedAt,\n );\n if (existing) {\n return existing.id;\n }\n\n // The backend owns path traversal and race handling. One atomic request\n // replaces the obsolete per-segment files.folders PostgREST reads.\n const requestId = newRequestId();\n registerRequest({\n requestId,\n kind: \"folder-create\",\n resourceId: null,\n resourceType: \"folder\",\n });\n try {\n const { data: row } = await Folders.ensureFolderPath(\n normalizedPath,\n arg.visibility ?? \"personal\",\n { requestId },\n );\n const folder = dbRowToCloudFolder(row);\n dispatch(upsertFolder(folder));\n dispatch(\n attachChildToFolder({\n parentFolderId: folder.parentId,\n kind: \"folder\",\n id: folder.id,\n }),\n );\n return folder.id;\n } finally {\n releaseRequest(requestId);\n }\n});\n\n// ---------------------------------------------------------------------------\n// Writes — uploads (multi-file with progress)\n// ---------------------------------------------------------------------------\n\n/**\n * Say, on the screen the person is actually looking at, that an upload failed —\n * in the SERVER'S words.\n *\n * 🚨 THE 2026-09-19 SILENT FAILURE (`common-docs/systems/knowledge/ingestion/projects/acquisition-frontier/\n * own-files/VERIFICATION.md` §15/§16). A 21-file drop on a fresh Rulebook fired\n * 21 uploads and every one came back `400 matrx-files: this write carries no\n * organization`. Every sentence was captured correctly — into\n * `failed[].error` and into the upload slice — and then shown NOWHERE, because:\n *\n * - `mutation-toast-middleware` deliberately skips `uploadFiles`, on the\n * reasoning that \"uploads surface per-file errors inline in the\n * UploadProgressList\". That is true on the Files surface and FALSE\n * everywhere else — the Rulebook sources panel never renders that list.\n * - the capture toolbar that started the drop collapses N failures into\n * \"Failed to upload N files\", dropping every reason.\n *\n * So the person saw a card that still read \"Nothing attached yet\" and no reason\n * anywhere. An upload that failed must never be indistinguishable from an\n * upload nobody started.\n *\n * This announces from the THUNK, which every caller shares, so no surface can\n * forget. Reasons are deduped (21 identical 400s are one sentence, not 21\n * toasts), and the toast carries a stable id so the surrounding UploadProgressList\n * or capture toolbar cannot stack a second copy of the same news.\n */\nexport function announceUploadFailures(\n failed: ReadonlyArray<{ name: string; error: string }>,\n): void {\n if (failed.length === 0) return;\n const reasons = Array.from(\n new Set(failed.map((f) => f.error).filter((e) => e && e.trim().length > 0)),\n );\n const title =\n failed.length === 1\n ? `Couldn't upload ${failed[0].name}`\n : `Couldn't upload ${failed.length} files`;\n toast.error(title, {\n // The server's own sentence, never a sentence of ours. When several\n // distinct things went wrong, each one gets said.\n description:\n reasons.length > 0\n ? reasons.join(\" · \")\n : \"The server refused the upload and gave no reason.\",\n id: `upload-failed:${failed.map((f) => f.name).join(\"|\")}`,\n duration: 12000,\n });\n}\n\nexport const uploadFiles = createAsyncThunk<\n { uploaded: string[]; failed: Array<{ name: string; error: string }> },\n UploadFilesArg,\n ThunkApi\n>(\"cloudFiles/uploadFiles\", async (arg, { dispatch, getState }) => {\n const concurrency = Math.max(1, arg.concurrency ?? 3);\n const uploaded: string[] = [];\n // Track REAL error per file (not just filename). Without this, every\n // upload failure surfaces to callers as the file's name rather than the\n // backend's actual error code/message — which is what made the Phase\n // 11 migration look \"broken\" when really we just couldn't see why.\n const failed: Array<{ name: string; error: string }> = [];\n\n // Resolve logical path prefix.\n //\n // Order of preference:\n // 1. `folderPath` arg — passed directly (the Python backend\n // auto-creates the hierarchy server-side; the browser never has to\n // query `cld_folders` via supabase-js, which avoids the\n // well-known RLS recursion bug on the legacy file-permission table).\n // 2. `parentFolderId` — look up the folder in slice state (works when\n // the folder is already loaded from the tree RPC or realtime).\n // 3. Empty prefix — file lands at root.\n const state = getState();\n let prefix = \"\";\n if (arg.folderPath) {\n prefix = `${arg.folderPath.replace(/^\\/+|\\/+$/g, \"\")}/`;\n } else if (arg.parentFolderId) {\n const parentFolder = state.cloudFiles.foldersById[arg.parentFolderId];\n if (parentFolder) {\n prefix = `${parentFolder.folderPath}/`;\n }\n }\n\n // ─── Collision handling ───────────────────────────────────────────────\n //\n // Build a case-insensitive set of file names already present in the\n // destination. Used by `uniqueName` below to auto-rename uploads that\n // collide (\"report.pdf\" → \"report (1).pdf\"). Without this, the backend\n // either silently overwrites or version-bumps the existing file —\n // neither matches user expectations from Drive / Dropbox where a\n // duplicate upload becomes \"report (1).pdf\" by default.\n //\n // We only consider files in the SAME parent folder. The lookup is by\n // path-prefix because virtual folders may not have stable ids in\n // local state at upload time (e.g. `folderPath` arg with no\n // pre-loaded folder row).\n const existingNames = (() => {\n const out = new Set<string>();\n const targetPrefix = prefix; // captured outside; matches \"<path>/<name>\"\n for (const f of Object.values(state.cloudFiles.filesById)) {\n if (!f) continue;\n if (f.deletedAt) continue;\n // Match by storage path under the prefix. `f.filePath` is the\n // backend's canonical path including the file name.\n if (!f.filePath) continue;\n const normalized = f.filePath.replace(/^\\/+/, \"\");\n if (!normalized.startsWith(targetPrefix)) continue;\n const tail = normalized.slice(targetPrefix.length);\n // Skip files nested deeper than this folder.\n if (tail.includes(\"/\")) continue;\n if (tail) out.add(tail.toLowerCase());\n }\n return out;\n })();\n\n /**\n * Pick the next non-colliding name, mutating the queue:\n * \"report.pdf\" → \"report.pdf\" (no collision)\n * \"report.pdf\" → \"report (1).pdf\" (one collision)\n * \"report.pdf\" → \"report (2).pdf\" (two collisions)\n * \".env\" → \".env (1)\" (hidden file: suffix only)\n *\n * Reserves the chosen name in `existingNames` so back-to-back uploads\n * of the same source file (drop the same File twice) don't collide\n * with each other.\n */\n function uniqueName(originalName: string): string {\n const lowered = originalName.toLowerCase();\n if (!existingNames.has(lowered)) {\n existingNames.add(lowered);\n return originalName;\n }\n const dot = originalName.lastIndexOf(\".\");\n const isHidden = originalName.startsWith(\".\");\n const stem =\n dot > 0 && !isHidden ? originalName.slice(0, dot) : originalName;\n const ext = dot > 0 && !isHidden ? originalName.slice(dot) : \"\";\n for (let i = 1; i < 1000; i++) {\n const candidate = `${stem} (${i})${ext}`;\n if (!existingNames.has(candidate.toLowerCase())) {\n existingNames.add(candidate.toLowerCase());\n return candidate;\n }\n }\n // 1000 collisions — fall back to original name and let the backend\n // version-bump. Practically never reached.\n return originalName;\n }\n\n // ─── Per-file overrides from the duplicate-upload dialog ────────────\n //\n // When a user picks \"Overwrite\" on a duplicate, the dialog passes the\n // existing file's exact name back as a `filenameOverrides[index]`\n // entry. We honour the override verbatim — the auto \" (1)\" rename\n // would defeat the point. When they pick \"Skip\", the dialog adds the\n // index to `skipIndices` and we drop that file from the queue.\n const overrides = arg.filenameOverrides ?? {};\n const skipSet = new Set(arg.skipIndices ?? []);\n const forceNewCopySet = new Set(arg.forceNewCopyIndices ?? []);\n\n // Reserve override target names in `existingNames` BEFORE the loop\n // runs, so two overrides into the same path can't both win and so\n // a file with no override doesn't accidentally collide-rename onto\n // an override target.\n for (const [, name] of Object.entries(overrides)) {\n if (typeof name === \"string\") existingNames.add(name.toLowerCase());\n }\n\n // Build the queue as [originalIndex, file] pairs so workers can\n // look up overrides by index regardless of dequeue order.\n const queue: Array<{ index: number; file: File }> = arg.files\n .map((file, index) => ({ index, file }))\n .filter(({ index }) => !skipSet.has(index));\n\n async function worker(): Promise<void> {\n while (queue.length) {\n const next = queue.shift();\n if (!next) return;\n const { file, index } = next;\n const overrideName = overrides[index];\n const forcedCopy = forceNewCopySet.has(index);\n\n // Dedup guard (VERIFICATION.md §12, 2026-09-18): a caller-level retry\n // after a transient failure (dev-server connection reset, etc.) can\n // re-dispatch `uploadFiles` for a file whose FIRST attempt already\n // landed — the client has no proof it didn't, and the backend's own\n // `X-Idempotency-Key` only replays a 412, never a success (see\n // `features/files/upload/uploadDedupGuard.ts` for the full trace).\n // Only the plain path participates: an explicit \"Overwrite\" override\n // or \"force new copy\" means the caller WANTS a fresh write.\n const dedupKey =\n !overrideName && !forcedCopy\n ? uploadDedupKey(prefix, file.name, file.size)\n : null;\n let dedupClaim: UploadClaim | null = null;\n if (dedupKey) {\n const claim = claimUpload(dedupKey);\n if (claim.shared) {\n try {\n const outcome = await claim.promise;\n // Reuse the earlier attempt's outcome — no bytes sent again.\n uploaded.push(outcome.fileId);\n continue;\n } catch {\n // The earlier attempt genuinely failed, and the guard already\n // cleared its slot — fall through and upload for real below.\n }\n } else {\n dedupClaim = claim;\n }\n }\n\n const requestId = newRequestId();\n // Override (from duplicate dialog \"Overwrite\") wins. Otherwise\n // the auto \" (1)\" rename runs. We track the display name\n // separately from the original file.name so progress + telemetry\n // continue to use what the user dragged in.\n const targetName =\n typeof overrideName === \"string\" && overrideName\n ? overrideName\n : uniqueName(file.name);\n dispatch(\n trackUploadStart({\n requestId,\n fileName: targetName,\n fileSize: file.size,\n parentFolderId: arg.parentFolderId ?? null,\n folderPath: arg.folderPath ?? null,\n }),\n );\n registerRequest({\n requestId,\n kind: \"upload\",\n resourceId: null,\n resourceType: \"file\",\n });\n try {\n const { data } = await Files.uploadFileWithProgress(\n {\n file,\n filePath: `${prefix}${targetName}`,\n visibility: arg.visibility ?? \"personal\",\n shareWith: arg.shareWith,\n shareLevel: arg.shareLevel,\n changeSummary: arg.changeSummary,\n metadata: arg.metadata,\n options: arg.options,\n ...(forceNewCopySet.has(index)\n ? {\n intent: \"force_new_copy\" as const,\n reason:\n \"User selected Make a copy in the duplicate upload dialog\",\n }\n : {}),\n },\n (ev) =>\n dispatch(\n updateUploadProgress({\n requestId,\n bytesUploaded: ev.loaded,\n }),\n ),\n {\n requestId,\n // Idempotency key — same value across automatic retries of the\n // same intended upload. The backend stores it in\n // `metadata._idempotency_key` so retries don't create duplicate\n // version rows. We reuse `requestId` because it's generated\n // once per intended upload, before any retry.\n idempotencyKey: requestId,\n },\n );\n // Upsert file into slice from response.\n dispatch(\n upsertFile(\n apiFileRecordToCloudFile({\n id: data.file_id,\n owner_id: \"\",\n file_path: data.file_path,\n file_name: data.file_path.split(\"/\").pop() ?? targetName,\n mime_type: file.type || null,\n // Phase 0 rename — see docs/PYTHON_UPDATES.md §3.\n size_bytes: data.size_bytes,\n checksum: data.checksum,\n visibility: arg.visibility ?? \"personal\",\n current_version: data.version_number,\n parent_folder_id: arg.parentFolderId ?? null,\n metadata: arg.metadata ?? {},\n // P1-7: stamp the optimistic row with the current time instead of\n // null. The upload just happened, so \"now\" is accurate for the\n // recents filter / \"sort by modified\" until the realtime echo\n // delivers the authoritative server timestamp. Null sorted the\n // freshly-uploaded file to the wrong end of the list.\n created_at: new Date().toISOString(),\n updated_at: new Date().toISOString(),\n deleted_at: null,\n }),\n ),\n );\n // If this upload replaced an existing file (same logical path,\n // backend bumped current_version), the cached blob for the\n // original version is now stale. Invalidate so the next preview\n // open re-fetches the latest bytes.\n invalidateBlobCache(data.file_id);\n invalidateOfficeExtraction(data.file_id);\n dispatch(\n attachChildToFolder({\n parentFolderId: arg.parentFolderId ?? null,\n kind: \"file\",\n id: data.file_id,\n }),\n );\n dispatch(\n updateUploadStatus({\n requestId,\n status: \"success\",\n fileId: data.file_id,\n }),\n );\n uploaded.push(data.file_id);\n dedupClaim?.settle(\n Promise.resolve({ fileId: data.file_id, filePath: data.file_path }),\n );\n } catch (err) {\n const message = extractErrorMessage(err);\n dispatch(\n updateUploadStatus({\n requestId,\n status: \"error\",\n error: message,\n }),\n );\n failed.push({ name: file.name, error: message });\n dedupClaim?.settle(Promise.reject(err));\n } finally {\n releaseRequest(requestId);\n }\n }\n }\n\n await Promise.all(\n Array.from({ length: Math.min(concurrency, arg.files.length) }, () =>\n worker(),\n ),\n );\n\n announceUploadFailures(failed);\n return { uploaded, failed };\n});\n\nexport const clearUploadEntry = createAsyncThunk<\n void,\n { requestId: string },\n ThunkApi\n>(\"cloudFiles/clearUpload\", async ({ requestId }, { dispatch }) => {\n dispatch(clearUpload({ requestId }));\n});\n\n// ---------------------------------------------------------------------------\n// Writes — optimistic metadata updates\n// ---------------------------------------------------------------------------\n\nexport const renameFile = createAsyncThunk<void, RenameFileArg, ThunkApi>(\n \"cloudFiles/rename\",\n async ({ fileId, newName }, { dispatch, getState }) => {\n const record = getFileFromState(getState(), fileId);\n if (!record)\n throw new Error(\n \"We couldn't find that file in this view. Refresh and try again.\",\n );\n\n const requestId = newRequestId();\n // Per-resource op sequence — guards against an older response clobbering a\n // newer optimistic state (rename A→B→C out of order, or a double-click).\n const seq = beginResourceOp(fileId);\n const snapshot: CloudFileFieldSnapshot = {\n fileName: record.fileName,\n filePath: record.filePath,\n };\n\n // Compute new file path (replace final segment).\n const pathParts = record.filePath.split(\"/\");\n pathParts[pathParts.length - 1] = newName;\n const newPath = pathParts.join(\"/\");\n\n dispatch(setFileField({ id: fileId, field: \"fileName\", value: newName }));\n dispatch(setFileField({ id: fileId, field: \"filePath\", value: newPath }));\n dispatch(addFilePendingRequest({ id: fileId, requestId }));\n registerRequest({\n requestId,\n kind: \"rename\",\n resourceId: fileId,\n resourceType: \"file\",\n });\n\n try {\n // Use the dedicated rename endpoint — it accepts a full new path,\n // auto-creates parent folders if any segment is missing, and lets\n // the backend handle the file_path / storage-location update atomically.\n const { data } = await Files.renameFile(\n fileId,\n { new_path: newPath },\n { requestId },\n );\n // Only apply if no newer op for this file has begun since (out-of-order\n // guard); otherwise the newer op owns the state.\n if (isLatestResourceOp(fileId, seq)) {\n dispatch(upsertFile(apiFileRecordToCloudFile(data)));\n dispatch(markFileSaved({ id: fileId }));\n }\n } catch (err) {\n // Don't roll back if a newer op has superseded this one.\n if (isLatestResourceOp(fileId, seq)) {\n dispatch(rollbackFileOptimisticUpdate({ id: fileId, snapshot }));\n const msg = extractErrorMessage(err);\n dispatch(setFileError({ id: fileId, error: msg }));\n }\n throw err;\n } finally {\n dispatch(removeFilePendingRequest({ id: fileId, requestId }));\n releaseRequest(requestId);\n }\n },\n);\n\nexport const moveFile = createAsyncThunk<void, MoveFileArg, ThunkApi>(\n \"cloudFiles/move\",\n async ({ fileId, newParentFolderId }, { dispatch, getState }) => {\n const record = getFileFromState(getState(), fileId);\n if (!record)\n throw new Error(\n \"We couldn't find that file in this view. Refresh and try again.\",\n );\n const oldParentId = record.parentFolderId;\n const requestId = newRequestId();\n const seq = beginResourceOp(fileId);\n const snapshot: CloudFileFieldSnapshot = {\n parentFolderId: oldParentId,\n };\n\n dispatch(\n setFileField({\n id: fileId,\n field: \"parentFolderId\",\n value: newParentFolderId,\n }),\n );\n dispatch(\n detachChildFromFolder({\n parentFolderId: oldParentId,\n kind: \"file\",\n id: fileId,\n }),\n );\n dispatch(\n attachChildToFolder({\n parentFolderId: newParentFolderId,\n kind: \"file\",\n id: fileId,\n }),\n );\n dispatch(addFilePendingRequest({ id: fileId, requestId }));\n registerRequest({\n requestId,\n kind: \"move\",\n resourceId: fileId,\n resourceType: \"file\",\n });\n\n try {\n // Move = rename to a new full logical path. The backend's rename\n // endpoint auto-creates missing parent folders, so we just compute\n // `<targetFolderPath>/<filename>` (or `<filename>` for root) and\n // hand it over.\n const targetFolder =\n newParentFolderId === null\n ? null\n : (getState().cloudFiles.foldersById[newParentFolderId] ?? null);\n const targetPrefix = targetFolder ? `${targetFolder.folderPath}/` : \"\";\n const newPath = `${targetPrefix}${record.fileName}`;\n\n const { data } = await Files.renameFile(\n fileId,\n { new_path: newPath },\n { requestId },\n );\n if (isLatestResourceOp(fileId, seq)) {\n dispatch(upsertFile(apiFileRecordToCloudFile(data)));\n dispatch(markFileSaved({ id: fileId }));\n }\n } catch (err) {\n if (isLatestResourceOp(fileId, seq)) {\n // Rollback tree membership + field.\n dispatch(\n detachChildFromFolder({\n parentFolderId: newParentFolderId,\n kind: \"file\",\n id: fileId,\n }),\n );\n dispatch(\n attachChildToFolder({\n parentFolderId: oldParentId,\n kind: \"file\",\n id: fileId,\n }),\n );\n dispatch(rollbackFileOptimisticUpdate({ id: fileId, snapshot }));\n const msg = extractErrorMessage(err);\n dispatch(setFileError({ id: fileId, error: msg }));\n }\n throw err;\n } finally {\n dispatch(removeFilePendingRequest({ id: fileId, requestId }));\n releaseRequest(requestId);\n }\n },\n);\n\nexport const updateFileMetadata = createAsyncThunk<\n void,\n UpdateFileMetadataArg,\n ThunkApi\n>(\n \"cloudFiles/updateMetadata\",\n async ({ fileId, patch }, { dispatch, getState }) => {\n const record = getFileFromState(getState(), fileId);\n if (!record)\n throw new Error(\n \"We couldn't find that file in this view. Refresh and try again.\",\n );\n\n const requestId = newRequestId();\n const seq = beginResourceOp(fileId);\n const snapshot: CloudFileFieldSnapshot = {\n visibility: record.visibility,\n metadata: record.metadata,\n };\n\n if (patch.visibility !== undefined) {\n dispatch(\n setFileField({\n id: fileId,\n field: \"visibility\",\n value: patch.visibility,\n }),\n );\n }\n if (patch.metadata !== undefined) {\n dispatch(\n setFileField({\n id: fileId,\n field: \"metadata\",\n value: patch.metadata,\n }),\n );\n }\n dispatch(addFilePendingRequest({ id: fileId, requestId }));\n registerRequest({\n requestId,\n kind: \"update\",\n resourceId: fileId,\n resourceType: \"file\",\n });\n\n try {\n const { data } = await Files.patchFile(\n fileId,\n {\n visibility: patch.visibility,\n metadata: patch.metadata,\n },\n { requestId },\n );\n if (isLatestResourceOp(fileId, seq)) {\n dispatch(upsertFile(apiFileRecordToCloudFile(data)));\n dispatch(markFileSaved({ id: fileId }));\n }\n } catch (err) {\n if (isLatestResourceOp(fileId, seq)) {\n dispatch(rollbackFileOptimisticUpdate({ id: fileId, snapshot }));\n const msg = extractErrorMessage(err);\n dispatch(setFileError({ id: fileId, error: msg }));\n }\n throw err;\n } finally {\n dispatch(removeFilePendingRequest({ id: fileId, requestId }));\n releaseRequest(requestId);\n }\n },\n);\n\n// ---------------------------------------------------------------------------\n// Writes — delete\n// ---------------------------------------------------------------------------\n\nexport const deleteFile = createAsyncThunk<void, DeleteFileArg, ThunkApi>(\n \"cloudFiles/delete\",\n async ({ fileId }, { dispatch, getState }) => {\n const record = getFileFromState(getState(), fileId);\n if (!record) return; // nothing to do\n const parentFolderId = record.parentFolderId;\n const requestId = newRequestId();\n\n // Optimistic remove.\n dispatch(removeFile({ id: fileId }));\n dispatch(\n detachChildFromFolder({\n parentFolderId,\n kind: \"file\",\n id: fileId,\n }),\n );\n // Drop the cached blob bytes — the file is gone, no point holding\n // memory for something the user can't open anymore (and if the\n // delete is rolled back on error, the next open will re-fetch).\n invalidateBlobCache(fileId);\n invalidateOfficeExtraction(fileId);\n registerRequest({\n requestId,\n kind: \"delete\",\n resourceId: fileId,\n resourceType: \"file\",\n });\n\n try {\n // Soft delete (Trash) is a pure metadata write → direct (canonical).\n // There is no hard delete; the file stays restorable from Trash.\n await softDeleteFileDirect(fileId);\n } catch (err) {\n // Rollback — reinsert the record and reattach to its parent.\n dispatch(upsertFile(toCloudFilePartial(record)));\n dispatch(\n attachChildToFolder({\n parentFolderId,\n kind: \"file\",\n id: fileId,\n }),\n );\n throw err;\n } finally {\n releaseRequest(requestId);\n }\n },\n);\n\n// ---------------------------------------------------------------------------\n// Trash — hydrate, restore (delete means archive: no purge from the client)\n// ---------------------------------------------------------------------------\n\n/**\n * Hydrate the trash section. The main tree load runs with\n * `p_include_deleted: false` and every delete/echo REMOVES rows from the\n * store, so soft-deleted records never reach Redux on their own — this thunk\n * pages the same RPC with deleted rows included and upserts ONLY the trashed\n * ones (`deletedAt` set). It never touches the folder tree spine, so trashed\n * rows can't leak into live folder listings; the section filters in\n * PageShell/row-data pick them up by `deletedAt`.\n */\n\n/**\n * A store record carries immer-frozen runtime bookkeeping (`_loadedFields`,\n * `_dirtyFields`, ...). Re-dispatching it into `upsertFile` on the INSERT path\n * would adopt those frozen objects into the new draft and crash the next\n * merge (\"object is not extensible\") — strip to domain fields first. Used by\n * every optimistic-rollback site.\n */\nfunction toCloudFilePartial(record: CloudFileRecord): Partial<CloudFile> {\n const {\n _dirty,\n _dirtyFields,\n _fieldHistory,\n _loadedFields,\n _loading,\n _error,\n _pendingRequestIds,\n ...domain\n } = record;\n return domain;\n}\n\n/** Folder twin of `toCloudFilePartial` — same frozen-runtime-fields hazard. */\nfunction toCloudFolderPartial(record: CloudFolderRecord): Partial<CloudFolder> {\n const {\n _dirty,\n _dirtyFields,\n _fieldHistory,\n _loadedFields,\n _loading,\n _error,\n _pendingRequestIds,\n ...domain\n } = record;\n return domain;\n}\n\nexport const loadTrash = createAsyncThunk<void, { userId: string }, ThunkApi>(\n \"cloudFiles/loadTrash\",\n async ({ userId }, { dispatch }) => {\n const rows: ReturnType<typeof parseCloudTreeRows> = [];\n for (let page = 0; page < TREE_MAX_PAGES; page += 1) {\n const { data, error } = await supabase.rpc(\"get_user_file_tree\", {\n p_user_id: userId,\n p_limit: TREE_PAGE_SIZE,\n p_offset: page * TREE_PAGE_SIZE,\n p_include_folders: true,\n p_include_deleted: true,\n });\n if (error) throw error;\n const pageRows = parseCloudTreeRows(data);\n rows.push(...pageRows);\n if (pageRows.length < TREE_PAGE_SIZE) break;\n }\n\n const files: Partial<CloudFile>[] = [];\n const folders: Partial<CloudFolder>[] = [];\n for (const row of rows) {\n if (!row.deleted_at) continue;\n if (row.kind === \"file\") {\n // Already filtered by the RPC's predicate — see above.\n files.push({\n id: row.id,\n ownerId: row.owner_id,\n filePath: row.file_path,\n fileName: row.file_name,\n parentFolderId: row.parent_folder_id,\n mimeType: row.mime_type,\n fileSize: row.size_bytes,\n visibility: row.visibility,\n currentVersion: row.current_version,\n createdAt: row.created_at,\n updatedAt: row.updated_at,\n deletedAt: row.deleted_at,\n originDeviceId: row.origin_device_id,\n });\n } else {\n folders.push({\n id: row.id,\n ownerId: row.owner_id,\n folderPath: row.folder_path,\n folderName: row.folder_name,\n parentId: row.parent_id,\n visibility: row.visibility,\n createdAt: row.created_at,\n updatedAt: row.updated_at,\n deletedAt: row.deleted_at,\n });\n }\n }\n dispatch(upsertFiles(files));\n dispatch(upsertFolders(folders));\n },\n);\n\n/** Restore a trashed file: flip `deletedAt` off and reattach to its folder. */\nexport const restoreFile = createAsyncThunk<void, { fileId: string }, ThunkApi>(\n \"cloudFiles/restoreFile\",\n async ({ fileId }, { dispatch, getState }) => {\n const record = getFileFromState(getState(), fileId);\n if (!record?.deletedAt) return;\n const requestId = newRequestId();\n\n // Optimistic: back to live, back under its parent.\n dispatch(upsertFile({ ...toCloudFilePartial(record), deletedAt: null }));\n dispatch(\n attachChildToFolder({\n parentFolderId: record.parentFolderId,\n kind: \"file\",\n id: fileId,\n }),\n );\n registerRequest({\n requestId,\n kind: \"file-restore\",\n resourceId: fileId,\n resourceType: \"file\",\n });\n try {\n await restoreFileDirect(fileId);\n } catch (err) {\n dispatch(upsertFile(toCloudFilePartial(record))); // rollback to trashed (clone — state objects are frozen)\n dispatch(\n detachChildFromFolder({\n parentFolderId: record.parentFolderId,\n kind: \"file\",\n id: fileId,\n }),\n );\n throw err;\n } finally {\n releaseRequest(requestId);\n }\n },\n);\n\n/**\n * Restore a trashed folder + its subtree. The RPC un-deletes descendants in\n * SQL; rather than mirroring that cascade optimistically we reconcile by\n * reloading the live tree afterwards (folder restore is rare — correctness\n * over cleverness).\n */\nexport const restoreFolder = createAsyncThunk<\n void,\n { folderId: string },\n ThunkApi\n>(\"cloudFiles/restoreFolder\", async ({ folderId }, { dispatch, getState }) => {\n const folder = getState().cloudFiles.foldersById[folderId];\n if (!folder?.deletedAt) return;\n await restoreFolderDirect(folderId);\n dispatch(upsertFolder({ ...toCloudFolderPartial(folder), deletedAt: null }));\n const userId = folder.ownerId;\n await dispatch(loadUserFileTree({ userId }));\n await dispatch(loadTrash({ userId }));\n});\n\n// ---------------------------------------------------------------------------\n// Writes — versions\n// ---------------------------------------------------------------------------\n\nexport const restoreVersion = createAsyncThunk<\n void,\n RestoreVersionArg,\n ThunkApi\n>(\n \"cloudFiles/restoreVersion\",\n async ({ fileId, versionNumber }, { dispatch }) => {\n if (isVirtualResourceId(fileId)) return;\n dispatch(setFileLoading({ id: fileId, loading: true }));\n const requestId = newRequestId();\n registerRequest({\n requestId,\n kind: \"restore-version\",\n resourceId: fileId,\n resourceType: \"file\",\n });\n try {\n const { data } = await Versions.restoreVersion(fileId, versionNumber, {\n requestId,\n });\n dispatch(upsertFile(apiFileRecordToCloudFile(data)));\n // The current bytes just changed — drop any cached blob so the\n // next preview reads the restored version, not the in-memory\n // copy of the version that was active before restore.\n invalidateBlobCache(fileId);\n invalidateOfficeExtraction(fileId);\n // Reload version list to pick up the new synthetic version row.\n await dispatch(loadFileVersions({ fileId })).unwrap();\n } finally {\n dispatch(setFileLoading({ id: fileId, loading: false }));\n releaseRequest(requestId);\n }\n },\n);\n\n/**\n * THE edit-save path: write new content as the next version of the SAME file.\n *\n * Every editor that saves a file's text goes through here — never `uploadFiles`.\n * `uploadFiles` is the NEW-file path: it auto-renames a colliding name to\n * \"name (1).ext\" (Drive semantics for a dropped duplicate), so an editor that\n * re-uploaded its own file created a second file and left the original at\n * version 1 while the UI said \"Saved\" (2026-09-30).\n *\n * ONE path for everyone: `POST /files/{id}/versions` writes by file ID, so the\n * owner and an editor holding an edit grant on someone else's file save the\n * same way (the server checks the caller's write right, keeps the file's owner\n * and path, and records the caller + `changeSummary` on the version). The\n * answer is still REFUSED if it is not that same row (a different `file_id` or\n * `is_new: true`), so a save can never silently become a copy.\n */\nexport const saveFileNewVersion = createAsyncThunk<\n SaveFileNewVersionResult,\n SaveFileNewVersionArg,\n ThunkApi\n>(\n \"cloudFiles/saveFileNewVersion\",\n async ({ fileId, content, changeSummary }, { dispatch, getState }) => {\n if (isVirtualResourceId(fileId)) {\n throw new Error(\n \"saveFileNewVersion is for stored files; virtual files save through writeAny.\",\n );\n }\n let record: CloudFile | undefined = getState().cloudFiles.filesById[fileId];\n // A row hydrated for rendering only may not carry its name/mime — read the\n // file's own metadata rather than guessing.\n if (!record?.fileName) {\n const { data } = await Files.getFileMetadata(fileId);\n const fresh = apiFileRecordToCloudFile(data);\n dispatch(upsertFile(fresh));\n record = fresh;\n }\n const mimeType = record.mimeType ?? \"text/plain\";\n const body =\n typeof content === \"string\"\n ? new Blob([content], { type: mimeType })\n : content;\n const upload = new File([body], record.fileName, { type: mimeType });\n\n dispatch(setFileLoading({ id: fileId, loading: true }));\n const requestId = newRequestId();\n registerRequest({\n requestId,\n kind: \"upload\",\n resourceId: fileId,\n resourceType: \"file\",\n });\n try {\n const reqOpts = { requestId, idempotencyKey: requestId };\n const summary = changeSummary ?? \"Edited in place\";\n const { data } = await Files.uploadNewVersion(\n fileId,\n { file: upload, changeSummary: summary },\n reqOpts,\n );\n if (data.file_id !== fileId || data.is_new) {\n throw new Error(\n `Saving \"${record.fileName}\" did not update it — the files service wrote ` +\n `a separate file (${data.file_path}, id ${data.file_id}) instead of ` +\n `version ${record.currentVersion + 1}. Your original is unchanged; ` +\n `remove the extra file from the folder.`,\n );\n }\n dispatch(\n upsertFile({\n id: fileId,\n currentVersion: data.version_number,\n fileSize: data.size_bytes,\n checksum: data.checksum,\n updatedAt: new Date().toISOString(),\n }),\n );\n // The current bytes are the ones just uploaded — cache them as the new\n // version so every viewer shows it without downloading them back.\n replaceWithSavedBytes(fileId, body, data.version_number);\n invalidateOfficeExtraction(fileId);\n // Refresh the history without holding the save on it — the save has\n // landed, and \"Saving…\" must not wait on a list read.\n void dispatch(loadFileVersions({ fileId }))\n .unwrap()\n .catch((err: unknown) => {\n // The save landed; only the history refresh failed. Say so.\n console.error(\n \"[saveFileNewVersion] saved, but the version list did not refresh:\",\n err,\n );\n });\n return { fileId, versionNumber: data.version_number };\n } finally {\n dispatch(setFileLoading({ id: fileId, loading: false }));\n releaseRequest(requestId);\n }\n },\n);\n\n// ---------------------------------------------------------------------------\n// Writes — permissions\n// ---------------------------------------------------------------------------\n\n/**\n * The by-grantee REST grant/revoke endpoints (`GrantPermissionRequest`,\n * `?grantee_type=`) accept only `user | group`. PUBLIC access is not a\n * by-grantee grant — it is toggled on the resource's visibility. Reaching the\n * grant/revoke thunks with `granteeType: \"public\"` is a programming error, so\n * fail loudly (repo principle) rather than silently coercing it to a bogus\n * \"user\"/\"group\" call against an empty/row-id grantee.\n */\nfunction requireByGranteeType(\n granteeType: GranteeType | undefined,\n): \"user\" | \"group\" {\n if (granteeType === \"public\") {\n throw new Error(\n \"Public access is toggled via the resource's visibility, not the by-grantee grant/revoke path.\",\n );\n }\n return granteeType ?? \"user\";\n}\n\nexport const grantPermission = createAsyncThunk<\n void,\n GrantPermissionArg,\n ThunkApi\n>(\"cloudFiles/grantPermission\", async (arg, { dispatch }) => {\n if (isVirtualResourceId(arg.resourceId)) {\n throw new Error(\"Sharing isn't available for this source yet.\");\n }\n const requestId = newRequestId();\n registerRequest({\n requestId,\n kind: \"grant-permission\",\n resourceId: arg.resourceId,\n resourceType: arg.resourceType,\n });\n try {\n const body = {\n grantee_id: arg.granteeId,\n level: arg.level,\n grantee_type: requireByGranteeType(arg.granteeType),\n expires_at: arg.expiresAt ?? null,\n };\n const { data } =\n arg.resourceType === \"folder\"\n ? await Permissions.grantFolderPermission(arg.resourceId, body, {\n requestId,\n })\n : await Permissions.grantFilePermission(arg.resourceId, body, {\n requestId,\n });\n dispatch(\n upsertPermissionsForResource({\n resourceId: arg.resourceId,\n permissions: [rpcPermissionRowToCloudFilePermission(data)],\n }),\n );\n // P1-6: the returned row already makes the UI correct. Reconcile the full\n // list in the BACKGROUND (don't await) so the dialog doesn't stall on a\n // second round-trip after every grant.\n void dispatch(loadPermissions({ resourceId: arg.resourceId }));\n } finally {\n releaseRequest(requestId);\n }\n});\n\nexport const revokePermission = createAsyncThunk<\n void,\n RevokePermissionArg,\n ThunkApi\n>(\"cloudFiles/revokePermission\", async (arg, { dispatch }) => {\n if (isVirtualResourceId(arg.resourceId)) {\n throw new Error(\"Sharing isn't available for this source yet.\");\n }\n const requestId = newRequestId();\n registerRequest({\n requestId,\n kind: \"revoke-permission\",\n resourceId: arg.resourceId,\n resourceType: arg.resourceType,\n });\n try {\n const granteeType = requireByGranteeType(arg.granteeType);\n if (arg.resourceType === \"folder\") {\n await Permissions.revokeFolderPermission(\n arg.resourceId,\n arg.granteeId,\n { granteeType },\n { requestId },\n );\n } else {\n await Permissions.revokeFilePermission(\n arg.resourceId,\n arg.granteeId,\n { granteeType },\n { requestId },\n );\n }\n dispatch(\n removePermissionForResource({\n resourceId: arg.resourceId,\n granteeId: arg.granteeId,\n granteeType,\n }),\n );\n } finally {\n releaseRequest(requestId);\n }\n});\n\n// ---------------------------------------------------------------------------\n// Writes — share links\n// ---------------------------------------------------------------------------\n\nexport const createShareLink = createAsyncThunk<\n CloudShareLink,\n CreateShareLinkArg,\n ThunkApi\n>(\"cloudFiles/createShareLink\", async (arg, { dispatch, getState }) => {\n if (isVirtualResourceId(arg.resourceId)) {\n throw new Error(\"Share links aren't available for this source yet.\");\n }\n // P1-1: a second click while the first create is in flight would mint a\n // second token for the same resource. Suppress the duplicate submit.\n if (hasInFlight(arg.resourceId, \"create-share-link\")) {\n throw new Error(\"A share link is already being created for this item.\");\n }\n const requestId = newRequestId();\n registerRequest({\n requestId,\n kind: \"create-share-link\",\n resourceId: arg.resourceId,\n resourceType: arg.resourceType,\n });\n try {\n // Canonical owner-gated mint over platform.share_links.\n const created = await createCanonicalShareLink({\n resourceType: arg.resourceType,\n resourceId: arg.resourceId,\n permissionLevel: arg.permissionLevel,\n expiresAt: arg.expiresAt ?? null,\n maxUses: arg.maxUses ?? null,\n });\n if (!created.success || !created.token) {\n throw new Error(created.error ?? \"Failed to create share link\");\n }\n // Reload the full list — the create RPC returns only { token, id }.\n await dispatch(\n loadShareLinks({\n resourceId: arg.resourceId,\n resourceType: arg.resourceType,\n }),\n ).unwrap();\n const link = (\n getState().cloudFiles.shareLinksByResourceId[arg.resourceId] ?? []\n ).find((l) => l.shareToken === created.token);\n if (!link) {\n throw new Error(\"Share link was created but could not be loaded back\");\n }\n return link;\n } finally {\n releaseRequest(requestId);\n }\n});\n\nexport const revokeShareLink = createAsyncThunk<\n void,\n RevokeShareLinkArg,\n ThunkApi\n>(\"cloudFiles/revokeShareLink\", async ({ linkId }, { dispatch }) => {\n const requestId = newRequestId();\n registerRequest({\n requestId,\n kind: \"revoke-share-link\",\n resourceId: null,\n resourceType: null,\n });\n try {\n const result = await revokeCanonicalShareLink(linkId);\n if (!result.success) {\n throw new Error(result.error ?? \"Failed to revoke share link\");\n }\n dispatch(removeShareLink({ linkId }));\n } finally {\n releaseRequest(requestId);\n }\n});\n\n// ---------------------------------------------------------------------------\n// Utility — getFileUrl (no slice state change).\n//\n// Routes through the universal handler and returns the durable renderable\n// URL (permanent CDN for public files, the durable authenticated download\n// route otherwise). Durable URLs never expire.\n// ---------------------------------------------------------------------------\n\nexport const getFileUrl = createAsyncThunk<\n { url: string },\n { fileId: string },\n ThunkApi\n>(\"cloudFiles/getFileUrl\", async ({ fileId }) => {\n if (isVirtualResourceId(fileId)) {\n // Virtual files don't have stored bytes — there's no byte URL. Callers\n // already hide the buttons that surface URLs (Download, Copy link,\n // Open in new tab) for virtual rows; this guards the API path for any\n // remaining callers.\n throw new Error(\"Byte URLs aren't available for virtual sources\");\n }\n const url = await fileHandler\n .use({ kind: \"file_id\", fileId })\n .as({ kind: \"html_src\" });\n if (!url) {\n throw new Error(`Could not resolve a URL for file ${fileId}`);\n }\n return { url };\n});\n\n// ---------------------------------------------------------------------------\n// Bulk operations (Python P-7)\n// ---------------------------------------------------------------------------\n//\n// Each bulk thunk applies the optimistic local change up front, then makes a\n// single REST round-trip. The backend returns a per-item succeeded/failed\n// envelope; we re-apply the failed entries by re-upserting their pre-change\n// state from a snapshot taken before the optimistic update.\n\n/**\n * Move many files to Trash (soft delete) in one round-trip.\n *\n * Returns the standard `BulkResponse` envelope so the caller can decide\n * how to surface partial failures (toast vs. row-level error chips).\n * Successful ids are removed from the local store immediately for\n * snappy UI; failed ids are restored from the pre-change snapshot.\n */\n/**\n * P1-5: bulk ops resolve successfully even when some items fail (per-item\n * results in the envelope), so the rejected-thunk toast middleware never sees\n * them. Surface partial failures here so the user isn't left thinking every\n * item succeeded. Full success stays silent (the rows visibly changed).\n */\nfunction toastBulkPartialFailure(result: BulkResponse, verb: string): void {\n if (result.failed > 0) {\n const total = result.succeeded + result.failed;\n toast.error(`Couldn't ${verb} ${result.failed} of ${total} items`, {\n description:\n result.succeeded > 0\n ? `${result.succeeded} succeeded. The rest were restored.`\n : \"All items were restored.\",\n });\n }\n}\n\nexport const bulkDeleteFiles = createAsyncThunk<\n BulkResponse,\n BulkDeleteFilesArg,\n ThunkApi\n>(\"cloudFiles/bulkDeleteFiles\", async (arg, { dispatch, getState }) => {\n if (arg.fileIds.length === 0) {\n return { results: [], succeeded: 0, failed: 0 };\n }\n\n // Snapshot pre-change records for rollback on per-item failures.\n const state = getState();\n const snapshots = new Map<string, CloudFile>();\n for (const id of arg.fileIds) {\n const file = state.cloudFiles.filesById[id];\n if (file) snapshots.set(id, file);\n }\n\n const requestId = newRequestId();\n registerRequest({\n requestId,\n kind: \"bulk-delete-files\",\n resourceId: null,\n resourceType: \"file\",\n });\n\n // Optimistic: remove all targets from the local store and drop their\n // cached blobs (deletion makes the cache entry useless and keeping\n // bytes around for soft-deleted items wastes the LRU budget).\n for (const id of arg.fileIds) {\n dispatch(removeFile({ id }));\n invalidateBlobCache(id);\n invalidateOfficeExtraction(id);\n }\n\n try {\n const { data } = await Files.bulkDeleteFiles(\n { file_ids: arg.fileIds },\n { requestId },\n );\n const result: BulkResponse = data ?? {\n results: arg.fileIds.map((id) => ({ id, ok: true, error: null })),\n succeeded: arg.fileIds.length,\n failed: 0,\n };\n\n // Roll back any items the backend reports as failed.\n for (const r of result.results) {\n if (r.ok) continue;\n const snap = snapshots.get(r.id);\n if (snap) dispatch(upsertFile(snap));\n }\n toastBulkPartialFailure(result, \"delete\");\n return result;\n } catch (err) {\n // Whole-call failure — restore everything we removed.\n for (const snap of snapshots.values()) dispatch(upsertFile(snap));\n throw err;\n } finally {\n releaseRequest(requestId);\n }\n});\n\n/**\n * Move many files to a new parent folder in one round-trip. Pass `null` to\n * move to root.\n */\nexport const bulkMoveFiles = createAsyncThunk<\n BulkResponse,\n BulkMoveFilesArg,\n ThunkApi\n>(\"cloudFiles/bulkMoveFiles\", async (arg, { dispatch, getState }) => {\n if (arg.fileIds.length === 0) {\n return { results: [], succeeded: 0, failed: 0 };\n }\n\n const state = getState();\n const snapshots = new Map<string, CloudFile>();\n for (const id of arg.fileIds) {\n const file = state.cloudFiles.filesById[id];\n if (file) snapshots.set(id, file);\n }\n\n const requestId = newRequestId();\n registerRequest({\n requestId,\n kind: \"bulk-move-files\",\n resourceId: null,\n resourceType: \"file\",\n });\n\n // Optimistic: re-parent + retract from old parent / attach to new in tree.\n for (const [id, file] of snapshots) {\n if (file.parentFolderId === arg.newParentFolderId) continue;\n dispatch(\n detachChildFromFolder({\n parentFolderId: file.parentFolderId,\n kind: \"file\",\n id,\n }),\n );\n dispatch(\n attachChildToFolder({\n parentFolderId: arg.newParentFolderId,\n kind: \"file\",\n id,\n }),\n );\n dispatch(upsertFile({ ...file, parentFolderId: arg.newParentFolderId }));\n }\n\n try {\n const { data } = await Files.bulkMoveFiles(\n {\n file_ids: arg.fileIds,\n new_parent_folder_id: arg.newParentFolderId,\n },\n { requestId },\n );\n\n // Roll back per-item failures.\n for (const r of data.results) {\n if (r.ok) continue;\n const snap = snapshots.get(r.id);\n if (!snap || snap.parentFolderId === arg.newParentFolderId) continue;\n dispatch(\n detachChildFromFolder({\n parentFolderId: arg.newParentFolderId,\n kind: \"file\",\n id: snap.id,\n }),\n );\n dispatch(\n attachChildToFolder({\n parentFolderId: snap.parentFolderId,\n kind: \"file\",\n id: snap.id,\n }),\n );\n dispatch(upsertFile(snap));\n }\n toastBulkPartialFailure(data, \"move\");\n return data;\n } catch (err) {\n // Whole-call failure — restore everything.\n for (const snap of snapshots.values()) {\n if (snap.parentFolderId === arg.newParentFolderId) continue;\n dispatch(\n detachChildFromFolder({\n parentFolderId: arg.newParentFolderId,\n kind: \"file\",\n id: snap.id,\n }),\n );\n dispatch(\n attachChildToFolder({\n parentFolderId: snap.parentFolderId,\n kind: \"file\",\n id: snap.id,\n }),\n );\n dispatch(upsertFile(snap));\n }\n throw err;\n } finally {\n releaseRequest(requestId);\n }\n});\n\n/**\n * Move many folders to a new parent in one round-trip. Pass `null` to move\n * to root. The backend cascades `folder_path` updates to descendants.\n */\nexport const bulkMoveFolders = createAsyncThunk<\n BulkResponse,\n BulkMoveFoldersArg,\n ThunkApi\n>(\"cloudFiles/bulkMoveFolders\", async (arg, { dispatch, getState }) => {\n if (arg.folderIds.length === 0) {\n return { results: [], succeeded: 0, failed: 0 };\n }\n\n const state = getState();\n const snapshots = new Map<string, CloudFolder>();\n for (const id of arg.folderIds) {\n const folder = state.cloudFiles.foldersById[id];\n if (folder) snapshots.set(id, folder);\n }\n\n const requestId = newRequestId();\n registerRequest({\n requestId,\n kind: \"bulk-move-folders\",\n resourceId: null,\n resourceType: \"folder\",\n });\n\n // Optimistic: rewire each folder under the new parent.\n for (const [id, folder] of snapshots) {\n if (folder.parentId === arg.newParentId) continue;\n dispatch(\n detachChildFromFolder({\n parentFolderId: folder.parentId,\n kind: \"folder\",\n id,\n }),\n );\n dispatch(\n attachChildToFolder({\n parentFolderId: arg.newParentId,\n kind: \"folder\",\n id,\n }),\n );\n dispatch(upsertFolder({ ...folder, parentId: arg.newParentId }));\n }\n\n try {\n const { data } = await Folders.bulkMoveFolders(\n { folder_ids: arg.folderIds, new_parent_id: arg.newParentId },\n { requestId },\n );\n\n // Roll back per-item failures.\n for (const r of data.results) {\n if (r.ok) continue;\n const snap = snapshots.get(r.id);\n if (!snap || snap.parentId === arg.newParentId) continue;\n dispatch(\n detachChildFromFolder({\n parentFolderId: arg.newParentId,\n kind: \"folder\",\n id: snap.id,\n }),\n );\n dispatch(\n attachChildToFolder({\n parentFolderId: snap.parentId,\n kind: \"folder\",\n id: snap.id,\n }),\n );\n dispatch(upsertFolder(snap));\n }\n toastBulkPartialFailure(data, \"move\");\n return data;\n } catch (err) {\n for (const snap of snapshots.values()) {\n if (snap.parentId === arg.newParentId) continue;\n dispatch(\n detachChildFromFolder({\n parentFolderId: arg.newParentId,\n kind: \"folder\",\n id: snap.id,\n }),\n );\n dispatch(\n attachChildToFolder({\n parentFolderId: snap.parentId,\n kind: \"folder\",\n id: snap.id,\n }),\n );\n dispatch(upsertFolder(snap));\n }\n throw err;\n } finally {\n releaseRequest(requestId);\n }\n});\n"],"mappings":"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","names":["rows"]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const DEBUG_FLAG_KEY = "matrx:sync:debug";
|
|
2
2
|
const DEBUG_URL_PARAM = "sync-debug";
|
|
3
3
|
function isDebugEnabled() {
|
|
4
|
-
if (
|
|
4
|
+
if (process.env.NODE_ENV !== "production") return true;
|
|
5
5
|
if (typeof window === "undefined") return false;
|
|
6
6
|
try {
|
|
7
7
|
if (window.localStorage?.getItem(DEBUG_FLAG_KEY) === "1") return true;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/files/engine/support/logger.ts"],"sourcesContent":["/**\n * lib/sync/logger.ts\n *\n * Single prefixed logger for the sync engine. Silent in production unless\n * `?sync-debug=1` is in the URL or `localStorage.getItem('matrx:sync:debug') === '1'`.\n *\n * Phase 12 expands this into the debug-panel feed + observability hooks (Q8).\n */\n\ntype LogLevel = \"debug\" | \"info\" | \"warn\" | \"error\";\n\ninterface LogEvent {\n event: string;\n sliceName?: string;\n ms?: number;\n meta?: Record<string, unknown>;\n}\n\nconst DEBUG_FLAG_KEY = \"matrx:sync:debug\";\nconst DEBUG_URL_PARAM = \"sync-debug\";\n\nfunction isDebugEnabled(): boolean {\n if (process.env.NODE_ENV !== \"production\") return true;\n if (typeof window === \"undefined\") return false;\n try {\n if (window.localStorage?.getItem(DEBUG_FLAG_KEY) === \"1\") return true;\n const params = new URLSearchParams(window.location.search);\n return params.get(DEBUG_URL_PARAM) === \"1\";\n } catch {\n return false;\n }\n}\n\nfunction emit(level: LogLevel, evt: LogEvent): void {\n if (!isDebugEnabled() && level !== \"error\") return;\n const line = `[sync] ${evt.event}`;\n const payload: Record<string, unknown> = {};\n if (evt.sliceName) payload.sliceName = evt.sliceName;\n if (evt.ms != null) payload.ms = evt.ms;\n if (evt.meta) Object.assign(payload, evt.meta);\n const hasPayload = Object.keys(payload).length > 0;\n // eslint-disable-next-line no-console\n const fn = console[level] ?? console.log;\n hasPayload ? fn(line, payload) : fn(line);\n}\n\nexport const logger = {\n debug: (event: string, meta?: Omit<LogEvent, \"event\">) => emit(\"debug\", { event, ...meta }),\n info: (event: string, meta?: Omit<LogEvent, \"event\">) => emit(\"info\", { event, ...meta }),\n warn: (event: string, meta?: Omit<LogEvent, \"event\">) => emit(\"warn\", { event, ...meta }),\n error: (event: string, meta?: Omit<LogEvent, \"event\">) => emit(\"error\", { event, ...meta }),\n};\n\n/** Visible for tests only. */\nexport const __internal = { DEBUG_FLAG_KEY, DEBUG_URL_PARAM, isDebugEnabled };\n"],"mappings":"AAkBA,MAAM,iBAAiB;AACvB,MAAM,kBAAkB;AAExB,SAAS,iBAA0B;AAC/B,MAAI,
|
|
1
|
+
{"version":3,"sources":["../../../../src/files/engine/support/logger.ts"],"sourcesContent":["/**\n * lib/sync/logger.ts\n *\n * Single prefixed logger for the sync engine. Silent in production unless\n * `?sync-debug=1` is in the URL or `localStorage.getItem('matrx:sync:debug') === '1'`.\n *\n * Phase 12 expands this into the debug-panel feed + observability hooks (Q8).\n */\n\ntype LogLevel = \"debug\" | \"info\" | \"warn\" | \"error\";\n\ninterface LogEvent {\n event: string;\n sliceName?: string;\n ms?: number;\n meta?: Record<string, unknown>;\n}\n\nconst DEBUG_FLAG_KEY = \"matrx:sync:debug\";\nconst DEBUG_URL_PARAM = \"sync-debug\";\n\nfunction isDebugEnabled(): boolean {\n if (process.env.NODE_ENV !== \"production\") return true;\n if (typeof window === \"undefined\") return false;\n try {\n if (window.localStorage?.getItem(DEBUG_FLAG_KEY) === \"1\") return true;\n const params = new URLSearchParams(window.location.search);\n return params.get(DEBUG_URL_PARAM) === \"1\";\n } catch {\n return false;\n }\n}\n\nfunction emit(level: LogLevel, evt: LogEvent): void {\n if (!isDebugEnabled() && level !== \"error\") return;\n const line = `[sync] ${evt.event}`;\n const payload: Record<string, unknown> = {};\n if (evt.sliceName) payload.sliceName = evt.sliceName;\n if (evt.ms != null) payload.ms = evt.ms;\n if (evt.meta) Object.assign(payload, evt.meta);\n const hasPayload = Object.keys(payload).length > 0;\n // eslint-disable-next-line no-console\n const fn = console[level] ?? console.log;\n hasPayload ? fn(line, payload) : fn(line);\n}\n\nexport const logger = {\n debug: (event: string, meta?: Omit<LogEvent, \"event\">) => emit(\"debug\", { event, ...meta }),\n info: (event: string, meta?: Omit<LogEvent, \"event\">) => emit(\"info\", { event, ...meta }),\n warn: (event: string, meta?: Omit<LogEvent, \"event\">) => emit(\"warn\", { event, ...meta }),\n error: (event: string, meta?: Omit<LogEvent, \"event\">) => emit(\"error\", { event, ...meta }),\n};\n\n/** Visible for tests only. */\nexport const __internal = { DEBUG_FLAG_KEY, DEBUG_URL_PARAM, isDebugEnabled };\n"],"mappings":"AAkBA,MAAM,iBAAiB;AACvB,MAAM,kBAAkB;AAExB,SAAS,iBAA0B;AAC/B,MAAI,yBAAyB,aAAc,QAAO;AAClD,MAAI,OAAO,WAAW,YAAa,QAAO;AAC1C,MAAI;AACA,QAAI,OAAO,cAAc,QAAQ,cAAc,MAAM,IAAK,QAAO;AACjE,UAAM,SAAS,IAAI,gBAAgB,OAAO,SAAS,MAAM;AACzD,WAAO,OAAO,IAAI,eAAe,MAAM;AAAA,EAC3C,QAAQ;AACJ,WAAO;AAAA,EACX;AACJ;AAEA,SAAS,KAAK,OAAiB,KAAqB;AAChD,MAAI,CAAC,eAAe,KAAK,UAAU,QAAS;AAC5C,QAAM,OAAO,UAAU,IAAI,KAAK;AAChC,QAAM,UAAmC,CAAC;AAC1C,MAAI,IAAI,UAAW,SAAQ,YAAY,IAAI;AAC3C,MAAI,IAAI,MAAM,KAAM,SAAQ,KAAK,IAAI;AACrC,MAAI,IAAI,KAAM,QAAO,OAAO,SAAS,IAAI,IAAI;AAC7C,QAAM,aAAa,OAAO,KAAK,OAAO,EAAE,SAAS;AAEjD,QAAM,KAAK,QAAQ,KAAK,KAAK,QAAQ;AACrC,eAAa,GAAG,MAAM,OAAO,IAAI,GAAG,IAAI;AAC5C;AAEO,MAAM,SAAS;AAAA,EAClB,OAAO,CAAC,OAAe,SAAmC,KAAK,SAAS,EAAE,OAAO,GAAG,KAAK,CAAC;AAAA,EAC1F,MAAM,CAAC,OAAe,SAAmC,KAAK,QAAQ,EAAE,OAAO,GAAG,KAAK,CAAC;AAAA,EACxF,MAAM,CAAC,OAAe,SAAmC,KAAK,QAAQ,EAAE,OAAO,GAAG,KAAK,CAAC;AAAA,EACxF,OAAO,CAAC,OAAe,SAAmC,KAAK,SAAS,EAAE,OAAO,GAAG,KAAK,CAAC;AAC9F;AAGO,MAAM,aAAa,EAAE,gBAAgB,iBAAiB,eAAe;","names":[]}
|
|
@@ -10,7 +10,7 @@ function buildUploadMetadataEnvelope(metadata) {
|
|
|
10
10
|
origin: "cloudUpload",
|
|
11
11
|
...metadata ?? {}
|
|
12
12
|
};
|
|
13
|
-
if (!hasUploadOrganization(envelope)) {
|
|
13
|
+
if (process.env.NODE_ENV !== "production" && !hasUploadOrganization(envelope)) {
|
|
14
14
|
console.warn(
|
|
15
15
|
"[buildUploadMetadataEnvelope] upload carries no scope.organization_id \u2014 it will be filed in an organization nobody chose. Route this upload through cloudUpload/cloudUploadRaw so the organization gate runs."
|
|
16
16
|
);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/files/engine/upload/tusUpload.ts"],"sourcesContent":["/**\n * features/files/upload/tusUpload.ts\n *\n * Resumable (TUS) upload transport for the file handler — the large-file\n * sibling of the buffered multipart path in `cloudUpload.ts`. Callers never\n * import this directly: `cloudUpload` routes here per the transport policy\n * (size ≥ `TUS_TRANSPORT_THRESHOLD_BYTES`, or an explicit\n * `transport: \"tus\"` override).\n *\n * Wire contract (common-docs/systems/media/media-capture/FEATURE.md § TUS):\n * - Endpoint: `${PYTHON_BACKEND}/files/upload/tus` (resolved through the same\n * base-url helper the python-client uses — server toggle respected).\n * - Explicit chunk size 16 MiB (server bounds 8–32 MiB; never the client\n * default).\n * - `Upload-Metadata` carries `filename`, `filepath`, and ONE `metadata_json`\n * key — the base64 of the SAME JSON object the buffered path sends as its\n * `metadata_json` form field (tus-js-client base64-encodes values; the\n * object is built by the shared `buildUploadMetadataEnvelope`). Parity with\n * buffered uploads is a tested invariant.\n * - FRESH Authorization per request (`onBeforeRequest` re-reads the Supabase\n * session — long uploads outlive a single JWT).\n * - `X-Idempotency-Key` on the creation POST only.\n * - Final `X-Cld-File-Id` captured from response headers (final PATCH, or the\n * completed-session HEAD/POST recovery paths) — a lost final response never\n * forces a re-upload.\n * - Resume URLs live in a dedicated tiny IndexedDB (`mtx-tus-urls`) — NEVER\n * the recorder chunk journal DB.\n *\n * NOT LIVE-TESTED against production: the server side (CORS, metadata parity,\n * completed-HEAD recovery) exists in aidream locally but is not deployed —\n * see features/files/handler/FEATURE.md § Transport policy for the pending\n * E2E note. The client is unit-tested with an injected HttpStack.\n */\n\nimport * as tus from \"tus-js-client\";\nimport { buildHeaders, resolveBaseUrlForPath } from \"../host/python-client\";\n// eslint-disable-next-line no-restricted-imports -- transport sibling INSIDE the ring-fenced upload internals (same as cloudUpload.ts's own internal imports)\nimport * as Files from \"../api/files\";\nimport { extractErrorMessage } from \"@ai-matrx/data/net\";\n// eslint-disable-next-line no-restricted-imports -- type-only import between the two upload transports (both internal to features/files/upload)\nimport type {\n CloudUploadOptions,\n CloudUploadResult,\n} from \"./cloudUpload\";\n\n// ─── Constants ───────────────────────────────────────────────────────────────\n\n/** Server-bounded chunk size (8–32 MiB allowed): 16 MiB. */\nexport const TUS_CHUNK_SIZE_BYTES = 16 * 1024 * 1024;\n\nexport const TUS_UPLOAD_PATH = \"/files/upload/tus\";\n\n// ─── Shared metadata envelope (buffered ↔ TUS parity) ────────────────────────\n\n/**\n * The ONE builder for the upload metadata JSON object. The buffered path\n * serializes this into the `metadata_json` form field\n * (`features/files/api/files.ts`); the TUS path serializes the SAME object\n * into the `metadata_json` Upload-Metadata key. One builder ⇒ the two\n * transports can never drift (parity is unit-tested).\n *\n * 🚨 `scope.organization_id` is the OWNING WORKSPACE of the uploaded file, and\n * it is not decoration. The server reads it (`SyncEngine._metadata_organization_id`,\n * membership-checked against `iam.has_org_access_for`) and stamps\n * `files.files.organization_id` from it. Omit it and the server falls back to\n * the uploader's OWN organization — which is how, on 2026-08-30, a screenshot\n * attached from the composer landed in a own organization nobody had chosen\n * and then disagreed with the team organization the person actually picked.\n *\n * Callers never set it by hand: `bindUploadOrganization` in `cloudUpload.ts`\n * resolves it for every upload before a byte moves, so it is already present in\n * `metadata` by the time this builder runs. This function keeps the shape\n * canonical and is where the contract is written down.\n */\nexport function buildUploadMetadataEnvelope(\n metadata: Record<string, unknown> | undefined,\n): Record<string, unknown> {\n const envelope: Record<string, unknown> = {\n // Origin tag aids backend log triage when something goes wrong.\n origin: \"cloudUpload\",\n ...(metadata ?? {}),\n };\n if (\n process.env.NODE_ENV !== \"production\" &&\n !hasUploadOrganization(envelope)\n ) {\n // Loud in development, never a hard failure in production: a missing\n // organization is a silent mis-filing, and silence is exactly what made the\n // original bug survive. The server still fails closed on its own terms.\n console.warn(\n \"[buildUploadMetadataEnvelope] upload carries no scope.organization_id — \" +\n \"it will be filed in an organization nobody chose. Route this \" +\n \"upload through cloudUpload/cloudUploadRaw so the organization gate runs.\",\n );\n }\n return envelope;\n}\n\nfunction hasUploadOrganization(envelope: Record<string, unknown>): boolean {\n if (typeof envelope.organization_id === \"string\" && envelope.organization_id)\n return true;\n const scope = envelope.scope;\n return Boolean(\n scope &&\n typeof scope === \"object\" &&\n typeof (scope as Record<string, unknown>).organization_id === \"string\" &&\n (scope as Record<string, unknown>).organization_id,\n );\n}\n\n// ─── Dedicated resume-URL storage (mtx-tus-urls) ─────────────────────────────\n\nconst URL_DB_NAME = \"mtx-tus-urls\";\nconst URL_STORE = \"urls\";\n\ninterface StoredTusUpload {\n urlStorageKey: string;\n fingerprint: string;\n size: number | null;\n metadata: Record<string, string>;\n creationTime: string;\n uploadUrl: string | null;\n parallelUploadUrls: string[] | null;\n}\n\nlet urlDbPromise: Promise<IDBDatabase> | null = null;\n\nfunction openUrlDb(): Promise<IDBDatabase> {\n if (urlDbPromise) return urlDbPromise;\n urlDbPromise = new Promise<IDBDatabase>((resolve, reject) => {\n if (typeof indexedDB === \"undefined\") {\n reject(new Error(\"[tusUpload] IndexedDB unavailable\"));\n return;\n }\n const req = indexedDB.open(URL_DB_NAME, 1);\n req.onupgradeneeded = () => {\n const db = req.result;\n if (!db.objectStoreNames.contains(URL_STORE)) {\n const store = db.createObjectStore(URL_STORE, {\n keyPath: \"urlStorageKey\",\n });\n store.createIndex(\"fingerprint\", \"fingerprint\", { unique: false });\n }\n };\n req.onsuccess = () => resolve(req.result);\n req.onerror = () =>\n reject(req.error ?? new Error(\"[tusUpload] failed to open mtx-tus-urls\"));\n });\n urlDbPromise.catch(() => {\n urlDbPromise = null;\n });\n return urlDbPromise;\n}\n\nfunction idbRequest<T>(req: IDBRequest<T>): Promise<T> {\n return new Promise<T>((resolve, reject) => {\n req.onsuccess = () => resolve(req.result);\n req.onerror = () => reject(req.error ?? new Error(\"IndexedDB failed\"));\n });\n}\n\ntype PreviousUpload = Awaited<\n ReturnType<tus.Upload[\"findPreviousUploads\"]>\n>[number];\n\n/** tus-js-client UrlStorage backed by the dedicated `mtx-tus-urls` DB —\n * deliberately SEPARATE from the recorder chunk journal. */\nexport function createTusUrlStorage(): NonNullable<\n ConstructorParameters<typeof tus.Upload>[1][\"urlStorage\"]\n> {\n return {\n async findAllUploads(): Promise<PreviousUpload[]> {\n const db = await openUrlDb();\n const tx = db.transaction(URL_STORE, \"readonly\");\n const all = await idbRequest(\n tx.objectStore(URL_STORE).getAll() as IDBRequest<StoredTusUpload[]>,\n );\n return all;\n },\n async findUploadsByFingerprint(\n fingerprint: string,\n ): Promise<PreviousUpload[]> {\n const db = await openUrlDb();\n const tx = db.transaction(URL_STORE, \"readonly\");\n const matches = await idbRequest(\n tx\n .objectStore(URL_STORE)\n .index(\"fingerprint\")\n .getAll(fingerprint) as IDBRequest<StoredTusUpload[]>,\n );\n return matches;\n },\n async removeUpload(urlStorageKey: string): Promise<void> {\n const db = await openUrlDb();\n const tx = db.transaction(URL_STORE, \"readwrite\");\n tx.objectStore(URL_STORE).delete(urlStorageKey);\n await new Promise<void>((resolve, reject) => {\n tx.oncomplete = () => resolve();\n tx.onerror = () => reject(tx.error ?? new Error(\"IndexedDB tx failed\"));\n });\n },\n async addUpload(\n fingerprint: string,\n upload: PreviousUpload,\n ): Promise<string> {\n const urlStorageKey = `tus::${fingerprint}::${Date.now().toString(36)}`;\n const record: StoredTusUpload = {\n urlStorageKey,\n fingerprint,\n size: upload.size,\n metadata: upload.metadata,\n creationTime: upload.creationTime,\n uploadUrl: upload.uploadUrl,\n parallelUploadUrls: upload.parallelUploadUrls,\n };\n const db = await openUrlDb();\n const tx = db.transaction(URL_STORE, \"readwrite\");\n tx.objectStore(URL_STORE).put(record);\n await new Promise<void>((resolve, reject) => {\n tx.oncomplete = () => resolve();\n tx.onerror = () => reject(tx.error ?? new Error(\"IndexedDB tx failed\"));\n });\n return urlStorageKey;\n },\n };\n}\n\n/** Read-only summary of a stored TUS resume session (diagnostics surfaces —\n * /camera's resume-pending indicator, /camera/admin). */\nexport interface StoredTusUploadSummary {\n urlStorageKey: string;\n fingerprint: string;\n size: number | null;\n metadata: Record<string, string>;\n creationTime: string;\n uploadUrl: string | null;\n}\n\n/**\n * List every stored TUS resume-URL entry (the `mtx-tus-urls` DB). Read-only —\n * resuming still happens inside `tusUploadRaw` via the UrlStorage; this only\n * lets management surfaces SHOW that a resumable session is pending. Returns\n * [] where IndexedDB is unavailable (never throws — diagnostics must not\n * break a page).\n */\nexport async function listStoredTusUploads(): Promise<\n StoredTusUploadSummary[]\n> {\n try {\n const db = await openUrlDb();\n const tx = db.transaction(URL_STORE, \"readonly\");\n const all = await idbRequest(\n tx.objectStore(URL_STORE).getAll() as IDBRequest<StoredTusUpload[]>,\n );\n return all.map((r) => ({\n urlStorageKey: r.urlStorageKey,\n fingerprint: r.fingerprint,\n size: r.size,\n metadata: r.metadata,\n creationTime: r.creationTime,\n uploadUrl: r.uploadUrl,\n }));\n } catch (err) {\n console.error(\"[tusUpload] listStoredTusUploads failed:\", err);\n return [];\n }\n}\n\n// ─── The transport ───────────────────────────────────────────────────────────\n\nexport interface TusUploadDeps {\n /** Injected HttpStack for unit tests (mock wire, no XHR). */\n httpStack?: tus.HttpStack;\n /** Injected file reader for unit tests (Node builds of tus-js-client\n * cannot slice browser Files). */\n fileReader?: ConstructorParameters<typeof tus.Upload>[1][\"fileReader\"];\n /** Override the endpoint (tests). */\n endpointOverride?: string;\n /** Override URL storage (tests). */\n urlStorage?: ReturnType<typeof createTusUrlStorage>;\n}\n\n/**\n * Resumable upload of one file via TUS. Same result contract as\n * `cloudUploadRaw` — `{ ok: true, fileId, ... } | { ok: false, error }`,\n * never throws. Automatically resumes a previous session for the same file\n * (custom UrlStorage + server HEAD), including the completed-session\n * recovery: when the server reports the session already finished and hands\n * back `X-Cld-File-Id`, the upload is skipped and the file resolves directly.\n */\nexport async function tusUploadRaw(\n file: File,\n filePath: string,\n options: CloudUploadOptions,\n idempotencyKey: string,\n deps: TusUploadDeps = {},\n): Promise<CloudUploadResult> {\n const endpoint =\n deps.endpointOverride ??\n `${resolveBaseUrlForPath(TUS_UPLOAD_PATH, undefined, \"POST\")}${TUS_UPLOAD_PATH}`;\n\n const metadataEnvelope = buildUploadMetadataEnvelope(options.metadata);\n\n let cldFileId: string | null = null;\n let aborted = false;\n\n try {\n await new Promise<void>((resolve, reject) => {\n const upload: tus.Upload = new tus.Upload(file, {\n endpoint,\n chunkSize: TUS_CHUNK_SIZE_BYTES,\n retryDelays: [0, 1000, 3000, 5000, 10000],\n removeFingerprintOnSuccess: true,\n urlStorage: deps.urlStorage ?? createTusUrlStorage(),\n ...(deps.httpStack ? { httpStack: deps.httpStack } : {}),\n ...(deps.fileReader ? { fileReader: deps.fileReader } : {}),\n metadata: {\n filename: file.name,\n filepath: filePath,\n // ONE validated JSON envelope — tus-js-client base64-encodes the\n // value per the Upload-Metadata spec; the server parses and merges\n // it exactly like the buffered `metadata_json` form field.\n metadata_json: JSON.stringify(metadataEnvelope),\n ...(options.visibility ? { visibility: options.visibility } : {}),\n },\n onBeforeRequest: async (req) => {\n // FRESH auth on EVERY request — long uploads outlive a JWT.\n //\n // `buildHeaders` (lib/python-client.ts) is mandatory-org, fail-closed:\n // it resolves the organization the SAME way `bindUploadOrganization`\n // in cloudUpload.ts already committed it (that gate dispatches into\n // Redux BEFORE this ever runs — see OrganizationGateDialog's\n // `resolveOrganizationForBlockedAction` — so the selection is already\n // there by the time the first chunk goes out). Until this fix,\n // X-Organization-Id was computed here and then silently dropped —\n // only Authorization and X-Guest-Fingerprint were copied onto the\n // TUS request, so every resumable upload reached the server\n // completely unscoped (aidream commit 8e5ee0b93 now 400s that).\n const { headers } = await buildHeaders({}, false);\n if (headers.Authorization) {\n req.setHeader(\"Authorization\", headers.Authorization);\n }\n if (headers[\"X-Guest-Fingerprint\"]) {\n req.setHeader(\"X-Guest-Fingerprint\", headers[\"X-Guest-Fingerprint\"]);\n }\n if (headers[\"X-Organization-Id\"]) {\n req.setHeader(\"X-Organization-Id\", headers[\"X-Organization-Id\"]);\n }\n if (req.getMethod() === \"POST\") {\n // Creation only — one intended upload, one key.\n req.setHeader(\"X-Idempotency-Key\", idempotencyKey);\n }\n },\n onAfterResponse: (_req, res) => {\n // The final PATCH (and the completed-session HEAD recovery) carry\n // the created file id. Capture it whenever it appears.\n const id = res.getHeader(\"X-Cld-File-Id\");\n if (id) cldFileId = id;\n },\n onProgress: (bytesSent, bytesTotal) => {\n options.onProgress?.({ loaded: bytesSent, total: bytesTotal });\n },\n onSuccess: () => resolve(),\n onError: (error) => reject(error),\n });\n\n if (options.signal) {\n if (options.signal.aborted) {\n aborted = true;\n reject(new Error(\"Upload cancelled\"));\n return;\n }\n options.signal.addEventListener(\n \"abort\",\n () => {\n aborted = true;\n void upload.abort();\n reject(new Error(\"Upload cancelled\"));\n },\n { once: true },\n );\n }\n\n // Resume automatically when a previous session for this file exists —\n // tus-js-client HEADs the stored URL; a completed session resolves via\n // the X-Cld-File-Id captured in onAfterResponse.\n void upload\n .findPreviousUploads()\n .then((previous) => {\n if (previous.length > 0) {\n upload.resumeFromPreviousUpload(previous[0]);\n }\n upload.start();\n })\n .catch((err) => {\n console.warn(\n \"[tusUpload] previous-upload lookup failed — starting fresh:\",\n err,\n );\n upload.start();\n });\n });\n } catch (err) {\n if (cldFileId && !aborted) {\n // Completed-session recovery: the transfer errored (e.g. a lost final\n // response surfaced as an error) but the server told us the file\n // exists. Resolve it instead of failing/re-uploading. LOUD by design.\n console.warn(\n `[tusUpload] transfer reported an error but the server exposed ` +\n `X-Cld-File-Id=${cldFileId} — recovering the completed session.`,\n );\n } else {\n return {\n ok: false,\n error: extractErrorMessage(err),\n errorCode: aborted ? \"upload_cancelled\" : \"tus_upload_failed\",\n fileName: file.name,\n };\n }\n }\n\n if (!cldFileId) {\n return {\n ok: false,\n error:\n \"TUS upload finished but the server never exposed X-Cld-File-Id — \" +\n \"the file cannot be resolved. Check server CORS expose_headers.\",\n errorCode: \"tus_missing_file_id\",\n fileName: file.name,\n };\n }\n\n // Hydrate the canonical row so the result matches the buffered contract.\n try {\n const { data: record } = await Files.getFile(cldFileId);\n return {\n ok: true,\n fileId: record.id,\n filePath: record.file_path,\n fileSize: record.size_bytes ?? file.size,\n versionNumber: record.current_version ?? 1,\n url: null,\n };\n } catch (err) {\n return {\n ok: false,\n error: `TUS upload completed (file ${cldFileId}) but the record fetch failed: ${extractErrorMessage(err)}`,\n errorCode: \"tus_record_fetch_failed\",\n fileName: file.name,\n };\n }\n}\n"],"mappings":"AAkCA,YAAY,SAAS;AACrB,SAAS,cAAc,6BAA6B;AAEpD,YAAY,WAAW;AACvB,SAAS,2BAA2B;AAU7B,MAAM,uBAAuB,KAAK,OAAO;AAEzC,MAAM,kBAAkB;AAwBxB,SAAS,4BACd,UACyB;AACzB,QAAM,WAAoC;AAAA;AAAA,IAExC,QAAQ;AAAA,IACR,GAAI,YAAY,CAAC;AAAA,EACnB;AACA,MAEE,CAAC,sBAAsB,QAAQ,GAC/B;AAIA,YAAQ;AAAA,MACN;AAAA,IAGF;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,sBAAsB,UAA4C;AACzE,MAAI,OAAO,SAAS,oBAAoB,YAAY,SAAS;AAC3D,WAAO;AACT,QAAM,QAAQ,SAAS;AACvB,SAAO;AAAA,IACL,SACE,OAAO,UAAU,YACjB,OAAQ,MAAkC,oBAAoB,YAC7D,MAAkC;AAAA,EACvC;AACF;AAIA,MAAM,cAAc;AACpB,MAAM,YAAY;AAYlB,IAAI,eAA4C;AAEhD,SAAS,YAAkC;AACzC,MAAI,aAAc,QAAO;AACzB,iBAAe,IAAI,QAAqB,CAAC,SAAS,WAAW;AAC3D,QAAI,OAAO,cAAc,aAAa;AACpC,aAAO,IAAI,MAAM,mCAAmC,CAAC;AACrD;AAAA,IACF;AACA,UAAM,MAAM,UAAU,KAAK,aAAa,CAAC;AACzC,QAAI,kBAAkB,MAAM;AAC1B,YAAM,KAAK,IAAI;AACf,UAAI,CAAC,GAAG,iBAAiB,SAAS,SAAS,GAAG;AAC5C,cAAM,QAAQ,GAAG,kBAAkB,WAAW;AAAA,UAC5C,SAAS;AAAA,QACX,CAAC;AACD,cAAM,YAAY,eAAe,eAAe,EAAE,QAAQ,MAAM,CAAC;AAAA,MACnE;AAAA,IACF;AACA,QAAI,YAAY,MAAM,QAAQ,IAAI,MAAM;AACxC,QAAI,UAAU,MACZ,OAAO,IAAI,SAAS,IAAI,MAAM,yCAAyC,CAAC;AAAA,EAC5E,CAAC;AACD,eAAa,MAAM,MAAM;AACvB,mBAAe;AAAA,EACjB,CAAC;AACD,SAAO;AACT;AAEA,SAAS,WAAc,KAAgC;AACrD,SAAO,IAAI,QAAW,CAAC,SAAS,WAAW;AACzC,QAAI,YAAY,MAAM,QAAQ,IAAI,MAAM;AACxC,QAAI,UAAU,MAAM,OAAO,IAAI,SAAS,IAAI,MAAM,kBAAkB,CAAC;AAAA,EACvE,CAAC;AACH;AAQO,SAAS,sBAEd;AACA,SAAO;AAAA,IACL,MAAM,iBAA4C;AAChD,YAAM,KAAK,MAAM,UAAU;AAC3B,YAAM,KAAK,GAAG,YAAY,WAAW,UAAU;AAC/C,YAAM,MAAM,MAAM;AAAA,QAChB,GAAG,YAAY,SAAS,EAAE,OAAO;AAAA,MACnC;AACA,aAAO;AAAA,IACT;AAAA,IACA,MAAM,yBACJ,aAC2B;AAC3B,YAAM,KAAK,MAAM,UAAU;AAC3B,YAAM,KAAK,GAAG,YAAY,WAAW,UAAU;AAC/C,YAAM,UAAU,MAAM;AAAA,QACpB,GACG,YAAY,SAAS,EACrB,MAAM,aAAa,EACnB,OAAO,WAAW;AAAA,MACvB;AACA,aAAO;AAAA,IACT;AAAA,IACA,MAAM,aAAa,eAAsC;AACvD,YAAM,KAAK,MAAM,UAAU;AAC3B,YAAM,KAAK,GAAG,YAAY,WAAW,WAAW;AAChD,SAAG,YAAY,SAAS,EAAE,OAAO,aAAa;AAC9C,YAAM,IAAI,QAAc,CAAC,SAAS,WAAW;AAC3C,WAAG,aAAa,MAAM,QAAQ;AAC9B,WAAG,UAAU,MAAM,OAAO,GAAG,SAAS,IAAI,MAAM,qBAAqB,CAAC;AAAA,MACxE,CAAC;AAAA,IACH;AAAA,IACA,MAAM,UACJ,aACA,QACiB;AACjB,YAAM,gBAAgB,QAAQ,WAAW,KAAK,KAAK,IAAI,EAAE,SAAS,EAAE,CAAC;AACrE,YAAM,SAA0B;AAAA,QAC9B;AAAA,QACA;AAAA,QACA,MAAM,OAAO;AAAA,QACb,UAAU,OAAO;AAAA,QACjB,cAAc,OAAO;AAAA,QACrB,WAAW,OAAO;AAAA,QAClB,oBAAoB,OAAO;AAAA,MAC7B;AACA,YAAM,KAAK,MAAM,UAAU;AAC3B,YAAM,KAAK,GAAG,YAAY,WAAW,WAAW;AAChD,SAAG,YAAY,SAAS,EAAE,IAAI,MAAM;AACpC,YAAM,IAAI,QAAc,CAAC,SAAS,WAAW;AAC3C,WAAG,aAAa,MAAM,QAAQ;AAC9B,WAAG,UAAU,MAAM,OAAO,GAAG,SAAS,IAAI,MAAM,qBAAqB,CAAC;AAAA,MACxE,CAAC;AACD,aAAO;AAAA,IACT;AAAA,EACF;AACF;AAoBA,eAAsB,uBAEpB;AACA,MAAI;AACF,UAAM,KAAK,MAAM,UAAU;AAC3B,UAAM,KAAK,GAAG,YAAY,WAAW,UAAU;AAC/C,UAAM,MAAM,MAAM;AAAA,MAChB,GAAG,YAAY,SAAS,EAAE,OAAO;AAAA,IACnC;AACA,WAAO,IAAI,IAAI,CAAC,OAAO;AAAA,MACrB,eAAe,EAAE;AAAA,MACjB,aAAa,EAAE;AAAA,MACf,MAAM,EAAE;AAAA,MACR,UAAU,EAAE;AAAA,MACZ,cAAc,EAAE;AAAA,MAChB,WAAW,EAAE;AAAA,IACf,EAAE;AAAA,EACJ,SAAS,KAAK;AACZ,YAAQ,MAAM,4CAA4C,GAAG;AAC7D,WAAO,CAAC;AAAA,EACV;AACF;AAwBA,eAAsB,aACpB,MACA,UACA,SACA,gBACA,OAAsB,CAAC,GACK;AAC5B,QAAM,WACJ,KAAK,oBACL,GAAG,sBAAsB,iBAAiB,QAAW,MAAM,CAAC,GAAG,eAAe;AAEhF,QAAM,mBAAmB,4BAA4B,QAAQ,QAAQ;AAErE,MAAI,YAA2B;AAC/B,MAAI,UAAU;AAEd,MAAI;AACF,UAAM,IAAI,QAAc,CAAC,SAAS,WAAW;AAC3C,YAAM,SAAqB,IAAI,IAAI,OAAO,MAAM;AAAA,QAC9C;AAAA,QACA,WAAW;AAAA,QACX,aAAa,CAAC,GAAG,KAAM,KAAM,KAAM,GAAK;AAAA,QACxC,4BAA4B;AAAA,QAC5B,YAAY,KAAK,cAAc,oBAAoB;AAAA,QACnD,GAAI,KAAK,YAAY,EAAE,WAAW,KAAK,UAAU,IAAI,CAAC;AAAA,QACtD,GAAI,KAAK,aAAa,EAAE,YAAY,KAAK,WAAW,IAAI,CAAC;AAAA,QACzD,UAAU;AAAA,UACR,UAAU,KAAK;AAAA,UACf,UAAU;AAAA;AAAA;AAAA;AAAA,UAIV,eAAe,KAAK,UAAU,gBAAgB;AAAA,UAC9C,GAAI,QAAQ,aAAa,EAAE,YAAY,QAAQ,WAAW,IAAI,CAAC;AAAA,QACjE;AAAA,QACA,iBAAiB,OAAO,QAAQ;AAa9B,gBAAM,EAAE,QAAQ,IAAI,MAAM,aAAa,CAAC,GAAG,KAAK;AAChD,cAAI,QAAQ,eAAe;AACzB,gBAAI,UAAU,iBAAiB,QAAQ,aAAa;AAAA,UACtD;AACA,cAAI,QAAQ,qBAAqB,GAAG;AAClC,gBAAI,UAAU,uBAAuB,QAAQ,qBAAqB,CAAC;AAAA,UACrE;AACA,cAAI,QAAQ,mBAAmB,GAAG;AAChC,gBAAI,UAAU,qBAAqB,QAAQ,mBAAmB,CAAC;AAAA,UACjE;AACA,cAAI,IAAI,UAAU,MAAM,QAAQ;AAE9B,gBAAI,UAAU,qBAAqB,cAAc;AAAA,UACnD;AAAA,QACF;AAAA,QACA,iBAAiB,CAAC,MAAM,QAAQ;AAG9B,gBAAM,KAAK,IAAI,UAAU,eAAe;AACxC,cAAI,GAAI,aAAY;AAAA,QACtB;AAAA,QACA,YAAY,CAAC,WAAW,eAAe;AACrC,kBAAQ,aAAa,EAAE,QAAQ,WAAW,OAAO,WAAW,CAAC;AAAA,QAC/D;AAAA,QACA,WAAW,MAAM,QAAQ;AAAA,QACzB,SAAS,CAAC,UAAU,OAAO,KAAK;AAAA,MAClC,CAAC;AAED,UAAI,QAAQ,QAAQ;AAClB,YAAI,QAAQ,OAAO,SAAS;AAC1B,oBAAU;AACV,iBAAO,IAAI,MAAM,kBAAkB,CAAC;AACpC;AAAA,QACF;AACA,gBAAQ,OAAO;AAAA,UACb;AAAA,UACA,MAAM;AACJ,sBAAU;AACV,iBAAK,OAAO,MAAM;AAClB,mBAAO,IAAI,MAAM,kBAAkB,CAAC;AAAA,UACtC;AAAA,UACA,EAAE,MAAM,KAAK;AAAA,QACf;AAAA,MACF;AAKA,WAAK,OACF,oBAAoB,EACpB,KAAK,CAAC,aAAa;AAClB,YAAI,SAAS,SAAS,GAAG;AACvB,iBAAO,yBAAyB,SAAS,CAAC,CAAC;AAAA,QAC7C;AACA,eAAO,MAAM;AAAA,MACf,CAAC,EACA,MAAM,CAAC,QAAQ;AACd,gBAAQ;AAAA,UACN;AAAA,UACA;AAAA,QACF;AACA,eAAO,MAAM;AAAA,MACf,CAAC;AAAA,IACL,CAAC;AAAA,EACH,SAAS,KAAK;AACZ,QAAI,aAAa,CAAC,SAAS;AAIzB,cAAQ;AAAA,QACN,+EACmB,SAAS;AAAA,MAC9B;AAAA,IACF,OAAO;AACL,aAAO;AAAA,QACL,IAAI;AAAA,QACJ,OAAO,oBAAoB,GAAG;AAAA,QAC9B,WAAW,UAAU,qBAAqB;AAAA,QAC1C,UAAU,KAAK;AAAA,MACjB;AAAA,IACF;AAAA,EACF;AAEA,MAAI,CAAC,WAAW;AACd,WAAO;AAAA,MACL,IAAI;AAAA,MACJ,OACE;AAAA,MAEF,WAAW;AAAA,MACX,UAAU,KAAK;AAAA,IACjB;AAAA,EACF;AAGA,MAAI;AACF,UAAM,EAAE,MAAM,OAAO,IAAI,MAAM,MAAM,QAAQ,SAAS;AACtD,WAAO;AAAA,MACL,IAAI;AAAA,MACJ,QAAQ,OAAO;AAAA,MACf,UAAU,OAAO;AAAA,MACjB,UAAU,OAAO,cAAc,KAAK;AAAA,MACpC,eAAe,OAAO,mBAAmB;AAAA,MACzC,KAAK;AAAA,IACP;AAAA,EACF,SAAS,KAAK;AACZ,WAAO;AAAA,MACL,IAAI;AAAA,MACJ,OAAO,8BAA8B,SAAS,kCAAkC,oBAAoB,GAAG,CAAC;AAAA,MACxG,WAAW;AAAA,MACX,UAAU,KAAK;AAAA,IACjB;AAAA,EACF;AACF;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../../src/files/engine/upload/tusUpload.ts"],"sourcesContent":["/**\n * features/files/upload/tusUpload.ts\n *\n * Resumable (TUS) upload transport for the file handler — the large-file\n * sibling of the buffered multipart path in `cloudUpload.ts`. Callers never\n * import this directly: `cloudUpload` routes here per the transport policy\n * (size ≥ `TUS_TRANSPORT_THRESHOLD_BYTES`, or an explicit\n * `transport: \"tus\"` override).\n *\n * Wire contract (common-docs/systems/media/media-capture/FEATURE.md § TUS):\n * - Endpoint: `${PYTHON_BACKEND}/files/upload/tus` (resolved through the same\n * base-url helper the python-client uses — server toggle respected).\n * - Explicit chunk size 16 MiB (server bounds 8–32 MiB; never the client\n * default).\n * - `Upload-Metadata` carries `filename`, `filepath`, and ONE `metadata_json`\n * key — the base64 of the SAME JSON object the buffered path sends as its\n * `metadata_json` form field (tus-js-client base64-encodes values; the\n * object is built by the shared `buildUploadMetadataEnvelope`). Parity with\n * buffered uploads is a tested invariant.\n * - FRESH Authorization per request (`onBeforeRequest` re-reads the Supabase\n * session — long uploads outlive a single JWT).\n * - `X-Idempotency-Key` on the creation POST only.\n * - Final `X-Cld-File-Id` captured from response headers (final PATCH, or the\n * completed-session HEAD/POST recovery paths) — a lost final response never\n * forces a re-upload.\n * - Resume URLs live in a dedicated tiny IndexedDB (`mtx-tus-urls`) — NEVER\n * the recorder chunk journal DB.\n *\n * NOT LIVE-TESTED against production: the server side (CORS, metadata parity,\n * completed-HEAD recovery) exists in aidream locally but is not deployed —\n * see features/files/handler/FEATURE.md § Transport policy for the pending\n * E2E note. The client is unit-tested with an injected HttpStack.\n */\n\nimport * as tus from \"tus-js-client\";\nimport { buildHeaders, resolveBaseUrlForPath } from \"../host/python-client\";\n// eslint-disable-next-line no-restricted-imports -- transport sibling INSIDE the ring-fenced upload internals (same as cloudUpload.ts's own internal imports)\nimport * as Files from \"../api/files\";\nimport { extractErrorMessage } from \"@ai-matrx/data/net\";\n// eslint-disable-next-line no-restricted-imports -- type-only import between the two upload transports (both internal to features/files/upload)\nimport type {\n CloudUploadOptions,\n CloudUploadResult,\n} from \"./cloudUpload\";\n\n// ─── Constants ───────────────────────────────────────────────────────────────\n\n/** Server-bounded chunk size (8–32 MiB allowed): 16 MiB. */\nexport const TUS_CHUNK_SIZE_BYTES = 16 * 1024 * 1024;\n\nexport const TUS_UPLOAD_PATH = \"/files/upload/tus\";\n\n// ─── Shared metadata envelope (buffered ↔ TUS parity) ────────────────────────\n\n/**\n * The ONE builder for the upload metadata JSON object. The buffered path\n * serializes this into the `metadata_json` form field\n * (`features/files/api/files.ts`); the TUS path serializes the SAME object\n * into the `metadata_json` Upload-Metadata key. One builder ⇒ the two\n * transports can never drift (parity is unit-tested).\n *\n * 🚨 `scope.organization_id` is the OWNING WORKSPACE of the uploaded file, and\n * it is not decoration. The server reads it (`SyncEngine._metadata_organization_id`,\n * membership-checked against `iam.has_org_access_for`) and stamps\n * `files.files.organization_id` from it. Omit it and the server falls back to\n * the uploader's OWN organization — which is how, on 2026-08-30, a screenshot\n * attached from the composer landed in a own organization nobody had chosen\n * and then disagreed with the team organization the person actually picked.\n *\n * Callers never set it by hand: `bindUploadOrganization` in `cloudUpload.ts`\n * resolves it for every upload before a byte moves, so it is already present in\n * `metadata` by the time this builder runs. This function keeps the shape\n * canonical and is where the contract is written down.\n */\nexport function buildUploadMetadataEnvelope(\n metadata: Record<string, unknown> | undefined,\n): Record<string, unknown> {\n const envelope: Record<string, unknown> = {\n // Origin tag aids backend log triage when something goes wrong.\n origin: \"cloudUpload\",\n ...(metadata ?? {}),\n };\n if (\n process.env.NODE_ENV !== \"production\" &&\n !hasUploadOrganization(envelope)\n ) {\n // Loud in development, never a hard failure in production: a missing\n // organization is a silent mis-filing, and silence is exactly what made the\n // original bug survive. The server still fails closed on its own terms.\n console.warn(\n \"[buildUploadMetadataEnvelope] upload carries no scope.organization_id — \" +\n \"it will be filed in an organization nobody chose. Route this \" +\n \"upload through cloudUpload/cloudUploadRaw so the organization gate runs.\",\n );\n }\n return envelope;\n}\n\nfunction hasUploadOrganization(envelope: Record<string, unknown>): boolean {\n if (typeof envelope.organization_id === \"string\" && envelope.organization_id)\n return true;\n const scope = envelope.scope;\n return Boolean(\n scope &&\n typeof scope === \"object\" &&\n typeof (scope as Record<string, unknown>).organization_id === \"string\" &&\n (scope as Record<string, unknown>).organization_id,\n );\n}\n\n// ─── Dedicated resume-URL storage (mtx-tus-urls) ─────────────────────────────\n\nconst URL_DB_NAME = \"mtx-tus-urls\";\nconst URL_STORE = \"urls\";\n\ninterface StoredTusUpload {\n urlStorageKey: string;\n fingerprint: string;\n size: number | null;\n metadata: Record<string, string>;\n creationTime: string;\n uploadUrl: string | null;\n parallelUploadUrls: string[] | null;\n}\n\nlet urlDbPromise: Promise<IDBDatabase> | null = null;\n\nfunction openUrlDb(): Promise<IDBDatabase> {\n if (urlDbPromise) return urlDbPromise;\n urlDbPromise = new Promise<IDBDatabase>((resolve, reject) => {\n if (typeof indexedDB === \"undefined\") {\n reject(new Error(\"[tusUpload] IndexedDB unavailable\"));\n return;\n }\n const req = indexedDB.open(URL_DB_NAME, 1);\n req.onupgradeneeded = () => {\n const db = req.result;\n if (!db.objectStoreNames.contains(URL_STORE)) {\n const store = db.createObjectStore(URL_STORE, {\n keyPath: \"urlStorageKey\",\n });\n store.createIndex(\"fingerprint\", \"fingerprint\", { unique: false });\n }\n };\n req.onsuccess = () => resolve(req.result);\n req.onerror = () =>\n reject(req.error ?? new Error(\"[tusUpload] failed to open mtx-tus-urls\"));\n });\n urlDbPromise.catch(() => {\n urlDbPromise = null;\n });\n return urlDbPromise;\n}\n\nfunction idbRequest<T>(req: IDBRequest<T>): Promise<T> {\n return new Promise<T>((resolve, reject) => {\n req.onsuccess = () => resolve(req.result);\n req.onerror = () => reject(req.error ?? new Error(\"IndexedDB failed\"));\n });\n}\n\ntype PreviousUpload = Awaited<\n ReturnType<tus.Upload[\"findPreviousUploads\"]>\n>[number];\n\n/** tus-js-client UrlStorage backed by the dedicated `mtx-tus-urls` DB —\n * deliberately SEPARATE from the recorder chunk journal. */\nexport function createTusUrlStorage(): NonNullable<\n ConstructorParameters<typeof tus.Upload>[1][\"urlStorage\"]\n> {\n return {\n async findAllUploads(): Promise<PreviousUpload[]> {\n const db = await openUrlDb();\n const tx = db.transaction(URL_STORE, \"readonly\");\n const all = await idbRequest(\n tx.objectStore(URL_STORE).getAll() as IDBRequest<StoredTusUpload[]>,\n );\n return all;\n },\n async findUploadsByFingerprint(\n fingerprint: string,\n ): Promise<PreviousUpload[]> {\n const db = await openUrlDb();\n const tx = db.transaction(URL_STORE, \"readonly\");\n const matches = await idbRequest(\n tx\n .objectStore(URL_STORE)\n .index(\"fingerprint\")\n .getAll(fingerprint) as IDBRequest<StoredTusUpload[]>,\n );\n return matches;\n },\n async removeUpload(urlStorageKey: string): Promise<void> {\n const db = await openUrlDb();\n const tx = db.transaction(URL_STORE, \"readwrite\");\n tx.objectStore(URL_STORE).delete(urlStorageKey);\n await new Promise<void>((resolve, reject) => {\n tx.oncomplete = () => resolve();\n tx.onerror = () => reject(tx.error ?? new Error(\"IndexedDB tx failed\"));\n });\n },\n async addUpload(\n fingerprint: string,\n upload: PreviousUpload,\n ): Promise<string> {\n const urlStorageKey = `tus::${fingerprint}::${Date.now().toString(36)}`;\n const record: StoredTusUpload = {\n urlStorageKey,\n fingerprint,\n size: upload.size,\n metadata: upload.metadata,\n creationTime: upload.creationTime,\n uploadUrl: upload.uploadUrl,\n parallelUploadUrls: upload.parallelUploadUrls,\n };\n const db = await openUrlDb();\n const tx = db.transaction(URL_STORE, \"readwrite\");\n tx.objectStore(URL_STORE).put(record);\n await new Promise<void>((resolve, reject) => {\n tx.oncomplete = () => resolve();\n tx.onerror = () => reject(tx.error ?? new Error(\"IndexedDB tx failed\"));\n });\n return urlStorageKey;\n },\n };\n}\n\n/** Read-only summary of a stored TUS resume session (diagnostics surfaces —\n * /camera's resume-pending indicator, /camera/admin). */\nexport interface StoredTusUploadSummary {\n urlStorageKey: string;\n fingerprint: string;\n size: number | null;\n metadata: Record<string, string>;\n creationTime: string;\n uploadUrl: string | null;\n}\n\n/**\n * List every stored TUS resume-URL entry (the `mtx-tus-urls` DB). Read-only —\n * resuming still happens inside `tusUploadRaw` via the UrlStorage; this only\n * lets management surfaces SHOW that a resumable session is pending. Returns\n * [] where IndexedDB is unavailable (never throws — diagnostics must not\n * break a page).\n */\nexport async function listStoredTusUploads(): Promise<\n StoredTusUploadSummary[]\n> {\n try {\n const db = await openUrlDb();\n const tx = db.transaction(URL_STORE, \"readonly\");\n const all = await idbRequest(\n tx.objectStore(URL_STORE).getAll() as IDBRequest<StoredTusUpload[]>,\n );\n return all.map((r) => ({\n urlStorageKey: r.urlStorageKey,\n fingerprint: r.fingerprint,\n size: r.size,\n metadata: r.metadata,\n creationTime: r.creationTime,\n uploadUrl: r.uploadUrl,\n }));\n } catch (err) {\n console.error(\"[tusUpload] listStoredTusUploads failed:\", err);\n return [];\n }\n}\n\n// ─── The transport ───────────────────────────────────────────────────────────\n\nexport interface TusUploadDeps {\n /** Injected HttpStack for unit tests (mock wire, no XHR). */\n httpStack?: tus.HttpStack;\n /** Injected file reader for unit tests (Node builds of tus-js-client\n * cannot slice browser Files). */\n fileReader?: ConstructorParameters<typeof tus.Upload>[1][\"fileReader\"];\n /** Override the endpoint (tests). */\n endpointOverride?: string;\n /** Override URL storage (tests). */\n urlStorage?: ReturnType<typeof createTusUrlStorage>;\n}\n\n/**\n * Resumable upload of one file via TUS. Same result contract as\n * `cloudUploadRaw` — `{ ok: true, fileId, ... } | { ok: false, error }`,\n * never throws. Automatically resumes a previous session for the same file\n * (custom UrlStorage + server HEAD), including the completed-session\n * recovery: when the server reports the session already finished and hands\n * back `X-Cld-File-Id`, the upload is skipped and the file resolves directly.\n */\nexport async function tusUploadRaw(\n file: File,\n filePath: string,\n options: CloudUploadOptions,\n idempotencyKey: string,\n deps: TusUploadDeps = {},\n): Promise<CloudUploadResult> {\n const endpoint =\n deps.endpointOverride ??\n `${resolveBaseUrlForPath(TUS_UPLOAD_PATH, undefined, \"POST\")}${TUS_UPLOAD_PATH}`;\n\n const metadataEnvelope = buildUploadMetadataEnvelope(options.metadata);\n\n let cldFileId: string | null = null;\n let aborted = false;\n\n try {\n await new Promise<void>((resolve, reject) => {\n const upload: tus.Upload = new tus.Upload(file, {\n endpoint,\n chunkSize: TUS_CHUNK_SIZE_BYTES,\n retryDelays: [0, 1000, 3000, 5000, 10000],\n removeFingerprintOnSuccess: true,\n urlStorage: deps.urlStorage ?? createTusUrlStorage(),\n ...(deps.httpStack ? { httpStack: deps.httpStack } : {}),\n ...(deps.fileReader ? { fileReader: deps.fileReader } : {}),\n metadata: {\n filename: file.name,\n filepath: filePath,\n // ONE validated JSON envelope — tus-js-client base64-encodes the\n // value per the Upload-Metadata spec; the server parses and merges\n // it exactly like the buffered `metadata_json` form field.\n metadata_json: JSON.stringify(metadataEnvelope),\n ...(options.visibility ? { visibility: options.visibility } : {}),\n },\n onBeforeRequest: async (req) => {\n // FRESH auth on EVERY request — long uploads outlive a JWT.\n //\n // `buildHeaders` (lib/python-client.ts) is mandatory-org, fail-closed:\n // it resolves the organization the SAME way `bindUploadOrganization`\n // in cloudUpload.ts already committed it (that gate dispatches into\n // Redux BEFORE this ever runs — see OrganizationGateDialog's\n // `resolveOrganizationForBlockedAction` — so the selection is already\n // there by the time the first chunk goes out). Until this fix,\n // X-Organization-Id was computed here and then silently dropped —\n // only Authorization and X-Guest-Fingerprint were copied onto the\n // TUS request, so every resumable upload reached the server\n // completely unscoped (aidream commit 8e5ee0b93 now 400s that).\n const { headers } = await buildHeaders({}, false);\n if (headers.Authorization) {\n req.setHeader(\"Authorization\", headers.Authorization);\n }\n if (headers[\"X-Guest-Fingerprint\"]) {\n req.setHeader(\"X-Guest-Fingerprint\", headers[\"X-Guest-Fingerprint\"]);\n }\n if (headers[\"X-Organization-Id\"]) {\n req.setHeader(\"X-Organization-Id\", headers[\"X-Organization-Id\"]);\n }\n if (req.getMethod() === \"POST\") {\n // Creation only — one intended upload, one key.\n req.setHeader(\"X-Idempotency-Key\", idempotencyKey);\n }\n },\n onAfterResponse: (_req, res) => {\n // The final PATCH (and the completed-session HEAD recovery) carry\n // the created file id. Capture it whenever it appears.\n const id = res.getHeader(\"X-Cld-File-Id\");\n if (id) cldFileId = id;\n },\n onProgress: (bytesSent, bytesTotal) => {\n options.onProgress?.({ loaded: bytesSent, total: bytesTotal });\n },\n onSuccess: () => resolve(),\n onError: (error) => reject(error),\n });\n\n if (options.signal) {\n if (options.signal.aborted) {\n aborted = true;\n reject(new Error(\"Upload cancelled\"));\n return;\n }\n options.signal.addEventListener(\n \"abort\",\n () => {\n aborted = true;\n void upload.abort();\n reject(new Error(\"Upload cancelled\"));\n },\n { once: true },\n );\n }\n\n // Resume automatically when a previous session for this file exists —\n // tus-js-client HEADs the stored URL; a completed session resolves via\n // the X-Cld-File-Id captured in onAfterResponse.\n void upload\n .findPreviousUploads()\n .then((previous) => {\n if (previous.length > 0) {\n upload.resumeFromPreviousUpload(previous[0]);\n }\n upload.start();\n })\n .catch((err) => {\n console.warn(\n \"[tusUpload] previous-upload lookup failed — starting fresh:\",\n err,\n );\n upload.start();\n });\n });\n } catch (err) {\n if (cldFileId && !aborted) {\n // Completed-session recovery: the transfer errored (e.g. a lost final\n // response surfaced as an error) but the server told us the file\n // exists. Resolve it instead of failing/re-uploading. LOUD by design.\n console.warn(\n `[tusUpload] transfer reported an error but the server exposed ` +\n `X-Cld-File-Id=${cldFileId} — recovering the completed session.`,\n );\n } else {\n return {\n ok: false,\n error: extractErrorMessage(err),\n errorCode: aborted ? \"upload_cancelled\" : \"tus_upload_failed\",\n fileName: file.name,\n };\n }\n }\n\n if (!cldFileId) {\n return {\n ok: false,\n error:\n \"TUS upload finished but the server never exposed X-Cld-File-Id — \" +\n \"the file cannot be resolved. Check server CORS expose_headers.\",\n errorCode: \"tus_missing_file_id\",\n fileName: file.name,\n };\n }\n\n // Hydrate the canonical row so the result matches the buffered contract.\n try {\n const { data: record } = await Files.getFile(cldFileId);\n return {\n ok: true,\n fileId: record.id,\n filePath: record.file_path,\n fileSize: record.size_bytes ?? file.size,\n versionNumber: record.current_version ?? 1,\n url: null,\n };\n } catch (err) {\n return {\n ok: false,\n error: `TUS upload completed (file ${cldFileId}) but the record fetch failed: ${extractErrorMessage(err)}`,\n errorCode: \"tus_record_fetch_failed\",\n fileName: file.name,\n };\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -9,7 +9,7 @@ async function requestUpload(arg) {
|
|
|
9
9
|
if (registered) {
|
|
10
10
|
return registered(arg);
|
|
11
11
|
}
|
|
12
|
-
if (
|
|
12
|
+
if (process.env.NODE_ENV !== "production") {
|
|
13
13
|
console.warn(
|
|
14
14
|
"[upload-guard] requestUpload called before <UploadGuardHost/> mounted \u2014 falling back to direct dispatch with no duplicate check."
|
|
15
15
|
);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/files/engine/upload/uploadGuardOpeners.ts"],"sourcesContent":["/**\n * features/files/upload/uploadGuardOpeners.ts\n *\n * Module-level imperative API for the upload-guard system. Same shape\n * as `cloudFilesPickerOpeners` — a single host component registers a\n * handler at mount, and any code anywhere can call `requestUpload`\n * without threading state.\n *\n * The host pre-flights uploaded files for duplicates (identical\n * checksum or filename collisions in the target folder), shows a\n * resolution dialog if needed, and only then dispatches the upload.\n * If the host hasn't mounted yet, `requestUpload` warns in dev and\n * falls back to a direct thunk dispatch so we don't break the upload\n * for the (rare) caller that fires during boot.\n *\n * Pure TypeScript — no React, no dialog markup. Safe to import from\n * anywhere, including the static graph of route entries.\n */\n\nimport type { UploadFilesArg } from \"../types\";\nimport type { AppDispatch } from \"../host/store\";\n\n/**\n * The handler the host registers at mount. Returns a promise that\n * resolves with the same shape as the underlying `uploadFiles` thunk\n * payload — `{ uploaded: string[], failed: [...] }`.\n */\nexport type UploadGuardHandler = (\n arg: UploadFilesArg,\n) => Promise<UploadGuardResult>;\n\nexport interface UploadGuardResult {\n uploaded: string[];\n failed: Array<{ name: string; error: string }>;\n /** True when the user dismissed the duplicate dialog. */\n cancelled: boolean;\n /**\n * Files the user chose to NOT re-upload because a duplicate already\n * exists, and they explicitly picked \"use the existing one.\" Each\n * entry maps the index of the original input file (in `arg.files`)\n * to the existing `cld_files.id` the user picked.\n *\n * The chat-resource-attach flow uses this to wire the existing\n * fileId into its parent context (e.g. attach the existing PDF to\n * the conversation) so the user gets a confirmed-and-attached file\n * with zero wasted bytes. Other callers can iterate `aliased` to\n * surface \"found N existing matches\" UI.\n *\n * Empty for the normal \"no duplicates\" upload path.\n */\n aliased: Array<{ inputIndex: number; existingFileId: string }>;\n}\n\nlet registered: UploadGuardHandler | null = null;\n\n/** Called once by the host's `useEffect` mount. */\nexport function registerUploadGuardHandler(handler: UploadGuardHandler): void {\n registered = handler;\n}\n\n/** Called by the host's cleanup. */\nexport function unregisterUploadGuardHandler(\n handler: UploadGuardHandler,\n): void {\n if (registered === handler) registered = null;\n}\n\n/**\n * Public entry point. Call from any UI surface that initiates an\n * upload (drag-drop, paste, FAB, picker). Returns the same shape as\n * `uploadFiles.unwrap()` plus a `cancelled` flag.\n *\n * Behaviour:\n * - If the host is mounted: pre-flight scan → optional dialog →\n * dispatch with the user's choices. Resolves when uploads finish\n * or when the user cancels (`cancelled: true`, no dispatch).\n * - If the host is NOT mounted: dispatches directly with no\n * duplicate check, so old call paths keep working. Logs a dev\n * warning so the missing host gets noticed.\n */\nexport async function requestUpload(\n arg: UploadFilesArg,\n): Promise<UploadGuardResult> {\n if (registered) {\n return registered(arg);\n }\n // Fallback path — should rarely fire in practice. Imported lazily\n // to keep this opener module dispatch-free at module load.\n if (process.env.NODE_ENV !== \"production\") {\n // eslint-disable-next-line no-console\n console.warn(\n \"[upload-guard] requestUpload called before <UploadGuardHost/> mounted — falling back to direct dispatch with no duplicate check.\",\n );\n }\n const [{ getStore }, { uploadFiles }] = await Promise.all([\n import(\"../host/store\"),\n import(\"../redux/thunks\"),\n ]);\n const store = getStore();\n if (!store) {\n return {\n uploaded: [],\n failed: arg.files.map((f) => ({\n name: f.name,\n error: \"Store not ready\",\n })),\n cancelled: false,\n aliased: [],\n };\n }\n try {\n const dispatch = store.dispatch as AppDispatch;\n const result = await dispatch(uploadFiles(arg)).unwrap();\n return { ...result, cancelled: false, aliased: [] };\n } catch (err) {\n return {\n uploaded: [],\n failed: arg.files.map((f) => ({\n name: f.name,\n error: err instanceof Error ? err.message : String(err),\n })),\n cancelled: false,\n aliased: [],\n };\n }\n}\n"],"mappings":"AAqDA,IAAI,aAAwC;AAGrC,SAAS,2BAA2B,SAAmC;AAC5E,eAAa;AACf;AAGO,SAAS,6BACd,SACM;AACN,MAAI,eAAe,QAAS,cAAa;AAC3C;AAeA,eAAsB,cACpB,KAC4B;AAC5B,MAAI,YAAY;AACd,WAAO,WAAW,GAAG;AAAA,EACvB;AAGA,MAAI,
|
|
1
|
+
{"version":3,"sources":["../../../../src/files/engine/upload/uploadGuardOpeners.ts"],"sourcesContent":["/**\n * features/files/upload/uploadGuardOpeners.ts\n *\n * Module-level imperative API for the upload-guard system. Same shape\n * as `cloudFilesPickerOpeners` — a single host component registers a\n * handler at mount, and any code anywhere can call `requestUpload`\n * without threading state.\n *\n * The host pre-flights uploaded files for duplicates (identical\n * checksum or filename collisions in the target folder), shows a\n * resolution dialog if needed, and only then dispatches the upload.\n * If the host hasn't mounted yet, `requestUpload` warns in dev and\n * falls back to a direct thunk dispatch so we don't break the upload\n * for the (rare) caller that fires during boot.\n *\n * Pure TypeScript — no React, no dialog markup. Safe to import from\n * anywhere, including the static graph of route entries.\n */\n\nimport type { UploadFilesArg } from \"../types\";\nimport type { AppDispatch } from \"../host/store\";\n\n/**\n * The handler the host registers at mount. Returns a promise that\n * resolves with the same shape as the underlying `uploadFiles` thunk\n * payload — `{ uploaded: string[], failed: [...] }`.\n */\nexport type UploadGuardHandler = (\n arg: UploadFilesArg,\n) => Promise<UploadGuardResult>;\n\nexport interface UploadGuardResult {\n uploaded: string[];\n failed: Array<{ name: string; error: string }>;\n /** True when the user dismissed the duplicate dialog. */\n cancelled: boolean;\n /**\n * Files the user chose to NOT re-upload because a duplicate already\n * exists, and they explicitly picked \"use the existing one.\" Each\n * entry maps the index of the original input file (in `arg.files`)\n * to the existing `cld_files.id` the user picked.\n *\n * The chat-resource-attach flow uses this to wire the existing\n * fileId into its parent context (e.g. attach the existing PDF to\n * the conversation) so the user gets a confirmed-and-attached file\n * with zero wasted bytes. Other callers can iterate `aliased` to\n * surface \"found N existing matches\" UI.\n *\n * Empty for the normal \"no duplicates\" upload path.\n */\n aliased: Array<{ inputIndex: number; existingFileId: string }>;\n}\n\nlet registered: UploadGuardHandler | null = null;\n\n/** Called once by the host's `useEffect` mount. */\nexport function registerUploadGuardHandler(handler: UploadGuardHandler): void {\n registered = handler;\n}\n\n/** Called by the host's cleanup. */\nexport function unregisterUploadGuardHandler(\n handler: UploadGuardHandler,\n): void {\n if (registered === handler) registered = null;\n}\n\n/**\n * Public entry point. Call from any UI surface that initiates an\n * upload (drag-drop, paste, FAB, picker). Returns the same shape as\n * `uploadFiles.unwrap()` plus a `cancelled` flag.\n *\n * Behaviour:\n * - If the host is mounted: pre-flight scan → optional dialog →\n * dispatch with the user's choices. Resolves when uploads finish\n * or when the user cancels (`cancelled: true`, no dispatch).\n * - If the host is NOT mounted: dispatches directly with no\n * duplicate check, so old call paths keep working. Logs a dev\n * warning so the missing host gets noticed.\n */\nexport async function requestUpload(\n arg: UploadFilesArg,\n): Promise<UploadGuardResult> {\n if (registered) {\n return registered(arg);\n }\n // Fallback path — should rarely fire in practice. Imported lazily\n // to keep this opener module dispatch-free at module load.\n if (process.env.NODE_ENV !== \"production\") {\n // eslint-disable-next-line no-console\n console.warn(\n \"[upload-guard] requestUpload called before <UploadGuardHost/> mounted — falling back to direct dispatch with no duplicate check.\",\n );\n }\n const [{ getStore }, { uploadFiles }] = await Promise.all([\n import(\"../host/store\"),\n import(\"../redux/thunks\"),\n ]);\n const store = getStore();\n if (!store) {\n return {\n uploaded: [],\n failed: arg.files.map((f) => ({\n name: f.name,\n error: \"Store not ready\",\n })),\n cancelled: false,\n aliased: [],\n };\n }\n try {\n const dispatch = store.dispatch as AppDispatch;\n const result = await dispatch(uploadFiles(arg)).unwrap();\n return { ...result, cancelled: false, aliased: [] };\n } catch (err) {\n return {\n uploaded: [],\n failed: arg.files.map((f) => ({\n name: f.name,\n error: err instanceof Error ? err.message : String(err),\n })),\n cancelled: false,\n aliased: [],\n };\n }\n}\n"],"mappings":"AAqDA,IAAI,aAAwC;AAGrC,SAAS,2BAA2B,SAAmC;AAC5E,eAAa;AACf;AAGO,SAAS,6BACd,SACM;AACN,MAAI,eAAe,QAAS,cAAa;AAC3C;AAeA,eAAsB,cACpB,KAC4B;AAC5B,MAAI,YAAY;AACd,WAAO,WAAW,GAAG;AAAA,EACvB;AAGA,MAAI,yBAAyB,cAAc;AAEzC,YAAQ;AAAA,MACN;AAAA,IACF;AAAA,EACF;AACA,QAAM,CAAC,EAAE,SAAS,GAAG,EAAE,YAAY,CAAC,IAAI,MAAM,QAAQ,IAAI;AAAA,IACxD,OAAO,eAAe;AAAA,IACtB,OAAO,iBAAiB;AAAA,EAC1B,CAAC;AACD,QAAM,QAAQ,SAAS;AACvB,MAAI,CAAC,OAAO;AACV,WAAO;AAAA,MACL,UAAU,CAAC;AAAA,MACX,QAAQ,IAAI,MAAM,IAAI,CAAC,OAAO;AAAA,QAC5B,MAAM,EAAE;AAAA,QACR,OAAO;AAAA,MACT,EAAE;AAAA,MACF,WAAW;AAAA,MACX,SAAS,CAAC;AAAA,IACZ;AAAA,EACF;AACA,MAAI;AACF,UAAM,WAAW,MAAM;AACvB,UAAM,SAAS,MAAM,SAAS,YAAY,GAAG,CAAC,EAAE,OAAO;AACvD,WAAO,EAAE,GAAG,QAAQ,WAAW,OAAO,SAAS,CAAC,EAAE;AAAA,EACpD,SAAS,KAAK;AACZ,WAAO;AAAA,MACL,UAAU,CAAC;AAAA,MACX,QAAQ,IAAI,MAAM,IAAI,CAAC,OAAO;AAAA,QAC5B,MAAM,EAAE;AAAA,QACR,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAAA,MACxD,EAAE;AAAA,MACF,WAAW;AAAA,MACX,SAAS,CAAC;AAAA,IACZ;AAAA,EACF;AACF;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-matrx/media",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.2",
|
|
4
4
|
"description": "The AI Matrx media components kit: durable-ref-only renderers for images, video, audio and file thumbnails plus live TTS playback that just work with the Matrx file system — headless core hooks plus DOM bindings, wired to an injected MediaClient. A signed URL is a handoff, never an identity.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -516,10 +516,10 @@
|
|
|
516
516
|
"tus-js-client": "^4.3.1",
|
|
517
517
|
"@ai-matrx/associations": "latest",
|
|
518
518
|
"@ai-matrx/data": "latest",
|
|
519
|
-
"@ai-matrx/agents": "latest",
|
|
520
519
|
"@ai-matrx/design-system": "latest",
|
|
520
|
+
"@ai-matrx/realtime": "latest",
|
|
521
521
|
"@ai-matrx/kit": "latest",
|
|
522
|
-
"@ai-matrx/
|
|
522
|
+
"@ai-matrx/agents": "latest"
|
|
523
523
|
},
|
|
524
524
|
"peerDependencies": {
|
|
525
525
|
"@reduxjs/toolkit": ">=2.0.0",
|
|
@@ -568,7 +568,7 @@
|
|
|
568
568
|
},
|
|
569
569
|
"sideEffects": false,
|
|
570
570
|
"scripts": {
|
|
571
|
-
"build": "pnpm clean && tsup && node ./scripts/engine-build.mjs",
|
|
571
|
+
"build": "pnpm clean && tsup && node ./scripts/engine-build.mjs && node ../scripts/check-node-env-not-inlined.mjs",
|
|
572
572
|
"clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
573
573
|
"check:package": "pnpm build && node ../scripts/check-exports-agreement.mjs && publint && pnpm verify:tarball",
|
|
574
574
|
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.engine.json --noEmit",
|