@lapis-notes/api 0.1.3 → 0.2.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/CHANGELOG.md +304 -13
- package/README.md +19 -0
- 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 +24 -11
- 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 +23 -3
- package/dist/context.svelte.js +68 -30
- package/dist/diagnostics.d.ts +8 -0
- package/dist/diagnostics.js +6 -0
- package/dist/editor-save-context.d.ts +35 -0
- package/dist/editor-save-context.js +132 -0
- package/dist/editor-view-registry.d.ts +10 -0
- package/dist/editor-view-registry.js +44 -4
- package/dist/editor.svelte.d.ts +19 -1
- package/dist/editor.svelte.js +528 -55
- 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 +76 -0
- package/dist/file-editing-sessions.js +367 -0
- package/dist/file-manager.d.ts +9 -1
- package/dist/file-manager.js +45 -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 +114 -34
- package/dist/generated/plugin-host-modules.schema.generated.js +132 -34
- 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 +6 -1
- package/dist/index.js +4 -1
- package/dist/lapis-extension.d.ts +6 -2
- package/dist/lapis-extension.js +2 -0
- package/dist/metadata-store.d.ts +23 -0
- package/dist/metadata-store.js +1 -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 +21 -5
- 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 +19 -3
- package/dist/plugin.js +110 -44
- 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 +9 -23
- 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-editor-flush.d.ts +6 -0
- package/dist/sync-editor-flush.js +22 -0
- package/dist/sync-host-local.d.ts +15 -0
- package/dist/sync-host-local.js +228 -0
- package/dist/sync-host-security.d.ts +24 -9
- package/dist/sync-host-security.js +47 -14
- package/dist/sync-host.d.ts +33 -4
- package/dist/sync-host.js +160 -10
- package/dist/sync-repository.d.ts +106 -0
- package/dist/sync-repository.js +18 -0
- 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 +39 -21
- 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 +61 -8
package/dist/embed-registry.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { App } from "./context.svelte";
|
|
2
|
+
import type { TFile } from "./storage/fs";
|
|
2
3
|
export type EmbedViewHandle = {
|
|
3
4
|
destroy?: () => void | Promise<void>;
|
|
4
5
|
};
|
|
@@ -7,9 +8,18 @@ export type EmbedView = (props: {
|
|
|
7
8
|
containerEl: HTMLElement;
|
|
8
9
|
state: Record<string, any>;
|
|
9
10
|
}) => void | EmbedViewHandle;
|
|
11
|
+
export type MarkdownEmbedContext = {
|
|
12
|
+
app: App;
|
|
13
|
+
containerEl: HTMLElement;
|
|
14
|
+
file: TFile;
|
|
15
|
+
};
|
|
16
|
+
export type MarkdownEmbedder = (props: MarkdownEmbedContext) => false | EmbedViewHandle;
|
|
10
17
|
export declare class EmbedRegistry {
|
|
18
|
+
#private;
|
|
11
19
|
readonly embedByExtension: Record<string, EmbedView>;
|
|
12
20
|
constructor();
|
|
21
|
+
registerMarkdownEmbedder(embedder: MarkdownEmbedder): () => void;
|
|
22
|
+
renderMarkdownEmbed(props: MarkdownEmbedContext): false | EmbedViewHandle;
|
|
13
23
|
register(extension: string, view: EmbedView): () => void;
|
|
14
24
|
get(extension: string): EmbedView | null;
|
|
15
25
|
}
|
package/dist/embed-registry.js
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
export class EmbedRegistry {
|
|
2
2
|
embedByExtension = {};
|
|
3
|
+
#markdownEmbedders = [];
|
|
3
4
|
constructor() { }
|
|
5
|
+
registerMarkdownEmbedder(embedder) {
|
|
6
|
+
this.#markdownEmbedders = [...this.#markdownEmbedders, embedder];
|
|
7
|
+
return () => {
|
|
8
|
+
this.#markdownEmbedders = this.#markdownEmbedders.filter((candidate) => candidate !== embedder);
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
renderMarkdownEmbed(props) {
|
|
12
|
+
for (const embedder of this.#markdownEmbedders) {
|
|
13
|
+
const handle = embedder(props);
|
|
14
|
+
if (handle !== false)
|
|
15
|
+
return handle;
|
|
16
|
+
}
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
4
19
|
register(extension, view) {
|
|
5
20
|
const normalizedExtension = extension.toLowerCase();
|
|
6
21
|
this.embedByExtension[normalizedExtension] = view;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** Keep primary failures intact; cleanup may only re-report their object refs. */
|
|
2
|
+
export function unreportedCleanup(cleanup, primary) {
|
|
3
|
+
const reported = new Set();
|
|
4
|
+
const remember = (error) => {
|
|
5
|
+
if (error === null || typeof error !== "object" || reported.has(error))
|
|
6
|
+
return;
|
|
7
|
+
reported.add(error);
|
|
8
|
+
if (error instanceof AggregateError)
|
|
9
|
+
error.errors.forEach(remember);
|
|
10
|
+
};
|
|
11
|
+
primary.forEach(remember);
|
|
12
|
+
const retained = (error, ancestors) => {
|
|
13
|
+
if (reported.has(error))
|
|
14
|
+
return [];
|
|
15
|
+
if (!(error instanceof AggregateError) || ancestors.has(error))
|
|
16
|
+
return [error];
|
|
17
|
+
const next = new Set(ancestors).add(error);
|
|
18
|
+
const errors = error.errors.flatMap((child) => retained(child, next));
|
|
19
|
+
if (errors.length === error.errors.length &&
|
|
20
|
+
errors.every((child, index) => child === error.errors[index]))
|
|
21
|
+
return [error];
|
|
22
|
+
return errors.length
|
|
23
|
+
? [
|
|
24
|
+
new AggregateError(errors, error.message, "cause" in error ? { cause: error.cause } : undefined),
|
|
25
|
+
]
|
|
26
|
+
: [];
|
|
27
|
+
};
|
|
28
|
+
return cleanup.flatMap((error) => retained(error, new Set()));
|
|
29
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { NativeEditingIssue, NativeRegisteredEditingSession } from "./native-editing-session";
|
|
2
|
+
export interface NativeEditingProblemIssue extends NativeEditingIssue {
|
|
3
|
+
readonly registration: string;
|
|
4
|
+
}
|
|
5
|
+
type Entry = {
|
|
6
|
+
path: string;
|
|
7
|
+
session: NativeRegisteredEditingSession;
|
|
8
|
+
retry(check: () => void): Promise<void>;
|
|
9
|
+
};
|
|
10
|
+
/** App-local failures remain visible even when native admission never succeeded. */
|
|
11
|
+
export declare class FileEditingNativeIssues {
|
|
12
|
+
#private;
|
|
13
|
+
get entries(): readonly NativeEditingProblemIssue[];
|
|
14
|
+
subscribe(listener: () => void): () => void;
|
|
15
|
+
/** @internal The editing registry owns registration and its guarded retry. */
|
|
16
|
+
register(path: string, session: NativeRegisteredEditingSession, retry: Entry["retry"]): () => void;
|
|
17
|
+
retry(captured: NativeEditingProblemIssue): Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/** App-local failures remain visible even when native admission never succeeded. */
|
|
2
|
+
export class FileEditingNativeIssues {
|
|
3
|
+
#entries = new Map();
|
|
4
|
+
#listeners = new Set();
|
|
5
|
+
get entries() {
|
|
6
|
+
return [...this.#entries].flatMap(([registration, entry]) => {
|
|
7
|
+
const issue = entry.session.issue;
|
|
8
|
+
return issue ? [Object.freeze({ ...issue, registration })] : [];
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
subscribe(listener) {
|
|
12
|
+
this.#listeners.add(listener);
|
|
13
|
+
listener();
|
|
14
|
+
return () => {
|
|
15
|
+
this.#listeners.delete(listener);
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/** @internal The editing registry owns registration and its guarded retry. */
|
|
19
|
+
register(path, session, retry) {
|
|
20
|
+
const registration = crypto.randomUUID();
|
|
21
|
+
this.#entries.set(registration, { path, session, retry });
|
|
22
|
+
const stop = session.subscribe(() => this.#notify());
|
|
23
|
+
this.#notify();
|
|
24
|
+
return () => {
|
|
25
|
+
stop();
|
|
26
|
+
if (this.#entries.delete(registration))
|
|
27
|
+
this.#notify();
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
retry(captured) {
|
|
31
|
+
try {
|
|
32
|
+
const entry = this.#require(captured);
|
|
33
|
+
return entry.retry(() => {
|
|
34
|
+
this.#require(captured);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
return Promise.reject(error);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
#require(captured) {
|
|
42
|
+
const entry = this.#entries.get(captured.registration);
|
|
43
|
+
const issue = entry?.session.issue;
|
|
44
|
+
if (!entry ||
|
|
45
|
+
entry.path !== captured.path ||
|
|
46
|
+
!issue ||
|
|
47
|
+
issue.id !== captured.id ||
|
|
48
|
+
issue.path !== captured.path ||
|
|
49
|
+
issue.phase !== captured.phase ||
|
|
50
|
+
captured.status !== "failed" ||
|
|
51
|
+
issue.status !== "failed")
|
|
52
|
+
throw new Error("Editing admission changed; refresh Problems");
|
|
53
|
+
return entry;
|
|
54
|
+
}
|
|
55
|
+
#notify() {
|
|
56
|
+
for (const listener of this.#listeners) {
|
|
57
|
+
try {
|
|
58
|
+
listener();
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
console.error("Editing admission subscriber failed", error);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
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
|
+
*/
|
|
14
|
+
export interface FileEditingSession {
|
|
15
|
+
/** Required only when the App selects native editing intents. */
|
|
16
|
+
readonly native?: NativeEditingSession;
|
|
17
|
+
/** Includes queued, in-flight and failed persistence. */
|
|
18
|
+
readonly dirty: boolean;
|
|
19
|
+
flush(): Promise<void>;
|
|
20
|
+
/**
|
|
21
|
+
* Block edits before yielding; drain pending saves and return an idempotent
|
|
22
|
+
* release.
|
|
23
|
+
*/
|
|
24
|
+
protect(): Promise<() => void | Promise<void>>;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
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.
|
|
30
|
+
*/
|
|
31
|
+
export declare class FileEditingSessionRegistry {
|
|
32
|
+
#private;
|
|
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;
|
|
56
|
+
/** Flush the captured sessions; protection is required for mutation admission. */
|
|
57
|
+
flush(path?: string): Promise<{
|
|
58
|
+
flushed: boolean;
|
|
59
|
+
path?: string;
|
|
60
|
+
}>;
|
|
61
|
+
/**
|
|
62
|
+
* Serialize mutations, holding matching sessions protected throughout the
|
|
63
|
+
* callback. Callbacks and session methods must not re-enter queued registry
|
|
64
|
+
* operations.
|
|
65
|
+
*/
|
|
66
|
+
guard<T>(path: string | undefined, operation: () => Promise<T>): Promise<T>;
|
|
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
|
+
*/
|
|
75
|
+
dispose(): Promise<void>;
|
|
76
|
+
}
|
|
@@ -0,0 +1,367 @@
|
|
|
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";
|
|
8
|
+
function filePath(path) {
|
|
9
|
+
if (typeof path !== "string" || /[\\\0]/u.test(path))
|
|
10
|
+
throw new Error("Editing sessions require a vault-relative file path");
|
|
11
|
+
const parts = path
|
|
12
|
+
.replace(/^\/+/, "")
|
|
13
|
+
.split("/")
|
|
14
|
+
.filter((part) => part !== "." && part !== "");
|
|
15
|
+
if (!parts.length || parts.some((part) => part === ".."))
|
|
16
|
+
throw new Error("Editing sessions require a vault-relative file path");
|
|
17
|
+
return parts.join("/");
|
|
18
|
+
}
|
|
19
|
+
function throwFailures(errors) {
|
|
20
|
+
if (errors.length === 1)
|
|
21
|
+
throw errors[0];
|
|
22
|
+
if (errors.length)
|
|
23
|
+
throw new AggregateError(errors, "File editing-session cleanup failed");
|
|
24
|
+
}
|
|
25
|
+
function failures(results) {
|
|
26
|
+
return results.flatMap((result) => result.status === "rejected" ? [result.reason] : []);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
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.
|
|
32
|
+
*/
|
|
33
|
+
export class FileEditingSessionRegistry {
|
|
34
|
+
native;
|
|
35
|
+
textConflicts = new FileTextConflictRegistry();
|
|
36
|
+
nativeIssues = new FileEditingNativeIssues();
|
|
37
|
+
nativeRecovery;
|
|
38
|
+
#sessions = new Map();
|
|
39
|
+
#barriers = new Map();
|
|
40
|
+
#releases = new Set();
|
|
41
|
+
#held = new Set();
|
|
42
|
+
#pendingOperations = new Set();
|
|
43
|
+
#tail = Promise.resolve();
|
|
44
|
+
#closing = false;
|
|
45
|
+
#closed = false;
|
|
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
|
+
}
|
|
122
|
+
register(path, session) {
|
|
123
|
+
this.#assertOpen();
|
|
124
|
+
const capturedPath = filePath(path);
|
|
125
|
+
if ([...this.#barriers.values()].some((scope) => scope === undefined || scope === capturedPath))
|
|
126
|
+
throw new Error(`File editing is protected: ${capturedPath}`);
|
|
127
|
+
if (!session ||
|
|
128
|
+
typeof session.flush !== "function" ||
|
|
129
|
+
typeof session.protect !== "function" ||
|
|
130
|
+
typeof session.dirty !== "boolean")
|
|
131
|
+
throw new Error("Invalid file editing session");
|
|
132
|
+
let entry = this.#sessions.get(session);
|
|
133
|
+
if (entry && entry.path !== capturedPath)
|
|
134
|
+
throw new Error("An editing session cannot change its registered file");
|
|
135
|
+
if (!entry) {
|
|
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
|
+
}
|
|
142
|
+
this.#sessions.set(session, entry);
|
|
143
|
+
}
|
|
144
|
+
entry.references++;
|
|
145
|
+
let registered = true;
|
|
146
|
+
return Object.assign(() => {
|
|
147
|
+
if (!registered)
|
|
148
|
+
return;
|
|
149
|
+
registered = false;
|
|
150
|
+
entry.references--;
|
|
151
|
+
this.#prune(entry);
|
|
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
|
+
});
|
|
158
|
+
}
|
|
159
|
+
/** Flush the captured sessions; protection is required for mutation admission. */
|
|
160
|
+
async flush(path) {
|
|
161
|
+
this.#assertOpen();
|
|
162
|
+
const scope = path === undefined ? undefined : filePath(path);
|
|
163
|
+
return this.#enqueue(async () => {
|
|
164
|
+
const entries = this.#matching(scope);
|
|
165
|
+
const results = await Promise.allSettled(entries.map(async (entry) => {
|
|
166
|
+
await entry.session.flush();
|
|
167
|
+
if (entry.session.dirty)
|
|
168
|
+
throw new Error(`File editing session remains dirty: ${entry.path}`);
|
|
169
|
+
this.#prune(entry);
|
|
170
|
+
}));
|
|
171
|
+
throwFailures(failures(results));
|
|
172
|
+
return {
|
|
173
|
+
flushed: entries.length > 0,
|
|
174
|
+
...(scope === undefined ? {} : { path: scope }),
|
|
175
|
+
};
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Serialize mutations, holding matching sessions protected throughout the
|
|
180
|
+
* callback. Callbacks and session methods must not re-enter queued registry
|
|
181
|
+
* operations.
|
|
182
|
+
*/
|
|
183
|
+
guard(path, operation) {
|
|
184
|
+
try {
|
|
185
|
+
this.#assertOpen();
|
|
186
|
+
const scope = path === undefined ? undefined : filePath(path);
|
|
187
|
+
return this.#enqueue(() => this.#guard(scope, operation));
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
return Promise.reject(error);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
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
|
+
*/
|
|
238
|
+
dispose() {
|
|
239
|
+
if (this.#disposal)
|
|
240
|
+
return this.#disposal;
|
|
241
|
+
this.#closing = true;
|
|
242
|
+
this.#disposal = (async () => {
|
|
243
|
+
await this.#tail;
|
|
244
|
+
if (this.#closed)
|
|
245
|
+
return;
|
|
246
|
+
await this.#guard(undefined, async () => undefined, true);
|
|
247
|
+
for (const entry of this.#sessions.values())
|
|
248
|
+
entry.stopNativeIssues?.();
|
|
249
|
+
this.#sessions.clear();
|
|
250
|
+
this.#closed = true;
|
|
251
|
+
})();
|
|
252
|
+
// Keep closure permanent, but allow explicitly requested cleanup retries.
|
|
253
|
+
void this.#disposal.catch(() => {
|
|
254
|
+
this.#disposal = undefined;
|
|
255
|
+
});
|
|
256
|
+
return this.#disposal;
|
|
257
|
+
}
|
|
258
|
+
#assertOpen() {
|
|
259
|
+
if (this.#closing)
|
|
260
|
+
throw new Error("File editing-session registry is closed");
|
|
261
|
+
}
|
|
262
|
+
#enqueue(operation, recovery = false) {
|
|
263
|
+
const run = this.#tail.then(() => {
|
|
264
|
+
if (!recovery)
|
|
265
|
+
this.#assertOpen();
|
|
266
|
+
else if (this.#closed)
|
|
267
|
+
throw new Error("File editing-session registry is closed");
|
|
268
|
+
return operation();
|
|
269
|
+
});
|
|
270
|
+
this.#tail = run.then(() => undefined, () => undefined);
|
|
271
|
+
return run;
|
|
272
|
+
}
|
|
273
|
+
#matching(path) {
|
|
274
|
+
return [...this.#sessions.values()].filter((entry) => path === undefined || entry.path === path);
|
|
275
|
+
}
|
|
276
|
+
#prune(entry) {
|
|
277
|
+
// A detached writer's failed or in-flight persistence remains a barrier.
|
|
278
|
+
if (entry.references ||
|
|
279
|
+
this.#held.has(entry) ||
|
|
280
|
+
![...this.#sessions.values()].includes(entry))
|
|
281
|
+
return;
|
|
282
|
+
try {
|
|
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
|
+
}
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
// A failing dirty-state getter must not silently remove a writer.
|
|
302
|
+
}
|
|
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
|
+
}
|
|
312
|
+
async #release(pending) {
|
|
313
|
+
const results = await Promise.allSettled(pending.map(async (lease) => {
|
|
314
|
+
await lease.release();
|
|
315
|
+
this.#releases.delete(lease);
|
|
316
|
+
this.#held.delete(lease.entry);
|
|
317
|
+
this.#prune(lease.entry);
|
|
318
|
+
}));
|
|
319
|
+
for (const barrier of this.#barriers.keys())
|
|
320
|
+
if (![...this.#releases].some((lease) => lease.barrier === barrier))
|
|
321
|
+
this.#barriers.delete(barrier);
|
|
322
|
+
return failures(results);
|
|
323
|
+
}
|
|
324
|
+
async #guard(path, operation, shutdown = false, excluded) {
|
|
325
|
+
// A previous release failure must be repaired before another mutation starts.
|
|
326
|
+
throwFailures(await this.#release([...this.#releases]));
|
|
327
|
+
const barrier = {};
|
|
328
|
+
this.#barriers.set(barrier, path);
|
|
329
|
+
const entries = this.#matching(path).filter((entry) => entry !== excluded);
|
|
330
|
+
for (const entry of entries)
|
|
331
|
+
this.#held.add(entry);
|
|
332
|
+
let result;
|
|
333
|
+
const errors = [];
|
|
334
|
+
try {
|
|
335
|
+
const protectedEntries = [];
|
|
336
|
+
const admission = await Promise.allSettled(entries.map(async (entry) => {
|
|
337
|
+
const release = await entry.session.protect();
|
|
338
|
+
if (typeof release !== "function")
|
|
339
|
+
throw new Error("Invalid editing-session protection release");
|
|
340
|
+
this.#releases.add({ barrier, entry, release });
|
|
341
|
+
protectedEntries.push(entry);
|
|
342
|
+
}));
|
|
343
|
+
const saved = await Promise.allSettled(protectedEntries.map(async (entry) => {
|
|
344
|
+
await entry.session.flush();
|
|
345
|
+
if (entry.session.dirty)
|
|
346
|
+
throw new Error(`File editing session remains dirty: ${entry.path}`);
|
|
347
|
+
this.#prune(entry);
|
|
348
|
+
}));
|
|
349
|
+
throwFailures([...failures(admission), ...failures(saved)]);
|
|
350
|
+
if (!shutdown)
|
|
351
|
+
this.#assertOpen();
|
|
352
|
+
result = await operation();
|
|
353
|
+
}
|
|
354
|
+
catch (error) {
|
|
355
|
+
errors.push(error);
|
|
356
|
+
}
|
|
357
|
+
errors.push(...unreportedCleanup(await this.#release([...this.#releases].filter((lease) => lease.barrier === barrier)), errors));
|
|
358
|
+
for (const entry of entries) {
|
|
359
|
+
if (![...this.#releases].some((lease) => lease.entry === entry)) {
|
|
360
|
+
this.#held.delete(entry);
|
|
361
|
+
this.#prune(entry);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
throwFailures(errors);
|
|
365
|
+
return result;
|
|
366
|
+
}
|
|
367
|
+
}
|
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
|
|
@@ -41,6 +48,7 @@ export declare class FileManager {
|
|
|
41
48
|
* @public
|
|
42
49
|
*/
|
|
43
50
|
renameFile(file: TAbstractFile, newPath: string): Promise<void>;
|
|
51
|
+
private renameProtectedFile;
|
|
44
52
|
/**
|
|
45
53
|
* Remove a file or a folder from the vault according the user's preferred
|
|
46
54
|
* 'trash' options (either moving the file to .trash/ or the OS trash bin).
|
|
@@ -86,5 +94,5 @@ export declare class FileManager {
|
|
|
86
94
|
* @throws Any errors that your callback function throws
|
|
87
95
|
* @public
|
|
88
96
|
*/
|
|
89
|
-
processFrontMatter(file: TFile, fn: (frontmatter: any) => void, options?:
|
|
97
|
+
processFrontMatter(file: TFile, fn: (frontmatter: any) => void, options?: FrontmatterMutationOptions): Promise<void>;
|
|
90
98
|
}
|