@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,7 +1,10 @@
1
+ import { projectVaultTree, scanVaultTree } from "./vault-reconciliation";
2
+ import { DEFAULT_APP_STATE_DIRECTORY } from "./path";
1
3
  import { EventDispatcher } from "../events";
2
4
  import { hasEditorAssociationGlobMagic, matchesEditorAssociationGlob, normalizeEditorAssociationGlob, validateEditorAssociationGlob, } from "../glob";
3
5
  import { FileCache, TFile, TFolder, } from "./fs";
4
6
  import { dirname, joinPath, normalizePath } from "./path";
7
+ import { createVaultListingFilter, DEFAULT_VAULT_EXCLUDES, } from "./vault-listing";
5
8
  function Err(name) {
6
9
  return class extends Error {
7
10
  code = "";
@@ -28,13 +31,37 @@ export const ENOTDIR = Err("ENOTDIR");
28
31
  */
29
32
  export class Vault extends EventDispatcher {
30
33
  adapter;
34
+ operations;
31
35
  files = {};
32
36
  filesByName = new Map();
33
- configDir = ".obsidian";
37
+ mutationTail = Promise.resolve();
38
+ mutate(operation) {
39
+ const enqueue = () => {
40
+ const pending = this.mutationTail.then(async () => {
41
+ await this.load();
42
+ return operation();
43
+ });
44
+ this.mutationTail = pending.then(() => { }, () => { });
45
+ return pending;
46
+ };
47
+ return this.operations ? this.operations.track(enqueue) : enqueue();
48
+ }
49
+ mutateFile(file, operation) {
50
+ const path = file.path;
51
+ return this.mutate(async () => {
52
+ if (file.path !== path)
53
+ throw new Error("Mutation target changed during admission");
54
+ return operation();
55
+ });
56
+ }
57
+ configDir = DEFAULT_APP_STATE_DIRECTORY;
34
58
  cache = new FileCache(1024);
35
- constructor(adapter) {
59
+ listingFilter;
60
+ constructor(adapter, operations, options = {}) {
36
61
  super();
37
62
  this.adapter = adapter;
63
+ this.operations = operations;
64
+ this.listingFilter = createVaultListingFilter(options.listingExclusions ?? (() => DEFAULT_VAULT_EXCLUDES));
38
65
  this.load();
39
66
  }
40
67
  getPath(path) {
@@ -53,7 +80,14 @@ export class Vault extends EventDispatcher {
53
80
  return this.adapter.stat(this.getPath(path));
54
81
  }
55
82
  list(path = "/") {
56
- return this.adapter.list(this.getPath(path));
83
+ const base = this.getPath(path);
84
+ const included = this.listingFilter();
85
+ if (!included(base))
86
+ return Promise.resolve({ files: [], folders: [] });
87
+ return this.adapter.list(base).then(({ files, folders }) => ({
88
+ files: files.filter((entry) => included(joinPath(base, entry))),
89
+ folders: folders.filter((entry) => included(joinPath(base, entry))),
90
+ }));
57
91
  }
58
92
  #loaded = null;
59
93
  load() {
@@ -67,14 +101,54 @@ export class Vault extends EventDispatcher {
67
101
  return this.#loaded;
68
102
  }
69
103
  reload() {
70
- const files = {};
71
- return this.loadPath("/", files).then(() => {
72
- this.files = files;
73
- this.rebuildFileNameIndex();
104
+ return this.mutate(async () => {
105
+ await this.reconcileNow();
74
106
  this.trigger("load");
75
107
  return this.getRoot();
76
108
  });
77
109
  }
110
+ /** Reconcile a complete disk snapshot without replacing live resources. */
111
+ reconcile(hints = [], current = () => true, context = {}) {
112
+ return this.mutate(() => this.reconcileNow(hints, current, context));
113
+ }
114
+ async reconcileNow(hints = [], current = () => true, context = {}) {
115
+ await this.load();
116
+ const snapshot = await scanVaultTree(this.adapter, current);
117
+ if (!snapshot || !current())
118
+ return;
119
+ const projected = projectVaultTree(this, this.files, snapshot, hints);
120
+ this.files = projected.files;
121
+ this.rebuildFileNameIndex();
122
+ for (const event of projected.events) {
123
+ if (event.type === "rename")
124
+ this.cache.rename(event.oldPath, event.file.path);
125
+ else
126
+ this.cache.invalidate(event.file.path);
127
+ if (event.type === "rename")
128
+ this.dispatch("rename", event.file, event.oldPath);
129
+ else
130
+ this.dispatch(event.type, event.file);
131
+ this.dispatch("all", event.type, event.file, {
132
+ ...context,
133
+ ...(event.oldPath === undefined ? {} : { oldPath: event.oldPath }),
134
+ });
135
+ }
136
+ }
137
+ recordFile(path, stat) {
138
+ const existing = this.files[path];
139
+ if (existing instanceof TFile) {
140
+ Object.assign(existing.stat, stat);
141
+ return existing;
142
+ }
143
+ const parent = this.files[dirname(path)];
144
+ if (!(parent instanceof TFolder))
145
+ throw new ENOTDIR(dirname(path));
146
+ const file = new TFile(path, stat, parent, this);
147
+ this.files[path] = file;
148
+ this.addToFileNameIndex(file);
149
+ parent.children.push(file);
150
+ return file;
151
+ }
78
152
  addToFileNameIndex(file) {
79
153
  const paths = this.filesByName.get(file.name) ?? new Set();
80
154
  paths.add(file.path);
@@ -136,10 +210,14 @@ export class Vault extends EventDispatcher {
136
210
  * @public
137
211
  */
138
212
  create(path, data, options) {
213
+ const run = () => this.createNow(path, data, options);
214
+ return this.mutate(run);
215
+ }
216
+ createNow(path, data, options) {
139
217
  path = this.getPath(path);
140
218
  const file = this.getFileByPath(path);
141
219
  if (file) {
142
- return this.modify(file, data, options).then(() => file);
220
+ return this.modifyNow(file, data, options).then(() => file);
143
221
  }
144
222
  return this.adapter
145
223
  .write(path, data, options)
@@ -148,16 +226,51 @@ export class Vault extends EventDispatcher {
148
226
  if (!stat) {
149
227
  return Promise.reject(`Unable to write file: ${path}`);
150
228
  }
151
- let parent = this.files[dirname(path)];
229
+ const file = this.recordFile(path, stat);
230
+ this.cache.put(path, data);
231
+ this.dispatch("create", file);
232
+ this.dispatch("all", "create", file, {});
233
+ return file;
234
+ });
235
+ }
236
+ get supportsExclusiveCreate() {
237
+ const adapter = this.adapter;
238
+ return (adapter.getCapabilities?.().exclusiveCreate === true &&
239
+ typeof adapter.writeExclusive === "function");
240
+ }
241
+ /** Create new bytes without replacing a cached or newly arriving file. */
242
+ createExclusive(path, data, options) {
243
+ if (!this.supportsExclusiveCreate)
244
+ return Promise.reject(Object.assign(new Error("Exclusive file creation is unavailable"), {
245
+ code: "ENOTSUP",
246
+ }));
247
+ const run = async () => {
248
+ await this.load();
249
+ path = this.getPath(path);
250
+ if (this.files[path])
251
+ throw Object.assign(new Error(`A file already exists at ${path}`), {
252
+ code: "EEXIST",
253
+ });
254
+ const parent = this.files[dirname(path)];
255
+ if (!(parent instanceof TFolder))
256
+ throw new ENOTDIR(dirname(path));
257
+ await this.adapter.writeExclusive(path, data, options);
258
+ const stat = await this.adapter.stat(path);
259
+ if (!stat || stat.type !== "file")
260
+ throw new Error(`Unable to create file: ${path}`);
152
261
  const file = new TFile(path, stat, parent, this);
153
262
  this.files[path] = file;
154
263
  this.addToFileNameIndex(file);
155
- this.cache.put(path, data);
264
+ if (typeof data === "string")
265
+ this.cache.put(path, data);
266
+ else
267
+ this.cache.invalidate(path);
156
268
  parent.children.push(file);
157
269
  this.dispatch("create", file);
158
270
  this.dispatch("all", "create", file, {});
159
271
  return file;
160
- });
272
+ };
273
+ return this.mutate(run);
161
274
  }
162
275
  /**
163
276
  * Create a new binary file inside the vault.
@@ -169,6 +282,10 @@ export class Vault extends EventDispatcher {
169
282
  * @public
170
283
  */
171
284
  createBinary(path, data, options) {
285
+ const run = () => this.createBinaryNow(path, data, options);
286
+ return this.mutate(run);
287
+ }
288
+ createBinaryNow(path, data, options) {
172
289
  path = this.getPath(path);
173
290
  return this.adapter
174
291
  .writeBinary(path, data, options)
@@ -177,11 +294,8 @@ export class Vault extends EventDispatcher {
177
294
  if (!stat) {
178
295
  return Promise.reject(`Unable to write file: ${path}`);
179
296
  }
180
- let parent = this.files[dirname(path)];
181
- const file = new TFile(path, stat, parent, this);
182
- this.files[path] = file;
183
- this.addToFileNameIndex(file);
184
- parent.children.push(file);
297
+ const file = this.recordFile(path, stat);
298
+ this.cache.invalidate(path);
185
299
  this.dispatch("create", file);
186
300
  this.dispatch("all", "create", file, {});
187
301
  return file;
@@ -194,7 +308,11 @@ export class Vault extends EventDispatcher {
194
308
  * @throws Error if folder already exists
195
309
  * @public
196
310
  */
197
- async createFolder(path) {
311
+ createFolder(path) {
312
+ const run = () => this.createFolderNow(path);
313
+ return this.mutate(run);
314
+ }
315
+ async createFolderNow(path) {
198
316
  path = this.getPath(path);
199
317
  try {
200
318
  const folder = await this.adapter.mkdir(path).then(() => {
@@ -261,6 +379,9 @@ export class Vault extends EventDispatcher {
261
379
  * @public
262
380
  */
263
381
  delete(file, force, context = {}) {
382
+ return this.mutateFile(file, () => this.deleteNow(file, force, context));
383
+ }
384
+ deleteNow(file, force, context = {}) {
264
385
  const remove = file instanceof TFolder
265
386
  ? this.adapter.rmdir(file.path, force ?? true)
266
387
  : this.adapter.remove(file.path);
@@ -286,18 +407,16 @@ export class Vault extends EventDispatcher {
286
407
  });
287
408
  }
288
409
  trash(file, system = true) {
410
+ return this.mutateFile(file, () => this.trashNow(file, system));
411
+ }
412
+ trashNow(file, system = true) {
289
413
  const trash = system
290
414
  ? this.adapter.trashSystem(file.path)
291
415
  : Promise.resolve(false);
292
- return trash.then((handled) => {
293
- if (handled) {
294
- return this.delete(file, true);
295
- }
296
- return this.adapter.trashLocal(file.path).then(async () => {
297
- await this.reload();
298
- this.dispatch("delete", file);
299
- this.dispatch("all", "delete", file, { system });
300
- });
416
+ return trash.then(async (handled) => {
417
+ if (!handled)
418
+ await this.adapter.trashLocal(file.path);
419
+ await this.reconcileNow([], () => true, { system });
301
420
  });
302
421
  }
303
422
  /**
@@ -360,7 +479,10 @@ export class Vault extends EventDispatcher {
360
479
  };
361
480
  }
362
481
  /** Stream a file into the adapter's atomic or strongest safe replacement path. */
363
- async atomicWrite(path, source, options = {}) {
482
+ atomicWrite(path, source, options = {}) {
483
+ return this.mutate(() => this.atomicWriteNow(path, source, options));
484
+ }
485
+ async atomicWriteNow(path, source, options = {}) {
364
486
  path = this.getPath(path);
365
487
  const existed = this.getFileByPath(path);
366
488
  const adapter = this.adapter;
@@ -374,7 +496,7 @@ export class Vault extends EventDispatcher {
374
496
  try {
375
497
  while (true) {
376
498
  if (options.signal?.aborted) {
377
- throw options.signal.reason ?? new DOMException("Aborted", "AbortError");
499
+ throw (options.signal.reason ?? new DOMException("Aborted", "AbortError"));
378
500
  }
379
501
  const next = await reader.read();
380
502
  if (next.done)
@@ -394,17 +516,10 @@ export class Vault extends EventDispatcher {
394
516
  }
395
517
  await adapter.writeBinary(path, data.buffer, options);
396
518
  }
397
- this.files = await this.loadPath("/", {});
398
- this.rebuildFileNameIndex();
519
+ await this.reconcileNow([{ type: existed ? "modify" : "create", path }], () => true, { operationOrigin: options.operationOrigin });
399
520
  const file = this.getFileByPath(path);
400
521
  if (!file)
401
522
  throw new Error(`Unable to write file: ${path}`);
402
- this.cache.invalidate(path);
403
- const event = existed ? "modify" : "create";
404
- this.dispatch(event, file);
405
- this.dispatch("all", event, file, {
406
- operationOrigin: options.operationOrigin,
407
- });
408
523
  return file;
409
524
  }
410
525
  /**
@@ -416,33 +531,19 @@ export class Vault extends EventDispatcher {
416
531
  * @public
417
532
  */
418
533
  rename(file, newPath) {
534
+ return this.mutateFile(file, () => this.renameNow(file, newPath));
535
+ }
536
+ renameNow(file, newPath) {
419
537
  newPath = this.getPath(newPath);
420
538
  if (file.path == newPath) {
421
539
  return Promise.resolve();
422
540
  }
423
541
  const oldPath = file.path;
424
- const paths = new Set();
425
- if (file instanceof TFile) {
426
- paths.add(file.parent?.path ?? "/");
427
- paths.add(dirname(newPath));
428
- }
429
- else {
430
- paths.add(file.path);
431
- paths.add(newPath);
432
- }
433
- return this.adapter.rename(file.path, newPath).then(async () => {
434
- this.files = await this.loadPath("/", {});
435
- this.rebuildFileNameIndex();
436
- let newFile = this.getAbstractFileByPath(newPath);
437
- if (newFile) {
438
- this.cache.rename(file.path, newFile.path);
439
- this.dispatch("rename", newFile, oldPath);
440
- this.dispatch("all", "rename", newFile, { oldPath });
441
- }
442
- else {
443
- console.warn(`Failed to rename file: ${file.path}`);
444
- }
445
- });
542
+ return this.adapter
543
+ .rename(oldPath, newPath)
544
+ .then(() => this.reconcileNow([
545
+ { type: "create", path: newPath, previousPath: oldPath },
546
+ ]));
446
547
  }
447
548
  /**
448
549
  * Modify the contents of a plaintext file.
@@ -453,21 +554,41 @@ export class Vault extends EventDispatcher {
453
554
  * @public
454
555
  */
455
556
  modify(file, data, options) {
557
+ return this.mutateFile(file, () => this.modifyNow(file, data, options));
558
+ }
559
+ modifyNow(file, data, options) {
456
560
  if (!this.getFileByPath(file.path)) {
457
- return this.create(file.path, data, options).then(() => { });
561
+ return this.createNow(file.path, data, options).then(() => { });
458
562
  }
459
563
  const path = file.path;
460
564
  return this.adapter
461
565
  .write(path, data, options)
462
- .then(() => this.adapter.stat(path))
463
- .then((stat) => {
566
+ .then(() => this.#recordTextWrite(path, data, options));
567
+ }
568
+ get supportsConditionalTextWrites() {
569
+ const adapter = this.adapter;
570
+ return (adapter.getCapabilities?.().conditionalTextWrite === true &&
571
+ typeof adapter.writeTextIfUnchanged === "function");
572
+ }
573
+ /** Persist only the caller's captured text version; never fall back to write. */
574
+ modifyIfUnchanged(file, data, expected, options) {
575
+ return this.mutateFile(file, () => this.modifyIfUnchangedNow(file, data, expected, options));
576
+ }
577
+ async modifyIfUnchangedNow(file, data, expected, options) {
578
+ if (!this.supportsConditionalTextWrites)
579
+ throw new Error("Conditional text writes are unavailable");
580
+ if (expected !== null && typeof expected !== "string")
581
+ throw new Error("Invalid expected text snapshot");
582
+ const path = file.path;
583
+ await this.adapter.writeTextIfUnchanged(path, data, expected, options);
584
+ await this.#recordTextWrite(path, data, options);
585
+ }
586
+ #recordTextWrite(path, data, options) {
587
+ return this.adapter.stat(path).then((stat) => {
464
588
  if (!stat) {
465
589
  return Promise.reject(`Unable to write file: ${path}`);
466
590
  }
467
- let parent = this.files[dirname(path)];
468
- const file = new TFile(path, stat, parent, this);
469
- this.files[path] = file;
470
- this.cache.put(path, data);
591
+ const file = this.recordFile(path, stat);
471
592
  this.cache.put(file.path, data);
472
593
  this.dispatch("modify", file);
473
594
  this.dispatch("all", "modify", file, { data, options });
@@ -482,8 +603,11 @@ export class Vault extends EventDispatcher {
482
603
  * @public
483
604
  */
484
605
  modifyBinary(file, data, options) {
606
+ return this.mutateFile(file, () => this.modifyBinaryNow(file, data, options));
607
+ }
608
+ modifyBinaryNow(file, data, options) {
485
609
  if (!this.getFileByPath(file.path)) {
486
- return this.createBinary(file.path, data, options).then(() => { });
610
+ return this.createBinaryNow(file.path, data, options).then(() => { });
487
611
  }
488
612
  const path = file.path;
489
613
  return this.adapter
@@ -493,9 +617,8 @@ export class Vault extends EventDispatcher {
493
617
  if (!stat) {
494
618
  return Promise.reject(`Unable to write file: ${path}`);
495
619
  }
496
- let parent = this.files[dirname(path)];
497
- const file = new TFile(path, stat, parent, this);
498
- this.files[path] = file;
620
+ const file = this.recordFile(path, stat);
621
+ this.cache.invalidate(path);
499
622
  this.dispatch("modify", file);
500
623
  this.dispatch("all", "modify", file, { data, options });
501
624
  });
@@ -518,14 +641,18 @@ export class Vault extends EventDispatcher {
518
641
  * @public
519
642
  */
520
643
  process(file, fn, options) {
521
- return this.adapter.process(file.path, fn, options).then((data) => {
522
- this.cache.put(file.path, data);
523
- this.dispatch("modify", file);
524
- this.dispatch("all", "modify", file, { data, options });
644
+ return this.mutateFile(file, () => this.processNow(file, fn, options));
645
+ }
646
+ processNow(file, fn, options) {
647
+ return this.adapter.process(file.path, fn, options).then(async (data) => {
648
+ await this.#recordTextWrite(file.path, data, options);
525
649
  return data;
526
650
  });
527
651
  }
528
652
  append(file, data, options) {
653
+ return this.mutateFile(file, () => this.appendNow(file, data, options));
654
+ }
655
+ appendNow(file, data, options) {
529
656
  return this.adapter
530
657
  .append(file.path, data, options)
531
658
  .then(() => this.adapter.stat(file.path))
@@ -533,15 +660,16 @@ export class Vault extends EventDispatcher {
533
660
  if (!stat) {
534
661
  return Promise.reject(`Unable to append file: ${file.path}`);
535
662
  }
536
- const parent = this.files[dirname(file.path)];
537
- const next = new TFile(file.path, stat, parent, this);
538
- this.files[file.path] = next;
663
+ const next = this.recordFile(file.path, stat);
539
664
  this.cache.invalidate(file.path);
540
665
  this.dispatch("modify", next);
541
666
  this.dispatch("all", "modify", next, { data, options });
542
667
  });
543
668
  }
544
669
  appendBinary(file, data, options) {
670
+ return this.mutateFile(file, () => this.appendBinaryNow(file, data, options));
671
+ }
672
+ appendBinaryNow(file, data, options) {
545
673
  return this.adapter
546
674
  .appendBinary(file.path, data, options)
547
675
  .then(() => this.adapter.stat(file.path))
@@ -549,24 +677,23 @@ export class Vault extends EventDispatcher {
549
677
  if (!stat) {
550
678
  return Promise.reject(`Unable to append file: ${file.path}`);
551
679
  }
552
- const parent = this.files[dirname(file.path)];
553
- const next = new TFile(file.path, stat, parent, this);
554
- this.files[file.path] = next;
680
+ const next = this.recordFile(file.path, stat);
555
681
  this.cache.invalidate(file.path);
556
682
  this.dispatch("modify", next);
557
683
  this.dispatch("all", "modify", next, { data, options });
558
684
  });
559
685
  }
560
- async copy(file, newPath) {
686
+ copy(file, newPath) {
687
+ return this.mutateFile(file, () => this.copyNow(file, newPath));
688
+ }
689
+ async copyNow(file, newPath) {
561
690
  newPath = this.getPath(newPath);
562
691
  await this.adapter.copy(file.path, newPath);
563
- await this.reload();
692
+ await this.reconcileNow([], () => true, { originalPath: file.path });
564
693
  const copied = this.getAbstractFileByPath(newPath);
565
694
  if (!copied) {
566
695
  throw new Error(`Unable to copy file: ${file.path}`);
567
696
  }
568
- this.dispatch("create", copied);
569
- this.dispatch("all", "create", copied, { originalPath: file.path });
570
697
  return copied;
571
698
  }
572
699
  /**
@@ -636,7 +763,8 @@ export class Vault extends EventDispatcher {
636
763
  adapter.revokeResourceUrl?.(url);
637
764
  }
638
765
  getAllLoadedFiles() {
639
- return Object.values(this.files);
766
+ const included = this.listingFilter();
767
+ return Object.values(this.files).filter((file) => included(file.path));
640
768
  }
641
769
  /**
642
770
  * Get all folders in the vault.
@@ -645,7 +773,10 @@ export class Vault extends EventDispatcher {
645
773
  * @public
646
774
  */
647
775
  getAllFolders(includeRoot) {
648
- return Object.values(this.files).filter((it) => it instanceof TFolder && (includeRoot || it.path !== "/"));
776
+ const included = this.listingFilter();
777
+ return Object.values(this.files).filter((it) => it instanceof TFolder &&
778
+ included(it.path) &&
779
+ (includeRoot || it.path !== "/"));
649
780
  }
650
781
  /**
651
782
  * Get all files in the vault.
@@ -653,7 +784,8 @@ export class Vault extends EventDispatcher {
653
784
  * @public
654
785
  */
655
786
  getFiles() {
656
- return Object.values(this.files).filter((it) => it instanceof TFile);
787
+ const included = this.listingFilter();
788
+ return Object.values(this.files).filter((it) => it instanceof TFile && included(it.path));
657
789
  }
658
790
  /**
659
791
  * Iterate loaded files without allocating a second vault-sized collection.
@@ -666,8 +798,11 @@ export class Vault extends EventDispatcher {
666
798
  const root = this.files["/"];
667
799
  if (!(root instanceof TFolder))
668
800
  return;
801
+ const included = this.listingFilter();
669
802
  function* visit(folder) {
670
803
  for (const child of folder.children) {
804
+ if (!included(child.path))
805
+ continue;
671
806
  if (child instanceof TFile)
672
807
  yield child.copy();
673
808
  else if (child instanceof TFolder)
@@ -715,9 +850,10 @@ export class Vault extends EventDispatcher {
715
850
  }
716
851
  }
717
852
  }
853
+ const included = this.listingFilter();
718
854
  return [...paths]
719
855
  .map((path) => this.getFileByPath(path))
720
- .filter((file) => Boolean(file))
856
+ .filter((file) => Boolean(file) && included(file.path))
721
857
  .sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0);
722
858
  }
723
859
  getMarkdownFiles() {
@@ -141,14 +141,20 @@ export type SyncEndpointSecurityCapabilities = Pick<SyncHostCapabilities, "crypt
141
141
  /** Trusted host custody. This controller is never included in plugin facades. */
142
142
  export interface SyncHostSecurityProvider {
143
143
  forRuntime(runtimeId: string): SyncEndpointSecurityCapabilities;
144
- /** Must stop admission synchronously, then drain and destroy this runtime's keys. */
144
+ /**
145
+ * Must stop admission synchronously, then drain and destroy this runtime's
146
+ * keys.
147
+ */
145
148
  revokeRuntime(runtimeId: string): void | Promise<void>;
146
149
  }
147
150
  export interface SyncHostServiceOptions {
148
151
  localProvider?: (pluginId: string) => SyncHostLocalProvider | undefined;
149
152
  /** Required before local mutations or receipt recovery can enter a provider. */
150
153
  guardLocalApply?: SyncLocalApplyGuard;
151
- /** Select custody per plugin; returning undefined explicitly retains legacy custody. */
154
+ /**
155
+ * Select custody per plugin; returning undefined explicitly retains legacy
156
+ * custody.
157
+ */
152
158
  securityProvider?: (pluginId: string) => SyncHostSecurityProvider | undefined;
153
159
  /** Dedicated host custody; omitted only for legacy AppDatabase-backed callers. */
154
160
  securityStore?: SyncSecurityStore;
@@ -180,7 +186,10 @@ export declare class SyncHostService {
180
186
  revokePlugin(pluginId: string): Promise<void>;
181
187
  /** Invalidate first, then wait for every admitted host cleanup and file event. */
182
188
  dispose(): Promise<void>;
183
- /** Host-controlled enrollment; this operation is intentionally absent from plugin facades. */
189
+ /**
190
+ * Host-controlled enrollment; this operation is intentionally absent from
191
+ * plugin facades.
192
+ */
184
193
  enrollPluginCrypto(pluginId: string, passphrase: string, request?: SyncCryptoEnrollmentRequest): Promise<SyncCryptoStatus>;
185
194
  /** Host-controlled unlock; plugins can only observe locked state. */
186
195
  unlockPluginCrypto(pluginId: string, passphrase: string): Promise<SyncCryptoStatus>;
package/dist/sync-host.js CHANGED
@@ -113,6 +113,7 @@ export class SyncHostService {
113
113
  : undefined;
114
114
  this.#eventQueue = this.#eventQueue
115
115
  .then(async () => {
116
+ await this.#ready;
116
117
  await this.#recordFileChange(event, file, previousPath, originId);
117
118
  })
118
119
  .catch(() => undefined);
@@ -454,7 +455,10 @@ export class SyncHostService {
454
455
  if (this.#securityProvider(pluginId))
455
456
  throw new Error(`Sync security for ${pluginId} is delegated to its host provider`);
456
457
  }
457
- /** Host-controlled enrollment; this operation is intentionally absent from plugin facades. */
458
+ /**
459
+ * Host-controlled enrollment; this operation is intentionally absent from
460
+ * plugin facades.
461
+ */
458
462
  async enrollPluginCrypto(pluginId, passphrase, request) {
459
463
  this.#assertLocalSecurity(pluginId);
460
464
  return this.#security.enroll(requireBoundedString(pluginId, "Plugin ID", 128), passphrase, request);
@@ -0,0 +1,4 @@
1
+ import type { App } from "./context.svelte";
2
+ import { TFile } from "./storage/fs";
3
+ /** Project into existing owned views; no replacement editor or persistence path. */
4
+ export declare function projectVaultChange(app: App, event: string, file: TFile, context: Record<string, unknown>, current: () => boolean): Promise<void>;
@@ -0,0 +1,43 @@
1
+ import { TFile } from "./storage/fs";
2
+ import { FileView } from "./view.svelte";
3
+ /** Project into existing owned views; no replacement editor or persistence path. */
4
+ export async function projectVaultChange(app, event, file, context, current) {
5
+ if (!current())
6
+ return;
7
+ if (context.external === true)
8
+ app.workspace.trigger("file-change", file, event);
9
+ if (event !== "rename" && event !== "delete")
10
+ return;
11
+ const oldPath = event === "rename" ? String(context.oldPath) : file.path;
12
+ if (app.fileEditingSessions.hasDirty(oldPath)) {
13
+ app.notifications.notify({
14
+ id: `vault-file-change:${oldPath}`,
15
+ title: "File changed on disk",
16
+ message: event === "rename"
17
+ ? `${oldPath} moved to ${file.path}. Local edits remain in the original view for recovery.`
18
+ : `${oldPath} was removed on disk. Local edits remain in the original view for recovery.`,
19
+ severity: "warning",
20
+ source: "vault",
21
+ });
22
+ return;
23
+ }
24
+ const leaves = [];
25
+ app.workspace.iterateAllLeaves((leaf) => {
26
+ if (leaf.view instanceof FileView && leaf.view.file?.path === oldPath)
27
+ leaves.push(leaf);
28
+ });
29
+ for (const leaf of leaves) {
30
+ if (!current() ||
31
+ !(leaf.view instanceof FileView) ||
32
+ leaf.view.file?.path !== oldPath)
33
+ continue;
34
+ if (event === "rename")
35
+ await leaf.view.onRename(file.copy());
36
+ else
37
+ await leaf.setViewState({ type: "empty", state: {} });
38
+ if (current())
39
+ app.workspace.requestSaveLayout();
40
+ }
41
+ if (current())
42
+ app.workspace.reconcileFileReference(oldPath, event === "rename" ? file : null);
43
+ }