@bendyline/docblocks 1.1.2 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +76 -11
- package/dist/chunk-6IYMTYMQ.js +1692 -0
- package/dist/chunk-6IYMTYMQ.js.map +1 -0
- package/dist/chunk-AEHCVWU5.js +25 -0
- package/dist/chunk-AEHCVWU5.js.map +1 -0
- package/dist/chunk-B56KWCSE.js +802 -0
- package/dist/chunk-B56KWCSE.js.map +1 -0
- package/dist/{chunk-ME76RUMR.js → chunk-BNKV7MTO.js} +15 -1
- package/dist/chunk-BNKV7MTO.js.map +1 -0
- package/dist/chunk-BXMF5YQJ.js +149 -0
- package/dist/chunk-BXMF5YQJ.js.map +1 -0
- package/dist/chunk-COYQXQK3.js +1071 -0
- package/dist/chunk-COYQXQK3.js.map +1 -0
- package/dist/chunk-DN7A33LL.js +780 -0
- package/dist/chunk-DN7A33LL.js.map +1 -0
- package/dist/chunk-EZKJZUNJ.js +13 -0
- package/dist/chunk-EZKJZUNJ.js.map +1 -0
- package/dist/chunk-FA7UNOXK.js +20 -0
- package/dist/chunk-FA7UNOXK.js.map +1 -0
- package/dist/chunk-FNW556FN.js +75 -0
- package/dist/chunk-FNW556FN.js.map +1 -0
- package/dist/chunk-GM5YXU4L.js +29 -0
- package/dist/chunk-GM5YXU4L.js.map +1 -0
- package/dist/chunk-JS6XFWQK.js +225 -0
- package/dist/chunk-JS6XFWQK.js.map +1 -0
- package/dist/chunk-MNSZ7EZN.js +111 -0
- package/dist/chunk-MNSZ7EZN.js.map +1 -0
- package/dist/chunk-P3DVZQ6S.js +228 -0
- package/dist/chunk-P3DVZQ6S.js.map +1 -0
- package/dist/chunk-P4WATND4.js +52 -0
- package/dist/chunk-P4WATND4.js.map +1 -0
- package/dist/chunk-RVPGQBWR.js +169 -0
- package/dist/chunk-RVPGQBWR.js.map +1 -0
- package/dist/chunk-U722UUFN.js +9 -0
- package/dist/chunk-U722UUFN.js.map +1 -0
- package/dist/chunk-VTCSF7VS.js +553 -0
- package/dist/chunk-VTCSF7VS.js.map +1 -0
- package/dist/chunk-WJKYY5QR.js +1350 -0
- package/dist/chunk-WJKYY5QR.js.map +1 -0
- package/dist/chunk-XUML3MKV.js +945 -0
- package/dist/chunk-XUML3MKV.js.map +1 -0
- package/dist/document/index.d.ts +358 -0
- package/dist/document/index.js +25 -0
- package/dist/document/index.js.map +1 -0
- package/dist/filesystem/electron.d.ts +73 -0
- package/dist/filesystem/electron.js +17 -0
- package/dist/filesystem/electron.js.map +1 -0
- package/dist/filesystem/index.d.ts +228 -8
- package/dist/filesystem/index.js +133 -6
- package/dist/filesystem/index.js.map +1 -1
- package/dist/filesystem/indexeddb.d.ts +284 -0
- package/dist/filesystem/indexeddb.js +23 -0
- package/dist/filesystem/indexeddb.js.map +1 -0
- package/dist/filesystem/memory.d.ts +152 -0
- package/dist/filesystem/memory.js +13 -0
- package/dist/filesystem/memory.js.map +1 -0
- package/dist/filesystem/native.d.ts +136 -0
- package/dist/filesystem/native.js +28 -0
- package/dist/filesystem/native.js.map +1 -0
- package/dist/host/index.d.ts +32 -6
- package/dist/host/index.js +26 -27
- package/dist/host/index.js.map +1 -1
- package/dist/index.d.ts +14 -9
- package/dist/index.js +204 -8
- package/dist/index.js.map +1 -1
- package/dist/mcp/index.d.ts +33 -0
- package/dist/mcp/index.js +32 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/zod.d.ts +5765 -0
- package/dist/mcp/zod.js +724 -0
- package/dist/mcp/zod.js.map +1 -0
- package/dist/share/index.d.ts +43 -0
- package/dist/share/index.js +13 -0
- package/dist/share/index.js.map +1 -0
- package/dist/types-BEg5SQ1_.d.ts +301 -0
- package/dist/types-CX893hgH.d.ts +468 -0
- package/dist/types-CueK0tvU.d.ts +501 -0
- package/dist/vscode/index.d.ts +220 -0
- package/dist/vscode/index.js +383 -0
- package/dist/vscode/index.js.map +1 -0
- package/dist/workspace/index.d.ts +141 -3
- package/dist/workspace/index.js +28 -1
- package/dist/workspace/index.js.map +1 -1
- package/dist/workspace-path-CWVrcuPL.d.ts +21 -0
- package/package.json +50 -5
- package/dist/chunk-GUNM43XZ.js +0 -63
- package/dist/chunk-GUNM43XZ.js.map +0 -1
- package/dist/chunk-IM7LCQJ6.js +0 -850
- package/dist/chunk-IM7LCQJ6.js.map +0 -1
- package/dist/chunk-ME76RUMR.js.map +0 -1
- package/dist/filesystem/electron-provider.d.ts +0 -32
- package/dist/filesystem/electron-provider.d.ts.map +0 -1
- package/dist/filesystem/electron-provider.js +0 -64
- package/dist/filesystem/electron-provider.js.map +0 -1
- package/dist/filesystem/file-media-provider.d.ts +0 -23
- package/dist/filesystem/file-media-provider.d.ts.map +0 -1
- package/dist/filesystem/file-media-provider.js +0 -85
- package/dist/filesystem/file-media-provider.js.map +0 -1
- package/dist/filesystem/filesystem-content-container.d.ts +0 -24
- package/dist/filesystem/filesystem-content-container.d.ts.map +0 -1
- package/dist/filesystem/filesystem-content-container.js +0 -116
- package/dist/filesystem/filesystem-content-container.js.map +0 -1
- package/dist/filesystem/index.d.ts.map +0 -1
- package/dist/filesystem/indexeddb-content-container.d.ts +0 -20
- package/dist/filesystem/indexeddb-content-container.d.ts.map +0 -1
- package/dist/filesystem/indexeddb-content-container.js +0 -93
- package/dist/filesystem/indexeddb-content-container.js.map +0 -1
- package/dist/filesystem/indexeddb-provider.d.ts +0 -32
- package/dist/filesystem/indexeddb-provider.d.ts.map +0 -1
- package/dist/filesystem/indexeddb-provider.js +0 -295
- package/dist/filesystem/indexeddb-provider.js.map +0 -1
- package/dist/filesystem/native-provider.d.ts +0 -45
- package/dist/filesystem/native-provider.d.ts.map +0 -1
- package/dist/filesystem/native-provider.js +0 -336
- package/dist/filesystem/native-provider.js.map +0 -1
- package/dist/filesystem/types.d.ts +0 -50
- package/dist/filesystem/types.d.ts.map +0 -1
- package/dist/filesystem/types.js +0 -8
- package/dist/filesystem/types.js.map +0 -1
- package/dist/host/index.d.ts.map +0 -1
- package/dist/host/types.d.ts +0 -149
- package/dist/host/types.d.ts.map +0 -1
- package/dist/host/types.js +0 -10
- package/dist/host/types.js.map +0 -1
- package/dist/index-pgBHBQ9G.d.ts +0 -213
- package/dist/index.d.ts.map +0 -1
- package/dist/workspace/index.d.ts.map +0 -1
- package/dist/workspace/types.d.ts +0 -29
- package/dist/workspace/types.d.ts.map +0 -1
- package/dist/workspace/types.js +0 -6
- package/dist/workspace/types.js.map +0 -1
- package/dist/workspace/workspace-manager.d.ts +0 -35
- package/dist/workspace/workspace-manager.d.ts.map +0 -1
- package/dist/workspace/workspace-manager.js +0 -82
- package/dist/workspace/workspace-manager.js.map +0 -1
- package/src/filesystem/electron-provider.ts +0 -88
- package/src/filesystem/file-media-provider.ts +0 -103
- package/src/filesystem/filesystem-content-container.ts +0 -126
- package/src/filesystem/index.ts +0 -24
- package/src/filesystem/indexeddb-content-container.ts +0 -104
- package/src/filesystem/indexeddb-provider.ts +0 -345
- package/src/filesystem/native-provider.ts +0 -384
- package/src/filesystem/types.ts +0 -69
- package/src/host/index.ts +0 -47
- package/src/host/types.ts +0 -153
- package/src/index.ts +0 -9
- package/src/workspace/index.ts +0 -10
- package/src/workspace/types.ts +0 -29
- package/src/workspace/workspace-manager.ts +0 -90
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parseWorkspacePath
|
|
3
|
+
} from "./chunk-JS6XFWQK.js";
|
|
4
|
+
import {
|
|
5
|
+
getFileSystemProviderV2
|
|
6
|
+
} from "./chunk-EZKJZUNJ.js";
|
|
7
|
+
|
|
8
|
+
// src/filesystem/move-error.ts
|
|
9
|
+
function locationFromPresence(source, destination) {
|
|
10
|
+
if (source === "unknown" || destination === "unknown") return "unknown";
|
|
11
|
+
if (source === "present" && destination === "present") return "both";
|
|
12
|
+
if (source === "present") return "source";
|
|
13
|
+
if (destination === "present") return "destination";
|
|
14
|
+
return "neither";
|
|
15
|
+
}
|
|
16
|
+
var FileSystemMoveRecoveryError = class extends Error {
|
|
17
|
+
constructor(sourcePath, destinationPath, state, moveError) {
|
|
18
|
+
super(
|
|
19
|
+
`The move did not restore one authoritative path. Entry location: ${locationFromPresence(
|
|
20
|
+
state.source,
|
|
21
|
+
state.destination
|
|
22
|
+
)}.`
|
|
23
|
+
);
|
|
24
|
+
this.sourcePath = sourcePath;
|
|
25
|
+
this.destinationPath = destinationPath;
|
|
26
|
+
this.state = state;
|
|
27
|
+
this.moveError = moveError;
|
|
28
|
+
this.code = "partial-move";
|
|
29
|
+
this.name = "FileSystemMoveRecoveryError";
|
|
30
|
+
}
|
|
31
|
+
get location() {
|
|
32
|
+
return locationFromPresence(this.state.source, this.state.destination);
|
|
33
|
+
}
|
|
34
|
+
/** Alias used by DocumentSession's document-retarget recovery protocol. */
|
|
35
|
+
get documentLocation() {
|
|
36
|
+
return this.location;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
var FileSystemPartialMoveError = class extends Error {
|
|
40
|
+
constructor(sourcePath, destinationPath, companionSourcePath, companionDestinationPath, state, moveError, rollbackError) {
|
|
41
|
+
super(
|
|
42
|
+
`The document move could not be rolled back. Document location: ${locationFromPresence(
|
|
43
|
+
state.source,
|
|
44
|
+
state.destination
|
|
45
|
+
)}; companion location: ${locationFromPresence(
|
|
46
|
+
state.companionSource,
|
|
47
|
+
state.companionDestination
|
|
48
|
+
)}.`
|
|
49
|
+
);
|
|
50
|
+
this.sourcePath = sourcePath;
|
|
51
|
+
this.destinationPath = destinationPath;
|
|
52
|
+
this.companionSourcePath = companionSourcePath;
|
|
53
|
+
this.companionDestinationPath = companionDestinationPath;
|
|
54
|
+
this.state = state;
|
|
55
|
+
this.moveError = moveError;
|
|
56
|
+
this.rollbackError = rollbackError;
|
|
57
|
+
this.code = "partial-move";
|
|
58
|
+
this.name = "FileSystemPartialMoveError";
|
|
59
|
+
}
|
|
60
|
+
get documentLocation() {
|
|
61
|
+
return locationFromPresence(this.state.source, this.state.destination);
|
|
62
|
+
}
|
|
63
|
+
get companionLocation() {
|
|
64
|
+
return locationFromPresence(this.state.companionSource, this.state.companionDestination);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
function isFileSystemPartialMoveError(error) {
|
|
68
|
+
return error instanceof FileSystemPartialMoveError;
|
|
69
|
+
}
|
|
70
|
+
function isFileSystemMoveRecoveryError(error) {
|
|
71
|
+
return error instanceof FileSystemMoveRecoveryError;
|
|
72
|
+
}
|
|
73
|
+
function isFileSystemMoveStateError(error) {
|
|
74
|
+
return isFileSystemMoveRecoveryError(error) || isFileSystemPartialMoveError(error);
|
|
75
|
+
}
|
|
76
|
+
async function probePath(provider, path) {
|
|
77
|
+
try {
|
|
78
|
+
const providerV2 = getFileSystemProviderV2(provider);
|
|
79
|
+
const present = providerV2 ? await providerV2.stat(parseWorkspacePath(path)) !== null : await provider.exists(path);
|
|
80
|
+
return present ? "present" : "missing";
|
|
81
|
+
} catch {
|
|
82
|
+
return "unknown";
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async function describePartialMove(provider, sourcePath, destinationPath, companionSourcePath, companionDestinationPath) {
|
|
86
|
+
const [source, destination, companionSource, companionDestination] = await Promise.all([
|
|
87
|
+
probePath(provider, sourcePath),
|
|
88
|
+
probePath(provider, destinationPath),
|
|
89
|
+
probePath(provider, companionSourcePath),
|
|
90
|
+
probePath(provider, companionDestinationPath)
|
|
91
|
+
]);
|
|
92
|
+
return { source, destination, companionSource, companionDestination };
|
|
93
|
+
}
|
|
94
|
+
async function describeEntryMove(provider, sourcePath, destinationPath) {
|
|
95
|
+
const [source, destination] = await Promise.all([
|
|
96
|
+
probePath(provider, sourcePath),
|
|
97
|
+
probePath(provider, destinationPath)
|
|
98
|
+
]);
|
|
99
|
+
return { source, destination };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export {
|
|
103
|
+
FileSystemMoveRecoveryError,
|
|
104
|
+
FileSystemPartialMoveError,
|
|
105
|
+
isFileSystemPartialMoveError,
|
|
106
|
+
isFileSystemMoveRecoveryError,
|
|
107
|
+
isFileSystemMoveStateError,
|
|
108
|
+
describePartialMove,
|
|
109
|
+
describeEntryMove
|
|
110
|
+
};
|
|
111
|
+
//# sourceMappingURL=chunk-MNSZ7EZN.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/filesystem/move-error.ts"],"sourcesContent":["import { getFileSystemProviderV2, type FileSystemProvider } from './types.js';\nimport { parseWorkspacePath } from './workspace-path.js';\n\nexport type FileSystemPathPresence = 'present' | 'missing' | 'unknown';\n\nexport interface FileSystemPartialMoveState {\n readonly source: FileSystemPathPresence;\n readonly destination: FileSystemPathPresence;\n readonly companionSource: FileSystemPathPresence;\n readonly companionDestination: FileSystemPathPresence;\n}\n\nexport type FileSystemMoveLocation = 'source' | 'destination' | 'both' | 'neither' | 'unknown';\n\nexport interface FileSystemEntryMoveState {\n readonly source: FileSystemPathPresence;\n readonly destination: FileSystemPathPresence;\n}\n\nfunction locationFromPresence(\n source: FileSystemPathPresence,\n destination: FileSystemPathPresence,\n): FileSystemMoveLocation {\n if (source === 'unknown' || destination === 'unknown') return 'unknown';\n if (source === 'present' && destination === 'present') return 'both';\n if (source === 'present') return 'source';\n if (destination === 'present') return 'destination';\n return 'neither';\n}\n\n/**\n * One provider move failed without restoring a provable source-only state.\n * This is backend-neutral: callers must use `location`, never infer from the\n * original exception or a provider's advertised move atomicity.\n */\nexport class FileSystemMoveRecoveryError extends Error {\n public readonly code = 'partial-move' as const;\n\n public constructor(\n public readonly sourcePath: string,\n public readonly destinationPath: string,\n public readonly state: FileSystemEntryMoveState,\n public readonly moveError: unknown,\n ) {\n super(\n `The move did not restore one authoritative path. Entry location: ${locationFromPresence(\n state.source,\n state.destination,\n )}.`,\n );\n this.name = 'FileSystemMoveRecoveryError';\n }\n\n public get location(): FileSystemMoveLocation {\n return locationFromPresence(this.state.source, this.state.destination);\n }\n\n /** Alias used by DocumentSession's document-retarget recovery protocol. */\n public get documentLocation(): FileSystemMoveLocation {\n return this.location;\n }\n}\n\n/**\n * A compound document/companion move failed and the document rollback also\n * failed. Callers must use the recorded post-state instead of assuming the\n * original path is still authoritative.\n */\nexport class FileSystemPartialMoveError extends Error {\n readonly code = 'partial-move' as const;\n\n constructor(\n readonly sourcePath: string,\n readonly destinationPath: string,\n readonly companionSourcePath: string,\n readonly companionDestinationPath: string,\n readonly state: FileSystemPartialMoveState,\n readonly moveError: unknown,\n readonly rollbackError: unknown,\n ) {\n super(\n `The document move could not be rolled back. Document location: ${locationFromPresence(\n state.source,\n state.destination,\n )}; companion location: ${locationFromPresence(\n state.companionSource,\n state.companionDestination,\n )}.`,\n );\n this.name = 'FileSystemPartialMoveError';\n }\n\n get documentLocation(): FileSystemMoveLocation {\n return locationFromPresence(this.state.source, this.state.destination);\n }\n\n get companionLocation(): FileSystemMoveLocation {\n return locationFromPresence(this.state.companionSource, this.state.companionDestination);\n }\n}\n\nexport function isFileSystemPartialMoveError(error: unknown): error is FileSystemPartialMoveError {\n return error instanceof FileSystemPartialMoveError;\n}\n\nexport function isFileSystemMoveRecoveryError(\n error: unknown,\n): error is FileSystemMoveRecoveryError {\n return error instanceof FileSystemMoveRecoveryError;\n}\n\nexport function isFileSystemMoveStateError(\n error: unknown,\n): error is FileSystemMoveRecoveryError | FileSystemPartialMoveError {\n return isFileSystemMoveRecoveryError(error) || isFileSystemPartialMoveError(error);\n}\n\nasync function probePath(\n provider: FileSystemProvider,\n path: string,\n): Promise<FileSystemPathPresence> {\n try {\n const providerV2 = getFileSystemProviderV2(provider);\n const present = providerV2\n ? (await providerV2.stat(parseWorkspacePath(path))) !== null\n : await provider.exists(path);\n return present ? 'present' : 'missing';\n } catch {\n return 'unknown';\n }\n}\n\nexport async function describePartialMove(\n provider: FileSystemProvider,\n sourcePath: string,\n destinationPath: string,\n companionSourcePath: string,\n companionDestinationPath: string,\n): Promise<FileSystemPartialMoveState> {\n const [source, destination, companionSource, companionDestination] = await Promise.all([\n probePath(provider, sourcePath),\n probePath(provider, destinationPath),\n probePath(provider, companionSourcePath),\n probePath(provider, companionDestinationPath),\n ]);\n return { source, destination, companionSource, companionDestination };\n}\n\nexport async function describeEntryMove(\n provider: FileSystemProvider,\n sourcePath: string,\n destinationPath: string,\n): Promise<FileSystemEntryMoveState> {\n const [source, destination] = await Promise.all([\n probePath(provider, sourcePath),\n probePath(provider, destinationPath),\n ]);\n return { source, destination };\n}\n"],"mappings":";;;;;;;;AAmBA,SAAS,qBACP,QACA,aACwB;AACxB,MAAI,WAAW,aAAa,gBAAgB,UAAW,QAAO;AAC9D,MAAI,WAAW,aAAa,gBAAgB,UAAW,QAAO;AAC9D,MAAI,WAAW,UAAW,QAAO;AACjC,MAAI,gBAAgB,UAAW,QAAO;AACtC,SAAO;AACT;AAOO,IAAM,8BAAN,cAA0C,MAAM;AAAA,EAG9C,YACW,YACA,iBACA,OACA,WAChB;AACA;AAAA,MACE,oEAAoE;AAAA,QAClE,MAAM;AAAA,QACN,MAAM;AAAA,MACR,CAAC;AAAA,IACH;AAVgB;AACA;AACA;AACA;AANlB,SAAgB,OAAO;AAcrB,SAAK,OAAO;AAAA,EACd;AAAA,EAEA,IAAW,WAAmC;AAC5C,WAAO,qBAAqB,KAAK,MAAM,QAAQ,KAAK,MAAM,WAAW;AAAA,EACvE;AAAA;AAAA,EAGA,IAAW,mBAA2C;AACpD,WAAO,KAAK;AAAA,EACd;AACF;AAOO,IAAM,6BAAN,cAAyC,MAAM;AAAA,EAGpD,YACW,YACA,iBACA,qBACA,0BACA,OACA,WACA,eACT;AACA;AAAA,MACE,kEAAkE;AAAA,QAChE,MAAM;AAAA,QACN,MAAM;AAAA,MACR,CAAC,yBAAyB;AAAA,QACxB,MAAM;AAAA,QACN,MAAM;AAAA,MACR,CAAC;AAAA,IACH;AAhBS;AACA;AACA;AACA;AACA;AACA;AACA;AATX,SAAS,OAAO;AAoBd,SAAK,OAAO;AAAA,EACd;AAAA,EAEA,IAAI,mBAA2C;AAC7C,WAAO,qBAAqB,KAAK,MAAM,QAAQ,KAAK,MAAM,WAAW;AAAA,EACvE;AAAA,EAEA,IAAI,oBAA4C;AAC9C,WAAO,qBAAqB,KAAK,MAAM,iBAAiB,KAAK,MAAM,oBAAoB;AAAA,EACzF;AACF;AAEO,SAAS,6BAA6B,OAAqD;AAChG,SAAO,iBAAiB;AAC1B;AAEO,SAAS,8BACd,OACsC;AACtC,SAAO,iBAAiB;AAC1B;AAEO,SAAS,2BACd,OACmE;AACnE,SAAO,8BAA8B,KAAK,KAAK,6BAA6B,KAAK;AACnF;AAEA,eAAe,UACb,UACA,MACiC;AACjC,MAAI;AACF,UAAM,aAAa,wBAAwB,QAAQ;AACnD,UAAM,UAAU,aACX,MAAM,WAAW,KAAK,mBAAmB,IAAI,CAAC,MAAO,OACtD,MAAM,SAAS,OAAO,IAAI;AAC9B,WAAO,UAAU,YAAY;AAAA,EAC/B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,eAAsB,oBACpB,UACA,YACA,iBACA,qBACA,0BACqC;AACrC,QAAM,CAAC,QAAQ,aAAa,iBAAiB,oBAAoB,IAAI,MAAM,QAAQ,IAAI;AAAA,IACrF,UAAU,UAAU,UAAU;AAAA,IAC9B,UAAU,UAAU,eAAe;AAAA,IACnC,UAAU,UAAU,mBAAmB;AAAA,IACvC,UAAU,UAAU,wBAAwB;AAAA,EAC9C,CAAC;AACD,SAAO,EAAE,QAAQ,aAAa,iBAAiB,qBAAqB;AACtE;AAEA,eAAsB,kBACpB,UACA,YACA,iBACmC;AACnC,QAAM,CAAC,QAAQ,WAAW,IAAI,MAAM,QAAQ,IAAI;AAAA,IAC9C,UAAU,UAAU,UAAU;AAAA,IAC9B,UAAU,UAAU,eAAe;AAAA,EACrC,CAAC;AACD,SAAO,EAAE,QAAQ,YAAY;AAC/B;","names":[]}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import {
|
|
2
|
+
RawIndexedDBFileSystemStore
|
|
3
|
+
} from "./chunk-RVPGQBWR.js";
|
|
4
|
+
import {
|
|
5
|
+
getFileSystemProviderV2
|
|
6
|
+
} from "./chunk-EZKJZUNJ.js";
|
|
7
|
+
|
|
8
|
+
// src/workspace/workspace-schema.ts
|
|
9
|
+
var MAX_IDENTIFIER_CHARACTERS = 256;
|
|
10
|
+
var MAX_LABEL_CHARACTERS = 1024;
|
|
11
|
+
var MAX_PATH_CHARACTERS = 4096;
|
|
12
|
+
var MAX_WORKSPACES = 1e4;
|
|
13
|
+
function parsePersistedWorkspaceList(value) {
|
|
14
|
+
if (!Array.isArray(value)) throw new Error("Persisted workspaces must be an array.");
|
|
15
|
+
if (value.length > MAX_WORKSPACES) throw new Error("Persisted workspace list is too large.");
|
|
16
|
+
const parsed = value.map(parsePersistedWorkspace);
|
|
17
|
+
if (new Set(parsed.map((workspace) => workspace.id)).size !== parsed.length) {
|
|
18
|
+
throw new Error("Persisted workspace IDs must be unique.");
|
|
19
|
+
}
|
|
20
|
+
return parsed;
|
|
21
|
+
}
|
|
22
|
+
function parsePersistedWorkspace(value, index) {
|
|
23
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
24
|
+
throw new Error(`Persisted workspace ${index} must be an object.`);
|
|
25
|
+
}
|
|
26
|
+
const record = value;
|
|
27
|
+
const allowed = ["id", "name", "type", "lastOpened", "rootPath", "versioningOverride"];
|
|
28
|
+
const unknownKey = Object.keys(record).find((key) => !allowed.includes(key));
|
|
29
|
+
if (unknownKey) throw new Error(`Persisted workspace ${index} has unknown field ${unknownKey}.`);
|
|
30
|
+
const id = boundedString(record.id, MAX_IDENTIFIER_CHARACTERS, `workspace ${index} id`);
|
|
31
|
+
const name = boundedString(record.name, MAX_LABEL_CHARACTERS, `workspace ${index} name`);
|
|
32
|
+
if (record.type !== "indexeddb" && record.type !== "native" && record.type !== "electron-native") {
|
|
33
|
+
throw new Error(`Persisted workspace ${index} has an invalid provider type.`);
|
|
34
|
+
}
|
|
35
|
+
const lastOpened = boundedString(
|
|
36
|
+
record.lastOpened,
|
|
37
|
+
MAX_IDENTIFIER_CHARACTERS,
|
|
38
|
+
`workspace ${index} lastOpened`
|
|
39
|
+
);
|
|
40
|
+
if (!Number.isFinite(Date.parse(lastOpened)) || new Date(lastOpened).toISOString() !== lastOpened) {
|
|
41
|
+
throw new Error(`Persisted workspace ${index} has an invalid lastOpened timestamp.`);
|
|
42
|
+
}
|
|
43
|
+
const descriptor = { id, name, type: record.type, lastOpened };
|
|
44
|
+
if (record.rootPath !== void 0) {
|
|
45
|
+
descriptor.rootPath = boundedString(
|
|
46
|
+
record.rootPath,
|
|
47
|
+
MAX_PATH_CHARACTERS,
|
|
48
|
+
`workspace ${index} rootPath`
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
if (record.type === "electron-native" && descriptor.rootPath === void 0) {
|
|
52
|
+
throw new Error(`Persisted Electron workspace ${index} is missing rootPath.`);
|
|
53
|
+
}
|
|
54
|
+
if (record.type !== "electron-native" && descriptor.rootPath !== void 0) {
|
|
55
|
+
throw new Error(`Persisted workspace ${index} must not contain rootPath.`);
|
|
56
|
+
}
|
|
57
|
+
if (record.versioningOverride !== void 0) {
|
|
58
|
+
if (record.versioningOverride !== "inherit" && record.versioningOverride !== "on" && record.versioningOverride !== "off") {
|
|
59
|
+
throw new Error(`Persisted workspace ${index} has an invalid versioning override.`);
|
|
60
|
+
}
|
|
61
|
+
descriptor.versioningOverride = record.versioningOverride;
|
|
62
|
+
}
|
|
63
|
+
return descriptor;
|
|
64
|
+
}
|
|
65
|
+
function boundedString(value, maximum, label) {
|
|
66
|
+
if (typeof value !== "string" || value.length < 1 || value.length > maximum || value.includes("\0")) {
|
|
67
|
+
throw new Error(`Persisted ${label} must be a bounded non-empty string.`);
|
|
68
|
+
}
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// src/workspace/workspace-manager.ts
|
|
73
|
+
var DB_NAME = "docblocks-workspaces";
|
|
74
|
+
var STORE_NAME = "workspaces";
|
|
75
|
+
var LIST_KEY = "workspace-list";
|
|
76
|
+
var DEFAULT_WORKSPACE_ID = "default";
|
|
77
|
+
var registryStore = null;
|
|
78
|
+
function store() {
|
|
79
|
+
registryStore ?? (registryStore = new RawIndexedDBFileSystemStore(DB_NAME, STORE_NAME));
|
|
80
|
+
return registryStore;
|
|
81
|
+
}
|
|
82
|
+
var transientWorkspaces = /* @__PURE__ */ new Map();
|
|
83
|
+
var persistedMutationTail = Promise.resolve();
|
|
84
|
+
function registerTransientWorkspace(descriptor, provider) {
|
|
85
|
+
const previous = transientWorkspaces.get(descriptor.id);
|
|
86
|
+
if (previous && previous.provider !== provider) {
|
|
87
|
+
void getFileSystemProviderV2(previous.provider)?.dispose();
|
|
88
|
+
}
|
|
89
|
+
transientWorkspaces.set(descriptor.id, { descriptor, provider });
|
|
90
|
+
}
|
|
91
|
+
function getTransientWorkspace(id) {
|
|
92
|
+
return transientWorkspaces.get(id) ?? null;
|
|
93
|
+
}
|
|
94
|
+
async function unregisterTransientWorkspace(id) {
|
|
95
|
+
const entry = transientWorkspaces.get(id);
|
|
96
|
+
if (!entry) return;
|
|
97
|
+
transientWorkspaces.delete(id);
|
|
98
|
+
await getFileSystemProviderV2(entry.provider)?.dispose();
|
|
99
|
+
}
|
|
100
|
+
async function readPersistedIn(transaction) {
|
|
101
|
+
const list = await transaction.get(LIST_KEY);
|
|
102
|
+
return list == null ? [] : parsePersistedWorkspaceList(list);
|
|
103
|
+
}
|
|
104
|
+
async function readPersisted() {
|
|
105
|
+
return store().transaction("readonly", readPersistedIn);
|
|
106
|
+
}
|
|
107
|
+
async function listPersisted() {
|
|
108
|
+
await persistedMutationTail;
|
|
109
|
+
return readPersisted();
|
|
110
|
+
}
|
|
111
|
+
function mutatePersisted(mutation) {
|
|
112
|
+
const operation = persistedMutationTail.then(async () => {
|
|
113
|
+
await store().transaction("readwrite", async (transaction) => {
|
|
114
|
+
const current = await readPersistedIn(transaction);
|
|
115
|
+
const next = parsePersistedWorkspaceList(mutation([...current]));
|
|
116
|
+
await transaction.put(LIST_KEY, next);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
persistedMutationTail = operation.then(
|
|
120
|
+
() => void 0,
|
|
121
|
+
() => void 0
|
|
122
|
+
);
|
|
123
|
+
return operation;
|
|
124
|
+
}
|
|
125
|
+
async function listWorkspaces() {
|
|
126
|
+
const persisted = await listPersisted();
|
|
127
|
+
const transient = [...transientWorkspaces.values()].map((t) => t.descriptor);
|
|
128
|
+
return [...persisted, ...transient];
|
|
129
|
+
}
|
|
130
|
+
async function getWorkspace(id) {
|
|
131
|
+
const transient = transientWorkspaces.get(id);
|
|
132
|
+
if (transient) return transient.descriptor;
|
|
133
|
+
const list = await listPersisted();
|
|
134
|
+
return list.find((w) => w.id === id) ?? null;
|
|
135
|
+
}
|
|
136
|
+
async function saveWorkspace(workspace) {
|
|
137
|
+
if (workspace.type === "transient") return;
|
|
138
|
+
const persisted = parsePersistedWorkspaceList([workspace])[0];
|
|
139
|
+
await mutatePersisted((list) => {
|
|
140
|
+
const idx = list.findIndex((candidate) => candidate.id === persisted.id);
|
|
141
|
+
if (idx >= 0) list[idx] = persisted;
|
|
142
|
+
else list.push(persisted);
|
|
143
|
+
return list;
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
async function removeWorkspace(id) {
|
|
147
|
+
if (transientWorkspaces.has(id)) {
|
|
148
|
+
await unregisterTransientWorkspace(id);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
await mutatePersisted((list) => list.filter((workspace) => workspace.id !== id));
|
|
152
|
+
}
|
|
153
|
+
async function touchWorkspace(id) {
|
|
154
|
+
const transient = transientWorkspaces.get(id);
|
|
155
|
+
if (transient) {
|
|
156
|
+
transient.descriptor.lastOpened = (/* @__PURE__ */ new Date()).toISOString();
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
await mutatePersisted((list) => {
|
|
160
|
+
const workspace = list.find((candidate) => candidate.id === id);
|
|
161
|
+
if (workspace) workspace.lastOpened = (/* @__PURE__ */ new Date()).toISOString();
|
|
162
|
+
return list;
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
async function ensureDefaultWorkspace() {
|
|
166
|
+
const existing = await getWorkspace(DEFAULT_WORKSPACE_ID);
|
|
167
|
+
if (existing) return existing;
|
|
168
|
+
const descriptor = {
|
|
169
|
+
id: DEFAULT_WORKSPACE_ID,
|
|
170
|
+
name: "My Documents",
|
|
171
|
+
type: "indexeddb",
|
|
172
|
+
lastOpened: (/* @__PURE__ */ new Date()).toISOString()
|
|
173
|
+
};
|
|
174
|
+
await saveWorkspace(descriptor);
|
|
175
|
+
return descriptor;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// src/workspace/reconcile-electron.ts
|
|
179
|
+
function comparableRoot(rootPath, platform) {
|
|
180
|
+
const normalized = rootPath.replace(/\\/gu, "/").replace(/\/+$/gu, "");
|
|
181
|
+
return platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
182
|
+
}
|
|
183
|
+
function reconcileElectronWorkspaceDescriptors(local, hostWorkspaces, platform, now) {
|
|
184
|
+
const electron = local.filter((workspace) => workspace.type === "electron-native");
|
|
185
|
+
const matchedLocalIds = /* @__PURE__ */ new Set();
|
|
186
|
+
const idRemap = {};
|
|
187
|
+
const upsert = hostWorkspaces.map((hostWorkspace) => {
|
|
188
|
+
const root = comparableRoot(hostWorkspace.rootPath, platform);
|
|
189
|
+
const candidates = electron.filter(
|
|
190
|
+
(workspace) => workspace.rootPath && comparableRoot(workspace.rootPath, platform) === root
|
|
191
|
+
);
|
|
192
|
+
const matching = candidates.sort(
|
|
193
|
+
(left, right) => right.lastOpened.localeCompare(left.lastOpened)
|
|
194
|
+
)[0];
|
|
195
|
+
if (matching) {
|
|
196
|
+
matchedLocalIds.add(matching.id);
|
|
197
|
+
if (matching.id !== hostWorkspace.id) idRemap[matching.id] = hostWorkspace.id;
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
id: hostWorkspace.id,
|
|
201
|
+
name: matching?.name ?? hostWorkspace.name,
|
|
202
|
+
type: "electron-native",
|
|
203
|
+
rootPath: hostWorkspace.rootPath,
|
|
204
|
+
lastOpened: matching?.lastOpened ?? now,
|
|
205
|
+
...matching?.versioningOverride ? { versioningOverride: matching.versioningOverride } : {}
|
|
206
|
+
};
|
|
207
|
+
});
|
|
208
|
+
const canonicalIds = new Set(hostWorkspaces.map((workspace) => workspace.id));
|
|
209
|
+
const removeIds = electron.filter(
|
|
210
|
+
(workspace) => !canonicalIds.has(workspace.id) || matchedLocalIds.has(workspace.id) && !upsert.some((candidate) => candidate.id === workspace.id)
|
|
211
|
+
).map((workspace) => workspace.id);
|
|
212
|
+
return { removeIds, upsert, idRemap };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export {
|
|
216
|
+
parsePersistedWorkspaceList,
|
|
217
|
+
registerTransientWorkspace,
|
|
218
|
+
getTransientWorkspace,
|
|
219
|
+
unregisterTransientWorkspace,
|
|
220
|
+
listWorkspaces,
|
|
221
|
+
getWorkspace,
|
|
222
|
+
saveWorkspace,
|
|
223
|
+
removeWorkspace,
|
|
224
|
+
touchWorkspace,
|
|
225
|
+
ensureDefaultWorkspace,
|
|
226
|
+
reconcileElectronWorkspaceDescriptors
|
|
227
|
+
};
|
|
228
|
+
//# sourceMappingURL=chunk-P3DVZQ6S.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/workspace/workspace-schema.ts","../src/workspace/workspace-manager.ts","../src/workspace/reconcile-electron.ts"],"sourcesContent":["import type { WorkspaceDescriptor } from './types.js';\n\nconst MAX_IDENTIFIER_CHARACTERS = 256;\nconst MAX_LABEL_CHARACTERS = 1_024;\nconst MAX_PATH_CHARACTERS = 4_096;\nconst MAX_WORKSPACES = 10_000;\n\n/** Parse the complete persisted workspace list before it reaches startup code. */\nexport function parsePersistedWorkspaceList(value: unknown): WorkspaceDescriptor[] {\n if (!Array.isArray(value)) throw new Error('Persisted workspaces must be an array.');\n if (value.length > MAX_WORKSPACES) throw new Error('Persisted workspace list is too large.');\n const parsed = value.map(parsePersistedWorkspace);\n if (new Set(parsed.map((workspace) => workspace.id)).size !== parsed.length) {\n throw new Error('Persisted workspace IDs must be unique.');\n }\n return parsed;\n}\n\nfunction parsePersistedWorkspace(value: unknown, index: number): WorkspaceDescriptor {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new Error(`Persisted workspace ${index} must be an object.`);\n }\n const record = value as Record<string, unknown>;\n const allowed = ['id', 'name', 'type', 'lastOpened', 'rootPath', 'versioningOverride'];\n const unknownKey = Object.keys(record).find((key) => !allowed.includes(key));\n if (unknownKey) throw new Error(`Persisted workspace ${index} has unknown field ${unknownKey}.`);\n\n const id = boundedString(record.id, MAX_IDENTIFIER_CHARACTERS, `workspace ${index} id`);\n const name = boundedString(record.name, MAX_LABEL_CHARACTERS, `workspace ${index} name`);\n if (\n record.type !== 'indexeddb' &&\n record.type !== 'native' &&\n record.type !== 'electron-native'\n ) {\n throw new Error(`Persisted workspace ${index} has an invalid provider type.`);\n }\n const lastOpened = boundedString(\n record.lastOpened,\n MAX_IDENTIFIER_CHARACTERS,\n `workspace ${index} lastOpened`,\n );\n if (\n !Number.isFinite(Date.parse(lastOpened)) ||\n new Date(lastOpened).toISOString() !== lastOpened\n ) {\n throw new Error(`Persisted workspace ${index} has an invalid lastOpened timestamp.`);\n }\n\n const descriptor: WorkspaceDescriptor = { id, name, type: record.type, lastOpened };\n if (record.rootPath !== undefined) {\n descriptor.rootPath = boundedString(\n record.rootPath,\n MAX_PATH_CHARACTERS,\n `workspace ${index} rootPath`,\n );\n }\n if (record.type === 'electron-native' && descriptor.rootPath === undefined) {\n throw new Error(`Persisted Electron workspace ${index} is missing rootPath.`);\n }\n if (record.type !== 'electron-native' && descriptor.rootPath !== undefined) {\n throw new Error(`Persisted workspace ${index} must not contain rootPath.`);\n }\n if (record.versioningOverride !== undefined) {\n if (\n record.versioningOverride !== 'inherit' &&\n record.versioningOverride !== 'on' &&\n record.versioningOverride !== 'off'\n ) {\n throw new Error(`Persisted workspace ${index} has an invalid versioning override.`);\n }\n descriptor.versioningOverride = record.versioningOverride;\n }\n return descriptor;\n}\n\nfunction boundedString(value: unknown, maximum: number, label: string): string {\n if (\n typeof value !== 'string' ||\n value.length < 1 ||\n value.length > maximum ||\n value.includes('\\0')\n ) {\n throw new Error(`Persisted ${label} must be a bounded non-empty string.`);\n }\n return value;\n}\n","/**\n * WorkspaceManager — manages the list of known workspaces in IndexedDB\n * and provides helpers for creating / switching / removing them.\n *\n * The actual FileSystemProvider instances are created by the caller\n * (since native providers need a DirectoryHandle from user interaction).\n */\n\nimport {\n RawIndexedDBFileSystemStore,\n type IndexedDBFileSystemStore,\n type IndexedDBFileSystemTransaction,\n} from '../filesystem/indexeddb-store.js';\nimport { getFileSystemProviderV2, type FileSystemProvider } from '../filesystem/types.js';\nimport type { WorkspaceDescriptor } from './types.js';\nimport { parsePersistedWorkspaceList } from './workspace-schema.js';\n\nconst DB_NAME = 'docblocks-workspaces';\nconst STORE_NAME = 'workspaces';\nconst LIST_KEY = 'workspace-list';\nconst DEFAULT_WORKSPACE_ID = 'default';\n\n/**\n * The registry deliberately uses the error-propagating store rather than the\n * Squisq LocalForage adapter: that adapter converts a failed read into `null`,\n * which this module cannot tell apart from \"no workspaces yet\". A single\n * transient IndexedDB failure therefore read as an empty registry, and the next\n * mutation persisted that emptiness over every workspace the user had.\n *\n * The database and object store names are unchanged, and the raw store uses the\n * same out-of-line keys LocalForage did, so existing registries load as-is.\n */\nlet registryStore: IndexedDBFileSystemStore | null = null;\n\nfunction store(): IndexedDBFileSystemStore {\n registryStore ??= new RawIndexedDBFileSystemStore(DB_NAME, STORE_NAME);\n return registryStore;\n}\n\n/**\n * Replace the registry's storage backend, or restore the default with `null`.\n * Mirrors the `store` option the IndexedDB providers accept; tests inject an\n * in-memory store through it and production code never calls it.\n */\nexport function setWorkspaceRegistryStore(next: IndexedDBFileSystemStore | null): void {\n registryStore = next;\n}\n\n/**\n * Session-only transient workspaces (a loose file/`.dbk` opened from the OS,\n * or a document copied from a shared URL). Kept purely in memory — never\n * written to the persisted list — with their pre-built provider registered\n * alongside the descriptor. Lost on reload, which is the intended\n * \"transient for the app session\" behaviour.\n */\ninterface TransientEntry {\n descriptor: WorkspaceDescriptor;\n provider: FileSystemProvider;\n}\nconst transientWorkspaces = new Map<string, TransientEntry>();\nlet persistedMutationTail: Promise<void> = Promise.resolve();\n\n/** Register a transient workspace and its (already-built) provider. */\nexport function registerTransientWorkspace(\n descriptor: WorkspaceDescriptor,\n provider: FileSystemProvider,\n): void {\n const previous = transientWorkspaces.get(descriptor.id);\n if (previous && previous.provider !== provider) {\n void getFileSystemProviderV2(previous.provider)?.dispose();\n }\n transientWorkspaces.set(descriptor.id, { descriptor, provider });\n}\n\n/** Look up a transient workspace (descriptor + provider) by id, if any. */\nexport function getTransientWorkspace(\n id: string,\n): { descriptor: WorkspaceDescriptor; provider: FileSystemProvider } | null {\n return transientWorkspaces.get(id) ?? null;\n}\n\n/** Forget a transient workspace (e.g. when it is closed). */\nexport async function unregisterTransientWorkspace(id: string): Promise<void> {\n const entry = transientWorkspaces.get(id);\n if (!entry) return;\n transientWorkspaces.delete(id);\n await getFileSystemProviderV2(entry.provider)?.dispose();\n}\n\n/**\n * The persisted (on-disk) workspace list only — excludes transient ones.\n *\n * An unreadable registry throws. Only a store that positively reports \"no such\n * key\" is an empty registry; anything else is an unknown registry, and callers\n * must not act as though the user has no workspaces.\n */\nasync function readPersistedIn(\n transaction: IndexedDBFileSystemTransaction,\n): Promise<WorkspaceDescriptor[]> {\n const list = await transaction.get<unknown>(LIST_KEY);\n return list == null ? [] : parsePersistedWorkspaceList(list);\n}\n\nasync function readPersisted(): Promise<WorkspaceDescriptor[]> {\n return store().transaction('readonly', readPersistedIn);\n}\n\nasync function listPersisted(): Promise<WorkspaceDescriptor[]> {\n await persistedMutationTail;\n return readPersisted();\n}\n\nfunction mutatePersisted(\n mutation: (workspaces: WorkspaceDescriptor[]) => WorkspaceDescriptor[],\n): Promise<void> {\n const operation = persistedMutationTail.then(async () => {\n // Read and write in one transaction. A failed read aborts the mutation\n // instead of rebuilding the registry from a phantom empty list, and no\n // concurrent writer (another tab) can land between the read and the write\n // only to be clobbered by this mutation's stale copy.\n await store().transaction('readwrite', async (transaction) => {\n const current = await readPersistedIn(transaction);\n const next = parsePersistedWorkspaceList(mutation([...current]));\n await transaction.put(LIST_KEY, next);\n });\n });\n persistedMutationTail = operation.then(\n () => undefined,\n () => undefined,\n );\n return operation;\n}\n\n/**\n * Get all known workspace descriptors — persisted plus any session-only\n * transient ones (so they show in the picker and resolve for the session).\n */\nexport async function listWorkspaces(): Promise<WorkspaceDescriptor[]> {\n const persisted = await listPersisted();\n const transient = [...transientWorkspaces.values()].map((t) => t.descriptor);\n return [...persisted, ...transient];\n}\n\n/**\n * Get a specific workspace descriptor by id (transient or persisted).\n */\nexport async function getWorkspace(id: string): Promise<WorkspaceDescriptor | null> {\n const transient = transientWorkspaces.get(id);\n if (transient) return transient.descriptor;\n const list = await listPersisted();\n return list.find((w) => w.id === id) ?? null;\n}\n\n/**\n * Add or update a workspace descriptor. If a workspace with the same id\n * already exists, it is replaced. Transient workspaces are never persisted.\n */\nexport async function saveWorkspace(workspace: WorkspaceDescriptor): Promise<void> {\n if (workspace.type === 'transient') return;\n const persisted = parsePersistedWorkspaceList([workspace])[0];\n await mutatePersisted((list) => {\n const idx = list.findIndex((candidate) => candidate.id === persisted.id);\n if (idx >= 0) list[idx] = persisted;\n else list.push(persisted);\n return list;\n });\n}\n\n/**\n * Remove a workspace descriptor by id (transient or persisted).\n */\nexport async function removeWorkspace(id: string): Promise<void> {\n if (transientWorkspaces.has(id)) {\n await unregisterTransientWorkspace(id);\n return;\n }\n await mutatePersisted((list) => list.filter((workspace) => workspace.id !== id));\n}\n\n/**\n * Touch the lastOpened timestamp on a workspace. For transient workspaces this\n * updates the in-memory descriptor only (nothing is persisted).\n */\nexport async function touchWorkspace(id: string): Promise<void> {\n const transient = transientWorkspaces.get(id);\n if (transient) {\n transient.descriptor.lastOpened = new Date().toISOString();\n return;\n }\n await mutatePersisted((list) => {\n const workspace = list.find((candidate) => candidate.id === id);\n if (workspace) workspace.lastOpened = new Date().toISOString();\n return list;\n });\n}\n\n/**\n * Ensure a default IndexedDB workspace exists. Called on app startup.\n * Returns the descriptor.\n */\nexport async function ensureDefaultWorkspace(): Promise<WorkspaceDescriptor> {\n const existing = await getWorkspace(DEFAULT_WORKSPACE_ID);\n if (existing) return existing;\n\n const descriptor: WorkspaceDescriptor = {\n id: DEFAULT_WORKSPACE_ID,\n name: 'My Documents',\n type: 'indexeddb',\n lastOpened: new Date().toISOString(),\n };\n await saveWorkspace(descriptor);\n return descriptor;\n}\n","import type { ElectronWorkspaceInfo, HostEnvironment } from '../host/types.js';\nimport type { WorkspaceDescriptor } from './types.js';\n\nexport interface ElectronWorkspaceReconciliation {\n /** Obsolete renderer descriptors to remove before attempting restoration. */\n readonly removeIds: readonly string[];\n /** Canonical main-authorized descriptors to insert or replace. */\n readonly upsert: readonly WorkspaceDescriptor[];\n /** Safe navigation migration for a local descriptor matched by root. */\n readonly idRemap: Readonly<Record<string, string>>;\n}\n\nfunction comparableRoot(rootPath: string, platform: HostEnvironment['platform']): string {\n const normalized = rootPath.replace(/\\\\/gu, '/').replace(/\\/+$/gu, '');\n return platform === 'win32' ? normalized.toLowerCase() : normalized;\n}\n\n/**\n * Reconcile renderer IndexedDB metadata with the main process authority list.\n * `hostWorkspaces` is authoritative: local Electron descriptors absent from\n * it cannot be opened safely and are removed. Root-path matching carries user\n * metadata across an id migration without treating that display path as\n * authority.\n */\nexport function reconcileElectronWorkspaceDescriptors(\n local: readonly WorkspaceDescriptor[],\n hostWorkspaces: readonly ElectronWorkspaceInfo[],\n platform: HostEnvironment['platform'],\n now: string,\n): ElectronWorkspaceReconciliation {\n const electron = local.filter((workspace) => workspace.type === 'electron-native');\n const matchedLocalIds = new Set<string>();\n const idRemap: Record<string, string> = {};\n const upsert = hostWorkspaces.map((hostWorkspace): WorkspaceDescriptor => {\n const root = comparableRoot(hostWorkspace.rootPath, platform);\n const candidates = electron.filter(\n (workspace) => workspace.rootPath && comparableRoot(workspace.rootPath, platform) === root,\n );\n const matching = candidates.sort((left, right) =>\n right.lastOpened.localeCompare(left.lastOpened),\n )[0];\n if (matching) {\n matchedLocalIds.add(matching.id);\n if (matching.id !== hostWorkspace.id) idRemap[matching.id] = hostWorkspace.id;\n }\n return {\n id: hostWorkspace.id,\n name: matching?.name ?? hostWorkspace.name,\n type: 'electron-native',\n rootPath: hostWorkspace.rootPath,\n lastOpened: matching?.lastOpened ?? now,\n ...(matching?.versioningOverride ? { versioningOverride: matching.versioningOverride } : {}),\n };\n });\n const canonicalIds = new Set(hostWorkspaces.map((workspace) => workspace.id));\n const removeIds = electron\n .filter(\n (workspace) =>\n !canonicalIds.has(workspace.id) ||\n (matchedLocalIds.has(workspace.id) &&\n !upsert.some((candidate) => candidate.id === workspace.id)),\n )\n .map((workspace) => workspace.id);\n return { removeIds, upsert, idRemap };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// src/filesystem/v2.ts
|
|
2
|
+
function parseFileSystemVersion(value) {
|
|
3
|
+
if (typeof value !== "string" || !value || value.length > 1024 || value.includes("\0")) {
|
|
4
|
+
throw new TypeError("Filesystem version tokens must be non-empty bounded strings.");
|
|
5
|
+
}
|
|
6
|
+
return value;
|
|
7
|
+
}
|
|
8
|
+
function tryParseFileSystemVersion(value) {
|
|
9
|
+
try {
|
|
10
|
+
return parseFileSystemVersion(value);
|
|
11
|
+
} catch {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// src/filesystem/internal/bytes.ts
|
|
17
|
+
function copyBytes(data) {
|
|
18
|
+
const source = ArrayBuffer.isView(data) ? new Uint8Array(data.buffer, data.byteOffset, data.byteLength) : new Uint8Array(data);
|
|
19
|
+
const copy = new Uint8Array(source.byteLength);
|
|
20
|
+
copy.set(source);
|
|
21
|
+
return copy.buffer;
|
|
22
|
+
}
|
|
23
|
+
function bytesEqual(left, right) {
|
|
24
|
+
const leftBytes = ArrayBuffer.isView(left) ? new Uint8Array(left.buffer, left.byteOffset, left.byteLength) : new Uint8Array(left);
|
|
25
|
+
const rightBytes = ArrayBuffer.isView(right) ? new Uint8Array(right.buffer, right.byteOffset, right.byteLength) : new Uint8Array(right);
|
|
26
|
+
if (leftBytes.byteLength !== rightBytes.byteLength) return false;
|
|
27
|
+
return leftBytes.every((byte, index) => byte === rightBytes[index]);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// src/filesystem/internal/entry-order.ts
|
|
31
|
+
function compareText(left, right) {
|
|
32
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
33
|
+
}
|
|
34
|
+
function compareSnapshotsByName(left, right) {
|
|
35
|
+
if (left.kind !== right.kind) return left.kind === "directory" ? -1 : 1;
|
|
36
|
+
return compareText(left.name, right.name);
|
|
37
|
+
}
|
|
38
|
+
function compareSnapshotsByPath(left, right) {
|
|
39
|
+
if (left.kind !== right.kind) return left.kind === "directory" ? -1 : 1;
|
|
40
|
+
return compareText(left.path, right.path);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export {
|
|
44
|
+
parseFileSystemVersion,
|
|
45
|
+
tryParseFileSystemVersion,
|
|
46
|
+
copyBytes,
|
|
47
|
+
bytesEqual,
|
|
48
|
+
compareText,
|
|
49
|
+
compareSnapshotsByName,
|
|
50
|
+
compareSnapshotsByPath
|
|
51
|
+
};
|
|
52
|
+
//# sourceMappingURL=chunk-P4WATND4.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/filesystem/v2.ts","../src/filesystem/internal/bytes.ts","../src/filesystem/internal/entry-order.ts"],"sourcesContent":["import type { FsError } from './fs-error.js';\nimport type { WorkspacePath } from './workspace-path.js';\n\n/**\n * Opaque provider-issued token representing one observed entry/tree state.\n * Tokens support equality only within the issuing provider; callers must not\n * parse them or assume a move or byte-identical rewrite changes the token.\n * A token need not be a content hash, but it must change whenever the issuing\n * provider observes a potentially different kind, payload, or descendant\n * state. Detection of non-cooperating external changes is bounded by the\n * backend's declared capabilities and metadata precision.\n */\ndeclare const fileSystemVersionBrand: unique symbol;\nexport type FileSystemVersion = string & {\n readonly [fileSystemVersionBrand]: 'FileSystemVersion';\n};\n\n/** Validate/rebrand a version received from a provider transport. */\nexport function parseFileSystemVersion(value: unknown): FileSystemVersion {\n if (typeof value !== 'string' || !value || value.length > 1024 || value.includes('\\0')) {\n throw new TypeError('Filesystem version tokens must be non-empty bounded strings.');\n }\n return value as FileSystemVersion;\n}\n\nexport function tryParseFileSystemVersion(value: unknown): FileSystemVersion | null {\n try {\n return parseFileSystemVersion(value);\n } catch {\n return null;\n }\n}\n\n/** `process` excludes this provider's local peers; `cross-context` also excludes cooperating tabs/processes. */\nexport type FileSystemAtomicity = 'none' | 'process' | 'cross-context';\nexport type FileSystemCaseSensitivity = 'sensitive' | 'insensitive' | 'platform';\nexport type FileSystemDurability = 'volatile' | 'best-effort' | 'durable';\nexport type FileSystemConditionalWriteStrength =\n | 'none'\n | 'process'\n | 'cross-context'\n | 'storage-atomic';\nexport type FileSystemSymlinkPolicy = 'unsupported' | 'reject' | 'follow-contained' | 'preserve';\n\n/** Serializable feature declaration; consumers never need provider instanceof checks. */\nexport interface FileSystemProviderCapabilities {\n /** Isolation of one successful file replacement from cooperating writers. */\n readonly writeAtomicity: FileSystemAtomicity;\n /** Isolation of one successful source-to-destination move. */\n readonly moveAtomicity: FileSystemAtomicity;\n /** Writers excluded while one complete snapshot is captured. */\n readonly snapshotAtomicity: FileSystemAtomicity;\n /** Strongest boundary at which expectedVersion is checked with mutation. */\n readonly conditionalWrite: FileSystemConditionalWriteStrength;\n readonly recursiveRemove: boolean;\n /** True only when watch has a real ready barrier and sequenced events. */\n readonly watch: boolean;\n readonly caseSensitivity: FileSystemCaseSensitivity;\n readonly symlinkPolicy: FileSystemSymlinkPolicy;\n readonly durability: FileSystemDurability;\n}\n\ninterface FileSystemEntrySnapshotBase {\n readonly path: WorkspacePath;\n readonly name: string;\n readonly version: FileSystemVersion;\n /** ISO-8601 timestamp supplied by the backend; versions remain authoritative. */\n readonly lastModified: string;\n}\n\nexport interface FileSystemFileSnapshot extends FileSystemEntrySnapshotBase {\n readonly kind: 'file';\n readonly size: number;\n}\n\nexport interface FileSystemDirectorySnapshot extends FileSystemEntrySnapshotBase {\n readonly kind: 'directory';\n readonly size: null;\n}\n\nexport type FileSystemEntrySnapshot = FileSystemFileSnapshot | FileSystemDirectorySnapshot;\n\n/** A read always couples owned bytes to the exact metadata version they represent. */\nexport interface FileSystemFileRead {\n readonly entry: FileSystemFileSnapshot;\n readonly data: ArrayBuffer;\n}\n\nexport interface FileSystemSnapshotFile extends FileSystemFileSnapshot {\n readonly data: ArrayBuffer;\n}\n\nexport type FileSystemSnapshotEntry = FileSystemSnapshotFile | FileSystemDirectorySnapshot;\n\n/**\n * Complete best-available tree traversal. `snapshotAtomicity` declares\n * whether concurrent local/cross-context mutations are excluded while it is\n * captured. Implementations must never expose aliased buffers.\n */\nexport interface FileSystemSnapshot {\n readonly version: FileSystemVersion;\n readonly entries: readonly FileSystemSnapshotEntry[];\n}\n\nexport type FileSystemWriteMode = 'upsert' | 'create' | 'replace';\n\nexport interface FileSystemWriteOptions {\n /** Defaults to upsert. */\n readonly mode?: FileSystemWriteMode;\n /** Defaults to false so structure creation is never implicit. */\n readonly createParents?: boolean;\n /** null means the entry must not exist; a token requires an exact match. */\n readonly expectedVersion?: FileSystemVersion | null;\n}\n\nexport type FileSystemCreateDirectoryMode = 'ensure' | 'create';\n\nexport interface FileSystemCreateDirectoryOptions {\n /** Defaults to ensure (idempotently return an existing directory). */\n readonly mode?: FileSystemCreateDirectoryMode;\n /** Defaults to false. */\n readonly createParents?: boolean;\n}\n\nexport interface FileSystemRemoveOptions {\n /** Defaults to false; non-empty directories otherwise fail with not-empty. */\n readonly recursive?: boolean;\n /** Defaults to error. */\n readonly missing?: 'ignore' | 'error';\n readonly expectedVersion?: FileSystemVersion;\n}\n\nexport interface FileSystemRemoveResult {\n readonly removed: boolean;\n /** Version of the complete tree after the removal attempt. */\n readonly version: FileSystemVersion;\n}\n\nexport interface FileSystemMoveOptions {\n /** Defaults to false. Destinations are never overwritten. */\n readonly createParents?: boolean;\n readonly expectedVersion?: FileSystemVersion;\n}\n\nexport type FileSystemWatchEventType = 'created' | 'modified' | 'removed' | 'moved' | 'overflow';\nexport type FileSystemWatchEventOrigin = 'local' | 'external' | 'unknown';\n\ninterface FileSystemWatchEventBase {\n readonly sequence: number;\n readonly origin: FileSystemWatchEventOrigin;\n readonly path: WorkspacePath;\n}\n\nexport interface FileSystemEntryWatchEvent extends FileSystemWatchEventBase {\n readonly type: Exclude<FileSystemWatchEventType, 'overflow'>;\n readonly kind: 'file' | 'directory';\n readonly destinationPath: WorkspacePath | null;\n readonly version: FileSystemVersion | null;\n readonly previousVersion: FileSystemVersion | null;\n}\n\nexport interface FileSystemOverflowWatchEvent extends FileSystemWatchEventBase {\n readonly type: 'overflow';\n readonly kind: null;\n readonly destinationPath: null;\n readonly version: null;\n readonly previousVersion: null;\n}\n\nexport type FileSystemWatchEvent = FileSystemEntryWatchEvent | FileSystemOverflowWatchEvent;\n\nexport type FileSystemWatchListener = (event: FileSystemWatchEvent) => void;\nexport type FileSystemWatchErrorListener = (error: FsError) => void;\n\nexport interface FileSystemWatchOptions {\n readonly onError?: FileSystemWatchErrorListener;\n}\n\nexport interface FileSystemWatchSubscription {\n readonly closed: boolean;\n /** Resolves after the backend watch is installed and no initial event can be missed. */\n readonly ready: Promise<void>;\n /** Idempotent and awaitable so resource release can join provider shutdown. */\n dispose(): Promise<void>;\n}\n\n/**\n * Correctness-first filesystem contract. Every logical path has exactly one\n * kind and one byte payload; conditional writes and lifecycle disposal are\n * mandatory rather than optional provider extensions.\n */\nexport interface FileSystemProviderV2 {\n readonly id: string;\n readonly label: string;\n readonly capabilities: FileSystemProviderCapabilities;\n\n /** Absence is represented only by null; permission/I/O/wrong-kind errors throw FsError. */\n stat(path: WorkspacePath): Promise<FileSystemEntrySnapshot | null>;\n /** Absence is represented only by null; a directory at path is type-mismatch. */\n readFile(path: WorkspacePath): Promise<FileSystemFileRead | null>;\n /** Missing paths and file paths throw; results are canonical and deterministic. */\n readDirectory(path: WorkspacePath): Promise<readonly FileSystemEntrySnapshot[]>;\n\n writeFile(\n path: WorkspacePath,\n data: ArrayBuffer | Uint8Array,\n options?: FileSystemWriteOptions,\n ): Promise<FileSystemFileSnapshot>;\n createDirectory(\n path: WorkspacePath,\n options?: FileSystemCreateDirectoryOptions,\n ): Promise<FileSystemDirectorySnapshot>;\n remove(path: WorkspacePath, options?: FileSystemRemoveOptions): Promise<FileSystemRemoveResult>;\n move(\n oldPath: WorkspacePath,\n newPath: WorkspacePath,\n options?: FileSystemMoveOptions,\n ): Promise<FileSystemEntrySnapshot>;\n\n snapshot(): Promise<FileSystemSnapshot>;\n /**\n * Always returns a disposable subscription. When `capabilities.watch` is\n * false, `ready` rejects with `FsError('not-supported')` and `onError`\n * observes the same failure; callers can therefore use one cleanup path.\n */\n watch(\n listener: FileSystemWatchListener,\n options?: FileSystemWatchOptions,\n ): FileSystemWatchSubscription;\n dispose(): Promise<void>;\n}\n","/**\n * Byte helpers shared by every filesystem provider.\n *\n * Providers own their stored buffers, so every value that crosses the provider\n * boundary is copied rather than aliased. Keeping one implementation here stops\n * the copy semantics (notably: honouring a view's byteOffset/byteLength instead\n * of its whole backing buffer) from drifting between backends.\n */\n\n/** Copy only the visible bytes of a buffer or view into a fresh ArrayBuffer. */\nexport function copyBytes(data: ArrayBuffer | Uint8Array): ArrayBuffer {\n const source = ArrayBuffer.isView(data)\n ? new Uint8Array(data.buffer, data.byteOffset, data.byteLength)\n : new Uint8Array(data);\n const copy = new Uint8Array(source.byteLength);\n copy.set(source);\n return copy.buffer as ArrayBuffer;\n}\n\n/** Compare the visible bytes of two buffers or views for exact equality. */\nexport function bytesEqual(\n left: ArrayBuffer | Uint8Array,\n right: ArrayBuffer | Uint8Array,\n): boolean {\n const leftBytes = ArrayBuffer.isView(left)\n ? new Uint8Array(left.buffer, left.byteOffset, left.byteLength)\n : new Uint8Array(left);\n const rightBytes = ArrayBuffer.isView(right)\n ? new Uint8Array(right.buffer, right.byteOffset, right.byteLength)\n : new Uint8Array(right);\n if (leftBytes.byteLength !== rightBytes.byteLength) return false;\n return leftBytes.every((byte, index) => byte === rightBytes[index]);\n}\n","/**\n * Deterministic listing order shared by every filesystem provider.\n *\n * Providers must return byte-stable orderings so snapshots can be compared\n * across backends and across runs. `compareText` is deliberately a raw code\n * unit comparison rather than `localeCompare`, which is locale-dependent.\n */\n\nimport type { FileSystemEntrySnapshot } from '../v2.js';\n\n/** Locale-independent, deterministic string ordering. */\nexport function compareText(left: string, right: string): number {\n return left < right ? -1 : left > right ? 1 : 0;\n}\n\n/** Directory-first, then by entry name. Used for sibling listings. */\nexport function compareSnapshotsByName(\n left: FileSystemEntrySnapshot,\n right: FileSystemEntrySnapshot,\n): number {\n if (left.kind !== right.kind) return left.kind === 'directory' ? -1 : 1;\n return compareText(left.name, right.name);\n}\n\n/**\n * Directory-first, then by full path.\n *\n * Equivalent to `compareSnapshotsByName` for siblings, and used where entries\n * may not share a parent — the Electron transport re-sorts host-supplied\n * listings defensively and cannot assume a single directory's children.\n */\nexport function compareSnapshotsByPath(\n left: FileSystemEntrySnapshot,\n right: FileSystemEntrySnapshot,\n): number {\n if (left.kind !== right.kind) return left.kind === 'directory' ? -1 : 1;\n return compareText(left.path, right.path);\n}\n"],"mappings":";AAkBO,SAAS,uBAAuB,OAAmC;AACxE,MAAI,OAAO,UAAU,YAAY,CAAC,SAAS,MAAM,SAAS,QAAQ,MAAM,SAAS,IAAI,GAAG;AACtF,UAAM,IAAI,UAAU,8DAA8D;AAAA,EACpF;AACA,SAAO;AACT;AAEO,SAAS,0BAA0B,OAA0C;AAClF,MAAI;AACF,WAAO,uBAAuB,KAAK;AAAA,EACrC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;;;ACrBO,SAAS,UAAU,MAA6C;AACrE,QAAM,SAAS,YAAY,OAAO,IAAI,IAClC,IAAI,WAAW,KAAK,QAAQ,KAAK,YAAY,KAAK,UAAU,IAC5D,IAAI,WAAW,IAAI;AACvB,QAAM,OAAO,IAAI,WAAW,OAAO,UAAU;AAC7C,OAAK,IAAI,MAAM;AACf,SAAO,KAAK;AACd;AAGO,SAAS,WACd,MACA,OACS;AACT,QAAM,YAAY,YAAY,OAAO,IAAI,IACrC,IAAI,WAAW,KAAK,QAAQ,KAAK,YAAY,KAAK,UAAU,IAC5D,IAAI,WAAW,IAAI;AACvB,QAAM,aAAa,YAAY,OAAO,KAAK,IACvC,IAAI,WAAW,MAAM,QAAQ,MAAM,YAAY,MAAM,UAAU,IAC/D,IAAI,WAAW,KAAK;AACxB,MAAI,UAAU,eAAe,WAAW,WAAY,QAAO;AAC3D,SAAO,UAAU,MAAM,CAAC,MAAM,UAAU,SAAS,WAAW,KAAK,CAAC;AACpE;;;ACrBO,SAAS,YAAY,MAAc,OAAuB;AAC/D,SAAO,OAAO,QAAQ,KAAK,OAAO,QAAQ,IAAI;AAChD;AAGO,SAAS,uBACd,MACA,OACQ;AACR,MAAI,KAAK,SAAS,MAAM,KAAM,QAAO,KAAK,SAAS,cAAc,KAAK;AACtE,SAAO,YAAY,KAAK,MAAM,MAAM,IAAI;AAC1C;AASO,SAAS,uBACd,MACA,OACQ;AACR,MAAI,KAAK,SAAS,MAAM,KAAM,QAAO,KAAK,SAAS,cAAc,KAAK;AACtE,SAAO,YAAY,KAAK,MAAM,MAAM,IAAI;AAC1C;","names":[]}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// src/filesystem/indexeddb-store.ts
|
|
2
|
+
function indexedDBWorkspaceDatabaseName(workspaceId) {
|
|
3
|
+
return `docblocks-fs-${workspaceId}`;
|
|
4
|
+
}
|
|
5
|
+
function deleteIndexedDBDatabase(databaseName) {
|
|
6
|
+
if (typeof indexedDB === "undefined") {
|
|
7
|
+
return Promise.reject(new Error("IndexedDB is not available in this environment."));
|
|
8
|
+
}
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
const request = indexedDB.deleteDatabase(databaseName);
|
|
11
|
+
request.onsuccess = () => resolve();
|
|
12
|
+
request.onerror = () => reject(request.error ?? new Error(`Failed to delete the "${databaseName}" database.`));
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
function deleteIndexedDBWorkspaceData(workspaceId) {
|
|
16
|
+
return deleteIndexedDBDatabase(indexedDBWorkspaceDatabaseName(workspaceId));
|
|
17
|
+
}
|
|
18
|
+
var RawIndexedDBFileSystemStore = class {
|
|
19
|
+
constructor(databaseName, storeName) {
|
|
20
|
+
this.databaseName = databaseName;
|
|
21
|
+
this.storeName = storeName;
|
|
22
|
+
this.database = null;
|
|
23
|
+
this.opening = null;
|
|
24
|
+
this.closed = false;
|
|
25
|
+
}
|
|
26
|
+
async transaction(mode, operation) {
|
|
27
|
+
const database = await this.getDatabase();
|
|
28
|
+
const nativeTransaction = database.transaction(this.storeName, mode);
|
|
29
|
+
const completion = waitForTransaction(nativeTransaction);
|
|
30
|
+
const transaction = new RawIndexedDBFileSystemTransaction(
|
|
31
|
+
nativeTransaction.objectStore(this.storeName)
|
|
32
|
+
);
|
|
33
|
+
let result;
|
|
34
|
+
try {
|
|
35
|
+
result = await operation(transaction);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
try {
|
|
38
|
+
nativeTransaction.abort();
|
|
39
|
+
} catch {
|
|
40
|
+
}
|
|
41
|
+
await completion.catch(() => void 0);
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
await completion;
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
close() {
|
|
48
|
+
if (this.closed) return;
|
|
49
|
+
this.closed = true;
|
|
50
|
+
this.database?.close();
|
|
51
|
+
this.database = null;
|
|
52
|
+
this.opening = null;
|
|
53
|
+
}
|
|
54
|
+
getDatabase() {
|
|
55
|
+
if (this.closed) {
|
|
56
|
+
return Promise.reject(new Error("The IndexedDB filesystem store is closed."));
|
|
57
|
+
}
|
|
58
|
+
if (this.database) return Promise.resolve(this.database);
|
|
59
|
+
if (this.opening) return this.opening;
|
|
60
|
+
if (typeof indexedDB === "undefined") {
|
|
61
|
+
return Promise.reject(new Error("IndexedDB is not available in this environment."));
|
|
62
|
+
}
|
|
63
|
+
this.opening = openDatabaseWithStore(this.databaseName, this.storeName).then((database) => {
|
|
64
|
+
if (this.closed) {
|
|
65
|
+
database.close();
|
|
66
|
+
throw new Error("The IndexedDB filesystem store is closed.");
|
|
67
|
+
}
|
|
68
|
+
this.database = database;
|
|
69
|
+
this.opening = null;
|
|
70
|
+
database.onversionchange = () => {
|
|
71
|
+
database.close();
|
|
72
|
+
if (this.database === database) this.database = null;
|
|
73
|
+
};
|
|
74
|
+
return database;
|
|
75
|
+
}).catch((error) => {
|
|
76
|
+
this.opening = null;
|
|
77
|
+
throw error;
|
|
78
|
+
});
|
|
79
|
+
return this.opening;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
var RawIndexedDBFileSystemTransaction = class {
|
|
83
|
+
constructor(store) {
|
|
84
|
+
this.store = store;
|
|
85
|
+
}
|
|
86
|
+
async get(key) {
|
|
87
|
+
const value = await requestResult(this.store.get(key));
|
|
88
|
+
return value === void 0 ? null : value;
|
|
89
|
+
}
|
|
90
|
+
async put(key, value) {
|
|
91
|
+
await requestResult(this.store.put(value, key));
|
|
92
|
+
}
|
|
93
|
+
async delete(key) {
|
|
94
|
+
await requestResult(this.store.delete(key));
|
|
95
|
+
}
|
|
96
|
+
async keys() {
|
|
97
|
+
const keys = await requestResult(this.store.getAllKeys());
|
|
98
|
+
return keys.map((key) => {
|
|
99
|
+
if (typeof key !== "string") {
|
|
100
|
+
throw new Error("The DocBlocks filesystem store contains a non-string key.");
|
|
101
|
+
}
|
|
102
|
+
return key;
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
async hasKeyWithPrefix(prefix) {
|
|
106
|
+
const cursor = await requestResult(this.store.openKeyCursor(IDBKeyRange.lowerBound(prefix)));
|
|
107
|
+
return cursor !== null && typeof cursor.key === "string" && cursor.key.startsWith(prefix);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
async function openDatabaseWithStore(databaseName, storeName) {
|
|
111
|
+
const database = await openDatabase(databaseName, void 0, (upgradeDatabase) => {
|
|
112
|
+
if (!upgradeDatabase.objectStoreNames.contains(storeName)) {
|
|
113
|
+
upgradeDatabase.createObjectStore(storeName);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
if (database.objectStoreNames.contains(storeName)) return database;
|
|
117
|
+
const nextVersion = database.version + 1;
|
|
118
|
+
database.close();
|
|
119
|
+
return openDatabase(databaseName, nextVersion, (upgradeDatabase) => {
|
|
120
|
+
if (!upgradeDatabase.objectStoreNames.contains(storeName)) {
|
|
121
|
+
upgradeDatabase.createObjectStore(storeName);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
function openDatabase(databaseName, version, upgrade) {
|
|
126
|
+
return new Promise((resolve, reject) => {
|
|
127
|
+
const request = version === void 0 ? indexedDB.open(databaseName) : indexedDB.open(databaseName, version);
|
|
128
|
+
let settled = false;
|
|
129
|
+
request.onupgradeneeded = () => upgrade(request.result);
|
|
130
|
+
request.onsuccess = () => {
|
|
131
|
+
if (settled) {
|
|
132
|
+
request.result.close();
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
settled = true;
|
|
136
|
+
resolve(request.result);
|
|
137
|
+
};
|
|
138
|
+
request.onerror = () => {
|
|
139
|
+
if (settled) return;
|
|
140
|
+
settled = true;
|
|
141
|
+
reject(request.error ?? new Error(`Could not open IndexedDB database ${databaseName}.`));
|
|
142
|
+
};
|
|
143
|
+
request.onblocked = () => {
|
|
144
|
+
if (settled) return;
|
|
145
|
+
settled = true;
|
|
146
|
+
reject(new Error(`IndexedDB database ${databaseName} is blocked by another open page.`));
|
|
147
|
+
};
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
function requestResult(request) {
|
|
151
|
+
return new Promise((resolve, reject) => {
|
|
152
|
+
request.onsuccess = () => resolve(request.result);
|
|
153
|
+
request.onerror = () => reject(request.error ?? new Error("IndexedDB request failed."));
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
function waitForTransaction(transaction) {
|
|
157
|
+
return new Promise((resolve, reject) => {
|
|
158
|
+
transaction.oncomplete = () => resolve();
|
|
159
|
+
transaction.onabort = () => reject(transaction.error ?? new Error("IndexedDB transaction was aborted."));
|
|
160
|
+
transaction.onerror = () => reject(transaction.error ?? new Error("IndexedDB transaction failed."));
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export {
|
|
165
|
+
indexedDBWorkspaceDatabaseName,
|
|
166
|
+
deleteIndexedDBWorkspaceData,
|
|
167
|
+
RawIndexedDBFileSystemStore
|
|
168
|
+
};
|
|
169
|
+
//# sourceMappingURL=chunk-RVPGQBWR.js.map
|