@ai-matrx/media 0.8.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/dist/files/engine/api/assets.d.ts +223 -0
- package/dist/files/engine/api/assets.js +124 -0
- package/dist/files/engine/api/assets.js.map +1 -0
- package/dist/files/engine/api/direct.d.ts +43 -0
- package/dist/files/engine/api/direct.js +73 -0
- package/dist/files/engine/api/direct.js.map +1 -0
- package/dist/files/engine/api/fileOrganization.d.ts +34 -0
- package/dist/files/engine/api/fileOrganization.js +38 -0
- package/dist/files/engine/api/fileOrganization.js.map +1 -0
- package/dist/files/engine/api/files.d.ts +249 -0
- package/dist/files/engine/api/files.js +231 -0
- package/dist/files/engine/api/files.js.map +1 -0
- package/dist/files/engine/api/folders.d.ts +63 -0
- package/dist/files/engine/api/folders.js +56 -0
- package/dist/files/engine/api/folders.js.map +1 -0
- package/dist/files/engine/api/office.d.ts +27 -0
- package/dist/files/engine/api/office.js +19 -0
- package/dist/files/engine/api/office.js.map +1 -0
- package/dist/files/engine/api/permissions.d.ts +37 -0
- package/dist/files/engine/api/permissions.js +109 -0
- package/dist/files/engine/api/permissions.js.map +1 -0
- package/dist/files/engine/api/versions.d.ts +26 -0
- package/dist/files/engine/api/versions.js +37 -0
- package/dist/files/engine/api/versions.js.map +1 -0
- package/dist/files/engine/cache/idb-store.d.ts +95 -0
- package/dist/files/engine/cache/idb-store.js +172 -0
- package/dist/files/engine/cache/idb-store.js.map +1 -0
- package/dist/files/engine/cache/policy.d.ts +14 -0
- package/dist/files/engine/cache/policy.js +40 -0
- package/dist/files/engine/cache/policy.js.map +1 -0
- package/dist/files/engine/cache/register-service-worker.d.ts +57 -0
- package/dist/files/engine/cache/register-service-worker.js +80 -0
- package/dist/files/engine/cache/register-service-worker.js.map +1 -0
- package/dist/files/engine/db-types.d.ts +48865 -0
- package/dist/files/engine/db-types.js +1 -0
- package/dist/files/engine/db-types.js.map +1 -0
- package/dist/files/engine/filesDb.d.ts +1913 -0
- package/dist/files/engine/filesDb.js +28 -0
- package/dist/files/engine/filesDb.js.map +1 -0
- package/dist/files/engine/handler/handler.d.ts +82 -0
- package/dist/files/engine/handler/handler.js +124 -0
- package/dist/files/engine/handler/handler.js.map +1 -0
- package/dist/files/engine/handler/hooks/useFile.d.ts +17 -0
- package/dist/files/engine/handler/hooks/useFile.js +75 -0
- package/dist/files/engine/handler/hooks/useFile.js.map +1 -0
- package/dist/files/engine/handler/hooks/useFileUpload.d.ts +76 -0
- package/dist/files/engine/handler/hooks/useFileUpload.js +67 -0
- package/dist/files/engine/handler/hooks/useFileUpload.js.map +1 -0
- package/dist/files/engine/handler/input/normalize.d.ts +14 -0
- package/dist/files/engine/handler/input/normalize.js +365 -0
- package/dist/files/engine/handler/input/normalize.js.map +1 -0
- package/dist/files/engine/handler/intelligence/access.d.ts +35 -0
- package/dist/files/engine/handler/intelligence/access.js +85 -0
- package/dist/files/engine/handler/intelligence/access.js.map +1 -0
- package/dist/files/engine/handler/intelligence/magic-bytes.d.ts +21 -0
- package/dist/files/engine/handler/intelligence/magic-bytes.js +67 -0
- package/dist/files/engine/handler/intelligence/magic-bytes.js.map +1 -0
- package/dist/files/engine/handler/output/target.d.ts +19 -0
- package/dist/files/engine/handler/output/target.js +222 -0
- package/dist/files/engine/handler/output/target.js.map +1 -0
- package/dist/files/engine/handler/resolver.d.ts +34 -0
- package/dist/files/engine/handler/resolver.js +133 -0
- package/dist/files/engine/handler/resolver.js.map +1 -0
- package/dist/files/engine/handler/types.d.ts +367 -0
- package/dist/files/engine/handler/types.js +1 -0
- package/dist/files/engine/handler/types.js.map +1 -0
- package/dist/files/engine/handler/upload.d.ts +32 -0
- package/dist/files/engine/handler/upload.js +295 -0
- package/dist/files/engine/handler/upload.js.map +1 -0
- package/dist/files/engine/handler/utils/classify.d.ts +14 -0
- package/dist/files/engine/handler/utils/classify.js +27 -0
- package/dist/files/engine/handler/utils/classify.js.map +1 -0
- package/dist/files/engine/handler/utils/prefer-locator.d.ts +32 -0
- package/dist/files/engine/handler/utils/prefer-locator.js +34 -0
- package/dist/files/engine/handler/utils/prefer-locator.js.map +1 -0
- package/dist/files/engine/handler/utils/python-base.d.ts +129 -0
- package/dist/files/engine/handler/utils/python-base.js +60 -0
- package/dist/files/engine/handler/utils/python-base.js.map +1 -0
- package/dist/files/engine/hooks/blob-cache.d.ts +130 -0
- package/dist/files/engine/hooks/blob-cache.js +162 -0
- package/dist/files/engine/hooks/blob-cache.js.map +1 -0
- package/dist/files/engine/hooks/office-extraction-cache.d.ts +28 -0
- package/dist/files/engine/hooks/office-extraction-cache.js +67 -0
- package/dist/files/engine/hooks/office-extraction-cache.js.map +1 -0
- package/dist/files/engine/host/configure.d.ts +166 -0
- package/dist/files/engine/host/configure.js +31 -0
- package/dist/files/engine/host/configure.js.map +1 -0
- package/dist/files/engine/host/org.d.ts +8 -0
- package/dist/files/engine/host/org.js +16 -0
- package/dist/files/engine/host/org.js.map +1 -0
- package/dist/files/engine/host/python-client.d.ts +60 -0
- package/dist/files/engine/host/python-client.js +46 -0
- package/dist/files/engine/host/python-client.js.map +1 -0
- package/dist/files/engine/host/share-links.d.ts +20 -0
- package/dist/files/engine/host/share-links.js +32 -0
- package/dist/files/engine/host/share-links.js.map +1 -0
- package/dist/files/engine/host/store.d.ts +24 -0
- package/dist/files/engine/host/store.js +23 -0
- package/dist/files/engine/host/store.js.map +1 -0
- package/dist/files/engine/host/supabase.d.ts +3808 -0
- package/dist/files/engine/host/supabase.js +33 -0
- package/dist/files/engine/host/supabase.js.map +1 -0
- package/dist/files/engine/host/toast.d.ts +14 -0
- package/dist/files/engine/host/toast.js +20 -0
- package/dist/files/engine/host/toast.js.map +1 -0
- package/dist/files/engine/host/typed-client.d.ts +136 -0
- package/dist/files/engine/host/typed-client.js +66 -0
- package/dist/files/engine/host/typed-client.js.map +1 -0
- package/dist/files/engine/index.d.ts +22 -0
- package/dist/files/engine/index.js +49 -0
- package/dist/files/engine/index.js.map +1 -0
- package/dist/files/engine/media/our-file-sources.d.ts +39 -0
- package/dist/files/engine/media/our-file-sources.js +83 -0
- package/dist/files/engine/media/our-file-sources.js.map +1 -0
- package/dist/files/engine/media/signed-url.d.ts +1 -0
- package/dist/files/engine/media/signed-url.js +6 -0
- package/dist/files/engine/media/signed-url.js.map +1 -0
- package/dist/files/engine/redux/converters.d.ts +73 -0
- package/dist/files/engine/redux/converters.js +286 -0
- package/dist/files/engine/redux/converters.js.map +1 -0
- package/dist/files/engine/redux/file-hydration.d.ts +15 -0
- package/dist/files/engine/redux/file-hydration.js +46 -0
- package/dist/files/engine/redux/file-hydration.js.map +1 -0
- package/dist/files/engine/redux/file-tree-auth-boundary.d.ts +5 -0
- package/dist/files/engine/redux/file-tree-auth-boundary.js +28 -0
- package/dist/files/engine/redux/file-tree-auth-boundary.js.map +1 -0
- package/dist/files/engine/redux/file-tree-timeout.d.ts +32 -0
- package/dist/files/engine/redux/file-tree-timeout.js +73 -0
- package/dist/files/engine/redux/file-tree-timeout.js.map +1 -0
- package/dist/files/engine/redux/mutation-toast-middleware.d.ts +27 -0
- package/dist/files/engine/redux/mutation-toast-middleware.js +103 -0
- package/dist/files/engine/redux/mutation-toast-middleware.js.map +1 -0
- package/dist/files/engine/redux/realtime-middleware.d.ts +61 -0
- package/dist/files/engine/redux/realtime-middleware.js +431 -0
- package/dist/files/engine/redux/realtime-middleware.js.map +1 -0
- package/dist/files/engine/redux/request-ledger.d.ts +68 -0
- package/dist/files/engine/redux/request-ledger.js +66 -0
- package/dist/files/engine/redux/request-ledger.js.map +1 -0
- package/dist/files/engine/redux/selectors.d.ts +3254 -0
- package/dist/files/engine/redux/selectors.js +428 -0
- package/dist/files/engine/redux/selectors.js.map +1 -0
- package/dist/files/engine/redux/slice.d.ts +157 -0
- package/dist/files/engine/redux/slice.js +836 -0
- package/dist/files/engine/redux/slice.js.map +1 -0
- package/dist/files/engine/redux/thunks.d.ts +449 -0
- package/dist/files/engine/redux/thunks.js +1684 -0
- package/dist/files/engine/redux/thunks.js.map +1 -0
- package/dist/files/engine/redux/tree-utils.d.ts +90 -0
- package/dist/files/engine/redux/tree-utils.js +200 -0
- package/dist/files/engine/redux/tree-utils.js.map +1 -0
- package/dist/files/engine/support/claimsUser.d.ts +77 -0
- package/dist/files/engine/support/claimsUser.js +48 -0
- package/dist/files/engine/support/claimsUser.js.map +1 -0
- package/dist/files/engine/support/datetime.d.ts +6 -0
- package/dist/files/engine/support/datetime.js +15 -0
- package/dist/files/engine/support/datetime.js.map +1 -0
- package/dist/files/engine/support/document-visibility.d.ts +14 -0
- package/dist/files/engine/support/document-visibility.js +28 -0
- package/dist/files/engine/support/document-visibility.js.map +1 -0
- package/dist/files/engine/support/logger.d.ts +28 -0
- package/dist/files/engine/support/logger.js +36 -0
- package/dist/files/engine/support/logger.js.map +1 -0
- package/dist/files/engine/types.d.ts +1081 -0
- package/dist/files/engine/types.js +30 -0
- package/dist/files/engine/types.js.map +1 -0
- package/dist/files/engine/upload/cloudUpload.d.ts +182 -0
- package/dist/files/engine/upload/cloudUpload.js +418 -0
- package/dist/files/engine/upload/cloudUpload.js.map +1 -0
- package/dist/files/engine/upload/tusUpload.d.ts +100 -0
- package/dist/files/engine/upload/tusUpload.js +258 -0
- package/dist/files/engine/upload/tusUpload.js.map +1 -0
- package/dist/files/engine/upload/uploadDedupGuard.d.ts +58 -0
- package/dist/files/engine/upload/uploadDedupGuard.js +41 -0
- package/dist/files/engine/upload/uploadDedupGuard.js.map +1 -0
- package/dist/files/engine/upload/uploadGuardOpeners.d.ts +70 -0
- package/dist/files/engine/upload/uploadGuardOpeners.js +54 -0
- package/dist/files/engine/upload/uploadGuardOpeners.js.map +1 -0
- package/dist/files/engine/utils/file-types.d.ts +243 -0
- package/dist/files/engine/utils/file-types.js +1722 -0
- package/dist/files/engine/utils/file-types.js.map +1 -0
- package/dist/files/engine/utils/user-visible.d.ts +133 -0
- package/dist/files/engine/utils/user-visible.js +115 -0
- package/dist/files/engine/utils/user-visible.js.map +1 -0
- package/package.json +392 -8
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { toast } from "../host/toast.js";
|
|
2
|
+
const MUTATION_THUNK_NAMES = /* @__PURE__ */ new Set([
|
|
3
|
+
// files
|
|
4
|
+
"renameFile",
|
|
5
|
+
"moveFile",
|
|
6
|
+
"deleteFile",
|
|
7
|
+
"restoreFile",
|
|
8
|
+
"updateFileMetadata",
|
|
9
|
+
"restoreVersion",
|
|
10
|
+
// (uploadFiles is intentionally excluded — uploads surface per-file errors
|
|
11
|
+
// inline in the UploadProgressList, so a toast would double up.)
|
|
12
|
+
// folders
|
|
13
|
+
"createFolder",
|
|
14
|
+
"updateFolder",
|
|
15
|
+
"deleteFolder",
|
|
16
|
+
"restoreFolder",
|
|
17
|
+
// bulk
|
|
18
|
+
"bulkDeleteFiles",
|
|
19
|
+
"bulkMoveFiles",
|
|
20
|
+
"bulkMoveFolders",
|
|
21
|
+
// permissions + sharing
|
|
22
|
+
"grantPermission",
|
|
23
|
+
"revokePermission",
|
|
24
|
+
"createShareLink",
|
|
25
|
+
"revokeShareLink",
|
|
26
|
+
// virtual sources (notes/code/etc. surfaced as files)
|
|
27
|
+
"renameAny",
|
|
28
|
+
"moveAny",
|
|
29
|
+
"deleteAny",
|
|
30
|
+
"writeAny"
|
|
31
|
+
]);
|
|
32
|
+
const TITLE = {
|
|
33
|
+
renameFile: "Couldn't rename",
|
|
34
|
+
renameAny: "Couldn't rename",
|
|
35
|
+
moveFile: "Couldn't move",
|
|
36
|
+
moveAny: "Couldn't move",
|
|
37
|
+
deleteFile: "Couldn't move to Trash",
|
|
38
|
+
deleteAny: "Couldn't move to Trash",
|
|
39
|
+
restoreFile: "Couldn't restore",
|
|
40
|
+
restoreFolder: "Couldn't restore folder",
|
|
41
|
+
updateFileMetadata: "Couldn't update file",
|
|
42
|
+
restoreVersion: "Couldn't restore that version",
|
|
43
|
+
uploadFiles: "Upload failed",
|
|
44
|
+
createFolder: "Couldn't create folder",
|
|
45
|
+
updateFolder: "Couldn't update folder",
|
|
46
|
+
deleteFolder: "Couldn't move folder to Trash",
|
|
47
|
+
bulkDeleteFiles: "Couldn't move some items to Trash",
|
|
48
|
+
bulkMoveFiles: "Couldn't move some items",
|
|
49
|
+
bulkMoveFolders: "Couldn't move some folders",
|
|
50
|
+
grantPermission: "Couldn't share",
|
|
51
|
+
revokePermission: "Couldn't update sharing",
|
|
52
|
+
createShareLink: "Couldn't create share link",
|
|
53
|
+
revokeShareLink: "Couldn't revoke share link",
|
|
54
|
+
writeAny: "Couldn't save"
|
|
55
|
+
};
|
|
56
|
+
function parseThunk(type, phase) {
|
|
57
|
+
const m = new RegExp(`^cloudFiles/([^/]+)/${phase}$`).exec(type);
|
|
58
|
+
return m ? m[1] : null;
|
|
59
|
+
}
|
|
60
|
+
const SUCCESS_TITLE = {
|
|
61
|
+
createShareLink: "Share link created",
|
|
62
|
+
revokeShareLink: "Share link revoked",
|
|
63
|
+
grantPermission: "Sharing updated",
|
|
64
|
+
revokePermission: "Sharing updated",
|
|
65
|
+
restoreVersion: "Version restored"
|
|
66
|
+
};
|
|
67
|
+
function messageFrom(action) {
|
|
68
|
+
if (typeof action.payload === "string" && action.payload.trim()) {
|
|
69
|
+
return action.payload.trim();
|
|
70
|
+
}
|
|
71
|
+
const msg = action.error?.message?.trim();
|
|
72
|
+
if (msg && msg.toLowerCase() !== "rejected") return msg;
|
|
73
|
+
return "Something went wrong.";
|
|
74
|
+
}
|
|
75
|
+
const cloudFilesMutationToastMiddleware = () => (next) => (action) => {
|
|
76
|
+
const result = next(action);
|
|
77
|
+
const a = action;
|
|
78
|
+
if (typeof a?.type !== "string") return result;
|
|
79
|
+
if (a.type.endsWith("/rejected")) {
|
|
80
|
+
const name = parseThunk(a.type, "rejected");
|
|
81
|
+
if (!name || !MUTATION_THUNK_NAMES.has(name)) return result;
|
|
82
|
+
if (a.meta?.aborted || a.meta?.condition) return result;
|
|
83
|
+
if (a.error?.name === "AbortError" || a.error?.name === "ConditionError") {
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
toast.error(TITLE[name] ?? "Something went wrong", {
|
|
87
|
+
description: messageFrom(a)
|
|
88
|
+
});
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
91
|
+
if (a.type.endsWith("/fulfilled")) {
|
|
92
|
+
const name = parseThunk(a.type, "fulfilled");
|
|
93
|
+
if (name && SUCCESS_TITLE[name]) {
|
|
94
|
+
toast.success(SUCCESS_TITLE[name]);
|
|
95
|
+
}
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
return result;
|
|
99
|
+
};
|
|
100
|
+
export {
|
|
101
|
+
cloudFilesMutationToastMiddleware
|
|
102
|
+
};
|
|
103
|
+
//# sourceMappingURL=mutation-toast-middleware.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../src/files/engine/redux/mutation-toast-middleware.ts"],"sourcesContent":["/**\n * features/files/redux/mutation-toast-middleware.ts\n *\n * Centralised user-facing error feedback for Cloud Files mutations (P0-4).\n *\n * Why this exists\n * ───────────────\n * Every file/folder mutation thunk already rolls back its optimistic update\n * and rethrows on failure — but the ~15 call sites (FileContextMenu,\n * BulkActionsBar, useFileActions, the canonical mutation hooks, …) `.unwrap()`\n * without a catch, so failures were SILENT: a rename that 403s just snapped\n * back with no explanation; a delete that failed left no trace. In an\n * enterprise file manager that is the single biggest \"feels broken\" driver.\n *\n * Rather than touch every call site (and every thunk's catch/finally shape),\n * we listen once, here, for the `<prefix>/rejected` action that RTK's\n * `createAsyncThunk` ALWAYS dispatches when a thunk throws — even when the\n * caller's `.unwrap()` rethrows. One middleware → 100% coverage, no thunk\n * edits, UI concern kept out of the thunk bodies.\n *\n * Reads (loadUserFileTree, loadPermissions, …) are intentionally excluded —\n * we only shout about MUTATIONS the user just attempted. Aborted/conditioned\n * dispatches (RTK `meta.aborted` / `meta.condition`) are skipped so a\n * superseded request never toasts.\n */\n\nimport type { Middleware } from \"@reduxjs/toolkit\";\nimport { toast } from \"../host/toast\";\n\n/**\n * Mutation thunk names (the segment between `cloudFiles/` and `/rejected`).\n * Keep in sync with features/files/redux/thunks.ts + virtual-thunks.ts. A\n * missing entry only means \"no toast for that op\" — never a crash.\n */\nconst MUTATION_THUNK_NAMES = new Set<string>([\n // files\n \"renameFile\",\n \"moveFile\",\n \"deleteFile\",\n \"restoreFile\",\n \"updateFileMetadata\",\n \"restoreVersion\",\n // (uploadFiles is intentionally excluded — uploads surface per-file errors\n // inline in the UploadProgressList, so a toast would double up.)\n // folders\n \"createFolder\",\n \"updateFolder\",\n \"deleteFolder\",\n \"restoreFolder\",\n // bulk\n \"bulkDeleteFiles\",\n \"bulkMoveFiles\",\n \"bulkMoveFolders\",\n // permissions + sharing\n \"grantPermission\",\n \"revokePermission\",\n \"createShareLink\",\n \"revokeShareLink\",\n // virtual sources (notes/code/etc. surfaced as files)\n \"renameAny\",\n \"moveAny\",\n \"deleteAny\",\n \"writeAny\",\n]);\n\n/**\n * Toast title per thunk name. Phrased so it reads on its own; the specific\n * error goes in the toast description.\n */\nconst TITLE: Record<string, string> = {\n renameFile: \"Couldn't rename\",\n renameAny: \"Couldn't rename\",\n moveFile: \"Couldn't move\",\n moveAny: \"Couldn't move\",\n deleteFile: \"Couldn't move to Trash\",\n deleteAny: \"Couldn't move to Trash\",\n restoreFile: \"Couldn't restore\",\n restoreFolder: \"Couldn't restore folder\",\n updateFileMetadata: \"Couldn't update file\",\n restoreVersion: \"Couldn't restore that version\",\n uploadFiles: \"Upload failed\",\n createFolder: \"Couldn't create folder\",\n updateFolder: \"Couldn't update folder\",\n deleteFolder: \"Couldn't move folder to Trash\",\n bulkDeleteFiles: \"Couldn't move some items to Trash\",\n bulkMoveFiles: \"Couldn't move some items\",\n bulkMoveFolders: \"Couldn't move some folders\",\n grantPermission: \"Couldn't share\",\n revokePermission: \"Couldn't update sharing\",\n createShareLink: \"Couldn't create share link\",\n revokeShareLink: \"Couldn't revoke share link\",\n writeAny: \"Couldn't save\",\n};\n\ninterface RejectedAction {\n type: string;\n error?: { name?: string; message?: string };\n meta?: { aborted?: boolean; condition?: boolean; rejectedWithValue?: boolean };\n payload?: unknown;\n}\n\nfunction parseThunk(type: string, phase: \"rejected\" | \"fulfilled\"): string | null {\n // `cloudFiles/renameFile/rejected` → `renameFile`\n const m = new RegExp(`^cloudFiles/([^/]+)/${phase}$`).exec(type);\n return m ? m[1] : null;\n}\n\n/**\n * P3-11: success toasts ONLY for actions whose result the user can't see\n * directly (a link/permission/version change has no visible row update).\n * Visible actions (rename shows the new name, move/delete change the list)\n * stay silent — a toast for those is noise.\n */\nconst SUCCESS_TITLE: Record<string, string> = {\n createShareLink: \"Share link created\",\n revokeShareLink: \"Share link revoked\",\n grantPermission: \"Sharing updated\",\n revokePermission: \"Sharing updated\",\n restoreVersion: \"Version restored\",\n};\n\nfunction messageFrom(action: RejectedAction): string {\n // Prefer a rejectWithValue string payload, then the serialized error.\n if (typeof action.payload === \"string\" && action.payload.trim()) {\n return action.payload.trim();\n }\n const msg = action.error?.message?.trim();\n if (msg && msg.toLowerCase() !== \"rejected\") return msg;\n return \"Something went wrong.\";\n}\n\nexport const cloudFilesMutationToastMiddleware: Middleware =\n () => (next) => (action) => {\n const result = next(action);\n\n const a = action as RejectedAction;\n if (typeof a?.type !== \"string\") return result;\n\n if (a.type.endsWith(\"/rejected\")) {\n const name = parseThunk(a.type, \"rejected\");\n if (!name || !MUTATION_THUNK_NAMES.has(name)) return result;\n // Skip superseded / conditioned dispatches — not real failures.\n if (a.meta?.aborted || a.meta?.condition) return result;\n if (a.error?.name === \"AbortError\" || a.error?.name === \"ConditionError\") {\n return result;\n }\n toast.error(TITLE[name] ?? \"Something went wrong\", {\n description: messageFrom(a),\n });\n return result;\n }\n\n if (a.type.endsWith(\"/fulfilled\")) {\n const name = parseThunk(a.type, \"fulfilled\");\n if (name && SUCCESS_TITLE[name]) {\n toast.success(SUCCESS_TITLE[name]);\n }\n return result;\n }\n\n return result;\n };\n"],"mappings":"AA2BA,SAAS,aAAa;AAOtB,MAAM,uBAAuB,oBAAI,IAAY;AAAA;AAAA,EAE3C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA;AAAA;AAAA,EAIA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAMD,MAAM,QAAgC;AAAA,EACpC,YAAY;AAAA,EACZ,WAAW;AAAA,EACX,UAAU;AAAA,EACV,SAAS;AAAA,EACT,YAAY;AAAA,EACZ,WAAW;AAAA,EACX,aAAa;AAAA,EACb,eAAe;AAAA,EACf,oBAAoB;AAAA,EACpB,gBAAgB;AAAA,EAChB,aAAa;AAAA,EACb,cAAc;AAAA,EACd,cAAc;AAAA,EACd,cAAc;AAAA,EACd,iBAAiB;AAAA,EACjB,eAAe;AAAA,EACf,iBAAiB;AAAA,EACjB,iBAAiB;AAAA,EACjB,kBAAkB;AAAA,EAClB,iBAAiB;AAAA,EACjB,iBAAiB;AAAA,EACjB,UAAU;AACZ;AASA,SAAS,WAAW,MAAc,OAAgD;AAEhF,QAAM,IAAI,IAAI,OAAO,uBAAuB,KAAK,GAAG,EAAE,KAAK,IAAI;AAC/D,SAAO,IAAI,EAAE,CAAC,IAAI;AACpB;AAQA,MAAM,gBAAwC;AAAA,EAC5C,iBAAiB;AAAA,EACjB,iBAAiB;AAAA,EACjB,iBAAiB;AAAA,EACjB,kBAAkB;AAAA,EAClB,gBAAgB;AAClB;AAEA,SAAS,YAAY,QAAgC;AAEnD,MAAI,OAAO,OAAO,YAAY,YAAY,OAAO,QAAQ,KAAK,GAAG;AAC/D,WAAO,OAAO,QAAQ,KAAK;AAAA,EAC7B;AACA,QAAM,MAAM,OAAO,OAAO,SAAS,KAAK;AACxC,MAAI,OAAO,IAAI,YAAY,MAAM,WAAY,QAAO;AACpD,SAAO;AACT;AAEO,MAAM,oCACX,MAAM,CAAC,SAAS,CAAC,WAAW;AAC1B,QAAM,SAAS,KAAK,MAAM;AAE1B,QAAM,IAAI;AACV,MAAI,OAAO,GAAG,SAAS,SAAU,QAAO;AAExC,MAAI,EAAE,KAAK,SAAS,WAAW,GAAG;AAChC,UAAM,OAAO,WAAW,EAAE,MAAM,UAAU;AAC1C,QAAI,CAAC,QAAQ,CAAC,qBAAqB,IAAI,IAAI,EAAG,QAAO;AAErD,QAAI,EAAE,MAAM,WAAW,EAAE,MAAM,UAAW,QAAO;AACjD,QAAI,EAAE,OAAO,SAAS,gBAAgB,EAAE,OAAO,SAAS,kBAAkB;AACxE,aAAO;AAAA,IACT;AACA,UAAM,MAAM,MAAM,IAAI,KAAK,wBAAwB;AAAA,MACjD,aAAa,YAAY,CAAC;AAAA,IAC5B,CAAC;AACD,WAAO;AAAA,EACT;AAEA,MAAI,EAAE,KAAK,SAAS,YAAY,GAAG;AACjC,UAAM,OAAO,WAAW,EAAE,MAAM,WAAW;AAC3C,QAAI,QAAQ,cAAc,IAAI,GAAG;AAC/B,YAAM,QAAQ,cAAc,IAAI,CAAC;AAAA,IACnC;AACA,WAAO;AAAA,EACT;AAEA,SAAO;AACT;","names":[]}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* features/files/redux/realtime-middleware.ts
|
|
3
|
+
*
|
|
4
|
+
* Bridges Supabase Realtime into Redux.
|
|
5
|
+
*
|
|
6
|
+
* LIFECYCLE
|
|
7
|
+
* ---------
|
|
8
|
+
* - Dispatch `cloudFilesRealtime/attach` (from
|
|
9
|
+
* CloudFilesRealtimeProvider on mount with a valid user id). Middleware
|
|
10
|
+
* opens a channel and subscribes to cld_* table changes.
|
|
11
|
+
* - Dispatch `cloudFilesRealtime/detach` on logout/unmount. Middleware tears
|
|
12
|
+
* the channel down.
|
|
13
|
+
* - On reconnect after error, middleware re-runs the tree RPC via
|
|
14
|
+
* `reconcileTree` to close any event gap.
|
|
15
|
+
*
|
|
16
|
+
* DEDUP
|
|
17
|
+
* -----
|
|
18
|
+
* Every REST write registers its `requestId` in
|
|
19
|
+
* [request-ledger.ts](./request-ledger.ts). Incoming realtime payloads are
|
|
20
|
+
* checked with `isOwnEcho`. Matches are skipped — the optimistic update
|
|
21
|
+
* already reflects the change. Non-matches are dispatched normally, so
|
|
22
|
+
* changes originating from other devices / the server / share-link
|
|
23
|
+
* visitors always reach our state.
|
|
24
|
+
*
|
|
25
|
+
* THAT LEDGER IS NOT A SECOND COPY OF THE PACKAGE'S. `@ai-matrx/realtime`'s
|
|
26
|
+
* write ledger correlates on (table, id, updated_at, content fingerprint);
|
|
27
|
+
* this one correlates on `metadata.request_id`, an end-to-end write id the
|
|
28
|
+
* Python backend stamps on every cloud_sync write under a documented wire
|
|
29
|
+
* contract (common-docs systems/files/file-service/WIRE_CONTRACT.md). That is
|
|
30
|
+
* strictly more precise than a timestamp+fingerprint inference and the package
|
|
31
|
+
* has no equivalent, so it stays — as the FEATURE's correlation, on top of
|
|
32
|
+
* everything the package does. (A consumer-supplied own-write predicate is a
|
|
33
|
+
* fair candidate for the package; until then this is the honest home.)
|
|
34
|
+
*
|
|
35
|
+
* REALTIME
|
|
36
|
+
* --------
|
|
37
|
+
* `@ai-matrx/realtime` owns the channel: unique instance topic (this used a
|
|
38
|
+
* STATIC `cloud-files:<userId>`, which React 19's double-invoked effects can
|
|
39
|
+
* collide on), dedup, the decoupled ordered handler queue, jittered reconnect
|
|
40
|
+
* with the stability reset, tab sleep and network awareness, diagnostics.
|
|
41
|
+
*
|
|
42
|
+
* The reconcile that used to hang off the SUBSCRIBED callback is now the
|
|
43
|
+
* package's `onBackfill` door, which is a strictly larger set of recovery
|
|
44
|
+
* paths: it also fires on tab wake, network restore and queue overflow, none
|
|
45
|
+
* of which the old status callback could see. Its cooldown and in-flight
|
|
46
|
+
* guards are kept — they are about not storming an expensive tree RPC, which
|
|
47
|
+
* is the feature's own economics, not reconnect logic.
|
|
48
|
+
*/
|
|
49
|
+
import type { Middleware } from "@reduxjs/toolkit";
|
|
50
|
+
export declare const ATTACH_ACTION: "cloudFilesRealtime/attach";
|
|
51
|
+
export declare const DETACH_ACTION: "cloudFilesRealtime/detach";
|
|
52
|
+
export declare function attachCloudFilesRealtime(userId: string): {
|
|
53
|
+
readonly type: "cloudFilesRealtime/attach";
|
|
54
|
+
readonly payload: {
|
|
55
|
+
readonly userId: string;
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
export declare function detachCloudFilesRealtime(): {
|
|
59
|
+
readonly type: "cloudFilesRealtime/detach";
|
|
60
|
+
};
|
|
61
|
+
export declare const cloudFilesRealtimeMiddleware: Middleware;
|
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import {
|
|
3
|
+
defineChannelNamespace,
|
|
4
|
+
subscribeToRealtimeManager
|
|
5
|
+
} from "@ai-matrx/realtime";
|
|
6
|
+
import {
|
|
7
|
+
canonicalShareLinkToCloudShareLink,
|
|
8
|
+
dbRowToCloudFile,
|
|
9
|
+
dbRowToCloudFilePermission,
|
|
10
|
+
dbRowToCloudFileVersion,
|
|
11
|
+
dbRowToCloudFolder
|
|
12
|
+
} from "./converters.js";
|
|
13
|
+
import { isOwnEcho, ledgerSize } from "./request-ledger.js";
|
|
14
|
+
import { loadUserFileTree, reconcileTree } from "./thunks.js";
|
|
15
|
+
import {
|
|
16
|
+
isDocumentHidden,
|
|
17
|
+
onceDocumentVisible
|
|
18
|
+
} from "../support/document-visibility.js";
|
|
19
|
+
import {
|
|
20
|
+
isUserVisibleFileRow,
|
|
21
|
+
isUserVisibleFolderPath
|
|
22
|
+
} from "../utils/user-visible.js";
|
|
23
|
+
import {
|
|
24
|
+
cachedVersion,
|
|
25
|
+
invalidate as invalidateBlobCache
|
|
26
|
+
} from "../hooks/blob-cache.js";
|
|
27
|
+
import { invalidateOfficeExtraction } from "../hooks/office-extraction-cache.js";
|
|
28
|
+
import {
|
|
29
|
+
attachChildToFolder,
|
|
30
|
+
clearSelection,
|
|
31
|
+
detachChildFromFolder,
|
|
32
|
+
removeFile,
|
|
33
|
+
removeFolder,
|
|
34
|
+
removeShareLink,
|
|
35
|
+
setRealtimeStatus,
|
|
36
|
+
touchRealtime,
|
|
37
|
+
upsertFile,
|
|
38
|
+
upsertFolder,
|
|
39
|
+
upsertPermissionsForResource,
|
|
40
|
+
upsertShareLinksForResource,
|
|
41
|
+
upsertVersionsForFile
|
|
42
|
+
} from "./slice.js";
|
|
43
|
+
const ATTACH_ACTION = "cloudFilesRealtime/attach";
|
|
44
|
+
const DETACH_ACTION = "cloudFilesRealtime/detach";
|
|
45
|
+
function attachCloudFilesRealtime(userId) {
|
|
46
|
+
return { type: ATTACH_ACTION, payload: { userId } };
|
|
47
|
+
}
|
|
48
|
+
function detachCloudFilesRealtime() {
|
|
49
|
+
return { type: DETACH_ACTION };
|
|
50
|
+
}
|
|
51
|
+
function isAttachAction(a) {
|
|
52
|
+
return typeof a === "object" && a !== null && a.type === ATTACH_ACTION;
|
|
53
|
+
}
|
|
54
|
+
function isDetachAction(a) {
|
|
55
|
+
return typeof a === "object" && a !== null && a.type === DETACH_ACTION;
|
|
56
|
+
}
|
|
57
|
+
function extractRequestId(payload) {
|
|
58
|
+
const row = payload.new ?? payload.old;
|
|
59
|
+
if (!row) return null;
|
|
60
|
+
const meta = row.metadata;
|
|
61
|
+
if (meta && typeof meta.request_id === "string") {
|
|
62
|
+
return meta.request_id;
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
const RECONCILE_COOLDOWN_MS = 1e4;
|
|
67
|
+
let lastReconcileAt = 0;
|
|
68
|
+
let initialSnapshotDoneAt = null;
|
|
69
|
+
const cloudFilesChannel = defineChannelNamespace({
|
|
70
|
+
namespace: "cloud-files",
|
|
71
|
+
parts: ["userId"],
|
|
72
|
+
description: "files.files / folders / file_versions + iam.permissions + platform.share_links for one user"
|
|
73
|
+
});
|
|
74
|
+
const cloudFilesRealtimeMiddleware = (store) => {
|
|
75
|
+
let stopChannel = null;
|
|
76
|
+
let subscribedUserId = null;
|
|
77
|
+
let cancelDeferredBackfill = null;
|
|
78
|
+
const dispatch = store.dispatch;
|
|
79
|
+
function teardown() {
|
|
80
|
+
cancelDeferredBackfill?.();
|
|
81
|
+
cancelDeferredBackfill = null;
|
|
82
|
+
if (stopChannel) {
|
|
83
|
+
stopChannel();
|
|
84
|
+
stopChannel = null;
|
|
85
|
+
}
|
|
86
|
+
subscribedUserId = null;
|
|
87
|
+
dispatch(setRealtimeStatus({ status: "detached", userId: null }));
|
|
88
|
+
}
|
|
89
|
+
function setup(userId) {
|
|
90
|
+
if (subscribedUserId === userId && stopChannel) return;
|
|
91
|
+
teardown();
|
|
92
|
+
subscribedUserId = userId;
|
|
93
|
+
initialSnapshotDoneAt = null;
|
|
94
|
+
dispatch(setRealtimeStatus({ status: "connecting", userId }));
|
|
95
|
+
const rowId = (row) => typeof row.id === "string" ? row.id : void 0;
|
|
96
|
+
stopChannel = subscribeToRealtimeManager(() => ({
|
|
97
|
+
topic: cloudFilesChannel.topic({ userId }),
|
|
98
|
+
postgresChanges: [
|
|
99
|
+
// Files — no server-side column filter (a created_by filter would drop
|
|
100
|
+
// updates to explicitly-shared files), BUT RLS alone is NOT the listing
|
|
101
|
+
// boundary: the `pub_read` policy makes every public file SELECT-visible,
|
|
102
|
+
// so handleFilePayload applies the LISTING gate client-side (owner or
|
|
103
|
+
// already-in-store only — mirror of DB `files.is_listable_for`). Never
|
|
104
|
+
// remove that gate; without it public files from other users stream
|
|
105
|
+
// into everyone's tree.
|
|
106
|
+
{
|
|
107
|
+
event: "*",
|
|
108
|
+
schema: "files",
|
|
109
|
+
table: "files",
|
|
110
|
+
rowId,
|
|
111
|
+
onChange: ({ payload }) => handleFilePayload(
|
|
112
|
+
payload
|
|
113
|
+
)
|
|
114
|
+
},
|
|
115
|
+
// Folders — NO owner filter, same RLS-bounded rationale as files.
|
|
116
|
+
{
|
|
117
|
+
event: "*",
|
|
118
|
+
schema: "files",
|
|
119
|
+
table: "folders",
|
|
120
|
+
rowId,
|
|
121
|
+
onChange: ({ payload }) => handleFolderPayload(
|
|
122
|
+
payload
|
|
123
|
+
)
|
|
124
|
+
},
|
|
125
|
+
// Versions — no owner filter (FK to files.files; RLS enforces).
|
|
126
|
+
{
|
|
127
|
+
event: "*",
|
|
128
|
+
schema: "files",
|
|
129
|
+
table: "file_versions",
|
|
130
|
+
rowId,
|
|
131
|
+
onChange: ({ payload }) => handleVersionPayload(
|
|
132
|
+
payload
|
|
133
|
+
)
|
|
134
|
+
},
|
|
135
|
+
// Permissions — canonical grant store `iam.permissions`. Filtered to
|
|
136
|
+
// grants made TO this user; the handler additionally guards
|
|
137
|
+
// `resource_type === 'file'` since this table holds grants for every
|
|
138
|
+
// resource type. (org-grant rows have no granted_to_user_id, so they
|
|
139
|
+
// won't arrive on this channel — the share panel reload picks them up.)
|
|
140
|
+
{
|
|
141
|
+
event: "*",
|
|
142
|
+
schema: "iam",
|
|
143
|
+
table: "permissions",
|
|
144
|
+
filter: `granted_to_user_id=eq.${userId}`,
|
|
145
|
+
rowId,
|
|
146
|
+
onChange: ({ payload }) => handlePermissionPayload(
|
|
147
|
+
payload
|
|
148
|
+
)
|
|
149
|
+
},
|
|
150
|
+
// Share links — canonical platform.share_links. RLS (owner-only
|
|
151
|
+
// SELECT) bounds delivery to links this user created. Requires the
|
|
152
|
+
// table in the realtime publication.
|
|
153
|
+
{
|
|
154
|
+
event: "*",
|
|
155
|
+
schema: "platform",
|
|
156
|
+
table: "share_links",
|
|
157
|
+
rowId,
|
|
158
|
+
onChange: ({ payload }) => handleShareLinkPayload(
|
|
159
|
+
payload
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
],
|
|
163
|
+
onStatusChange: (status) => {
|
|
164
|
+
dispatch(
|
|
165
|
+
setRealtimeStatus({
|
|
166
|
+
status: status === "connected" ? "subscribed" : status === "connecting" ? "connecting" : "errored",
|
|
167
|
+
userId: subscribedUserId
|
|
168
|
+
})
|
|
169
|
+
);
|
|
170
|
+
},
|
|
171
|
+
// THE BACKFILL DOOR — reconnect, tab wake, network restore, queue
|
|
172
|
+
// overflow. The old code hung this off SUBSCRIBED only, so a slept tab
|
|
173
|
+
// never reconciled and the tree silently drifted.
|
|
174
|
+
//
|
|
175
|
+
// The cooldown and in-flight guards below are NOT reconnect logic (the
|
|
176
|
+
// package owns that): they exist so a flapping connection cannot storm an
|
|
177
|
+
// expensive tree RPC or revert optimistic edits mid-write. First
|
|
178
|
+
// subscribe reconciles ONLY if the initial snapshot already resolved —
|
|
179
|
+
// its data predates the live channel, which is the gap window; a snapshot
|
|
180
|
+
// still in flight reads post-subscribe state, so firing here would only
|
|
181
|
+
// duplicate the RPC it is racing.
|
|
182
|
+
onBackfill: () => {
|
|
183
|
+
if (isDocumentHidden()) {
|
|
184
|
+
if (!cancelDeferredBackfill) {
|
|
185
|
+
cancelDeferredBackfill = onceDocumentVisible(() => {
|
|
186
|
+
cancelDeferredBackfill = null;
|
|
187
|
+
backfill();
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
backfill();
|
|
193
|
+
}
|
|
194
|
+
}));
|
|
195
|
+
function backfill() {
|
|
196
|
+
const now = Date.now();
|
|
197
|
+
if (initialSnapshotDoneAt !== null && now - lastReconcileAt >= RECONCILE_COOLDOWN_MS && ledgerSize() === 0) {
|
|
198
|
+
lastReconcileAt = now;
|
|
199
|
+
void dispatch(reconcileTree({ userId }));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function handleFilePayload(payload) {
|
|
204
|
+
dispatch(touchRealtime());
|
|
205
|
+
const requestId = extractRequestId(payload);
|
|
206
|
+
const rowId = payload.new?.id ?? payload.old?.id ?? null;
|
|
207
|
+
if (isOwnEcho({
|
|
208
|
+
requestId,
|
|
209
|
+
resourceId: rowId,
|
|
210
|
+
resourceType: "file"
|
|
211
|
+
})) {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (payload.eventType === "DELETE") {
|
|
215
|
+
if (rowId) {
|
|
216
|
+
dispatch(removeFile({ id: rowId }));
|
|
217
|
+
dispatch(clearSelection());
|
|
218
|
+
}
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
const newRow = payload.new;
|
|
222
|
+
if (!newRow?.id) return;
|
|
223
|
+
if (!isUserVisibleFileRow(newRow)) return;
|
|
224
|
+
const alreadyInStore = Boolean(
|
|
225
|
+
store.getState().cloudFiles.filesById[newRow.id]
|
|
226
|
+
);
|
|
227
|
+
if (newRow.created_by !== subscribedUserId && !alreadyInStore) return;
|
|
228
|
+
const file = dbRowToCloudFile(newRow);
|
|
229
|
+
const oldParent = store.getState().cloudFiles.filesById[file.id]?.parentFolderId ?? null;
|
|
230
|
+
dispatch(upsertFile(file));
|
|
231
|
+
if (file.deletedAt) {
|
|
232
|
+
dispatch(removeFile({ id: file.id }));
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (payload.eventType === "INSERT") {
|
|
236
|
+
dispatch(
|
|
237
|
+
attachChildToFolder({
|
|
238
|
+
parentFolderId: file.parentFolderId,
|
|
239
|
+
kind: "file",
|
|
240
|
+
id: file.id
|
|
241
|
+
})
|
|
242
|
+
);
|
|
243
|
+
} else if (payload.eventType === "UPDATE") {
|
|
244
|
+
if (oldParent !== file.parentFolderId) {
|
|
245
|
+
dispatch(
|
|
246
|
+
detachChildFromFolder({
|
|
247
|
+
parentFolderId: oldParent,
|
|
248
|
+
kind: "file",
|
|
249
|
+
id: file.id
|
|
250
|
+
})
|
|
251
|
+
);
|
|
252
|
+
dispatch(
|
|
253
|
+
attachChildToFolder({
|
|
254
|
+
parentFolderId: file.parentFolderId,
|
|
255
|
+
kind: "file",
|
|
256
|
+
id: file.id
|
|
257
|
+
})
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
function handleFolderPayload(payload) {
|
|
263
|
+
dispatch(touchRealtime());
|
|
264
|
+
const requestId = extractRequestId(payload);
|
|
265
|
+
const rowId = payload.new?.id ?? payload.old?.id ?? null;
|
|
266
|
+
if (isOwnEcho({
|
|
267
|
+
requestId,
|
|
268
|
+
resourceId: rowId,
|
|
269
|
+
resourceType: "folder"
|
|
270
|
+
})) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
if (payload.eventType === "DELETE") {
|
|
274
|
+
if (rowId) dispatch(removeFolder({ id: rowId }));
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const newRow = payload.new;
|
|
278
|
+
if (!newRow?.id) return;
|
|
279
|
+
if (!isUserVisibleFolderPath(newRow.folder_path)) return;
|
|
280
|
+
if (newRow.created_by !== subscribedUserId) return;
|
|
281
|
+
const folder = dbRowToCloudFolder(newRow);
|
|
282
|
+
const oldParent = store.getState().cloudFiles.foldersById[folder.id]?.parentId ?? null;
|
|
283
|
+
dispatch(upsertFolder(folder));
|
|
284
|
+
if (folder.deletedAt) {
|
|
285
|
+
dispatch(removeFolder({ id: folder.id }));
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
if (payload.eventType === "INSERT") {
|
|
289
|
+
dispatch(
|
|
290
|
+
attachChildToFolder({
|
|
291
|
+
parentFolderId: folder.parentId,
|
|
292
|
+
kind: "folder",
|
|
293
|
+
id: folder.id
|
|
294
|
+
})
|
|
295
|
+
);
|
|
296
|
+
} else if (payload.eventType === "UPDATE" && oldParent !== folder.parentId) {
|
|
297
|
+
dispatch(
|
|
298
|
+
detachChildFromFolder({
|
|
299
|
+
parentFolderId: oldParent,
|
|
300
|
+
kind: "folder",
|
|
301
|
+
id: folder.id
|
|
302
|
+
})
|
|
303
|
+
);
|
|
304
|
+
dispatch(
|
|
305
|
+
attachChildToFolder({
|
|
306
|
+
parentFolderId: folder.parentId,
|
|
307
|
+
kind: "folder",
|
|
308
|
+
id: folder.id
|
|
309
|
+
})
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
function handleVersionPayload(payload) {
|
|
314
|
+
dispatch(touchRealtime());
|
|
315
|
+
const newRow = payload.new;
|
|
316
|
+
const oldRow = payload.old;
|
|
317
|
+
const fileId = newRow?.file_id ?? oldRow?.file_id;
|
|
318
|
+
if (!fileId) return;
|
|
319
|
+
const state = store.getState();
|
|
320
|
+
const existing = state.cloudFiles.versionsByFileId[fileId] ?? [];
|
|
321
|
+
if (payload.eventType === "DELETE") {
|
|
322
|
+
const deletedId = oldRow?.id;
|
|
323
|
+
if (!deletedId) return;
|
|
324
|
+
dispatch(
|
|
325
|
+
upsertVersionsForFile({
|
|
326
|
+
fileId,
|
|
327
|
+
versions: existing.filter((v) => v.id !== deletedId)
|
|
328
|
+
})
|
|
329
|
+
);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
if (!newRow?.id) return;
|
|
333
|
+
const converted = dbRowToCloudFileVersion(newRow);
|
|
334
|
+
const next = [
|
|
335
|
+
converted,
|
|
336
|
+
...existing.filter((v) => v.id !== converted.id)
|
|
337
|
+
].sort((a, b) => b.versionNumber - a.versionNumber);
|
|
338
|
+
dispatch(upsertVersionsForFile({ fileId, versions: next }));
|
|
339
|
+
if (cachedVersion(fileId) === converted.versionNumber) return;
|
|
340
|
+
void invalidateBlobCache(fileId);
|
|
341
|
+
invalidateOfficeExtraction(fileId);
|
|
342
|
+
}
|
|
343
|
+
function handlePermissionPayload(payload) {
|
|
344
|
+
dispatch(touchRealtime());
|
|
345
|
+
const newRow = payload.new;
|
|
346
|
+
const oldRow = payload.old;
|
|
347
|
+
const resourceType = newRow?.resource_type ?? oldRow?.resource_type;
|
|
348
|
+
if (resourceType && resourceType !== "file") return;
|
|
349
|
+
const resourceId = newRow?.resource_id ?? oldRow?.resource_id;
|
|
350
|
+
if (!resourceId) return;
|
|
351
|
+
const state = store.getState();
|
|
352
|
+
const existing = state.cloudFiles.permissionsByResourceId[resourceId] ?? [];
|
|
353
|
+
if (payload.eventType === "DELETE") {
|
|
354
|
+
const deletedId = oldRow?.id;
|
|
355
|
+
if (!deletedId) return;
|
|
356
|
+
dispatch(
|
|
357
|
+
upsertPermissionsForResource({
|
|
358
|
+
resourceId,
|
|
359
|
+
permissions: existing.filter((p) => p.id !== deletedId)
|
|
360
|
+
})
|
|
361
|
+
);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
if (!newRow?.id) return;
|
|
365
|
+
const converted = dbRowToCloudFilePermission(newRow);
|
|
366
|
+
const next = [converted, ...existing.filter((p) => p.id !== converted.id)];
|
|
367
|
+
dispatch(upsertPermissionsForResource({ resourceId, permissions: next }));
|
|
368
|
+
}
|
|
369
|
+
function handleShareLinkPayload(payload) {
|
|
370
|
+
dispatch(touchRealtime());
|
|
371
|
+
const newRow = payload.new;
|
|
372
|
+
const oldRow = payload.old;
|
|
373
|
+
const resourceType = newRow?.resource_type ?? oldRow?.resource_type;
|
|
374
|
+
if (resourceType !== "file" && resourceType !== "folder") return;
|
|
375
|
+
const resourceId = newRow?.resource_id ?? oldRow?.resource_id;
|
|
376
|
+
if (!resourceId) return;
|
|
377
|
+
const state = store.getState();
|
|
378
|
+
const existing = state.cloudFiles.shareLinksByResourceId[resourceId] ?? [];
|
|
379
|
+
if (payload.eventType === "DELETE" || newRow?.is_active === false) {
|
|
380
|
+
const removedId = oldRow?.id ?? newRow?.id;
|
|
381
|
+
if (removedId) {
|
|
382
|
+
dispatch(removeShareLink({ linkId: removedId }));
|
|
383
|
+
}
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
if (!newRow?.id || !newRow.token) return;
|
|
387
|
+
const converted = canonicalShareLinkToCloudShareLink(
|
|
388
|
+
{
|
|
389
|
+
id: newRow.id,
|
|
390
|
+
token: newRow.token,
|
|
391
|
+
shortToken: newRow.short_token ?? null,
|
|
392
|
+
permissionLevel: newRow.permission_level ?? "viewer",
|
|
393
|
+
label: newRow.label ?? null,
|
|
394
|
+
expiresAt: newRow.expires_at ?? null,
|
|
395
|
+
maxUses: newRow.max_uses ?? null,
|
|
396
|
+
useCount: newRow.use_count ?? 0,
|
|
397
|
+
isActive: newRow.is_active ?? true,
|
|
398
|
+
createdAt: newRow.created_at ?? null,
|
|
399
|
+
lastUsedAt: newRow.last_used_at ?? null
|
|
400
|
+
},
|
|
401
|
+
resourceType,
|
|
402
|
+
resourceId
|
|
403
|
+
);
|
|
404
|
+
const next = [converted, ...existing.filter((l) => l.id !== converted.id)];
|
|
405
|
+
dispatch(upsertShareLinksForResource({ resourceId, shareLinks: next }));
|
|
406
|
+
}
|
|
407
|
+
return (next) => (action) => {
|
|
408
|
+
if (loadUserFileTree.fulfilled.match(action)) {
|
|
409
|
+
initialSnapshotDoneAt = Date.now();
|
|
410
|
+
return next(action);
|
|
411
|
+
}
|
|
412
|
+
if (isAttachAction(action)) {
|
|
413
|
+
const userId = action.payload.userId;
|
|
414
|
+
setup(userId);
|
|
415
|
+
return next(action);
|
|
416
|
+
}
|
|
417
|
+
if (isDetachAction(action)) {
|
|
418
|
+
teardown();
|
|
419
|
+
return next(action);
|
|
420
|
+
}
|
|
421
|
+
return next(action);
|
|
422
|
+
};
|
|
423
|
+
};
|
|
424
|
+
export {
|
|
425
|
+
ATTACH_ACTION,
|
|
426
|
+
DETACH_ACTION,
|
|
427
|
+
attachCloudFilesRealtime,
|
|
428
|
+
cloudFilesRealtimeMiddleware,
|
|
429
|
+
detachCloudFilesRealtime
|
|
430
|
+
};
|
|
431
|
+
//# sourceMappingURL=realtime-middleware.js.map
|