@bendyline/docblocks 2.0.0 → 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/dist/{chunk-J32GU3F3.js → chunk-6IYMTYMQ.js} +249 -221
- package/dist/chunk-6IYMTYMQ.js.map +1 -0
- package/dist/{chunk-75UUF6C4.js → chunk-B56KWCSE.js} +2 -2
- package/dist/{chunk-RVQHY3XU.js → chunk-BNKV7MTO.js} +1 -1
- package/dist/{chunk-RVQHY3XU.js.map → chunk-BNKV7MTO.js.map} +1 -1
- package/dist/chunk-BXMF5YQJ.js +149 -0
- package/dist/chunk-BXMF5YQJ.js.map +1 -0
- package/dist/{chunk-PGR7LK6D.js → chunk-COYQXQK3.js} +14 -5
- package/dist/chunk-COYQXQK3.js.map +1 -0
- package/dist/{chunk-RQJRQF46.js → chunk-DN7A33LL.js} +42 -49
- package/dist/chunk-DN7A33LL.js.map +1 -0
- package/dist/chunk-FA7UNOXK.js +20 -0
- package/dist/chunk-FA7UNOXK.js.map +1 -0
- package/dist/chunk-GM5YXU4L.js +29 -0
- package/dist/chunk-GM5YXU4L.js.map +1 -0
- package/dist/{chunk-SI2LU2RP.js → chunk-JS6XFWQK.js} +2 -1
- package/dist/chunk-JS6XFWQK.js.map +1 -0
- package/dist/{chunk-EGNDADXB.js → chunk-MNSZ7EZN.js} +2 -2
- 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-GCPBEMMH.js → chunk-VTCSF7VS.js} +11 -21
- package/dist/chunk-VTCSF7VS.js.map +1 -0
- package/dist/{chunk-2D7Y4IR3.js → chunk-WJKYY5QR.js} +123 -66
- package/dist/chunk-WJKYY5QR.js.map +1 -0
- package/dist/{chunk-5MYAICRE.js → chunk-XUML3MKV.js} +44 -56
- package/dist/chunk-XUML3MKV.js.map +1 -0
- package/dist/document/index.d.ts +358 -6
- package/dist/document/index.js +24 -3
- package/dist/document/index.js.map +1 -1
- package/dist/filesystem/electron.d.ts +73 -4
- package/dist/filesystem/electron.js +16 -3
- package/dist/filesystem/electron.js.map +1 -1
- package/dist/filesystem/index.d.ts +228 -22
- package/dist/filesystem/index.js +133 -19
- package/dist/filesystem/index.js.map +1 -1
- package/dist/filesystem/indexeddb.d.ts +284 -5
- package/dist/filesystem/indexeddb.js +22 -4
- package/dist/filesystem/indexeddb.js.map +1 -1
- package/dist/filesystem/memory.d.ts +152 -4
- package/dist/filesystem/memory.js +12 -3
- package/dist/filesystem/memory.js.map +1 -1
- package/dist/filesystem/native.d.ts +136 -4
- package/dist/filesystem/native.js +27 -3
- package/dist/filesystem/native.js.map +1 -1
- package/dist/host/index.d.ts +32 -9
- package/dist/host/index.js +26 -29
- package/dist/host/index.js.map +1 -1
- package/dist/index.d.ts +14 -10
- package/dist/index.js +204 -9
- package/dist/index.js.map +1 -1
- package/dist/mcp/index.d.ts +33 -4
- package/dist/mcp/index.js +31 -3
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/zod.d.ts +637 -635
- package/dist/mcp/zod.js +700 -884
- package/dist/mcp/zod.js.map +1 -1
- 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-CsX6EN2t.d.ts → types-BEg5SQ1_.d.ts} +1 -1
- package/dist/{types-eOlt9J7n.d.ts → types-CueK0tvU.d.ts} +34 -11
- package/dist/vscode/index.d.ts +220 -3
- package/dist/vscode/index.js +382 -2
- package/dist/vscode/index.js.map +1 -1
- package/dist/workspace/index.d.ts +141 -4
- package/dist/workspace/index.js +28 -2
- package/dist/workspace/index.js.map +1 -1
- package/package.json +9 -5
- package/dist/chunk-2D7Y4IR3.js.map +0 -1
- package/dist/chunk-5MYAICRE.js.map +0 -1
- package/dist/chunk-AYZ4HN5M.js +0 -20
- package/dist/chunk-AYZ4HN5M.js.map +0 -1
- package/dist/chunk-GCPBEMMH.js.map +0 -1
- package/dist/chunk-J32GU3F3.js.map +0 -1
- package/dist/chunk-PGR7LK6D.js.map +0 -1
- package/dist/chunk-RQJRQF46.js.map +0 -1
- package/dist/chunk-SI2LU2RP.js.map +0 -1
- package/dist/chunk-W5UUUII5.js +0 -142
- package/dist/chunk-W5UUUII5.js.map +0 -1
- package/dist/document/document-session.d.ts +0 -127
- package/dist/document/document-session.d.ts.map +0 -1
- package/dist/document/document-session.js +0 -749
- package/dist/document/document-session.js.map +0 -1
- package/dist/document/filesystem-target.d.ts +0 -17
- package/dist/document/filesystem-target.d.ts.map +0 -1
- package/dist/document/filesystem-target.js +0 -70
- package/dist/document/filesystem-target.js.map +0 -1
- package/dist/document/index.d.ts.map +0 -1
- package/dist/document/recovery-journal.d.ts +0 -125
- package/dist/document/recovery-journal.d.ts.map +0 -1
- package/dist/document/recovery-journal.js +0 -385
- package/dist/document/recovery-journal.js.map +0 -1
- package/dist/document/types.d.ts +0 -89
- package/dist/document/types.d.ts.map +0 -1
- package/dist/document/types.js +0 -9
- package/dist/document/types.js.map +0 -1
- package/dist/filesystem/commit-lock.d.ts +0 -3
- package/dist/filesystem/commit-lock.d.ts.map +0 -1
- package/dist/filesystem/commit-lock.js +0 -20
- package/dist/filesystem/commit-lock.js.map +0 -1
- package/dist/filesystem/dbk-workspace.d.ts +0 -34
- package/dist/filesystem/dbk-workspace.d.ts.map +0 -1
- package/dist/filesystem/dbk-workspace.js +0 -85
- package/dist/filesystem/dbk-workspace.js.map +0 -1
- package/dist/filesystem/electron-provider-v2.d.ts +0 -36
- package/dist/filesystem/electron-provider-v2.d.ts.map +0 -1
- package/dist/filesystem/electron-provider-v2.js +0 -503
- package/dist/filesystem/electron-provider-v2.js.map +0 -1
- package/dist/filesystem/electron-provider.d.ts +0 -35
- package/dist/filesystem/electron-provider.d.ts.map +0 -1
- package/dist/filesystem/electron-provider.js +0 -69
- package/dist/filesystem/electron-provider.js.map +0 -1
- package/dist/filesystem/electron.d.ts.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 -153
- package/dist/filesystem/filesystem-content-container.js.map +0 -1
- package/dist/filesystem/fs-error.d.ts +0 -46
- package/dist/filesystem/fs-error.d.ts.map +0 -1
- package/dist/filesystem/fs-error.js +0 -170
- package/dist/filesystem/fs-error.js.map +0 -1
- package/dist/filesystem/index.d.ts.map +0 -1
- package/dist/filesystem/indexeddb-content-container.d.ts +0 -22
- package/dist/filesystem/indexeddb-content-container.d.ts.map +0 -1
- package/dist/filesystem/indexeddb-content-container.js +0 -100
- package/dist/filesystem/indexeddb-content-container.js.map +0 -1
- package/dist/filesystem/indexeddb-provider-v2.d.ts +0 -96
- package/dist/filesystem/indexeddb-provider-v2.d.ts.map +0 -1
- package/dist/filesystem/indexeddb-provider-v2.js +0 -1241
- package/dist/filesystem/indexeddb-provider-v2.js.map +0 -1
- package/dist/filesystem/indexeddb-provider.d.ts +0 -39
- package/dist/filesystem/indexeddb-provider.d.ts.map +0 -1
- package/dist/filesystem/indexeddb-provider.js +0 -145
- package/dist/filesystem/indexeddb-provider.js.map +0 -1
- package/dist/filesystem/indexeddb-store.d.ts +0 -34
- package/dist/filesystem/indexeddb-store.d.ts.map +0 -1
- package/dist/filesystem/indexeddb-store.js +0 -165
- package/dist/filesystem/indexeddb-store.js.map +0 -1
- package/dist/filesystem/indexeddb.d.ts.map +0 -1
- package/dist/filesystem/legacy-v2-adapter.d.ts +0 -62
- package/dist/filesystem/legacy-v2-adapter.d.ts.map +0 -1
- package/dist/filesystem/legacy-v2-adapter.js +0 -499
- package/dist/filesystem/legacy-v2-adapter.js.map +0 -1
- package/dist/filesystem/memory-provider-v2.d.ts +0 -92
- package/dist/filesystem/memory-provider-v2.d.ts.map +0 -1
- package/dist/filesystem/memory-provider-v2.js +0 -615
- package/dist/filesystem/memory-provider-v2.js.map +0 -1
- package/dist/filesystem/memory-provider.d.ts +0 -54
- package/dist/filesystem/memory-provider.d.ts.map +0 -1
- package/dist/filesystem/memory-provider.js +0 -132
- package/dist/filesystem/memory-provider.js.map +0 -1
- package/dist/filesystem/memory.d.ts.map +0 -1
- package/dist/filesystem/move-entry.d.ts +0 -9
- package/dist/filesystem/move-entry.d.ts.map +0 -1
- package/dist/filesystem/move-entry.js +0 -103
- package/dist/filesystem/move-entry.js.map +0 -1
- package/dist/filesystem/move-error.d.ts +0 -53
- package/dist/filesystem/move-error.d.ts.map +0 -1
- package/dist/filesystem/move-error.js +0 -99
- package/dist/filesystem/move-error.js.map +0 -1
- package/dist/filesystem/native-provider-v2.d.ts +0 -75
- package/dist/filesystem/native-provider-v2.d.ts.map +0 -1
- package/dist/filesystem/native-provider-v2.js +0 -849
- package/dist/filesystem/native-provider-v2.js.map +0 -1
- package/dist/filesystem/native-provider.d.ts +0 -53
- package/dist/filesystem/native-provider.d.ts.map +0 -1
- package/dist/filesystem/native-provider.js +0 -471
- package/dist/filesystem/native-provider.js.map +0 -1
- package/dist/filesystem/native.d.ts.map +0 -1
- package/dist/filesystem/types.d.ts +0 -83
- package/dist/filesystem/types.d.ts.map +0 -1
- package/dist/filesystem/types.js +0 -16
- package/dist/filesystem/types.js.map +0 -1
- package/dist/filesystem/v2.d.ts +0 -172
- package/dist/filesystem/v2.d.ts.map +0 -1
- package/dist/filesystem/v2.js +0 -16
- package/dist/filesystem/v2.js.map +0 -1
- package/dist/filesystem/workspace-path.d.ts +0 -21
- package/dist/filesystem/workspace-path.d.ts.map +0 -1
- package/dist/filesystem/workspace-path.js +0 -80
- package/dist/filesystem/workspace-path.js.map +0 -1
- package/dist/host/filesystem-v2.d.ts +0 -45
- package/dist/host/filesystem-v2.d.ts.map +0 -1
- package/dist/host/filesystem-v2.js +0 -13
- package/dist/host/filesystem-v2.js.map +0 -1
- package/dist/host/git.d.ts +0 -200
- package/dist/host/git.d.ts.map +0 -1
- package/dist/host/git.js +0 -15
- package/dist/host/git.js.map +0 -1
- package/dist/host/index.d.ts.map +0 -1
- package/dist/host/types.d.ts +0 -232
- 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/host/wire-policy.d.ts +0 -22
- package/dist/host/wire-policy.d.ts.map +0 -1
- package/dist/host/wire-policy.js +0 -92
- package/dist/host/wire-policy.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/mcp/index.d.ts.map +0 -1
- package/dist/mcp/types.d.ts +0 -466
- package/dist/mcp/types.d.ts.map +0 -1
- package/dist/mcp/types.js +0 -25
- package/dist/mcp/types.js.map +0 -1
- package/dist/mcp/wire.d.ts +0 -29
- package/dist/mcp/wire.d.ts.map +0 -1
- package/dist/mcp/wire.js +0 -1046
- package/dist/mcp/wire.js.map +0 -1
- package/dist/mcp/zod.d.ts.map +0 -1
- package/dist/vscode/index.d.ts.map +0 -1
- package/dist/vscode/messages.d.ts +0 -204
- package/dist/vscode/messages.d.ts.map +0 -1
- package/dist/vscode/messages.js +0 -494
- package/dist/vscode/messages.js.map +0 -1
- package/dist/workspace/index.d.ts.map +0 -1
- package/dist/workspace/reconcile-electron.d.ts +0 -19
- package/dist/workspace/reconcile-electron.d.ts.map +0 -1
- package/dist/workspace/reconcile-electron.js +0 -42
- package/dist/workspace/reconcile-electron.js.map +0 -1
- package/dist/workspace/types.d.ts +0 -69
- package/dist/workspace/types.d.ts.map +0 -1
- package/dist/workspace/types.js +0 -2
- package/dist/workspace/types.js.map +0 -1
- package/dist/workspace/workspace-manager.d.ts +0 -47
- package/dist/workspace/workspace-manager.d.ts.map +0 -1
- package/dist/workspace/workspace-manager.js +0 -126
- package/dist/workspace/workspace-manager.js.map +0 -1
- package/src/document/document-session.ts +0 -834
- package/src/document/filesystem-target.ts +0 -109
- package/src/document/index.ts +0 -41
- package/src/document/recovery-journal.ts +0 -536
- package/src/document/types.ts +0 -116
- package/src/filesystem/commit-lock.ts +0 -29
- package/src/filesystem/dbk-workspace.ts +0 -135
- package/src/filesystem/electron-provider-v2.ts +0 -630
- package/src/filesystem/electron-provider.ts +0 -99
- package/src/filesystem/electron.ts +0 -3
- package/src/filesystem/file-media-provider.ts +0 -103
- package/src/filesystem/filesystem-content-container.ts +0 -164
- package/src/filesystem/fs-error.ts +0 -234
- package/src/filesystem/index.ts +0 -143
- package/src/filesystem/indexeddb-content-container.ts +0 -112
- package/src/filesystem/indexeddb-provider-v2.ts +0 -1740
- package/src/filesystem/indexeddb-provider.ts +0 -171
- package/src/filesystem/indexeddb-store.ts +0 -207
- package/src/filesystem/indexeddb.ts +0 -12
- package/src/filesystem/legacy-v2-adapter.ts +0 -779
- package/src/filesystem/memory-provider-v2.ts +0 -896
- package/src/filesystem/memory-provider.ts +0 -180
- package/src/filesystem/memory.ts +0 -15
- package/src/filesystem/move-entry.ts +0 -142
- package/src/filesystem/move-error.ts +0 -159
- package/src/filesystem/native-provider-v2.ts +0 -1165
- package/src/filesystem/native-provider.ts +0 -537
- package/src/filesystem/native.ts +0 -16
- package/src/filesystem/types.ts +0 -114
- package/src/filesystem/v2.ts +0 -231
- package/src/filesystem/workspace-path.ts +0 -92
- package/src/host/filesystem-v2.ts +0 -104
- package/src/host/git.ts +0 -246
- package/src/host/index.ts +0 -93
- package/src/host/types.ts +0 -274
- package/src/host/wire-policy.ts +0 -103
- package/src/index.ts +0 -10
- package/src/mcp/index.ts +0 -3
- package/src/mcp/types.ts +0 -594
- package/src/mcp/wire.ts +0 -1280
- package/src/mcp/zod.ts +0 -1000
- package/src/vscode/index.ts +0 -2
- package/src/vscode/messages.ts +0 -704
- package/src/workspace/index.ts +0 -17
- package/src/workspace/reconcile-electron.ts +0 -65
- package/src/workspace/types.ts +0 -67
- package/src/workspace/workspace-manager.ts +0 -151
- /package/dist/{chunk-75UUF6C4.js.map → chunk-B56KWCSE.js.map} +0 -0
- /package/dist/{chunk-EGNDADXB.js.map → chunk-MNSZ7EZN.js.map} +0 -0
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
import { getFileSystemProviderV2, type FileSystemProvider } from '../filesystem/types.js';
|
|
2
|
-
import { FsError } from '../filesystem/fs-error.js';
|
|
3
|
-
import { parseWorkspacePath } from '../filesystem/workspace-path.js';
|
|
4
|
-
import { DocumentCommitConflictError } from './document-session.js';
|
|
5
|
-
import type { DocumentCommitTarget, DocumentExternalVersion } from './types.js';
|
|
6
|
-
|
|
7
|
-
export interface FileSystemDocumentTargetOptions {
|
|
8
|
-
/**
|
|
9
|
-
* Compare the current file with the session's persisted baseline before
|
|
10
|
-
* replacing it. Enabled by default.
|
|
11
|
-
*/
|
|
12
|
-
verifyExternalChanges?: boolean;
|
|
13
|
-
onCommitted?: (content: string, revision: number) => void;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Adapt a FileSystemProvider path to the commit protocol used by
|
|
18
|
-
* DocumentSession. The optimistic read catches external edits/deletions
|
|
19
|
-
* before a local write recreates or silently replaces them.
|
|
20
|
-
*/
|
|
21
|
-
export function createFileSystemDocumentTarget(
|
|
22
|
-
provider: FileSystemProvider,
|
|
23
|
-
path: string,
|
|
24
|
-
options: FileSystemDocumentTargetOptions = {},
|
|
25
|
-
): DocumentCommitTarget {
|
|
26
|
-
const verifyExternalChanges = options.verifyExternalChanges ?? true;
|
|
27
|
-
const canonicalPath = parseWorkspacePath(path);
|
|
28
|
-
const providerV2 = getFileSystemProviderV2(provider);
|
|
29
|
-
return {
|
|
30
|
-
key: provider.id + ':' + canonicalPath,
|
|
31
|
-
async commit(request) {
|
|
32
|
-
if (providerV2 && providerV2.capabilities.conditionalWrite !== 'none') {
|
|
33
|
-
const current = await providerV2.readFile(canonicalPath);
|
|
34
|
-
const currentContent = current ? new TextDecoder().decode(current.data) : null;
|
|
35
|
-
if (currentContent !== request.persistedContent && currentContent !== request.content) {
|
|
36
|
-
throw new DocumentCommitConflictError(
|
|
37
|
-
'The document changed outside DocBlocks: ' + path,
|
|
38
|
-
currentContent,
|
|
39
|
-
current?.entry.version ?? null,
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
if (currentContent === request.content) {
|
|
43
|
-
options.onCommitted?.(request.content, request.revision);
|
|
44
|
-
return { version: current?.entry.version ?? null };
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
try {
|
|
48
|
-
const committed = await providerV2.writeFile(
|
|
49
|
-
canonicalPath,
|
|
50
|
-
new TextEncoder().encode(request.content),
|
|
51
|
-
{
|
|
52
|
-
mode: current ? 'replace' : 'create',
|
|
53
|
-
createParents: true,
|
|
54
|
-
expectedVersion: current?.entry.version ?? null,
|
|
55
|
-
},
|
|
56
|
-
);
|
|
57
|
-
options.onCommitted?.(request.content, request.revision);
|
|
58
|
-
return { version: committed.version };
|
|
59
|
-
} catch (error: unknown) {
|
|
60
|
-
if (
|
|
61
|
-
error instanceof FsError &&
|
|
62
|
-
(error.code === 'conflict' ||
|
|
63
|
-
error.code === 'already-exists' ||
|
|
64
|
-
error.code === 'not-found')
|
|
65
|
-
) {
|
|
66
|
-
const external = await providerV2.readFile(canonicalPath);
|
|
67
|
-
throw new DocumentCommitConflictError(
|
|
68
|
-
'The document changed outside DocBlocks: ' + path,
|
|
69
|
-
external ? new TextDecoder().decode(external.data) : null,
|
|
70
|
-
external?.entry.version ?? null,
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
throw error;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
if (provider.commitFile) {
|
|
78
|
-
const result = await provider.commitFile(path, request.content, request.persistedContent);
|
|
79
|
-
if (result.status === 'conflict') {
|
|
80
|
-
throw new DocumentCommitConflictError(
|
|
81
|
-
'The document changed outside DocBlocks: ' + path,
|
|
82
|
-
result.content,
|
|
83
|
-
result.version,
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
options.onCommitted?.(request.content, request.revision);
|
|
87
|
-
return { version: result.version };
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
if (verifyExternalChanges) {
|
|
91
|
-
const current = await provider.readFile(path);
|
|
92
|
-
if (current !== request.persistedContent && current !== request.content) {
|
|
93
|
-
const meta = await provider.stat(path).catch(() => null);
|
|
94
|
-
const version: DocumentExternalVersion = meta?.lastModified ?? null;
|
|
95
|
-
throw new DocumentCommitConflictError(
|
|
96
|
-
'The document changed outside DocBlocks: ' + path,
|
|
97
|
-
current,
|
|
98
|
-
version,
|
|
99
|
-
);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
await provider.writeFile(path, request.content);
|
|
104
|
-
options.onCommitted?.(request.content, request.revision);
|
|
105
|
-
const meta = await provider.stat(path).catch(() => null);
|
|
106
|
-
return { version: meta?.lastModified ?? null };
|
|
107
|
-
},
|
|
108
|
-
};
|
|
109
|
-
}
|
package/src/document/index.ts
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
export {
|
|
2
|
-
DocumentCommitConflictError,
|
|
3
|
-
DocumentSession,
|
|
4
|
-
DocumentSessionConflictError,
|
|
5
|
-
} from './document-session.js';
|
|
6
|
-
export {
|
|
7
|
-
createFileSystemDocumentTarget,
|
|
8
|
-
type FileSystemDocumentTargetOptions,
|
|
9
|
-
} from './filesystem-target.js';
|
|
10
|
-
export {
|
|
11
|
-
DOCUMENT_RECOVERY_JOURNAL_SCHEMA_VERSION,
|
|
12
|
-
DOCUMENT_RECOVERY_JOURNAL_STORAGE_KEY,
|
|
13
|
-
DocumentRecoveryJournal,
|
|
14
|
-
getDefaultDocumentRecoveryStorage,
|
|
15
|
-
} from './recovery-journal.js';
|
|
16
|
-
export type {
|
|
17
|
-
DocumentCommitRequest,
|
|
18
|
-
DocumentCommitResult,
|
|
19
|
-
DocumentCommitTarget,
|
|
20
|
-
DocumentConflictStrategy,
|
|
21
|
-
DocumentExternalChangeResult,
|
|
22
|
-
DocumentExternalSnapshot,
|
|
23
|
-
DocumentExternalVersion,
|
|
24
|
-
DocumentSaveReason,
|
|
25
|
-
DocumentSessionConflict,
|
|
26
|
-
DocumentSessionEditScope,
|
|
27
|
-
DocumentSessionLifecycle,
|
|
28
|
-
DocumentSessionOptions,
|
|
29
|
-
DocumentSessionSnapshot,
|
|
30
|
-
DocumentSessionStatus,
|
|
31
|
-
DocumentSessionTransition,
|
|
32
|
-
} from './types.js';
|
|
33
|
-
export type {
|
|
34
|
-
DocumentRecoveryAcknowledgement,
|
|
35
|
-
DocumentRecoveryJournalOptions,
|
|
36
|
-
DocumentRecoveryRecord,
|
|
37
|
-
DocumentRecoveryStorage,
|
|
38
|
-
DocumentRecoveryWrite,
|
|
39
|
-
DocumentRecoveryWriteFailure,
|
|
40
|
-
DocumentRecoveryWriteResult,
|
|
41
|
-
} from './recovery-journal.js';
|
|
@@ -1,536 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Synchronous crash-recovery journal for dirty document snapshots.
|
|
3
|
-
*
|
|
4
|
-
* The journal deliberately depends on a tiny injected Storage-like contract
|
|
5
|
-
* instead of reading `window.localStorage` at module load time. This keeps the
|
|
6
|
-
* document package usable in Node, Electron main, VS Code extension hosts, and
|
|
7
|
-
* tests while allowing browser renderers to use localStorage for a best-effort
|
|
8
|
-
* write that completes before the current JavaScript turn ends.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
export const DOCUMENT_RECOVERY_JOURNAL_STORAGE_KEY = 'docblocks:document-recovery:v1';
|
|
12
|
-
export const DOCUMENT_RECOVERY_JOURNAL_SCHEMA_VERSION = 1;
|
|
13
|
-
|
|
14
|
-
export interface DocumentRecoveryStorage {
|
|
15
|
-
getItem(key: string): string | null;
|
|
16
|
-
setItem(key: string, value: string): void;
|
|
17
|
-
removeItem(key: string): void;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface DocumentRecoveryRecord {
|
|
21
|
-
/** Stable commit-target key, including the workspace/provider scope. */
|
|
22
|
-
targetKey: string;
|
|
23
|
-
/** DocumentSession generation that produced this snapshot. */
|
|
24
|
-
generation: number;
|
|
25
|
-
/** Monotonic DocumentSession revision represented by `content`. */
|
|
26
|
-
revision: number;
|
|
27
|
-
/** Complete markdown snapshot. */
|
|
28
|
-
content: string;
|
|
29
|
-
/**
|
|
30
|
-
* Content last acknowledged by the commit target when this draft was made.
|
|
31
|
-
* Recovery may be applied automatically only when storage still matches this
|
|
32
|
-
* baseline; otherwise the caller must surface an external-change conflict.
|
|
33
|
-
*/
|
|
34
|
-
persistedContent: string | null;
|
|
35
|
-
/** Time the dirty record for this target was first created. */
|
|
36
|
-
createdAt: number;
|
|
37
|
-
/** Time the record was most recently replaced. */
|
|
38
|
-
updatedAt: number;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export interface DocumentRecoveryWrite {
|
|
42
|
-
targetKey: string;
|
|
43
|
-
generation: number;
|
|
44
|
-
revision: number;
|
|
45
|
-
content: string;
|
|
46
|
-
persistedContent: string | null;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export interface DocumentRecoveryAcknowledgement {
|
|
50
|
-
targetKey: string;
|
|
51
|
-
generation: number;
|
|
52
|
-
/** Highest revision durably acknowledged by the commit target. */
|
|
53
|
-
persistedRevision: number;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export type DocumentRecoveryWriteFailure =
|
|
57
|
-
| 'invalid-record'
|
|
58
|
-
| 'record-too-large'
|
|
59
|
-
| 'storage-unavailable'
|
|
60
|
-
| 'quota-exceeded';
|
|
61
|
-
|
|
62
|
-
export type DocumentRecoveryWriteResult =
|
|
63
|
-
| { status: 'stored'; evicted: number }
|
|
64
|
-
| { status: 'unchanged'; evicted: 0 }
|
|
65
|
-
| { status: 'ignored-stale'; evicted: 0 }
|
|
66
|
-
| { status: 'rejected'; reason: DocumentRecoveryWriteFailure; evicted: number };
|
|
67
|
-
|
|
68
|
-
export interface DocumentRecoveryJournalOptions {
|
|
69
|
-
storageKey?: string;
|
|
70
|
-
/** Maximum number of document records retained. Defaults to 20. */
|
|
71
|
-
maxEntries?: number;
|
|
72
|
-
/** Conservative UTF-16 estimate for the whole serialized journal. */
|
|
73
|
-
maxStorageBytes?: number;
|
|
74
|
-
/** Conservative UTF-16 estimate for one serialized record. */
|
|
75
|
-
maxRecordBytes?: number;
|
|
76
|
-
/** Records older than this are discarded while reading. Defaults to 7 days. */
|
|
77
|
-
maxAgeMs?: number;
|
|
78
|
-
/** Tolerated clock skew for records dated in the future. Defaults to 5 minutes. */
|
|
79
|
-
maxFutureSkewMs?: number;
|
|
80
|
-
/** Injected for deterministic tests. */
|
|
81
|
-
now?: () => number;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
interface DocumentRecoveryEnvelope {
|
|
85
|
-
schemaVersion: typeof DOCUMENT_RECOVERY_JOURNAL_SCHEMA_VERSION;
|
|
86
|
-
records: DocumentRecoveryRecord[];
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
interface LoadedRecords {
|
|
90
|
-
records: DocumentRecoveryRecord[];
|
|
91
|
-
needsRepair: boolean;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
interface PersistResult {
|
|
95
|
-
stored: boolean;
|
|
96
|
-
evicted: number;
|
|
97
|
-
reason?: 'storage-unavailable' | 'quota-exceeded';
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const DEFAULT_MAX_ENTRIES = 20;
|
|
101
|
-
const DEFAULT_MAX_STORAGE_BYTES = 2_000_000;
|
|
102
|
-
const DEFAULT_MAX_RECORD_BYTES = 1_000_000;
|
|
103
|
-
const DEFAULT_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
104
|
-
const DEFAULT_MAX_FUTURE_SKEW_MS = 5 * 60 * 1000;
|
|
105
|
-
const MAX_TARGET_KEY_LENGTH = 4096;
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Best-effort, synchronous recovery storage.
|
|
109
|
-
*
|
|
110
|
-
* Normal save acknowledgement is the only automatic path that removes the
|
|
111
|
-
* current record. Explicit discard, age expiry, corruption repair, and bounded
|
|
112
|
-
* eviction are intentional exceptions: callers can discard after the user
|
|
113
|
-
* rejects recovery, and the journal must never grow without limit.
|
|
114
|
-
*/
|
|
115
|
-
export class DocumentRecoveryJournal {
|
|
116
|
-
private readonly storage: DocumentRecoveryStorage | null;
|
|
117
|
-
private readonly storageKey: string;
|
|
118
|
-
private readonly maxEntries: number;
|
|
119
|
-
private readonly maxStorageBytes: number;
|
|
120
|
-
private readonly maxRecordBytes: number;
|
|
121
|
-
private readonly maxAgeMs: number;
|
|
122
|
-
private readonly maxFutureSkewMs: number;
|
|
123
|
-
private readonly now: () => number;
|
|
124
|
-
|
|
125
|
-
public constructor(
|
|
126
|
-
storage: DocumentRecoveryStorage | null,
|
|
127
|
-
options: DocumentRecoveryJournalOptions = {},
|
|
128
|
-
) {
|
|
129
|
-
this.storage = storage;
|
|
130
|
-
this.storageKey = options.storageKey?.trim() || DOCUMENT_RECOVERY_JOURNAL_STORAGE_KEY;
|
|
131
|
-
this.maxEntries = positiveInteger(options.maxEntries, DEFAULT_MAX_ENTRIES);
|
|
132
|
-
this.maxStorageBytes = positiveInteger(options.maxStorageBytes, DEFAULT_MAX_STORAGE_BYTES);
|
|
133
|
-
this.maxRecordBytes = Math.min(
|
|
134
|
-
positiveInteger(options.maxRecordBytes, DEFAULT_MAX_RECORD_BYTES),
|
|
135
|
-
this.maxStorageBytes,
|
|
136
|
-
);
|
|
137
|
-
this.maxAgeMs = nonNegativeFinite(options.maxAgeMs, DEFAULT_MAX_AGE_MS);
|
|
138
|
-
this.maxFutureSkewMs = nonNegativeFinite(options.maxFutureSkewMs, DEFAULT_MAX_FUTURE_SKEW_MS);
|
|
139
|
-
this.now = options.now ?? Date.now;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Synchronously persist the latest dirty snapshot for a target.
|
|
144
|
-
* Storage/quota failures are returned, never thrown into an editor onChange.
|
|
145
|
-
*/
|
|
146
|
-
public write(input: DocumentRecoveryWrite): DocumentRecoveryWriteResult {
|
|
147
|
-
const timestamp = this.now();
|
|
148
|
-
if (!isValidWrite(input) || !Number.isFinite(timestamp) || timestamp < 0) {
|
|
149
|
-
return { status: 'rejected', reason: 'invalid-record', evicted: 0 };
|
|
150
|
-
}
|
|
151
|
-
if (!this.storage) {
|
|
152
|
-
return { status: 'rejected', reason: 'storage-unavailable', evicted: 0 };
|
|
153
|
-
}
|
|
154
|
-
if (
|
|
155
|
-
estimatedBytes(input.content) > this.maxRecordBytes ||
|
|
156
|
-
(input.persistedContent !== null &&
|
|
157
|
-
estimatedBytes(input.persistedContent) > this.maxRecordBytes)
|
|
158
|
-
) {
|
|
159
|
-
return { status: 'rejected', reason: 'record-too-large', evicted: 0 };
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const loaded = this.load(timestamp);
|
|
163
|
-
const existing = loaded.records.find((record) => record.targetKey === input.targetKey);
|
|
164
|
-
if (existing) {
|
|
165
|
-
const ordering = compareGenerationRevision(input, existing);
|
|
166
|
-
if (ordering < 0) return { status: 'ignored-stale', evicted: 0 };
|
|
167
|
-
if (ordering === 0) {
|
|
168
|
-
if (
|
|
169
|
-
existing.content === input.content &&
|
|
170
|
-
existing.persistedContent === input.persistedContent
|
|
171
|
-
) {
|
|
172
|
-
return { status: 'unchanged', evicted: 0 };
|
|
173
|
-
}
|
|
174
|
-
// A revision is an immutable content snapshot. Reusing it for different
|
|
175
|
-
// content would make acknowledgement and recovery ambiguous.
|
|
176
|
-
return { status: 'rejected', reason: 'invalid-record', evicted: 0 };
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const candidate: DocumentRecoveryRecord = {
|
|
181
|
-
targetKey: input.targetKey,
|
|
182
|
-
generation: input.generation,
|
|
183
|
-
revision: input.revision,
|
|
184
|
-
content: input.content,
|
|
185
|
-
persistedContent: input.persistedContent,
|
|
186
|
-
createdAt: existing?.createdAt ?? timestamp,
|
|
187
|
-
updatedAt: timestamp,
|
|
188
|
-
};
|
|
189
|
-
if (estimatedBytes(JSON.stringify(candidate)) > this.maxRecordBytes) {
|
|
190
|
-
return { status: 'rejected', reason: 'record-too-large', evicted: 0 };
|
|
191
|
-
}
|
|
192
|
-
if (estimatedBytes(serializeEnvelope([candidate])) > this.maxStorageBytes) {
|
|
193
|
-
return { status: 'rejected', reason: 'record-too-large', evicted: 0 };
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
const next = loaded.records.filter((record) => record.targetKey !== input.targetKey);
|
|
197
|
-
next.push(candidate);
|
|
198
|
-
const persisted = this.persistBounded(next, input.targetKey);
|
|
199
|
-
if (!persisted.stored) {
|
|
200
|
-
return {
|
|
201
|
-
status: 'rejected',
|
|
202
|
-
reason: persisted.reason ?? 'quota-exceeded',
|
|
203
|
-
evicted: persisted.evicted,
|
|
204
|
-
};
|
|
205
|
-
}
|
|
206
|
-
return { status: 'stored', evicted: persisted.evicted };
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/** Return the recoverable snapshot for one target, if present and valid. */
|
|
210
|
-
public lookup(targetKey: string, generation?: number): DocumentRecoveryRecord | null {
|
|
211
|
-
if (!isValidTargetKey(targetKey) || !isOptionalGeneration(generation)) return null;
|
|
212
|
-
const record = this.load(this.now()).records.find(
|
|
213
|
-
(candidate) =>
|
|
214
|
-
candidate.targetKey === targetKey &&
|
|
215
|
-
(generation === undefined || candidate.generation === generation),
|
|
216
|
-
);
|
|
217
|
-
return record ? cloneRecord(record) : null;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/** List valid records, newest first. Optionally restrict to one target. */
|
|
221
|
-
public list(targetKey?: string): DocumentRecoveryRecord[] {
|
|
222
|
-
if (targetKey !== undefined && !isValidTargetKey(targetKey)) return [];
|
|
223
|
-
return this.load(this.now())
|
|
224
|
-
.records.filter((record) => targetKey === undefined || record.targetKey === targetKey)
|
|
225
|
-
.sort(compareNewestFirst)
|
|
226
|
-
.map(cloneRecord);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/**
|
|
230
|
-
* Clear a record only when the same generation has durably acknowledged at
|
|
231
|
-
* least the journaled revision. A stale acknowledgement cannot erase a newer
|
|
232
|
-
* crash-recovery snapshot.
|
|
233
|
-
*/
|
|
234
|
-
public acknowledge(input: DocumentRecoveryAcknowledgement): boolean {
|
|
235
|
-
if (!isValidAcknowledgement(input) || !this.storage) return false;
|
|
236
|
-
const records = this.load(this.now()).records;
|
|
237
|
-
const record = records.find((candidate) => candidate.targetKey === input.targetKey);
|
|
238
|
-
if (
|
|
239
|
-
!record ||
|
|
240
|
-
record.generation !== input.generation ||
|
|
241
|
-
record.revision > input.persistedRevision
|
|
242
|
-
) {
|
|
243
|
-
return false;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
return this.persistExact(records.filter((candidate) => candidate !== record));
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/** Explicitly discard recovery data after a user or lifecycle decision. */
|
|
250
|
-
public discard(targetKey: string, generation?: number): boolean {
|
|
251
|
-
if (!isValidTargetKey(targetKey) || !isOptionalGeneration(generation) || !this.storage) {
|
|
252
|
-
return false;
|
|
253
|
-
}
|
|
254
|
-
const records = this.load(this.now()).records;
|
|
255
|
-
const next = records.filter(
|
|
256
|
-
(record) =>
|
|
257
|
-
record.targetKey !== targetKey ||
|
|
258
|
-
(generation !== undefined && record.generation !== generation),
|
|
259
|
-
);
|
|
260
|
-
if (next.length === records.length) return false;
|
|
261
|
-
return this.persistExact(next);
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
private load(timestamp: number): LoadedRecords {
|
|
265
|
-
if (!this.storage) return { records: [], needsRepair: false };
|
|
266
|
-
|
|
267
|
-
let raw: string | null;
|
|
268
|
-
try {
|
|
269
|
-
raw = this.storage.getItem(this.storageKey);
|
|
270
|
-
} catch {
|
|
271
|
-
return { records: [], needsRepair: false };
|
|
272
|
-
}
|
|
273
|
-
if (raw === null) return { records: [], needsRepair: false };
|
|
274
|
-
|
|
275
|
-
let parsed: unknown;
|
|
276
|
-
try {
|
|
277
|
-
parsed = JSON.parse(raw) as unknown;
|
|
278
|
-
} catch {
|
|
279
|
-
this.tryRemove();
|
|
280
|
-
return { records: [], needsRepair: true };
|
|
281
|
-
}
|
|
282
|
-
if (!isEnvelope(parsed)) {
|
|
283
|
-
this.tryRemove();
|
|
284
|
-
return { records: [], needsRepair: true };
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
const byTarget = new Map<string, DocumentRecoveryRecord>();
|
|
288
|
-
let needsRepair = estimatedBytes(raw) > this.maxStorageBytes;
|
|
289
|
-
for (const candidate of parsed.records) {
|
|
290
|
-
if (!isValidStoredRecord(candidate, timestamp, this.maxAgeMs, this.maxFutureSkewMs)) {
|
|
291
|
-
needsRepair = true;
|
|
292
|
-
continue;
|
|
293
|
-
}
|
|
294
|
-
if (estimatedBytes(JSON.stringify(candidate)) > this.maxRecordBytes) {
|
|
295
|
-
needsRepair = true;
|
|
296
|
-
continue;
|
|
297
|
-
}
|
|
298
|
-
const current = byTarget.get(candidate.targetKey);
|
|
299
|
-
if (!current || compareStoredRecords(candidate, current) > 0) {
|
|
300
|
-
if (current) needsRepair = true;
|
|
301
|
-
byTarget.set(candidate.targetKey, cloneRecord(candidate));
|
|
302
|
-
} else {
|
|
303
|
-
needsRepair = true;
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
const records = [...byTarget.values()].sort(compareNewestFirst);
|
|
308
|
-
while (records.length > this.maxEntries) {
|
|
309
|
-
records.pop();
|
|
310
|
-
needsRepair = true;
|
|
311
|
-
}
|
|
312
|
-
while (
|
|
313
|
-
records.length > 0 &&
|
|
314
|
-
estimatedBytes(serializeEnvelope(records)) > this.maxStorageBytes
|
|
315
|
-
) {
|
|
316
|
-
records.pop();
|
|
317
|
-
needsRepair = true;
|
|
318
|
-
}
|
|
319
|
-
if (needsRepair) {
|
|
320
|
-
// Best effort only. Read APIs must still return the validated in-memory
|
|
321
|
-
// records even when storage is temporarily unavailable.
|
|
322
|
-
this.persistBounded(records);
|
|
323
|
-
}
|
|
324
|
-
return { records, needsRepair };
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
private persistBounded(
|
|
328
|
-
records: DocumentRecoveryRecord[],
|
|
329
|
-
protectedTarget?: string,
|
|
330
|
-
): PersistResult {
|
|
331
|
-
if (!this.storage) {
|
|
332
|
-
return { stored: false, evicted: 0, reason: 'storage-unavailable' };
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
const next = [...records].sort(compareNewestFirst);
|
|
336
|
-
let evicted = 0;
|
|
337
|
-
while (next.length > this.maxEntries) {
|
|
338
|
-
if (!evictOldest(next, protectedTarget)) break;
|
|
339
|
-
evicted += 1;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
let serialized = serializeEnvelope(next);
|
|
343
|
-
while (estimatedBytes(serialized) > this.maxStorageBytes) {
|
|
344
|
-
if (!evictOldest(next, protectedTarget)) {
|
|
345
|
-
return { stored: false, evicted, reason: 'quota-exceeded' };
|
|
346
|
-
}
|
|
347
|
-
evicted += 1;
|
|
348
|
-
serialized = serializeEnvelope(next);
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
for (;;) {
|
|
352
|
-
try {
|
|
353
|
-
this.storage.setItem(this.storageKey, serialized);
|
|
354
|
-
return { stored: true, evicted };
|
|
355
|
-
} catch {
|
|
356
|
-
if (!evictOldest(next, protectedTarget)) {
|
|
357
|
-
return { stored: false, evicted, reason: 'quota-exceeded' };
|
|
358
|
-
}
|
|
359
|
-
evicted += 1;
|
|
360
|
-
serialized = serializeEnvelope(next);
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
private persistExact(records: DocumentRecoveryRecord[]): boolean {
|
|
366
|
-
if (!this.storage) return false;
|
|
367
|
-
if (records.length === 0) {
|
|
368
|
-
try {
|
|
369
|
-
this.storage.removeItem(this.storageKey);
|
|
370
|
-
return true;
|
|
371
|
-
} catch {
|
|
372
|
-
return false;
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
try {
|
|
376
|
-
this.storage.setItem(this.storageKey, serializeEnvelope(records));
|
|
377
|
-
return true;
|
|
378
|
-
} catch {
|
|
379
|
-
return false;
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
private tryRemove(): void {
|
|
384
|
-
try {
|
|
385
|
-
this.storage?.removeItem(this.storageKey);
|
|
386
|
-
} catch {
|
|
387
|
-
// Recovery is best effort; a denied cleanup must not fail application startup.
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
/**
|
|
393
|
-
* Resolve browser localStorage on demand. Importing this module never touches
|
|
394
|
-
* browser globals, and access errors (sandboxing/security settings) yield null.
|
|
395
|
-
*/
|
|
396
|
-
export function getDefaultDocumentRecoveryStorage(): DocumentRecoveryStorage | null {
|
|
397
|
-
try {
|
|
398
|
-
const candidate = Reflect.get(globalThis, 'localStorage') as unknown;
|
|
399
|
-
if (!isStorage(candidate)) return null;
|
|
400
|
-
return candidate;
|
|
401
|
-
} catch {
|
|
402
|
-
return null;
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
function serializeEnvelope(records: DocumentRecoveryRecord[]): string {
|
|
407
|
-
const envelope: DocumentRecoveryEnvelope = {
|
|
408
|
-
schemaVersion: DOCUMENT_RECOVERY_JOURNAL_SCHEMA_VERSION,
|
|
409
|
-
records,
|
|
410
|
-
};
|
|
411
|
-
return JSON.stringify(envelope);
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
/** localStorage quotas are commonly measured from UTF-16 storage usage. */
|
|
415
|
-
function estimatedBytes(value: string): number {
|
|
416
|
-
return value.length * 2;
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
function positiveInteger(value: number | undefined, fallback: number): number {
|
|
420
|
-
return Number.isSafeInteger(value) && (value ?? 0) > 0 ? (value as number) : fallback;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
function nonNegativeFinite(value: number | undefined, fallback: number): number {
|
|
424
|
-
return Number.isFinite(value) && (value ?? -1) >= 0 ? (value as number) : fallback;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
function isEnvelope(value: unknown): value is DocumentRecoveryEnvelope {
|
|
428
|
-
if (!isObject(value)) return false;
|
|
429
|
-
return (
|
|
430
|
-
value.schemaVersion === DOCUMENT_RECOVERY_JOURNAL_SCHEMA_VERSION && Array.isArray(value.records)
|
|
431
|
-
);
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
function isValidStoredRecord(
|
|
435
|
-
value: unknown,
|
|
436
|
-
timestamp: number,
|
|
437
|
-
maxAgeMs: number,
|
|
438
|
-
maxFutureSkewMs: number,
|
|
439
|
-
): value is DocumentRecoveryRecord {
|
|
440
|
-
if (!isObject(value)) return false;
|
|
441
|
-
if (
|
|
442
|
-
!isValidTargetKey(value.targetKey) ||
|
|
443
|
-
!isNonNegativeSafeInteger(value.generation) ||
|
|
444
|
-
!isNonNegativeSafeInteger(value.revision) ||
|
|
445
|
-
typeof value.content !== 'string' ||
|
|
446
|
-
(value.persistedContent !== null && typeof value.persistedContent !== 'string') ||
|
|
447
|
-
!isNonNegativeFiniteNumber(value.createdAt) ||
|
|
448
|
-
!isNonNegativeFiniteNumber(value.updatedAt) ||
|
|
449
|
-
value.createdAt > value.updatedAt
|
|
450
|
-
) {
|
|
451
|
-
return false;
|
|
452
|
-
}
|
|
453
|
-
if (!Number.isFinite(timestamp) || timestamp < 0) return false;
|
|
454
|
-
if (value.updatedAt > timestamp + maxFutureSkewMs) return false;
|
|
455
|
-
return timestamp - value.updatedAt <= maxAgeMs;
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
function isValidWrite(value: DocumentRecoveryWrite): boolean {
|
|
459
|
-
return (
|
|
460
|
-
isValidTargetKey(value.targetKey) &&
|
|
461
|
-
isNonNegativeSafeInteger(value.generation) &&
|
|
462
|
-
isNonNegativeSafeInteger(value.revision) &&
|
|
463
|
-
typeof value.content === 'string' &&
|
|
464
|
-
(value.persistedContent === null || typeof value.persistedContent === 'string')
|
|
465
|
-
);
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
function isValidAcknowledgement(value: DocumentRecoveryAcknowledgement): boolean {
|
|
469
|
-
return (
|
|
470
|
-
isValidTargetKey(value.targetKey) &&
|
|
471
|
-
isNonNegativeSafeInteger(value.generation) &&
|
|
472
|
-
isNonNegativeSafeInteger(value.persistedRevision)
|
|
473
|
-
);
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
function isValidTargetKey(value: unknown): value is string {
|
|
477
|
-
return typeof value === 'string' && value.length > 0 && value.length <= MAX_TARGET_KEY_LENGTH;
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
function isOptionalGeneration(value: number | undefined): boolean {
|
|
481
|
-
return value === undefined || isNonNegativeSafeInteger(value);
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
function isNonNegativeSafeInteger(value: unknown): value is number {
|
|
485
|
-
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
function isNonNegativeFiniteNumber(value: unknown): value is number {
|
|
489
|
-
return typeof value === 'number' && Number.isFinite(value) && value >= 0;
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
function isObject(value: unknown): value is Record<string, unknown> {
|
|
493
|
-
return typeof value === 'object' && value !== null;
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
function isStorage(value: unknown): value is DocumentRecoveryStorage {
|
|
497
|
-
if (!isObject(value)) return false;
|
|
498
|
-
return (
|
|
499
|
-
typeof value.getItem === 'function' &&
|
|
500
|
-
typeof value.setItem === 'function' &&
|
|
501
|
-
typeof value.removeItem === 'function'
|
|
502
|
-
);
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
function compareGenerationRevision(
|
|
506
|
-
left: Pick<DocumentRecoveryRecord, 'generation' | 'revision'>,
|
|
507
|
-
right: Pick<DocumentRecoveryRecord, 'generation' | 'revision'>,
|
|
508
|
-
): number {
|
|
509
|
-
if (left.generation !== right.generation) return left.generation - right.generation;
|
|
510
|
-
return left.revision - right.revision;
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
function compareStoredRecords(left: DocumentRecoveryRecord, right: DocumentRecoveryRecord): number {
|
|
514
|
-
return compareGenerationRevision(left, right) || left.updatedAt - right.updatedAt;
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
function compareNewestFirst(left: DocumentRecoveryRecord, right: DocumentRecoveryRecord): number {
|
|
518
|
-
return right.updatedAt - left.updatedAt || right.revision - left.revision;
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
function cloneRecord(record: DocumentRecoveryRecord): DocumentRecoveryRecord {
|
|
522
|
-
return { ...record };
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
function evictOldest(records: DocumentRecoveryRecord[], protectedTarget?: string): boolean {
|
|
526
|
-
let oldestIndex = -1;
|
|
527
|
-
for (let index = 0; index < records.length; index += 1) {
|
|
528
|
-
if (records[index].targetKey === protectedTarget) continue;
|
|
529
|
-
if (oldestIndex < 0 || compareNewestFirst(records[index], records[oldestIndex]) > 0) {
|
|
530
|
-
oldestIndex = index;
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
if (oldestIndex < 0) return false;
|
|
534
|
-
records.splice(oldestIndex, 1);
|
|
535
|
-
return true;
|
|
536
|
-
}
|