@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.
Files changed (188) hide show
  1. package/CHANGELOG.md +206 -0
  2. package/README.md +21 -2
  3. package/dist/__tests__/fixtures/indexed-mutations.json +6862 -0
  4. package/dist/__tests__/fixtures/memory-storage.json +309 -0
  5. package/dist/__tests__/fixtures/native-facade-contract.json +3224 -0
  6. package/dist/__tests__/fixtures/projection-query.json +195 -0
  7. package/dist/__tests__/fixtures/projection-values.json +60 -0
  8. package/dist/__tests__/fixtures/search-preparation.json +1513 -0
  9. package/dist/__tests__/fixtures/search-retrieval.json +15409 -0
  10. package/dist/__tests__/fixtures/search-semantic-model.json +3691 -0
  11. package/dist/__tests__/fixtures/search-semantic-retrieval.json +6603 -0
  12. package/dist/__tests__/fixtures/search-storage.json +409 -0
  13. package/dist/__tests__/fixtures/task-projection.json +428 -0
  14. package/dist/__tests__/indexed-mutation-support.js +262 -0
  15. package/dist/__tests__/projection-query-support.js +40 -0
  16. package/dist/__tests__/search-retrieval-support.js +47 -0
  17. package/dist/app-database-host.d.ts +1 -0
  18. package/dist/app-shell-plugin-persistence.d.ts +3 -2
  19. package/dist/app-shell-plugin-persistence.js +37 -3
  20. package/dist/cache.svelte.d.ts +8 -1
  21. package/dist/cache.svelte.js +103 -5
  22. package/dist/command.svelte.js +29 -2
  23. package/dist/components/editor/editor.css +4 -0
  24. package/dist/components/editor/editor.svelte +21 -9
  25. package/dist/components/editor/embedded-editor-surface.js +3 -1
  26. package/dist/components/editor/inline-title-alignment.d.ts +3 -0
  27. package/dist/components/editor/inline-title-alignment.js +57 -0
  28. package/dist/configuration.svelte.d.ts +270 -206
  29. package/dist/configuration.svelte.js +161 -86
  30. package/dist/context.svelte.d.ts +21 -3
  31. package/dist/context.svelte.js +63 -20
  32. package/dist/diagnostics.d.ts +8 -0
  33. package/dist/diagnostics.js +6 -0
  34. package/dist/editor-save-context.d.ts +12 -1
  35. package/dist/editor-save-context.js +32 -1
  36. package/dist/editor-view-registry.d.ts +10 -0
  37. package/dist/editor-view-registry.js +44 -4
  38. package/dist/editor.svelte.js +128 -37
  39. package/dist/embed-registry.d.ts +10 -0
  40. package/dist/embed-registry.js +15 -0
  41. package/dist/file-editing-failures.d.ts +2 -0
  42. package/dist/file-editing-failures.js +29 -0
  43. package/dist/file-editing-native-issues.d.ts +19 -0
  44. package/dist/file-editing-native-issues.js +65 -0
  45. package/dist/file-editing-sessions.d.ts +56 -8
  46. package/dist/file-editing-sessions.js +185 -15
  47. package/dist/file-manager.d.ts +8 -1
  48. package/dist/file-manager.js +37 -3
  49. package/dist/file-state-store.d.ts +5 -0
  50. package/dist/file-state-store.js +34 -0
  51. package/dist/file-text-conflicts.d.ts +25 -0
  52. package/dist/file-text-conflicts.js +35 -0
  53. package/dist/file-text-mutation.d.ts +13 -0
  54. package/dist/file-text-mutation.js +100 -0
  55. package/dist/file-text-persistence.d.ts +36 -0
  56. package/dist/file-text-persistence.js +234 -0
  57. package/dist/file-text-state-writer.d.ts +34 -0
  58. package/dist/file-text-state-writer.js +183 -0
  59. package/dist/files-configuration.d.ts +20 -0
  60. package/dist/files-configuration.js +47 -0
  61. package/dist/generated/plugin-host-modules.schema.generated.d.ts +87 -37
  62. package/dist/generated/plugin-host-modules.schema.generated.js +102 -37
  63. package/dist/glob.d.ts +3 -0
  64. package/dist/glob.js +2 -1
  65. package/dist/icons/changes.d.ts +3 -0
  66. package/dist/icons/changes.js +16 -0
  67. package/dist/icons/index.d.ts +5 -1
  68. package/dist/icons/index.js +44 -3
  69. package/dist/icons/registry.d.ts +35 -0
  70. package/dist/icons/registry.js +154 -0
  71. package/dist/icons/safe-svg.d.ts +2 -0
  72. package/dist/icons/safe-svg.js +44 -0
  73. package/dist/index.d.ts +5 -1
  74. package/dist/index.js +3 -1
  75. package/dist/lapis-extension.d.ts +6 -2
  76. package/dist/lapis-extension.js +2 -0
  77. package/dist/metadata.svelte.js +45 -2
  78. package/dist/native-background-indexing.d.ts +48 -0
  79. package/dist/native-background-indexing.js +208 -0
  80. package/dist/native-database-protocol.d.ts +29 -0
  81. package/dist/native-database-protocol.js +1 -0
  82. package/dist/native-editing-intent-lease.d.ts +17 -0
  83. package/dist/native-editing-intent-lease.js +188 -0
  84. package/dist/native-editing-intents-protocol.d.ts +70 -0
  85. package/dist/native-editing-intents-protocol.js +1 -0
  86. package/dist/native-editing-intents.d.ts +8 -0
  87. package/dist/native-editing-intents.js +1 -0
  88. package/dist/native-editing-problems.d.ts +3 -0
  89. package/dist/native-editing-problems.js +180 -0
  90. package/dist/native-editing-recovery.d.ts +24 -0
  91. package/dist/native-editing-recovery.js +198 -0
  92. package/dist/native-editing-session.d.ts +51 -0
  93. package/dist/native-editing-session.js +251 -0
  94. package/dist/native-history-index-protocol.d.ts +80 -0
  95. package/dist/native-history-index-protocol.js +1 -0
  96. package/dist/native-history-index.d.ts +6 -0
  97. package/dist/native-history-index.js +1 -0
  98. package/dist/native-indexing-protocol.d.ts +63 -0
  99. package/dist/native-indexing-protocol.js +1 -0
  100. package/dist/native-participant-protocol.d.ts +67 -0
  101. package/dist/native-participant-protocol.js +3 -0
  102. package/dist/native-queue-protocol.d.ts +73 -0
  103. package/dist/native-queue-protocol.js +3 -0
  104. package/dist/native-tasks-protocol.d.ts +264 -0
  105. package/dist/native-tasks-protocol.js +1 -0
  106. package/dist/native-tasks.d.ts +35 -0
  107. package/dist/native-tasks.js +79 -0
  108. package/dist/native-turn-protocol.d.ts +81 -0
  109. package/dist/native-turn-protocol.js +1 -0
  110. package/dist/native-turns.d.ts +29 -0
  111. package/dist/native-turns.js +2 -0
  112. package/dist/plugin-asset-server.js +3 -2
  113. package/dist/plugin-dependency-resolver.d.ts +1 -1
  114. package/dist/plugin-distribution/compatibility.d.ts +1 -1
  115. package/dist/plugin-distribution/compatibility.js +10 -5
  116. package/dist/plugin-distribution/installed-plugin-state.js +2 -1
  117. package/dist/plugin-distribution/installer.d.ts +6 -0
  118. package/dist/plugin-distribution/installer.js +90 -37
  119. package/dist/plugin-distribution/manager.d.ts +4 -1
  120. package/dist/plugin-distribution/manager.js +12 -5
  121. package/dist/plugin-distribution/registry-cache.js +2 -1
  122. package/dist/plugin-distribution/registry-source-state.js +2 -1
  123. package/dist/plugin-distribution/schemas.d.ts +26 -8
  124. package/dist/plugin-distribution/schemas.js +2 -1
  125. package/dist/plugin-distribution/types.d.ts +4 -0
  126. package/dist/plugin-manager.d.ts +20 -4
  127. package/dist/plugin-manager.js +228 -64
  128. package/dist/plugin-runtime-entry.d.ts +1 -1
  129. package/dist/plugin-runtime-entry.js +16 -0
  130. package/dist/plugin.d.ts +18 -2
  131. package/dist/plugin.js +93 -39
  132. package/dist/registry-schema/catalog-v1.d.ts +60 -9
  133. package/dist/registry-schema/catalog-v1.js +2 -1
  134. package/dist/registry-schema/catalog-v2.d.ts +60 -9
  135. package/dist/registry-schema/catalog-v2.js +1 -1
  136. package/dist/search-result-provider.d.ts +95 -0
  137. package/dist/search-result-provider.js +42 -0
  138. package/dist/status-bar.svelte.d.ts +26 -20
  139. package/dist/status-bar.svelte.js +1 -0
  140. package/dist/storage/app-database.d.ts +6 -2
  141. package/dist/storage/app-database.js +41 -34
  142. package/dist/storage/desktop-native.d.ts +5 -3
  143. package/dist/storage/desktop-native.js +59 -6
  144. package/dist/storage/directory-watcher.d.ts +27 -0
  145. package/dist/storage/directory-watcher.js +201 -0
  146. package/dist/storage/fs.d.ts +14 -27
  147. package/dist/storage/fs.js +2 -168
  148. package/dist/storage/index-projection.js +33 -15
  149. package/dist/storage/memory-vault-adapter.d.ts +2 -0
  150. package/dist/storage/memory-vault-adapter.js +14 -1
  151. package/dist/storage/path.d.ts +6 -2
  152. package/dist/storage/path.js +8 -2
  153. package/dist/storage/projection-query-candidates.d.ts +3 -0
  154. package/dist/storage/projection-query-candidates.js +69 -0
  155. package/dist/storage/remote-app-database.d.ts +2 -2
  156. package/dist/storage/remote-app-database.js +1 -1
  157. package/dist/storage/runtime.d.ts +3 -0
  158. package/dist/storage/runtime.js +3 -0
  159. package/dist/storage/search-embedding-record.d.ts +3 -0
  160. package/dist/storage/search-embedding-record.js +18 -0
  161. package/dist/storage/turso-app-database.d.ts +2 -0
  162. package/dist/storage/turso-app-database.js +67 -30
  163. package/dist/storage/vault-listing.d.ts +3 -0
  164. package/dist/storage/vault-listing.js +36 -0
  165. package/dist/storage/vault-reconciliation.d.ts +19 -0
  166. package/dist/storage/vault-reconciliation.js +137 -0
  167. package/dist/storage/vault-session.d.ts +8 -0
  168. package/dist/storage/vault-session.js +10 -2
  169. package/dist/storage/vault.svelte.d.ts +53 -4
  170. package/dist/storage/vault.svelte.js +223 -87
  171. package/dist/sync-host.d.ts +12 -3
  172. package/dist/sync-host.js +5 -1
  173. package/dist/vault-change-views.d.ts +4 -0
  174. package/dist/vault-change-views.js +43 -0
  175. package/dist/vault-changes.d.ts +37 -0
  176. package/dist/vault-changes.js +228 -0
  177. package/dist/view.svelte.d.ts +182 -149
  178. package/dist/view.svelte.js +11 -2
  179. package/dist/workspace-close-guard.d.ts +3 -0
  180. package/dist/workspace-close-guard.js +35 -0
  181. package/dist/workspace-host-internal.js +10 -2
  182. package/dist/workspace-layout-normalizer.d.ts +4 -2
  183. package/dist/workspace-layout-normalizer.js +5 -5
  184. package/dist/workspace-view-document.d.ts +3 -0
  185. package/dist/workspace-view-document.js +12 -0
  186. package/dist/workspace.svelte.d.ts +13 -2
  187. package/dist/workspace.svelte.js +371 -82
  188. package/package.json +45 -8
@@ -1,28 +1,76 @@
1
- /** Independent file writers implement this contract without importing a renderer. */
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
- /** Block edits before yielding; drain pending saves and return an idempotent release. */
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 writers.
11
- * Hosts must register every writer before selecting this as an apply guard.
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
- register(path: string, session: FileEditingSession): () => void;
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 callback.
23
- * Callbacks and session methods must not re-enter queued registry operations.
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
- /** Stop admission immediately, drain callbacks and retry failed persistence/cleanup. */
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 writers.
23
- * Hosts must register every writer before selecting this as an apply guard.
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 callback.
82
- * Callbacks and session methods must not re-enter queued registry operations.
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
- /** Stop admission immediately, drain callbacks and retry failed persistence/cleanup. */
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
- this.#assertOpen();
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.get(entry.session) !== entry)
280
+ ![...this.#sessions.values()].includes(entry))
133
281
  return;
134
282
  try {
135
- if (!entry.session.dirty)
136
- this.#sessions.delete(entry.session);
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);
@@ -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?: DataWriteOptions): Promise<void>;
97
+ processFrontMatter(file: TFile, fn: (frontmatter: any) => void, options?: FrontmatterMutationOptions): Promise<void>;
91
98
  }
@@ -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 = (await this.app.metadataCache.getFileCacheAsync(file))?.frontmatter;
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
- return this.app.vault.trash(file, true);
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>;