@lapis-notes/api 0.1.4 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +206 -0
- package/README.md +21 -2
- package/dist/__tests__/fixtures/indexed-mutations.json +6862 -0
- package/dist/__tests__/fixtures/memory-storage.json +309 -0
- package/dist/__tests__/fixtures/native-facade-contract.json +3224 -0
- package/dist/__tests__/fixtures/projection-query.json +195 -0
- package/dist/__tests__/fixtures/projection-values.json +60 -0
- package/dist/__tests__/fixtures/search-preparation.json +1513 -0
- package/dist/__tests__/fixtures/search-retrieval.json +15409 -0
- package/dist/__tests__/fixtures/search-semantic-model.json +3691 -0
- package/dist/__tests__/fixtures/search-semantic-retrieval.json +6603 -0
- package/dist/__tests__/fixtures/search-storage.json +409 -0
- package/dist/__tests__/fixtures/task-projection.json +428 -0
- package/dist/__tests__/indexed-mutation-support.js +262 -0
- package/dist/__tests__/projection-query-support.js +40 -0
- package/dist/__tests__/search-retrieval-support.js +47 -0
- package/dist/app-database-host.d.ts +1 -0
- package/dist/app-shell-plugin-persistence.d.ts +3 -2
- package/dist/app-shell-plugin-persistence.js +37 -3
- package/dist/cache.svelte.d.ts +8 -1
- package/dist/cache.svelte.js +103 -5
- package/dist/command.svelte.js +29 -2
- package/dist/components/editor/editor.css +4 -0
- package/dist/components/editor/editor.svelte +21 -9
- package/dist/components/editor/embedded-editor-surface.js +3 -1
- package/dist/components/editor/inline-title-alignment.d.ts +3 -0
- package/dist/components/editor/inline-title-alignment.js +57 -0
- package/dist/configuration.svelte.d.ts +270 -206
- package/dist/configuration.svelte.js +161 -86
- package/dist/context.svelte.d.ts +21 -3
- package/dist/context.svelte.js +63 -20
- package/dist/diagnostics.d.ts +8 -0
- package/dist/diagnostics.js +6 -0
- package/dist/editor-save-context.d.ts +12 -1
- package/dist/editor-save-context.js +32 -1
- package/dist/editor-view-registry.d.ts +10 -0
- package/dist/editor-view-registry.js +44 -4
- package/dist/editor.svelte.js +128 -37
- package/dist/embed-registry.d.ts +10 -0
- package/dist/embed-registry.js +15 -0
- package/dist/file-editing-failures.d.ts +2 -0
- package/dist/file-editing-failures.js +29 -0
- package/dist/file-editing-native-issues.d.ts +19 -0
- package/dist/file-editing-native-issues.js +65 -0
- package/dist/file-editing-sessions.d.ts +56 -8
- package/dist/file-editing-sessions.js +185 -15
- package/dist/file-manager.d.ts +8 -1
- package/dist/file-manager.js +37 -3
- package/dist/file-state-store.d.ts +5 -0
- package/dist/file-state-store.js +34 -0
- package/dist/file-text-conflicts.d.ts +25 -0
- package/dist/file-text-conflicts.js +35 -0
- package/dist/file-text-mutation.d.ts +13 -0
- package/dist/file-text-mutation.js +100 -0
- package/dist/file-text-persistence.d.ts +36 -0
- package/dist/file-text-persistence.js +234 -0
- package/dist/file-text-state-writer.d.ts +34 -0
- package/dist/file-text-state-writer.js +183 -0
- package/dist/files-configuration.d.ts +20 -0
- package/dist/files-configuration.js +47 -0
- package/dist/generated/plugin-host-modules.schema.generated.d.ts +87 -37
- package/dist/generated/plugin-host-modules.schema.generated.js +102 -37
- package/dist/glob.d.ts +3 -0
- package/dist/glob.js +2 -1
- package/dist/icons/changes.d.ts +3 -0
- package/dist/icons/changes.js +16 -0
- package/dist/icons/index.d.ts +5 -1
- package/dist/icons/index.js +44 -3
- package/dist/icons/registry.d.ts +35 -0
- package/dist/icons/registry.js +154 -0
- package/dist/icons/safe-svg.d.ts +2 -0
- package/dist/icons/safe-svg.js +44 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.js +3 -1
- package/dist/lapis-extension.d.ts +6 -2
- package/dist/lapis-extension.js +2 -0
- package/dist/metadata.svelte.js +45 -2
- package/dist/native-background-indexing.d.ts +48 -0
- package/dist/native-background-indexing.js +208 -0
- package/dist/native-database-protocol.d.ts +29 -0
- package/dist/native-database-protocol.js +1 -0
- package/dist/native-editing-intent-lease.d.ts +17 -0
- package/dist/native-editing-intent-lease.js +188 -0
- package/dist/native-editing-intents-protocol.d.ts +70 -0
- package/dist/native-editing-intents-protocol.js +1 -0
- package/dist/native-editing-intents.d.ts +8 -0
- package/dist/native-editing-intents.js +1 -0
- package/dist/native-editing-problems.d.ts +3 -0
- package/dist/native-editing-problems.js +180 -0
- package/dist/native-editing-recovery.d.ts +24 -0
- package/dist/native-editing-recovery.js +198 -0
- package/dist/native-editing-session.d.ts +51 -0
- package/dist/native-editing-session.js +251 -0
- package/dist/native-history-index-protocol.d.ts +80 -0
- package/dist/native-history-index-protocol.js +1 -0
- package/dist/native-history-index.d.ts +6 -0
- package/dist/native-history-index.js +1 -0
- package/dist/native-indexing-protocol.d.ts +63 -0
- package/dist/native-indexing-protocol.js +1 -0
- package/dist/native-participant-protocol.d.ts +67 -0
- package/dist/native-participant-protocol.js +3 -0
- package/dist/native-queue-protocol.d.ts +73 -0
- package/dist/native-queue-protocol.js +3 -0
- package/dist/native-tasks-protocol.d.ts +264 -0
- package/dist/native-tasks-protocol.js +1 -0
- package/dist/native-tasks.d.ts +35 -0
- package/dist/native-tasks.js +79 -0
- package/dist/native-turn-protocol.d.ts +81 -0
- package/dist/native-turn-protocol.js +1 -0
- package/dist/native-turns.d.ts +29 -0
- package/dist/native-turns.js +2 -0
- package/dist/plugin-asset-server.js +3 -2
- package/dist/plugin-dependency-resolver.d.ts +1 -1
- package/dist/plugin-distribution/compatibility.d.ts +1 -1
- package/dist/plugin-distribution/compatibility.js +10 -5
- package/dist/plugin-distribution/installed-plugin-state.js +2 -1
- package/dist/plugin-distribution/installer.d.ts +6 -0
- package/dist/plugin-distribution/installer.js +90 -37
- package/dist/plugin-distribution/manager.d.ts +4 -1
- package/dist/plugin-distribution/manager.js +12 -5
- package/dist/plugin-distribution/registry-cache.js +2 -1
- package/dist/plugin-distribution/registry-source-state.js +2 -1
- package/dist/plugin-distribution/schemas.d.ts +26 -8
- package/dist/plugin-distribution/schemas.js +2 -1
- package/dist/plugin-distribution/types.d.ts +4 -0
- package/dist/plugin-manager.d.ts +20 -4
- package/dist/plugin-manager.js +228 -64
- package/dist/plugin-runtime-entry.d.ts +1 -1
- package/dist/plugin-runtime-entry.js +16 -0
- package/dist/plugin.d.ts +18 -2
- package/dist/plugin.js +93 -39
- package/dist/registry-schema/catalog-v1.d.ts +60 -9
- package/dist/registry-schema/catalog-v1.js +2 -1
- package/dist/registry-schema/catalog-v2.d.ts +60 -9
- package/dist/registry-schema/catalog-v2.js +1 -1
- package/dist/search-result-provider.d.ts +95 -0
- package/dist/search-result-provider.js +42 -0
- package/dist/status-bar.svelte.d.ts +26 -20
- package/dist/status-bar.svelte.js +1 -0
- package/dist/storage/app-database.d.ts +6 -2
- package/dist/storage/app-database.js +41 -34
- package/dist/storage/desktop-native.d.ts +5 -3
- package/dist/storage/desktop-native.js +59 -6
- package/dist/storage/directory-watcher.d.ts +27 -0
- package/dist/storage/directory-watcher.js +201 -0
- package/dist/storage/fs.d.ts +14 -27
- package/dist/storage/fs.js +2 -168
- package/dist/storage/index-projection.js +33 -15
- package/dist/storage/memory-vault-adapter.d.ts +2 -0
- package/dist/storage/memory-vault-adapter.js +14 -1
- package/dist/storage/path.d.ts +6 -2
- package/dist/storage/path.js +8 -2
- package/dist/storage/projection-query-candidates.d.ts +3 -0
- package/dist/storage/projection-query-candidates.js +69 -0
- package/dist/storage/remote-app-database.d.ts +2 -2
- package/dist/storage/remote-app-database.js +1 -1
- package/dist/storage/runtime.d.ts +3 -0
- package/dist/storage/runtime.js +3 -0
- package/dist/storage/search-embedding-record.d.ts +3 -0
- package/dist/storage/search-embedding-record.js +18 -0
- package/dist/storage/turso-app-database.d.ts +2 -0
- package/dist/storage/turso-app-database.js +67 -30
- package/dist/storage/vault-listing.d.ts +3 -0
- package/dist/storage/vault-listing.js +36 -0
- package/dist/storage/vault-reconciliation.d.ts +19 -0
- package/dist/storage/vault-reconciliation.js +137 -0
- package/dist/storage/vault-session.d.ts +8 -0
- package/dist/storage/vault-session.js +10 -2
- package/dist/storage/vault.svelte.d.ts +53 -4
- package/dist/storage/vault.svelte.js +223 -87
- package/dist/sync-host.d.ts +12 -3
- package/dist/sync-host.js +5 -1
- package/dist/vault-change-views.d.ts +4 -0
- package/dist/vault-change-views.js +43 -0
- package/dist/vault-changes.d.ts +37 -0
- package/dist/vault-changes.js +228 -0
- package/dist/view.svelte.d.ts +182 -149
- package/dist/view.svelte.js +11 -2
- package/dist/workspace-close-guard.d.ts +3 -0
- package/dist/workspace-close-guard.js +35 -0
- package/dist/workspace-host-internal.js +10 -2
- package/dist/workspace-layout-normalizer.d.ts +4 -2
- package/dist/workspace-layout-normalizer.js +5 -5
- package/dist/workspace-view-document.d.ts +3 -0
- package/dist/workspace-view-document.js +12 -0
- package/dist/workspace.svelte.d.ts +13 -2
- package/dist/workspace.svelte.js +371 -82
- package/package.json +45 -8
|
@@ -1,28 +1,76 @@
|
|
|
1
|
-
|
|
1
|
+
import { FileTextConflictRegistry } from "./file-text-conflicts";
|
|
2
|
+
import { FileEditingNativeIssues } from "./file-editing-native-issues";
|
|
3
|
+
import { NativeEditingRecovery } from "./native-editing-recovery";
|
|
4
|
+
import type { NativeEditingIntents } from "./native-editing-intents";
|
|
5
|
+
import { type NativeEditingSession, type FileEditingRegistration } from "./native-editing-session";
|
|
6
|
+
export type { NativeEditingSession, FileEditingRegistration, } from "./native-editing-session";
|
|
7
|
+
export * from "./file-text-conflicts";
|
|
8
|
+
export * from "./file-text-persistence";
|
|
9
|
+
export type { NativeEditingProblemIssue } from "./file-editing-native-issues";
|
|
10
|
+
/**
|
|
11
|
+
* Independent file writers implement this contract without importing a
|
|
12
|
+
* renderer.
|
|
13
|
+
*/
|
|
2
14
|
export interface FileEditingSession {
|
|
15
|
+
/** Required only when the App selects native editing intents. */
|
|
16
|
+
readonly native?: NativeEditingSession;
|
|
3
17
|
/** Includes queued, in-flight and failed persistence. */
|
|
4
18
|
readonly dirty: boolean;
|
|
5
19
|
flush(): Promise<void>;
|
|
6
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* Block edits before yielding; drain pending saves and return an idempotent
|
|
22
|
+
* release.
|
|
23
|
+
*/
|
|
7
24
|
protect(): Promise<() => void | Promise<void>>;
|
|
8
25
|
}
|
|
9
26
|
/**
|
|
10
|
-
* An App-owned persistence and protection boundary for independent file
|
|
11
|
-
* Hosts must register every writer before selecting this as an apply
|
|
27
|
+
* An App-owned persistence and protection boundary for independent file
|
|
28
|
+
* writers. Hosts must register every writer before selecting this as an apply
|
|
29
|
+
* guard.
|
|
12
30
|
*/
|
|
13
31
|
export declare class FileEditingSessionRegistry {
|
|
14
32
|
#private;
|
|
15
|
-
|
|
33
|
+
private readonly native?;
|
|
34
|
+
readonly textConflicts: FileTextConflictRegistry;
|
|
35
|
+
readonly nativeIssues: FileEditingNativeIssues;
|
|
36
|
+
readonly nativeRecovery: NativeEditingRecovery | undefined;
|
|
37
|
+
constructor(native?: NativeEditingIntents | undefined);
|
|
38
|
+
/** Hosts preflight before disposing the App, preserving failed-leave recovery. */
|
|
39
|
+
preflightNativeRecovery(): Promise<void>;
|
|
40
|
+
get closed(): boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Schedule owned event projection between mutations without acquiring an
|
|
43
|
+
* editing guard. Guard callbacks may schedule this but must not await it.
|
|
44
|
+
*/
|
|
45
|
+
afterMutations<T>(operation: () => Promise<T>): Promise<T>;
|
|
46
|
+
/** Includes retained writers; a failing dirty getter cannot waive protection. */
|
|
47
|
+
hasDirty(path?: string): boolean;
|
|
48
|
+
/** Admit a multi-step file operation before its first asynchronous boundary. */
|
|
49
|
+
track<T>(operation: () => Promise<T>): Promise<T>;
|
|
50
|
+
/**
|
|
51
|
+
* Hosts drain admitted operations before entering their global teardown
|
|
52
|
+
* guard.
|
|
53
|
+
*/
|
|
54
|
+
settleOperations(): Promise<void>;
|
|
55
|
+
register(path: string, session: FileEditingSession): FileEditingRegistration;
|
|
16
56
|
/** Flush the captured sessions; protection is required for mutation admission. */
|
|
17
57
|
flush(path?: string): Promise<{
|
|
18
58
|
flushed: boolean;
|
|
19
59
|
path?: string;
|
|
20
60
|
}>;
|
|
21
61
|
/**
|
|
22
|
-
* Serialize mutations, holding matching sessions protected throughout the
|
|
23
|
-
* Callbacks and session methods must not re-enter queued registry
|
|
62
|
+
* Serialize mutations, holding matching sessions protected throughout the
|
|
63
|
+
* callback. Callbacks and session methods must not re-enter queued registry
|
|
64
|
+
* operations.
|
|
24
65
|
*/
|
|
25
66
|
guard<T>(path: string | undefined, operation: () => Promise<T>): Promise<T>;
|
|
26
|
-
/**
|
|
67
|
+
/** Admit a one-shot writer only after existing document writers settle. */
|
|
68
|
+
mutate(path: string, create: () => FileEditingSession): Promise<void>;
|
|
69
|
+
/** Recover one retained writer while protecting all other writers of its file. */
|
|
70
|
+
recover<T>(session: FileEditingSession, operation: () => Promise<T>): Promise<T>;
|
|
71
|
+
/**
|
|
72
|
+
* Stop admission immediately, drain callbacks and retry failed
|
|
73
|
+
* persistence/cleanup.
|
|
74
|
+
*/
|
|
27
75
|
dispose(): Promise<void>;
|
|
28
76
|
}
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
import { FileTextConflictRegistry } from "./file-text-conflicts";
|
|
2
|
+
import { FileEditingNativeIssues } from "./file-editing-native-issues";
|
|
3
|
+
import { NativeEditingRecovery } from "./native-editing-recovery";
|
|
4
|
+
import { unreportedCleanup } from "./file-editing-failures";
|
|
5
|
+
import { NativeRegisteredEditingSession, } from "./native-editing-session";
|
|
6
|
+
export * from "./file-text-conflicts";
|
|
7
|
+
export * from "./file-text-persistence";
|
|
1
8
|
function filePath(path) {
|
|
2
9
|
if (typeof path !== "string" || /[\\\0]/u.test(path))
|
|
3
10
|
throw new Error("Editing sessions require a vault-relative file path");
|
|
@@ -19,18 +26,99 @@ function failures(results) {
|
|
|
19
26
|
return results.flatMap((result) => result.status === "rejected" ? [result.reason] : []);
|
|
20
27
|
}
|
|
21
28
|
/**
|
|
22
|
-
* An App-owned persistence and protection boundary for independent file
|
|
23
|
-
* Hosts must register every writer before selecting this as an apply
|
|
29
|
+
* An App-owned persistence and protection boundary for independent file
|
|
30
|
+
* writers. Hosts must register every writer before selecting this as an apply
|
|
31
|
+
* guard.
|
|
24
32
|
*/
|
|
25
33
|
export class FileEditingSessionRegistry {
|
|
34
|
+
native;
|
|
35
|
+
textConflicts = new FileTextConflictRegistry();
|
|
36
|
+
nativeIssues = new FileEditingNativeIssues();
|
|
37
|
+
nativeRecovery;
|
|
26
38
|
#sessions = new Map();
|
|
27
39
|
#barriers = new Map();
|
|
28
40
|
#releases = new Set();
|
|
29
41
|
#held = new Set();
|
|
42
|
+
#pendingOperations = new Set();
|
|
30
43
|
#tail = Promise.resolve();
|
|
31
44
|
#closing = false;
|
|
32
45
|
#closed = false;
|
|
33
46
|
#disposal;
|
|
47
|
+
constructor(native) {
|
|
48
|
+
this.native = native;
|
|
49
|
+
this.nativeRecovery = native
|
|
50
|
+
? new NativeEditingRecovery(native, (run) => this.track(run))
|
|
51
|
+
: undefined;
|
|
52
|
+
}
|
|
53
|
+
/** Hosts preflight before disposing the App, preserving failed-leave recovery. */
|
|
54
|
+
async preflightNativeRecovery() {
|
|
55
|
+
await this.settleOperations();
|
|
56
|
+
// Observe already admitted work without retrying a failed transition.
|
|
57
|
+
let tail;
|
|
58
|
+
do {
|
|
59
|
+
tail = this.#tail;
|
|
60
|
+
await tail;
|
|
61
|
+
await Promise.all([...this.#sessions.values()].map((entry) => entry.native?.settle()));
|
|
62
|
+
} while (tail !== this.#tail);
|
|
63
|
+
await this.nativeRecovery?.assertClear();
|
|
64
|
+
if (this.nativeIssues.entries.length)
|
|
65
|
+
throw new Error("Unresolved editing admission requires review in Problems");
|
|
66
|
+
}
|
|
67
|
+
get closed() {
|
|
68
|
+
return this.#closing;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Schedule owned event projection between mutations without acquiring an
|
|
72
|
+
* editing guard. Guard callbacks may schedule this but must not await it.
|
|
73
|
+
*/
|
|
74
|
+
afterMutations(operation) {
|
|
75
|
+
return this.#enqueue(operation);
|
|
76
|
+
}
|
|
77
|
+
/** Includes retained writers; a failing dirty getter cannot waive protection. */
|
|
78
|
+
hasDirty(path) {
|
|
79
|
+
const scope = path === undefined ? undefined : filePath(path);
|
|
80
|
+
return this.#matching(scope).some(({ session }) => {
|
|
81
|
+
try {
|
|
82
|
+
return session.dirty;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
/** Admit a multi-step file operation before its first asynchronous boundary. */
|
|
90
|
+
track(operation) {
|
|
91
|
+
try {
|
|
92
|
+
this.#assertOpen();
|
|
93
|
+
let resolve;
|
|
94
|
+
let reject;
|
|
95
|
+
const pending = new Promise((done, fail) => {
|
|
96
|
+
resolve = done;
|
|
97
|
+
reject = fail;
|
|
98
|
+
});
|
|
99
|
+
this.#pendingOperations.add(pending);
|
|
100
|
+
void pending.then(() => this.#pendingOperations.delete(pending), () => this.#pendingOperations.delete(pending));
|
|
101
|
+
try {
|
|
102
|
+
void operation().then(resolve, reject);
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
reject(error);
|
|
106
|
+
}
|
|
107
|
+
return pending;
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
return Promise.reject(error);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Hosts drain admitted operations before entering their global teardown
|
|
115
|
+
* guard.
|
|
116
|
+
*/
|
|
117
|
+
async settleOperations() {
|
|
118
|
+
this.#assertOpen();
|
|
119
|
+
while (this.#pendingOperations.size)
|
|
120
|
+
await Promise.all([...this.#pendingOperations]);
|
|
121
|
+
}
|
|
34
122
|
register(path, session) {
|
|
35
123
|
this.#assertOpen();
|
|
36
124
|
const capturedPath = filePath(path);
|
|
@@ -46,17 +134,27 @@ export class FileEditingSessionRegistry {
|
|
|
46
134
|
throw new Error("An editing session cannot change its registered file");
|
|
47
135
|
if (!entry) {
|
|
48
136
|
entry = { path: capturedPath, session, references: 0 };
|
|
137
|
+
if (this.native) {
|
|
138
|
+
entry.native = new NativeRegisteredEditingSession(session, this.native, capturedPath, () => this.#closing || entry.references === 0);
|
|
139
|
+
entry.session = entry.native;
|
|
140
|
+
this.#observeNativeIssues(entry);
|
|
141
|
+
}
|
|
49
142
|
this.#sessions.set(session, entry);
|
|
50
143
|
}
|
|
51
144
|
entry.references++;
|
|
52
145
|
let registered = true;
|
|
53
|
-
return () => {
|
|
146
|
+
return Object.assign(() => {
|
|
54
147
|
if (!registered)
|
|
55
148
|
return;
|
|
56
149
|
registered = false;
|
|
57
150
|
entry.references--;
|
|
58
151
|
this.#prune(entry);
|
|
59
|
-
}
|
|
152
|
+
}, {
|
|
153
|
+
native: !!entry.native,
|
|
154
|
+
ready: () => entry.native?.ready() ?? Promise.resolve(),
|
|
155
|
+
changed: () => entry.native?.changed(),
|
|
156
|
+
idle: () => entry.native?.idle() ?? Promise.resolve(),
|
|
157
|
+
});
|
|
60
158
|
}
|
|
61
159
|
/** Flush the captured sessions; protection is required for mutation admission. */
|
|
62
160
|
async flush(path) {
|
|
@@ -78,8 +176,9 @@ export class FileEditingSessionRegistry {
|
|
|
78
176
|
});
|
|
79
177
|
}
|
|
80
178
|
/**
|
|
81
|
-
* Serialize mutations, holding matching sessions protected throughout the
|
|
82
|
-
* Callbacks and session methods must not re-enter queued registry
|
|
179
|
+
* Serialize mutations, holding matching sessions protected throughout the
|
|
180
|
+
* callback. Callbacks and session methods must not re-enter queued registry
|
|
181
|
+
* operations.
|
|
83
182
|
*/
|
|
84
183
|
guard(path, operation) {
|
|
85
184
|
try {
|
|
@@ -91,7 +190,51 @@ export class FileEditingSessionRegistry {
|
|
|
91
190
|
return Promise.reject(error);
|
|
92
191
|
}
|
|
93
192
|
}
|
|
94
|
-
/**
|
|
193
|
+
/** Admit a one-shot writer only after existing document writers settle. */
|
|
194
|
+
mutate(path, create) {
|
|
195
|
+
const scope = filePath(path);
|
|
196
|
+
return this.guard(scope, async () => {
|
|
197
|
+
const session = create();
|
|
198
|
+
if (!session ||
|
|
199
|
+
typeof session.flush !== "function" ||
|
|
200
|
+
typeof session.protect !== "function" ||
|
|
201
|
+
typeof session.dirty !== "boolean")
|
|
202
|
+
throw new Error("Invalid file editing session");
|
|
203
|
+
const entry = { path: scope, session, references: 0 };
|
|
204
|
+
if (this.native) {
|
|
205
|
+
const native = new NativeRegisteredEditingSession(session, this.native, scope, () => true);
|
|
206
|
+
Object.assign(entry, { native, session: native });
|
|
207
|
+
this.#observeNativeIssues(entry);
|
|
208
|
+
}
|
|
209
|
+
this.#sessions.set(session, entry);
|
|
210
|
+
try {
|
|
211
|
+
await entry.session.flush();
|
|
212
|
+
if (session.dirty)
|
|
213
|
+
throw new Error(`File mutation remains dirty: ${scope}`);
|
|
214
|
+
}
|
|
215
|
+
finally {
|
|
216
|
+
this.#prune(entry);
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
/** Recover one retained writer while protecting all other writers of its file. */
|
|
221
|
+
recover(session, operation) {
|
|
222
|
+
const entry = this.#sessions.get(session);
|
|
223
|
+
if (!entry || this.#closed)
|
|
224
|
+
return Promise.reject(new Error("File writer is unavailable"));
|
|
225
|
+
return this.#enqueue(async () => {
|
|
226
|
+
try {
|
|
227
|
+
return await this.#guard(entry.path, operation, true, entry);
|
|
228
|
+
}
|
|
229
|
+
finally {
|
|
230
|
+
this.#prune(entry);
|
|
231
|
+
}
|
|
232
|
+
}, true);
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Stop admission immediately, drain callbacks and retry failed
|
|
236
|
+
* persistence/cleanup.
|
|
237
|
+
*/
|
|
95
238
|
dispose() {
|
|
96
239
|
if (this.#disposal)
|
|
97
240
|
return this.#disposal;
|
|
@@ -101,6 +244,8 @@ export class FileEditingSessionRegistry {
|
|
|
101
244
|
if (this.#closed)
|
|
102
245
|
return;
|
|
103
246
|
await this.#guard(undefined, async () => undefined, true);
|
|
247
|
+
for (const entry of this.#sessions.values())
|
|
248
|
+
entry.stopNativeIssues?.();
|
|
104
249
|
this.#sessions.clear();
|
|
105
250
|
this.#closed = true;
|
|
106
251
|
})();
|
|
@@ -114,9 +259,12 @@ export class FileEditingSessionRegistry {
|
|
|
114
259
|
if (this.#closing)
|
|
115
260
|
throw new Error("File editing-session registry is closed");
|
|
116
261
|
}
|
|
117
|
-
#enqueue(operation) {
|
|
262
|
+
#enqueue(operation, recovery = false) {
|
|
118
263
|
const run = this.#tail.then(() => {
|
|
119
|
-
|
|
264
|
+
if (!recovery)
|
|
265
|
+
this.#assertOpen();
|
|
266
|
+
else if (this.#closed)
|
|
267
|
+
throw new Error("File editing-session registry is closed");
|
|
120
268
|
return operation();
|
|
121
269
|
});
|
|
122
270
|
this.#tail = run.then(() => undefined, () => undefined);
|
|
@@ -129,16 +277,38 @@ export class FileEditingSessionRegistry {
|
|
|
129
277
|
// A detached writer's failed or in-flight persistence remains a barrier.
|
|
130
278
|
if (entry.references ||
|
|
131
279
|
this.#held.has(entry) ||
|
|
132
|
-
this.#sessions.
|
|
280
|
+
![...this.#sessions.values()].includes(entry))
|
|
133
281
|
return;
|
|
134
282
|
try {
|
|
135
|
-
if (
|
|
136
|
-
|
|
283
|
+
if (entry.session.dirty)
|
|
284
|
+
return;
|
|
285
|
+
if (entry.native && !entry.native.retired) {
|
|
286
|
+
if (!entry.retirement) {
|
|
287
|
+
entry.retirement = this.#enqueue(() => this.#guard(entry.path, async () => undefined), this.#closing);
|
|
288
|
+
void entry.retirement.catch(() => {
|
|
289
|
+
entry.retirement = undefined;
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
for (const [session, registered] of this.#sessions)
|
|
295
|
+
if (registered === entry) {
|
|
296
|
+
this.#sessions.delete(session);
|
|
297
|
+
entry.stopNativeIssues?.();
|
|
298
|
+
}
|
|
137
299
|
}
|
|
138
300
|
catch {
|
|
139
301
|
// A failing dirty-state getter must not silently remove a writer.
|
|
140
302
|
}
|
|
141
303
|
}
|
|
304
|
+
#observeNativeIssues(entry) {
|
|
305
|
+
if (!entry.native)
|
|
306
|
+
return;
|
|
307
|
+
entry.stopNativeIssues = this.nativeIssues.register(entry.path, entry.native, (check) => this.track(() => this.#enqueue(async () => {
|
|
308
|
+
check();
|
|
309
|
+
await this.#guard(entry.path, async () => undefined);
|
|
310
|
+
})));
|
|
311
|
+
}
|
|
142
312
|
async #release(pending) {
|
|
143
313
|
const results = await Promise.allSettled(pending.map(async (lease) => {
|
|
144
314
|
await lease.release();
|
|
@@ -151,12 +321,12 @@ export class FileEditingSessionRegistry {
|
|
|
151
321
|
this.#barriers.delete(barrier);
|
|
152
322
|
return failures(results);
|
|
153
323
|
}
|
|
154
|
-
async #guard(path, operation, shutdown = false) {
|
|
324
|
+
async #guard(path, operation, shutdown = false, excluded) {
|
|
155
325
|
// A previous release failure must be repaired before another mutation starts.
|
|
156
326
|
throwFailures(await this.#release([...this.#releases]));
|
|
157
327
|
const barrier = {};
|
|
158
328
|
this.#barriers.set(barrier, path);
|
|
159
|
-
const entries = this.#matching(path);
|
|
329
|
+
const entries = this.#matching(path).filter((entry) => entry !== excluded);
|
|
160
330
|
for (const entry of entries)
|
|
161
331
|
this.#held.add(entry);
|
|
162
332
|
let result;
|
|
@@ -184,7 +354,7 @@ export class FileEditingSessionRegistry {
|
|
|
184
354
|
catch (error) {
|
|
185
355
|
errors.push(error);
|
|
186
356
|
}
|
|
187
|
-
errors.push(...(await this.#release([...this.#releases].filter((lease) => lease.barrier === barrier))));
|
|
357
|
+
errors.push(...unreportedCleanup(await this.#release([...this.#releases].filter((lease) => lease.barrier === barrier)), errors));
|
|
188
358
|
for (const entry of entries) {
|
|
189
359
|
if (![...this.#releases].some((lease) => lease.entry === entry)) {
|
|
190
360
|
this.#held.delete(entry);
|
package/dist/file-manager.d.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import type { App } from "./context.svelte";
|
|
2
2
|
import { TFile } from "./storage/fs";
|
|
3
3
|
import type { DataWriteOptions, TAbstractFile, TFolder } from "./storage/fs";
|
|
4
|
+
import { type FileTextMutationOptions } from "./file-text-mutation";
|
|
5
|
+
export type FrontmatterMutationOptions = FileTextMutationOptions & {
|
|
6
|
+
/** The record shown by a Properties controller before its current edit. */
|
|
7
|
+
expectedFrontmatter?: Record<string, unknown>;
|
|
8
|
+
};
|
|
4
9
|
/**
|
|
5
10
|
* High-level helpers for vault file operations that need metadata-aware
|
|
6
11
|
* follow-up work.
|
|
@@ -31,6 +36,8 @@ export declare class FileManager {
|
|
|
31
36
|
* @public
|
|
32
37
|
*/
|
|
33
38
|
getNewFileParent(sourcePath: string, newFilePath?: string): TFolder;
|
|
39
|
+
/** New-file intent; exclusive-capable hosts never fall back to overwrite. */
|
|
40
|
+
createFile(path: string, data: string | ArrayBuffer, options?: DataWriteOptions): Promise<TFile>;
|
|
34
41
|
getAvailablePathForAttachment(filename: string, sourcePath?: string): string;
|
|
35
42
|
/**
|
|
36
43
|
* Rename or move a file safely, and update all links to it depending on the
|
|
@@ -87,5 +94,5 @@ export declare class FileManager {
|
|
|
87
94
|
* @throws Any errors that your callback function throws
|
|
88
95
|
* @public
|
|
89
96
|
*/
|
|
90
|
-
processFrontMatter(file: TFile, fn: (frontmatter: any) => void, options?:
|
|
97
|
+
processFrontMatter(file: TFile, fn: (frontmatter: any) => void, options?: FrontmatterMutationOptions): Promise<void>;
|
|
91
98
|
}
|
package/dist/file-manager.js
CHANGED
|
@@ -3,6 +3,7 @@ import { TFile } from "./storage/fs";
|
|
|
3
3
|
import { joinPath } from "./storage/path";
|
|
4
4
|
import { defaultLinkSettings, generateInternalLink, } from "./links";
|
|
5
5
|
import { parseLinktext } from "./utils";
|
|
6
|
+
import { mutateFileText, } from "./file-text-mutation";
|
|
6
7
|
const FRONTMATTER_RETRY_ERROR = "__lapis_frontmatter_retry__";
|
|
7
8
|
function linkOffsets(position) {
|
|
8
9
|
if (!position || typeof position !== "object")
|
|
@@ -28,7 +29,9 @@ export class FileManager {
|
|
|
28
29
|
return JSON.parse(JSON.stringify(frontmatter));
|
|
29
30
|
}
|
|
30
31
|
async readFrontmatterSeed(file, content) {
|
|
31
|
-
const cachedFrontmatter =
|
|
32
|
+
const cachedFrontmatter = content !== undefined && this.app.vault.supportsConditionalTextWrites
|
|
33
|
+
? undefined
|
|
34
|
+
: (await this.app.metadataCache.getFileCacheAsync(file))?.frontmatter;
|
|
32
35
|
if (cachedFrontmatter && typeof cachedFrontmatter === "object") {
|
|
33
36
|
return this.cloneFrontmatter(cachedFrontmatter);
|
|
34
37
|
}
|
|
@@ -151,6 +154,18 @@ export class FileManager {
|
|
|
151
154
|
getNewFileParent(sourcePath, newFilePath) {
|
|
152
155
|
return this.app.vault.getRoot();
|
|
153
156
|
}
|
|
157
|
+
/** New-file intent; exclusive-capable hosts never fall back to overwrite. */
|
|
158
|
+
createFile(path, data, options) {
|
|
159
|
+
return this.app.fileEditingSessions.track(async () => {
|
|
160
|
+
await this.app.vault.load();
|
|
161
|
+
if (this.app.vault.adapter.getCapabilities?.()
|
|
162
|
+
.exclusiveCreate === true)
|
|
163
|
+
return this.app.vault.createExclusive(path, data, options);
|
|
164
|
+
return typeof data === "string"
|
|
165
|
+
? this.app.vault.create(path, data, options)
|
|
166
|
+
: this.app.vault.createBinary(path, data, options);
|
|
167
|
+
});
|
|
168
|
+
}
|
|
154
169
|
getAvailablePathForAttachment(filename, sourcePath) {
|
|
155
170
|
const parent = this.getNewFileParent(sourcePath ?? "", filename);
|
|
156
171
|
const extMatch = /(\.[^.]*)$/.exec(filename);
|
|
@@ -210,8 +225,13 @@ export class FileManager {
|
|
|
210
225
|
* @param file
|
|
211
226
|
* @public
|
|
212
227
|
*/
|
|
213
|
-
trashFile(file) {
|
|
214
|
-
|
|
228
|
+
async trashFile(file) {
|
|
229
|
+
const capturedPath = file.path;
|
|
230
|
+
await this.app.fileEditingSessions.guard(undefined, async () => {
|
|
231
|
+
if (file.path !== capturedPath)
|
|
232
|
+
throw new Error("Trash target changed during editor admission");
|
|
233
|
+
await this.app.vault.trash(file, true);
|
|
234
|
+
});
|
|
215
235
|
}
|
|
216
236
|
promptForDeletion(file) {
|
|
217
237
|
return this.trashFile(file);
|
|
@@ -268,6 +288,20 @@ export class FileManager {
|
|
|
268
288
|
* @public
|
|
269
289
|
*/
|
|
270
290
|
processFrontMatter(file, fn, options) {
|
|
291
|
+
if (this.app.vault.supportsConditionalTextWrites) {
|
|
292
|
+
let matches = true;
|
|
293
|
+
return mutateFileText(this.app, file, async (snapshot) => {
|
|
294
|
+
const seed = await this.readFrontmatterSeed(file, snapshot);
|
|
295
|
+
matches =
|
|
296
|
+
options?.expectedFrontmatter === undefined ||
|
|
297
|
+
this.isFrontmatterEqual(seed, options.expectedFrontmatter);
|
|
298
|
+
const next = this.cloneFrontmatter(seed);
|
|
299
|
+
fn(next);
|
|
300
|
+
return this.isFrontmatterEqual(seed, next)
|
|
301
|
+
? snapshot
|
|
302
|
+
: this.writeUpdatedFrontmatter(file, snapshot, next);
|
|
303
|
+
}, { ...options, validateBaseline: () => matches });
|
|
304
|
+
}
|
|
271
305
|
const attempt = async () => {
|
|
272
306
|
const snapshot = await this.app.vault.read(file);
|
|
273
307
|
const seedFrontmatter = await this.readFrontmatterSeed(file, snapshot);
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { App } from "./context.svelte";
|
|
2
|
+
/** Capture the acknowledged state while the owner loads, before edits begin. */
|
|
3
|
+
export declare function readFileState(app: App, path: string, reset?: (text: string) => void | Promise<void>): Promise<string | null>;
|
|
4
|
+
/** Register synchronously so shutdown cannot overtake initial read or write IO. */
|
|
5
|
+
export declare function saveFileState(app: App, path: string, text: string): Promise<void>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { FileTextStateWriter } from "./file-text-state-writer";
|
|
2
|
+
import { TFile } from "./storage/fs";
|
|
3
|
+
import { normalizePath } from "./storage/path";
|
|
4
|
+
const stores = new WeakMap();
|
|
5
|
+
function writer(app, path, baseline, reset = () => { }) {
|
|
6
|
+
path = normalizePath(path).replace(/^\/+/, "");
|
|
7
|
+
let store = stores.get(app);
|
|
8
|
+
if (!store)
|
|
9
|
+
stores.set(app, (store = new Map()));
|
|
10
|
+
let entry = store.get(path);
|
|
11
|
+
if (!entry) {
|
|
12
|
+
const file = app.vault.getFileByPath(path) ??
|
|
13
|
+
new TFile(path, { ctime: 0, mtime: 0, size: 0 }, null);
|
|
14
|
+
entry = new FileTextStateWriter(app, file, baseline, reset, {
|
|
15
|
+
recoverOutsideQueue: true,
|
|
16
|
+
});
|
|
17
|
+
store.set(path, entry);
|
|
18
|
+
}
|
|
19
|
+
return entry;
|
|
20
|
+
}
|
|
21
|
+
/** Capture the acknowledged state while the owner loads, before edits begin. */
|
|
22
|
+
export async function readFileState(app, path, reset) {
|
|
23
|
+
const state = app.vault.supportsConditionalTextWrites
|
|
24
|
+
? writer(app, path, undefined, reset)
|
|
25
|
+
: undefined;
|
|
26
|
+
const adapter = app.vault.adapter;
|
|
27
|
+
const text = (await adapter.exists(path)) ? await adapter.read(path) : null;
|
|
28
|
+
state?.acceptBaseline(text);
|
|
29
|
+
return text;
|
|
30
|
+
}
|
|
31
|
+
/** Register synchronously so shutdown cannot overtake initial read or write IO. */
|
|
32
|
+
export function saveFileState(app, path, text) {
|
|
33
|
+
return writer(app, path).set(text);
|
|
34
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type FileTextResolution = "keep-local" | "use-external";
|
|
2
|
+
/** A captured recovery decision; stale objects cannot resolve newer conflicts. */
|
|
3
|
+
export interface FileTextConflict {
|
|
4
|
+
readonly id: string;
|
|
5
|
+
readonly path: string;
|
|
6
|
+
readonly baseline: string | null;
|
|
7
|
+
readonly local: string;
|
|
8
|
+
/** Undefined means the authoritative disk read failed; null means deletion. */
|
|
9
|
+
readonly external: string | null | undefined;
|
|
10
|
+
readonly readError?: string;
|
|
11
|
+
resolve(choice: FileTextResolution): Promise<void>;
|
|
12
|
+
/** Persist reviewed merge text against this exact disk snapshot. */
|
|
13
|
+
resolveMerged(contents: string): Promise<void>;
|
|
14
|
+
refresh(): Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
/** App-owned text recovery, independent of the lifetime of individual views. */
|
|
17
|
+
export declare class FileTextConflictRegistry {
|
|
18
|
+
#private;
|
|
19
|
+
get entries(): readonly FileTextConflict[];
|
|
20
|
+
subscribe(listener: (entries: readonly FileTextConflict[]) => void): () => void;
|
|
21
|
+
/** @internal Writer-owned publication; hosts consume entries and their actions. */
|
|
22
|
+
publish(conflict: FileTextConflict): void;
|
|
23
|
+
/** @internal Release only after persistence or an explicit discard succeeds. */
|
|
24
|
+
clear(id: string): void;
|
|
25
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** App-owned text recovery, independent of the lifetime of individual views. */
|
|
2
|
+
export class FileTextConflictRegistry {
|
|
3
|
+
#entries = new Map();
|
|
4
|
+
#listeners = new Set();
|
|
5
|
+
get entries() {
|
|
6
|
+
return [...this.#entries.values()];
|
|
7
|
+
}
|
|
8
|
+
subscribe(listener) {
|
|
9
|
+
this.#listeners.add(listener);
|
|
10
|
+
listener(this.entries);
|
|
11
|
+
return () => {
|
|
12
|
+
this.#listeners.delete(listener);
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
/** @internal Writer-owned publication; hosts consume entries and their actions. */
|
|
16
|
+
publish(conflict) {
|
|
17
|
+
this.#entries.set(conflict.id, Object.freeze(conflict));
|
|
18
|
+
this.#notify();
|
|
19
|
+
}
|
|
20
|
+
/** @internal Release only after persistence or an explicit discard succeeds. */
|
|
21
|
+
clear(id) {
|
|
22
|
+
if (this.#entries.delete(id))
|
|
23
|
+
this.#notify();
|
|
24
|
+
}
|
|
25
|
+
#notify() {
|
|
26
|
+
for (const listener of this.#listeners) {
|
|
27
|
+
try {
|
|
28
|
+
listener(this.entries);
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
console.error("Text conflict subscriber failed", error);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { App } from "./context.svelte";
|
|
2
|
+
import type { TFile, DataWriteOptions } from "./storage/fs";
|
|
3
|
+
export type FileTextMutationOptions = DataWriteOptions & {
|
|
4
|
+
/**
|
|
5
|
+
* A presentation-owned snapshot; omit to transform the authoritative latest
|
|
6
|
+
* text.
|
|
7
|
+
*/
|
|
8
|
+
expectedText?: string | null;
|
|
9
|
+
onSaved?(text: string): void;
|
|
10
|
+
onReset?(text: string, expected: string | null): void;
|
|
11
|
+
validateBaseline?(): boolean;
|
|
12
|
+
};
|
|
13
|
+
export declare function mutateFileText(app: App, file: TFile, transform: (text: string) => Promise<string>, options?: FileTextMutationOptions): Promise<void>;
|