@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,8 +1,10 @@
1
+ import { rebindSearchDocument } from "./search-embedding-record";
1
2
  import { canCollectSearchQueryPropertyNames, canCollectSearchQueryTerms, collectSearchQueryPropertyNames, collectSearchQueryTerms, parseSearchQueryAst, } from "../search-query";
2
3
  import { cosineSimilarity, createSearchEmbeddingProvider, } from "./search-embedding-provider";
3
4
  import { evaluateSearchQueryForDocument, findSearchQueryRangesInText, isStructuredSearchQuery, } from "./search-query-evaluator";
4
5
  import { assertProjectionReadAccess, assertProjectionWriteAccess, evaluateProjectionQuery, indexedValuesForRow, MAX_PROJECTION_ROWS_PER_SOURCE, projectionIndexStatus, PUBLIC_TASKS_PROJECTION_ID, sourceIsCurrent, } from "./index-projection";
5
6
  import { taskQueryToIndexQuery, TASK_PROJECTION_FIELDS, TASK_PROJECTION_VERSION, } from "./task-projection";
7
+ export { isNativeDesktopRuntime } from "./runtime";
6
8
  export function supportsSearchQueryEnhancementRuntime(runtimeTarget) {
7
9
  return runtimeTarget === "deno-desktop";
8
10
  }
@@ -245,7 +247,16 @@ function buildSearchChunks(path, content, metadata) {
245
247
  const chunkEnd = remaining > metadata.chunking.targetChars
246
248
  ? findBestCutoff(breakPoints, chunkStart, targetEnd, metadata.chunking.breakpointWindowChars, metadata.chunking.breakpointDecay, codeFences)
247
249
  : content.length;
248
- const safeEnd = chunkEnd > chunkStart ? chunkEnd : targetEnd;
250
+ let safeEnd = chunkEnd > chunkStart ? chunkEnd : targetEnd;
251
+ // Search offsets use UTF-16, but chunks must remain complete Unicode text.
252
+ if (safeEnd > 0 &&
253
+ safeEnd < content.length &&
254
+ content.charCodeAt(safeEnd - 1) >= 0xd800 &&
255
+ content.charCodeAt(safeEnd - 1) <= 0xdbff &&
256
+ content.charCodeAt(safeEnd) >= 0xdc00 &&
257
+ content.charCodeAt(safeEnd) <= 0xdfff) {
258
+ safeEnd += 1;
259
+ }
249
260
  const chunk = createChunk(path, content, metadata, chunkStart, safeEnd);
250
261
  if (!chunk) {
251
262
  if (safeEnd <= chunkStart) {
@@ -554,6 +565,18 @@ function renameChunkId(chunkId, oldPath, newPath) {
554
565
  }
555
566
  return `${newPath}${chunkId.slice(oldPath.length)}`;
556
567
  }
568
+ function searchSnippetBounds(text, start, end) {
569
+ const splitsPair = (offset) => offset > 0 &&
570
+ offset < text.length &&
571
+ text.charCodeAt(offset - 1) >= 0xd800 &&
572
+ text.charCodeAt(offset - 1) <= 0xdbff &&
573
+ text.charCodeAt(offset) >= 0xdc00 &&
574
+ text.charCodeAt(offset) <= 0xdfff;
575
+ return [
576
+ splitsPair(start) ? start - 1 : start,
577
+ splitsPair(end) ? end + 1 : end,
578
+ ];
579
+ }
557
580
  function buildFieldSnippet(field, text, ranges, snippetLength) {
558
581
  if (!ranges.length || !text.length)
559
582
  return [];
@@ -569,8 +592,9 @@ function buildFieldSnippet(field, text, ranges, snippetLength) {
569
592
  }
570
593
  const focus = ranges[0];
571
594
  const padding = Math.max(16, Math.floor(snippetLength / 3));
572
- const start = Math.max(0, focus.start - padding);
573
- const end = Math.min(text.length, Math.max(start + snippetLength, focus.end + padding));
595
+ const initialStart = Math.max(0, focus.start - padding);
596
+ const initialEnd = Math.min(text.length, Math.max(initialStart + snippetLength, focus.end + padding));
597
+ const [start, end] = searchSnippetBounds(text, initialStart, initialEnd);
574
598
  return [
575
599
  {
576
600
  field,
@@ -612,8 +636,9 @@ function buildContentSnippets(text, ranges, snippetLength) {
612
636
  return { ...snippet, ranges };
613
637
  }
614
638
  const focus = ranges[0];
615
- const start = Math.max(0, focus.start - Math.floor(snippetLength / 3));
616
- const end = Math.min(snippet.text.length, Math.max(start + snippetLength, focus.end + Math.floor(snippetLength / 3)));
639
+ const initialStart = Math.max(0, focus.start - Math.floor(snippetLength / 3));
640
+ const initialEnd = Math.min(snippet.text.length, Math.max(initialStart + snippetLength, focus.end + Math.floor(snippetLength / 3)));
641
+ const [start, end] = searchSnippetBounds(snippet.text, initialStart, initialEnd);
617
642
  return {
618
643
  field: "content",
619
644
  text: snippet.text.slice(start, end),
@@ -1335,7 +1360,7 @@ export class MemoryAppDatabase {
1335
1360
  ...clone(input.patch),
1336
1361
  ownerId: input.patch.status === "running" ? existing.ownerId : undefined,
1337
1362
  leaseUntil: input.patch.status === "running"
1338
- ? input.patch.leaseUntil ?? existing.leaseUntil
1363
+ ? (input.patch.leaseUntil ?? existing.leaseUntil)
1339
1364
  : undefined,
1340
1365
  };
1341
1366
  this.memoryJobs.set(updated.id, updated);
@@ -1898,13 +1923,15 @@ export class MemoryAppDatabase {
1898
1923
  const targetProjection = query.direction === "in"
1899
1924
  ? edge.projectionId
1900
1925
  : (edge.targetProjectionId ?? query.projectionId);
1926
+ const targetDefinition = this.projections.get(targetProjection);
1927
+ assertProjectionReadAccess(targetDefinition, readerPluginId);
1901
1928
  const row = query.direction === "in"
1902
1929
  ? this.projectionRows.get(this.projectionRowKey(targetProjection, edge.sourceRowId))
1903
1930
  : edge.targetRowId
1904
1931
  ? this.projectionRows.get(this.projectionRowKey(targetProjection, edge.targetRowId))
1905
1932
  : [...this.projectionRows.values()].find((candidate) => candidate.projectionId === targetProjection &&
1906
1933
  candidate.sourcePath === edge.targetPath);
1907
- if (!row)
1934
+ if (!row || !this.currentProjectionData(row, targetDefinition))
1908
1935
  continue;
1909
1936
  if (query.targetWhere &&
1910
1937
  !evaluateProjectionQuery([row], { where: query.targetWhere }, 0, "ready").rows.length) {
@@ -2103,28 +2130,7 @@ export class MemoryAppDatabase {
2103
2130
  }
2104
2131
  }
2105
2132
  rebindSearchDocumentForProvider(document) {
2106
- const provider = this.searchEmbeddingProvider;
2107
- if (!provider || !document.chunks?.length) {
2108
- return clone(document);
2109
- }
2110
- return {
2111
- ...clone(document),
2112
- chunks: document.chunks.map((chunk) => {
2113
- if (!chunk.text.trim().length) {
2114
- return clone(chunk);
2115
- }
2116
- return {
2117
- ...clone(chunk),
2118
- embedding: {
2119
- status: "pending",
2120
- modelId: provider.config.modelId ?? "lapis/token-hash-v0",
2121
- modelVersion: provider.config.modelVersion,
2122
- dimensions: provider.config.dimensions,
2123
- dirty: true,
2124
- },
2125
- };
2126
- }),
2127
- };
2133
+ return rebindSearchDocument(document, this.searchEmbeddingProvider?.config ?? null);
2128
2134
  }
2129
2135
  toState() {
2130
2136
  return {
@@ -2154,9 +2160,7 @@ export class MemoryAppDatabase {
2154
2160
  projectionRevision: this.projectionRevision,
2155
2161
  memorySources: [...this.memorySources.values()].map((value) => clone(value)),
2156
2162
  memoryCandidates: [...this.memoryCandidates.values()].map((value) => clone(value)),
2157
- memoryCandidateOrigins: [
2158
- ...this.memoryCandidateOrigins.values(),
2159
- ].flatMap((origins) => origins.map((origin) => clone(origin))),
2163
+ memoryCandidateOrigins: [...this.memoryCandidateOrigins.values()].flatMap((origins) => origins.map((origin) => clone(origin))),
2160
2164
  memoryRecallSignals: [...this.memoryRecallSignals.values()].map((value) => clone(value)),
2161
2165
  memoryJobs: [...this.memoryJobs.values()].map((value) => clone(value)),
2162
2166
  };
@@ -2252,6 +2256,9 @@ export class MemoryAppDatabase {
2252
2256
  errorChunkCount += 1;
2253
2257
  lastError ??= embedding.error ?? null;
2254
2258
  break;
2259
+ default:
2260
+ pendingChunkCount += 1;
2261
+ break;
2255
2262
  }
2256
2263
  }
2257
2264
  return {
@@ -2286,7 +2293,7 @@ export class MemoryAppDatabase {
2286
2293
  this.searchIndexStats.readyChunkCount += stats.readyChunkCount;
2287
2294
  this.searchIndexStats.pendingChunkCount += stats.pendingChunkCount;
2288
2295
  this.searchIndexStats.errorChunkCount += stats.errorChunkCount;
2289
- if (stats.lastError) {
2296
+ if (stats.lastError !== null) {
2290
2297
  this.searchIndexStats.lastError = stats.lastError;
2291
2298
  this.searchIndexLastErrorPath = path;
2292
2299
  }
@@ -2301,7 +2308,7 @@ export class MemoryAppDatabase {
2301
2308
  this.searchIndexStats.lastError = null;
2302
2309
  this.searchIndexLastErrorPath = null;
2303
2310
  for (const [nextPath, nextStats] of this.searchIndexStatsByPath) {
2304
- if (nextPath === path || !nextStats.lastError) {
2311
+ if (nextPath === path || nextStats.lastError === null) {
2305
2312
  continue;
2306
2313
  }
2307
2314
  this.searchIndexStats.lastError = nextStats.lastError;
@@ -8,7 +8,7 @@
8
8
  import type { AppDatabaseChangeSet, AppDatabaseNotificationSeverity } from "./app-database";
9
9
  import type { ChangeEvent, AsyncResourceAdapter, DataWriteOptions, ListedFiles, NativeWatchAdapter, NativeWatchSubscription, Stat, VaultAdapter, VaultAdapterCapabilities, VaultReadHandle, AtomicDataWriteOptions, VaultIdentityAdapter, WatchErrorEvent, WatchOptions } from "./fs";
10
10
  import { type KeyValueStore, type VaultProfile } from "./vault-state";
11
- export type NativeDesktopRuntime = "deno-desktop";
11
+ export type NativeDesktopRuntime = "deno-desktop" | "tauri-desktop";
12
12
  export type NativeDesktopPlatformOs = "macos" | "windows" | "linux" | "unknown";
13
13
  export interface NativeDesktopPlatformInfo {
14
14
  readonly runtime: NativeDesktopRuntime;
@@ -18,7 +18,7 @@ export interface NativeDesktopPlatformInfo {
18
18
  readonly appVersion?: string;
19
19
  readonly packaged?: boolean;
20
20
  }
21
- export type NativeDesktopCapabilityId = "resource" | "database" | "search" | "notebook" | "language-service" | "model" | "plugin-sidecar" | "plugin-assets" | "file-watch" | "notifications" | "file-system-actions" | "nostr-signer" | "agent-runtime" | "terminal-runtime" | "secure-storage";
21
+ export type NativeDesktopCapabilityId = "system-trash" | "exclusive-create" | "conditional-text-write" | "resource" | "database" | "search" | "notebook" | "language-service" | "model" | "plugin-sidecar" | "plugin-assets" | "file-watch" | "notifications" | "file-system-actions" | "nostr-signer" | "agent-runtime" | "terminal-runtime" | "secure-storage";
22
22
  export type NativeAgentProcessMessage = {
23
23
  processId: string;
24
24
  type: "stdout" | "stderr" | "exit";
@@ -151,8 +151,10 @@ export declare class NativeDesktopVaultAdapter implements VaultAdapter, VaultIde
151
151
  stat(normalizedPath: string): Promise<Stat | null>;
152
152
  read(normalizedPath: string): Promise<string>;
153
153
  readBinary(normalizedPath: string): Promise<ArrayBuffer>;
154
+ writeExclusive(normalizedPath: string, data: string | ArrayBuffer, options?: DataWriteOptions): Promise<void>;
154
155
  openRead(normalizedPath: string): Promise<VaultReadHandle>;
155
156
  atomicWrite(normalizedPath: string, source: ReadableStream<Uint8Array>, options?: AtomicDataWriteOptions): Promise<void>;
157
+ writeTextIfUnchanged(normalizedPath: string, data: string, expected: string | null, options?: DataWriteOptions): Promise<void>;
156
158
  write(normalizedPath: string, data: string, options?: DataWriteOptions): Promise<void>;
157
159
  writeBinary(normalizedPath: string, data: ArrayBuffer, options?: DataWriteOptions): Promise<void>;
158
160
  append(normalizedPath: string, data: string, options?: DataWriteOptions): Promise<void>;
@@ -169,7 +171,7 @@ export declare class NativeDesktopVaultAdapter implements VaultAdapter, VaultIde
169
171
  copy(normalizedPath: string, normalizedNewPath: string): Promise<void>;
170
172
  getResourcePath(normalizedPath: string): string;
171
173
  getResourceUrl(normalizedPath: string): Promise<string>;
172
- trashSystem(): Promise<boolean>;
174
+ trashSystem(normalizedPath: string): Promise<boolean>;
173
175
  trashLocal(normalizedPath: string): Promise<void>;
174
176
  }
175
177
  export declare function pickNativeDesktopVault(options?: NativeVaultOptions): Promise<{
@@ -237,13 +237,15 @@ export class NativeDesktopVaultAdapter {
237
237
  }
238
238
  getCapabilities() {
239
239
  return {
240
+ exclusiveCreate: hasNativeDesktopCapability("exclusive-create"),
241
+ conditionalTextWrite: hasNativeDesktopCapability("conditional-text-write"),
240
242
  persistent: true,
241
243
  userVisibleFiles: true,
242
244
  requiresPermission: false,
243
245
  nativeWatch: hasNativeDesktopCapability("file-watch") ||
244
246
  typeof getNativeDesktopBridge()?.watch === "function",
245
247
  resourceUrls: true,
246
- systemTrash: false,
248
+ systemTrash: hasNativeDesktopCapability("system-trash"),
247
249
  };
248
250
  }
249
251
  watch(normalizedPath, options, listener) {
@@ -251,9 +253,15 @@ export class NativeDesktopVaultAdapter {
251
253
  const subscriptions = normalizedPath
252
254
  .map((path) => this.watch(path, options, listener))
253
255
  .filter((subscription) => Boolean(subscription));
256
+ const ready = Promise.all(subscriptions.map((subscription) => subscription.ready)).then(() => { });
257
+ void ready.catch(() => { });
254
258
  return {
255
- close: () => {
256
- subscriptions.forEach((subscription) => subscription.close());
259
+ ready,
260
+ close: async () => {
261
+ const results = await Promise.allSettled(subscriptions.map(async (subscription) => subscription.close()));
262
+ const failed = results.find((result) => result.status === "rejected");
263
+ if (failed?.status === "rejected")
264
+ throw failed.reason;
257
265
  },
258
266
  };
259
267
  }
@@ -284,6 +292,20 @@ export class NativeDesktopVaultAdapter {
284
292
  });
285
293
  return new Uint8Array(data).buffer;
286
294
  }
295
+ async writeExclusive(normalizedPath, data, options = {}) {
296
+ const path = normalizeVaultPath(normalizedPath);
297
+ if (!hasNativeDesktopCapability("exclusive-create"))
298
+ throw createFsError("ENOTSUP", "Exclusive file creation is unavailable");
299
+ const bytes = typeof data === "string"
300
+ ? new TextEncoder().encode(data)
301
+ : new Uint8Array(data);
302
+ await invokeNative("desktop_fs_create_exclusive", {
303
+ rootPath: this.rootPath,
304
+ normalizedPath: path,
305
+ data: [...bytes],
306
+ options,
307
+ });
308
+ }
287
309
  async openRead(normalizedPath) {
288
310
  const path = normalizeVaultPath(normalizedPath);
289
311
  const rootPath = this.rootPath;
@@ -335,7 +357,7 @@ export class NativeDesktopVaultAdapter {
335
357
  try {
336
358
  while (true) {
337
359
  if (options.signal?.aborted) {
338
- throw options.signal.reason ?? new DOMException("Aborted", "AbortError");
360
+ throw (options.signal.reason ?? new DOMException("Aborted", "AbortError"));
339
361
  }
340
362
  const next = await reader.read();
341
363
  if (next.done)
@@ -365,6 +387,19 @@ export class NativeDesktopVaultAdapter {
365
387
  reader.releaseLock();
366
388
  }
367
389
  }
390
+ async writeTextIfUnchanged(normalizedPath, data, expected, options) {
391
+ if (expected !== null && typeof expected !== "string")
392
+ throw new Error("Invalid expected text snapshot");
393
+ if (!hasNativeDesktopCapability("conditional-text-write"))
394
+ throw new Error("Conditional text writes are unavailable in this native host");
395
+ await invokeNative("desktop_fs_write_text_if_unchanged", {
396
+ rootPath: this.rootPath,
397
+ normalizedPath: normalizeVaultPath(normalizedPath),
398
+ data,
399
+ expected,
400
+ options,
401
+ });
402
+ }
368
403
  async write(normalizedPath, data, options) {
369
404
  await invokeNative("desktop_fs_write_text", {
370
405
  rootPath: this.rootPath,
@@ -458,8 +493,26 @@ export class NativeDesktopVaultAdapter {
458
493
  }
459
494
  return this.getResourcePath(normalizedPath);
460
495
  }
461
- async trashSystem() {
462
- return false;
496
+ async trashSystem(normalizedPath) {
497
+ const path = normalizeVaultPath(normalizedPath);
498
+ if (path === "/")
499
+ throw createFsError("EINVAL", "Cannot trash the vault root");
500
+ if (!hasNativeDesktopCapability("system-trash"))
501
+ return false;
502
+ try {
503
+ const handled = await invokeNative("desktop_fs_trash_system", {
504
+ rootPath: this.rootPath,
505
+ normalizedPath: path,
506
+ });
507
+ if (typeof handled !== "boolean")
508
+ throw createFsError("EINVAL", "Invalid native trash acknowledgement");
509
+ return handled;
510
+ }
511
+ catch (error) {
512
+ if (error?.code === "ENOTSUP")
513
+ return false;
514
+ throw error;
515
+ }
463
516
  }
464
517
  async trashLocal(normalizedPath) {
465
518
  const path = normalizeVaultPath(normalizedPath);
@@ -0,0 +1,27 @@
1
+ import { EventDispatcher } from "../events";
2
+ import type { ChangeEvent, DataAdapter, NativeWatchSubscription, WatchErrorEvent, WatchOptions } from "./fs";
3
+ export declare class DirectoryWatcher extends EventDispatcher<{
4
+ error: [{
5
+ error: unknown;
6
+ path: string;
7
+ }];
8
+ all: [event: ChangeEvent | WatchErrorEvent];
9
+ create: [path: string];
10
+ modify: [path: string];
11
+ delete: [path: string];
12
+ }> {
13
+ readonly adapter: DataAdapter;
14
+ readonly pollingInterval: number;
15
+ private watchers;
16
+ private retired;
17
+ constructor(adapter: DataAdapter, pollingInterval?: number);
18
+ watch(path: string | string[], options?: WatchOptions): NativeWatchSubscription;
19
+ unwatch(path: string): Promise<void>;
20
+ close(): Promise<void>;
21
+ private live;
22
+ private retire;
23
+ private deliver;
24
+ private eventPath;
25
+ private snapshot;
26
+ private poll;
27
+ }
@@ -0,0 +1,201 @@
1
+ import { EventDispatcher } from "../events";
2
+ import { joinPath, normalizePath } from "./path";
3
+ async function drain(operations) {
4
+ const results = await Promise.allSettled(operations);
5
+ const failed = results.find((result) => result.status === "rejected");
6
+ if (failed?.status === "rejected")
7
+ throw failed.reason;
8
+ }
9
+ export class DirectoryWatcher extends EventDispatcher {
10
+ adapter;
11
+ pollingInterval;
12
+ watchers = new Map();
13
+ retired = new Set();
14
+ constructor(adapter, pollingInterval = 5000) {
15
+ super();
16
+ this.adapter = adapter;
17
+ this.pollingInterval = pollingInterval;
18
+ }
19
+ watch(path, options = {}) {
20
+ if (Array.isArray(path)) {
21
+ const subscriptions = path.map((entry) => this.watch(entry, options));
22
+ const ready = Promise.all(subscriptions.map((entry) => entry.ready)).then(() => { });
23
+ void ready.catch(() => { });
24
+ return {
25
+ ready,
26
+ close: () => drain(subscriptions.map(async (entry) => entry.close())),
27
+ };
28
+ }
29
+ path = normalizePath(path);
30
+ const previous = this.watchers.get(path);
31
+ if (previous)
32
+ void this.retire(previous).catch(() => { });
33
+ const state = {
34
+ path,
35
+ recursive: options.recursive ?? false,
36
+ interval: options.interval ?? this.pollingInterval,
37
+ active: true,
38
+ registering: false,
39
+ snapshot: null,
40
+ timeout: null,
41
+ native: null,
42
+ pending: Promise.resolve(),
43
+ };
44
+ this.watchers.set(path, state);
45
+ let ready = Promise.resolve();
46
+ const adapter = this.adapter;
47
+ if (typeof adapter.watch === "function" &&
48
+ adapter.getCapabilities?.().nativeWatch === true) {
49
+ state.registering = true;
50
+ try {
51
+ state.native =
52
+ adapter.watch(path, options, (event) => {
53
+ if (!this.live(state))
54
+ return;
55
+ this.deliver(event.type === "error"
56
+ ? { ...event, path: this.eventPath(event.path) }
57
+ : {
58
+ ...event,
59
+ path: this.eventPath(event.path),
60
+ ...(event.previousPath === undefined
61
+ ? {}
62
+ : { previousPath: this.eventPath(event.previousPath) }),
63
+ });
64
+ }) ?? null;
65
+ ready = state.native?.ready ?? ready;
66
+ }
67
+ catch (error) {
68
+ state.active = false;
69
+ if (this.watchers.get(path) === state)
70
+ this.watchers.delete(path);
71
+ throw error;
72
+ }
73
+ finally {
74
+ state.registering = false;
75
+ }
76
+ }
77
+ else {
78
+ state.pending = this.poll(state);
79
+ ready = state.pending;
80
+ }
81
+ void ready.catch(() => { });
82
+ return { ready, close: () => this.retire(state) };
83
+ }
84
+ unwatch(path) {
85
+ path = normalizePath(path);
86
+ return drain([...new Set([...this.watchers.values(), ...this.retired])]
87
+ .filter((state) => state.path === path)
88
+ .map((state) => this.retire(state)));
89
+ }
90
+ close() {
91
+ return drain([...new Set([...this.watchers.values(), ...this.retired])].map((state) => this.retire(state)));
92
+ }
93
+ live(state) {
94
+ return state.active && this.watchers.get(state.path) === state;
95
+ }
96
+ retire(state) {
97
+ state.active = false;
98
+ if (this.watchers.get(state.path) === state)
99
+ this.watchers.delete(state.path);
100
+ if (state.timeout)
101
+ clearTimeout(state.timeout);
102
+ state.timeout = null;
103
+ if (state.closing)
104
+ return state.closing;
105
+ this.retired.add(state);
106
+ let cleanup;
107
+ try {
108
+ cleanup = state.registering
109
+ ? Promise.resolve().then(() => state.native?.close())
110
+ : state.native?.close();
111
+ }
112
+ catch (error) {
113
+ cleanup = Promise.reject(error);
114
+ }
115
+ state.closing = drain([state.pending, cleanup]).then(() => {
116
+ this.retired.delete(state);
117
+ });
118
+ void state.closing.catch(() => {
119
+ state.closing = undefined;
120
+ });
121
+ return state.closing;
122
+ }
123
+ deliver(event) {
124
+ this.dispatch("all", event);
125
+ if (event.type === "error")
126
+ this.dispatch("error", { error: event.error, path: event.path });
127
+ else
128
+ this.dispatch(event.type, event.path);
129
+ }
130
+ eventPath(path) {
131
+ const normalized = normalizePath(path);
132
+ return normalized === "/" ? "/" : normalized.replace(/^\/+/, "");
133
+ }
134
+ async snapshot(path, recursive) {
135
+ const snapshot = new Map();
136
+ const stats = await this.adapter.stat(path);
137
+ if (!stats)
138
+ return snapshot;
139
+ snapshot.set(this.eventPath(path), stats);
140
+ if (stats.type !== "folder")
141
+ return snapshot;
142
+ const { files, folders } = await this.adapter.list(path);
143
+ for (const file of files) {
144
+ const entry = joinPath(path, file);
145
+ const stat = await this.adapter.stat(entry);
146
+ if (stat)
147
+ snapshot.set(this.eventPath(entry), stat);
148
+ }
149
+ for (const folder of folders) {
150
+ const entry = joinPath(path, folder);
151
+ if (recursive) {
152
+ for (const [key, stat] of await this.snapshot(entry, true))
153
+ snapshot.set(key, stat);
154
+ }
155
+ else {
156
+ const stat = await this.adapter.stat(entry);
157
+ if (stat)
158
+ snapshot.set(this.eventPath(entry), stat);
159
+ }
160
+ }
161
+ return snapshot;
162
+ }
163
+ async poll(state) {
164
+ try {
165
+ const next = await this.snapshot(state.path, state.recursive);
166
+ if (!this.live(state))
167
+ return;
168
+ if (state.snapshot) {
169
+ const changes = [];
170
+ for (const [path, stat] of next) {
171
+ const previous = state.snapshot.get(path);
172
+ if (!previous)
173
+ changes.push({ type: "create", path });
174
+ else if (previous.mtime !== stat.mtime ||
175
+ previous.size !== stat.size ||
176
+ previous.type !== stat.type)
177
+ changes.push({ type: "modify", path });
178
+ }
179
+ for (const path of state.snapshot.keys())
180
+ if (!next.has(path))
181
+ changes.push({ type: "delete", path });
182
+ for (const event of changes) {
183
+ if (!this.live(state))
184
+ break;
185
+ this.deliver(event);
186
+ }
187
+ }
188
+ state.snapshot = next;
189
+ }
190
+ catch (error) {
191
+ if (this.live(state))
192
+ this.deliver({ type: "error", path: state.path, error });
193
+ }
194
+ finally {
195
+ if (this.live(state))
196
+ state.timeout = setTimeout(() => {
197
+ state.pending = this.poll(state);
198
+ }, state.interval);
199
+ }
200
+ }
201
+ }
@@ -1,4 +1,3 @@
1
- import { EventDispatcher } from "../events";
2
1
  import type { Vault } from "./vault.svelte";
3
2
  interface FileProperties {
4
3
  /**
@@ -90,6 +89,7 @@ export type WatchOptions = {
90
89
  interval?: number;
91
90
  };
92
91
  export interface ChangeEvent {
92
+ previousPath?: string;
93
93
  type: "create" | "modify" | "delete";
94
94
  path: string;
95
95
  }
@@ -99,7 +99,9 @@ export interface WatchErrorEvent {
99
99
  error: unknown;
100
100
  }
101
101
  export interface NativeWatchSubscription {
102
- close(): void;
102
+ /** Resolves after native registration and its initial snapshot complete. */
103
+ readonly ready?: Promise<void>;
104
+ close(): void | Promise<void>;
103
105
  }
104
106
  export interface NativeWatchAdapter {
105
107
  watch(path: string | string[], options: WatchOptions, listener: (event: ChangeEvent | WatchErrorEvent) => void): NativeWatchSubscription | void;
@@ -140,6 +142,8 @@ export interface DataAdapter {
140
142
  trashLocal(normalizedPath: string): Promise<void>;
141
143
  }
142
144
  export interface VaultAdapterCapabilities {
145
+ exclusiveCreate?: boolean;
146
+ conditionalTextWrite?: boolean;
143
147
  persistent: boolean;
144
148
  userVisibleFiles: boolean;
145
149
  requiresPermission: boolean;
@@ -155,6 +159,13 @@ export interface VaultAdapterCapabilities {
155
159
  */
156
160
  export interface VaultAdapter extends DataAdapter {
157
161
  getCapabilities?(): Partial<VaultAdapterCapabilities>;
162
+ /** Publish new complete bytes without replacing an existing destination. */
163
+ writeExclusive?(normalizedPath: string, data: string | ArrayBuffer, options?: DataWriteOptions): Promise<void>;
164
+ /**
165
+ * Reject when the file no longer has these exact contents (null means
166
+ * absent).
167
+ */
168
+ writeTextIfUnchanged?(normalizedPath: string, data: string, expected: string | null, options?: DataWriteOptions): Promise<void>;
158
169
  openRead?(normalizedPath: string): Promise<VaultReadHandle>;
159
170
  atomicWrite?(normalizedPath: string, source: ReadableStream<Uint8Array>, options?: AtomicDataWriteOptions): Promise<void>;
160
171
  }
@@ -165,30 +176,7 @@ export interface AsyncResourceAdapter {
165
176
  getResourceUrl(normalizedPath: string): Promise<string>;
166
177
  revokeResourceUrl?(url: string): void;
167
178
  }
168
- export declare class DirectoryWatcher extends EventDispatcher<{
169
- error: [{
170
- error: unknown;
171
- path: string;
172
- }];
173
- all: [event: ChangeEvent | WatchErrorEvent];
174
- create: [path: string];
175
- modify: [path: string];
176
- delete: [path: string];
177
- }> {
178
- readonly adapter: DataAdapter;
179
- readonly pollingInterval: number;
180
- private watchers;
181
- constructor(adapter: DataAdapter, pollingInterval?: number);
182
- watch(path: string | string[], options?: WatchOptions): {
183
- close: () => void;
184
- } | undefined;
185
- unwatch(path: string): void;
186
- close(): void;
187
- private takeSnapshot;
188
- private toEventPath;
189
- private checkForChanges;
190
- private diffSnapshot;
191
- }
179
+ export { DirectoryWatcher } from "./directory-watcher";
192
180
  export declare class FileCache {
193
181
  private capacity;
194
182
  private cache;
@@ -202,4 +190,3 @@ export declare class FileCache {
202
190
  has(key: string): boolean;
203
191
  keys(): string[];
204
192
  }
205
- export {};