@bendyline/docblocks 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (286) hide show
  1. package/dist/{chunk-J32GU3F3.js → chunk-6IYMTYMQ.js} +249 -221
  2. package/dist/chunk-6IYMTYMQ.js.map +1 -0
  3. package/dist/{chunk-75UUF6C4.js → chunk-B56KWCSE.js} +2 -2
  4. package/dist/{chunk-RVQHY3XU.js → chunk-BNKV7MTO.js} +1 -1
  5. package/dist/{chunk-RVQHY3XU.js.map → chunk-BNKV7MTO.js.map} +1 -1
  6. package/dist/chunk-BXMF5YQJ.js +149 -0
  7. package/dist/chunk-BXMF5YQJ.js.map +1 -0
  8. package/dist/{chunk-PGR7LK6D.js → chunk-COYQXQK3.js} +14 -5
  9. package/dist/chunk-COYQXQK3.js.map +1 -0
  10. package/dist/{chunk-RQJRQF46.js → chunk-DN7A33LL.js} +42 -49
  11. package/dist/chunk-DN7A33LL.js.map +1 -0
  12. package/dist/chunk-FA7UNOXK.js +20 -0
  13. package/dist/chunk-FA7UNOXK.js.map +1 -0
  14. package/dist/chunk-GM5YXU4L.js +29 -0
  15. package/dist/chunk-GM5YXU4L.js.map +1 -0
  16. package/dist/{chunk-SI2LU2RP.js → chunk-JS6XFWQK.js} +2 -1
  17. package/dist/chunk-JS6XFWQK.js.map +1 -0
  18. package/dist/{chunk-EGNDADXB.js → chunk-MNSZ7EZN.js} +2 -2
  19. package/dist/chunk-P3DVZQ6S.js +228 -0
  20. package/dist/chunk-P3DVZQ6S.js.map +1 -0
  21. package/dist/chunk-P4WATND4.js +52 -0
  22. package/dist/chunk-P4WATND4.js.map +1 -0
  23. package/dist/chunk-RVPGQBWR.js +169 -0
  24. package/dist/chunk-RVPGQBWR.js.map +1 -0
  25. package/dist/chunk-U722UUFN.js +9 -0
  26. package/dist/chunk-U722UUFN.js.map +1 -0
  27. package/dist/{chunk-GCPBEMMH.js → chunk-VTCSF7VS.js} +11 -21
  28. package/dist/chunk-VTCSF7VS.js.map +1 -0
  29. package/dist/{chunk-2D7Y4IR3.js → chunk-WJKYY5QR.js} +123 -66
  30. package/dist/chunk-WJKYY5QR.js.map +1 -0
  31. package/dist/{chunk-5MYAICRE.js → chunk-XUML3MKV.js} +44 -56
  32. package/dist/chunk-XUML3MKV.js.map +1 -0
  33. package/dist/document/index.d.ts +358 -6
  34. package/dist/document/index.js +24 -3
  35. package/dist/document/index.js.map +1 -1
  36. package/dist/filesystem/electron.d.ts +73 -4
  37. package/dist/filesystem/electron.js +16 -3
  38. package/dist/filesystem/electron.js.map +1 -1
  39. package/dist/filesystem/index.d.ts +228 -22
  40. package/dist/filesystem/index.js +133 -19
  41. package/dist/filesystem/index.js.map +1 -1
  42. package/dist/filesystem/indexeddb.d.ts +284 -5
  43. package/dist/filesystem/indexeddb.js +22 -4
  44. package/dist/filesystem/indexeddb.js.map +1 -1
  45. package/dist/filesystem/memory.d.ts +152 -4
  46. package/dist/filesystem/memory.js +12 -3
  47. package/dist/filesystem/memory.js.map +1 -1
  48. package/dist/filesystem/native.d.ts +136 -4
  49. package/dist/filesystem/native.js +27 -3
  50. package/dist/filesystem/native.js.map +1 -1
  51. package/dist/host/index.d.ts +32 -9
  52. package/dist/host/index.js +26 -29
  53. package/dist/host/index.js.map +1 -1
  54. package/dist/index.d.ts +14 -10
  55. package/dist/index.js +204 -9
  56. package/dist/index.js.map +1 -1
  57. package/dist/mcp/index.d.ts +33 -4
  58. package/dist/mcp/index.js +31 -3
  59. package/dist/mcp/index.js.map +1 -1
  60. package/dist/mcp/zod.d.ts +637 -635
  61. package/dist/mcp/zod.js +700 -884
  62. package/dist/mcp/zod.js.map +1 -1
  63. package/dist/share/index.d.ts +43 -0
  64. package/dist/share/index.js +13 -0
  65. package/dist/share/index.js.map +1 -0
  66. package/dist/{types-CsX6EN2t.d.ts → types-BEg5SQ1_.d.ts} +1 -1
  67. package/dist/{types-eOlt9J7n.d.ts → types-CueK0tvU.d.ts} +34 -11
  68. package/dist/vscode/index.d.ts +220 -3
  69. package/dist/vscode/index.js +382 -2
  70. package/dist/vscode/index.js.map +1 -1
  71. package/dist/workspace/index.d.ts +141 -4
  72. package/dist/workspace/index.js +28 -2
  73. package/dist/workspace/index.js.map +1 -1
  74. package/package.json +9 -5
  75. package/dist/chunk-2D7Y4IR3.js.map +0 -1
  76. package/dist/chunk-5MYAICRE.js.map +0 -1
  77. package/dist/chunk-AYZ4HN5M.js +0 -20
  78. package/dist/chunk-AYZ4HN5M.js.map +0 -1
  79. package/dist/chunk-GCPBEMMH.js.map +0 -1
  80. package/dist/chunk-J32GU3F3.js.map +0 -1
  81. package/dist/chunk-PGR7LK6D.js.map +0 -1
  82. package/dist/chunk-RQJRQF46.js.map +0 -1
  83. package/dist/chunk-SI2LU2RP.js.map +0 -1
  84. package/dist/chunk-W5UUUII5.js +0 -142
  85. package/dist/chunk-W5UUUII5.js.map +0 -1
  86. package/dist/document/document-session.d.ts +0 -127
  87. package/dist/document/document-session.d.ts.map +0 -1
  88. package/dist/document/document-session.js +0 -749
  89. package/dist/document/document-session.js.map +0 -1
  90. package/dist/document/filesystem-target.d.ts +0 -17
  91. package/dist/document/filesystem-target.d.ts.map +0 -1
  92. package/dist/document/filesystem-target.js +0 -70
  93. package/dist/document/filesystem-target.js.map +0 -1
  94. package/dist/document/index.d.ts.map +0 -1
  95. package/dist/document/recovery-journal.d.ts +0 -125
  96. package/dist/document/recovery-journal.d.ts.map +0 -1
  97. package/dist/document/recovery-journal.js +0 -385
  98. package/dist/document/recovery-journal.js.map +0 -1
  99. package/dist/document/types.d.ts +0 -89
  100. package/dist/document/types.d.ts.map +0 -1
  101. package/dist/document/types.js +0 -9
  102. package/dist/document/types.js.map +0 -1
  103. package/dist/filesystem/commit-lock.d.ts +0 -3
  104. package/dist/filesystem/commit-lock.d.ts.map +0 -1
  105. package/dist/filesystem/commit-lock.js +0 -20
  106. package/dist/filesystem/commit-lock.js.map +0 -1
  107. package/dist/filesystem/dbk-workspace.d.ts +0 -34
  108. package/dist/filesystem/dbk-workspace.d.ts.map +0 -1
  109. package/dist/filesystem/dbk-workspace.js +0 -85
  110. package/dist/filesystem/dbk-workspace.js.map +0 -1
  111. package/dist/filesystem/electron-provider-v2.d.ts +0 -36
  112. package/dist/filesystem/electron-provider-v2.d.ts.map +0 -1
  113. package/dist/filesystem/electron-provider-v2.js +0 -503
  114. package/dist/filesystem/electron-provider-v2.js.map +0 -1
  115. package/dist/filesystem/electron-provider.d.ts +0 -35
  116. package/dist/filesystem/electron-provider.d.ts.map +0 -1
  117. package/dist/filesystem/electron-provider.js +0 -69
  118. package/dist/filesystem/electron-provider.js.map +0 -1
  119. package/dist/filesystem/electron.d.ts.map +0 -1
  120. package/dist/filesystem/file-media-provider.d.ts +0 -23
  121. package/dist/filesystem/file-media-provider.d.ts.map +0 -1
  122. package/dist/filesystem/file-media-provider.js +0 -85
  123. package/dist/filesystem/file-media-provider.js.map +0 -1
  124. package/dist/filesystem/filesystem-content-container.d.ts +0 -24
  125. package/dist/filesystem/filesystem-content-container.d.ts.map +0 -1
  126. package/dist/filesystem/filesystem-content-container.js +0 -153
  127. package/dist/filesystem/filesystem-content-container.js.map +0 -1
  128. package/dist/filesystem/fs-error.d.ts +0 -46
  129. package/dist/filesystem/fs-error.d.ts.map +0 -1
  130. package/dist/filesystem/fs-error.js +0 -170
  131. package/dist/filesystem/fs-error.js.map +0 -1
  132. package/dist/filesystem/index.d.ts.map +0 -1
  133. package/dist/filesystem/indexeddb-content-container.d.ts +0 -22
  134. package/dist/filesystem/indexeddb-content-container.d.ts.map +0 -1
  135. package/dist/filesystem/indexeddb-content-container.js +0 -100
  136. package/dist/filesystem/indexeddb-content-container.js.map +0 -1
  137. package/dist/filesystem/indexeddb-provider-v2.d.ts +0 -96
  138. package/dist/filesystem/indexeddb-provider-v2.d.ts.map +0 -1
  139. package/dist/filesystem/indexeddb-provider-v2.js +0 -1241
  140. package/dist/filesystem/indexeddb-provider-v2.js.map +0 -1
  141. package/dist/filesystem/indexeddb-provider.d.ts +0 -39
  142. package/dist/filesystem/indexeddb-provider.d.ts.map +0 -1
  143. package/dist/filesystem/indexeddb-provider.js +0 -145
  144. package/dist/filesystem/indexeddb-provider.js.map +0 -1
  145. package/dist/filesystem/indexeddb-store.d.ts +0 -34
  146. package/dist/filesystem/indexeddb-store.d.ts.map +0 -1
  147. package/dist/filesystem/indexeddb-store.js +0 -165
  148. package/dist/filesystem/indexeddb-store.js.map +0 -1
  149. package/dist/filesystem/indexeddb.d.ts.map +0 -1
  150. package/dist/filesystem/legacy-v2-adapter.d.ts +0 -62
  151. package/dist/filesystem/legacy-v2-adapter.d.ts.map +0 -1
  152. package/dist/filesystem/legacy-v2-adapter.js +0 -499
  153. package/dist/filesystem/legacy-v2-adapter.js.map +0 -1
  154. package/dist/filesystem/memory-provider-v2.d.ts +0 -92
  155. package/dist/filesystem/memory-provider-v2.d.ts.map +0 -1
  156. package/dist/filesystem/memory-provider-v2.js +0 -615
  157. package/dist/filesystem/memory-provider-v2.js.map +0 -1
  158. package/dist/filesystem/memory-provider.d.ts +0 -54
  159. package/dist/filesystem/memory-provider.d.ts.map +0 -1
  160. package/dist/filesystem/memory-provider.js +0 -132
  161. package/dist/filesystem/memory-provider.js.map +0 -1
  162. package/dist/filesystem/memory.d.ts.map +0 -1
  163. package/dist/filesystem/move-entry.d.ts +0 -9
  164. package/dist/filesystem/move-entry.d.ts.map +0 -1
  165. package/dist/filesystem/move-entry.js +0 -103
  166. package/dist/filesystem/move-entry.js.map +0 -1
  167. package/dist/filesystem/move-error.d.ts +0 -53
  168. package/dist/filesystem/move-error.d.ts.map +0 -1
  169. package/dist/filesystem/move-error.js +0 -99
  170. package/dist/filesystem/move-error.js.map +0 -1
  171. package/dist/filesystem/native-provider-v2.d.ts +0 -75
  172. package/dist/filesystem/native-provider-v2.d.ts.map +0 -1
  173. package/dist/filesystem/native-provider-v2.js +0 -849
  174. package/dist/filesystem/native-provider-v2.js.map +0 -1
  175. package/dist/filesystem/native-provider.d.ts +0 -53
  176. package/dist/filesystem/native-provider.d.ts.map +0 -1
  177. package/dist/filesystem/native-provider.js +0 -471
  178. package/dist/filesystem/native-provider.js.map +0 -1
  179. package/dist/filesystem/native.d.ts.map +0 -1
  180. package/dist/filesystem/types.d.ts +0 -83
  181. package/dist/filesystem/types.d.ts.map +0 -1
  182. package/dist/filesystem/types.js +0 -16
  183. package/dist/filesystem/types.js.map +0 -1
  184. package/dist/filesystem/v2.d.ts +0 -172
  185. package/dist/filesystem/v2.d.ts.map +0 -1
  186. package/dist/filesystem/v2.js +0 -16
  187. package/dist/filesystem/v2.js.map +0 -1
  188. package/dist/filesystem/workspace-path.d.ts +0 -21
  189. package/dist/filesystem/workspace-path.d.ts.map +0 -1
  190. package/dist/filesystem/workspace-path.js +0 -80
  191. package/dist/filesystem/workspace-path.js.map +0 -1
  192. package/dist/host/filesystem-v2.d.ts +0 -45
  193. package/dist/host/filesystem-v2.d.ts.map +0 -1
  194. package/dist/host/filesystem-v2.js +0 -13
  195. package/dist/host/filesystem-v2.js.map +0 -1
  196. package/dist/host/git.d.ts +0 -200
  197. package/dist/host/git.d.ts.map +0 -1
  198. package/dist/host/git.js +0 -15
  199. package/dist/host/git.js.map +0 -1
  200. package/dist/host/index.d.ts.map +0 -1
  201. package/dist/host/types.d.ts +0 -232
  202. package/dist/host/types.d.ts.map +0 -1
  203. package/dist/host/types.js +0 -10
  204. package/dist/host/types.js.map +0 -1
  205. package/dist/host/wire-policy.d.ts +0 -22
  206. package/dist/host/wire-policy.d.ts.map +0 -1
  207. package/dist/host/wire-policy.js +0 -92
  208. package/dist/host/wire-policy.js.map +0 -1
  209. package/dist/index.d.ts.map +0 -1
  210. package/dist/mcp/index.d.ts.map +0 -1
  211. package/dist/mcp/types.d.ts +0 -466
  212. package/dist/mcp/types.d.ts.map +0 -1
  213. package/dist/mcp/types.js +0 -25
  214. package/dist/mcp/types.js.map +0 -1
  215. package/dist/mcp/wire.d.ts +0 -29
  216. package/dist/mcp/wire.d.ts.map +0 -1
  217. package/dist/mcp/wire.js +0 -1046
  218. package/dist/mcp/wire.js.map +0 -1
  219. package/dist/mcp/zod.d.ts.map +0 -1
  220. package/dist/vscode/index.d.ts.map +0 -1
  221. package/dist/vscode/messages.d.ts +0 -204
  222. package/dist/vscode/messages.d.ts.map +0 -1
  223. package/dist/vscode/messages.js +0 -494
  224. package/dist/vscode/messages.js.map +0 -1
  225. package/dist/workspace/index.d.ts.map +0 -1
  226. package/dist/workspace/reconcile-electron.d.ts +0 -19
  227. package/dist/workspace/reconcile-electron.d.ts.map +0 -1
  228. package/dist/workspace/reconcile-electron.js +0 -42
  229. package/dist/workspace/reconcile-electron.js.map +0 -1
  230. package/dist/workspace/types.d.ts +0 -69
  231. package/dist/workspace/types.d.ts.map +0 -1
  232. package/dist/workspace/types.js +0 -2
  233. package/dist/workspace/types.js.map +0 -1
  234. package/dist/workspace/workspace-manager.d.ts +0 -47
  235. package/dist/workspace/workspace-manager.d.ts.map +0 -1
  236. package/dist/workspace/workspace-manager.js +0 -126
  237. package/dist/workspace/workspace-manager.js.map +0 -1
  238. package/src/document/document-session.ts +0 -834
  239. package/src/document/filesystem-target.ts +0 -109
  240. package/src/document/index.ts +0 -41
  241. package/src/document/recovery-journal.ts +0 -536
  242. package/src/document/types.ts +0 -116
  243. package/src/filesystem/commit-lock.ts +0 -29
  244. package/src/filesystem/dbk-workspace.ts +0 -135
  245. package/src/filesystem/electron-provider-v2.ts +0 -630
  246. package/src/filesystem/electron-provider.ts +0 -99
  247. package/src/filesystem/electron.ts +0 -3
  248. package/src/filesystem/file-media-provider.ts +0 -103
  249. package/src/filesystem/filesystem-content-container.ts +0 -164
  250. package/src/filesystem/fs-error.ts +0 -234
  251. package/src/filesystem/index.ts +0 -143
  252. package/src/filesystem/indexeddb-content-container.ts +0 -112
  253. package/src/filesystem/indexeddb-provider-v2.ts +0 -1740
  254. package/src/filesystem/indexeddb-provider.ts +0 -171
  255. package/src/filesystem/indexeddb-store.ts +0 -207
  256. package/src/filesystem/indexeddb.ts +0 -12
  257. package/src/filesystem/legacy-v2-adapter.ts +0 -779
  258. package/src/filesystem/memory-provider-v2.ts +0 -896
  259. package/src/filesystem/memory-provider.ts +0 -180
  260. package/src/filesystem/memory.ts +0 -15
  261. package/src/filesystem/move-entry.ts +0 -142
  262. package/src/filesystem/move-error.ts +0 -159
  263. package/src/filesystem/native-provider-v2.ts +0 -1165
  264. package/src/filesystem/native-provider.ts +0 -537
  265. package/src/filesystem/native.ts +0 -16
  266. package/src/filesystem/types.ts +0 -114
  267. package/src/filesystem/v2.ts +0 -231
  268. package/src/filesystem/workspace-path.ts +0 -92
  269. package/src/host/filesystem-v2.ts +0 -104
  270. package/src/host/git.ts +0 -246
  271. package/src/host/index.ts +0 -93
  272. package/src/host/types.ts +0 -274
  273. package/src/host/wire-policy.ts +0 -103
  274. package/src/index.ts +0 -10
  275. package/src/mcp/index.ts +0 -3
  276. package/src/mcp/types.ts +0 -594
  277. package/src/mcp/wire.ts +0 -1280
  278. package/src/mcp/zod.ts +0 -1000
  279. package/src/vscode/index.ts +0 -2
  280. package/src/vscode/messages.ts +0 -704
  281. package/src/workspace/index.ts +0 -17
  282. package/src/workspace/reconcile-electron.ts +0 -65
  283. package/src/workspace/types.ts +0 -67
  284. package/src/workspace/workspace-manager.ts +0 -151
  285. /package/dist/{chunk-75UUF6C4.js.map → chunk-B56KWCSE.js.map} +0 -0
  286. /package/dist/{chunk-EGNDADXB.js.map → chunk-MNSZ7EZN.js.map} +0 -0
@@ -1,834 +0,0 @@
1
- import type {
2
- DocumentCommitTarget,
3
- DocumentConflictStrategy,
4
- DocumentExternalChangeResult,
5
- DocumentExternalSnapshot,
6
- DocumentSaveReason,
7
- DocumentSessionConflict,
8
- DocumentSessionEditScope,
9
- DocumentSessionLifecycle,
10
- DocumentSessionOptions,
11
- DocumentSessionSnapshot,
12
- DocumentSessionStatus,
13
- DocumentSessionTransition,
14
- } from './types.js';
15
- import { isFileSystemMoveStateError } from '../filesystem/move-error.js';
16
- import type { DocumentRecoveryJournal } from './recovery-journal.js';
17
-
18
- type SessionListener = () => void;
19
-
20
- const DEFAULT_AUTO_SAVE_RETRY_DELAYS_MS = [1_000, 3_000, 10_000] as const;
21
-
22
- /**
23
- * Raised by a commit target when optimistic external-change verification
24
- * fails. The session converts it into durable conflict state.
25
- */
26
- export class DocumentCommitConflictError extends Error {
27
- public readonly externalContent: string | null;
28
- public readonly externalVersion: string | number | null;
29
-
30
- public constructor(
31
- message: string,
32
- externalContent: string | null,
33
- externalVersion: string | number | null = null,
34
- ) {
35
- super(message);
36
- this.name = 'DocumentCommitConflictError';
37
- this.externalContent = externalContent;
38
- this.externalVersion = externalVersion;
39
- }
40
- }
41
-
42
- export class DocumentSessionConflictError extends Error {
43
- public readonly conflict: DocumentSessionConflict;
44
-
45
- public constructor(conflict: DocumentSessionConflict) {
46
- super('The document changed outside DocBlocks while local edits were pending.');
47
- this.name = 'DocumentSessionConflictError';
48
- this.conflict = conflict;
49
- }
50
- }
51
-
52
- /**
53
- * A revisioned, serialized persistence boundary for one active document.
54
- *
55
- * Invariants:
56
- * - only drain() calls target.commit();
57
- * - at most one commit is in flight;
58
- * - revisions never decrease, including across target transitions;
59
- * - destructive operations wait for an in-flight commit and run last;
60
- * - a target is never changed by a React effect cleanup.
61
- */
62
- export class DocumentSession {
63
- private readonly autoSaveDelayMs: number;
64
- private readonly autoSaveRetryDelaysMs: readonly number[];
65
- private readonly recoveryJournal: DocumentRecoveryJournal | null;
66
- private readonly listeners = new Set<SessionListener>();
67
- private autoSaveEnabled: boolean;
68
-
69
- private target: DocumentCommitTarget | null = null;
70
- private content = '';
71
- private persistedContent: string | null = null;
72
- private revision = 0;
73
- private persistedRevision = 0;
74
- private savingRevision: number | null = null;
75
- private savingContent: string | null = null;
76
- private generation = 0;
77
- private externalSequence = -1;
78
- private lifecycle: DocumentSessionLifecycle = 'open';
79
- private frozen = false;
80
- private error: Error | null = null;
81
- private conflict: DocumentSessionConflict | null = null;
82
-
83
- private autoSaveTimer: ReturnType<typeof setTimeout> | null = null;
84
- private autoSaveRetryAttempt = 0;
85
- private drainPromise: Promise<void> | null = null;
86
- private haltDrain = false;
87
- private operationTail: Promise<void> = Promise.resolve();
88
- private snapshot: DocumentSessionSnapshot;
89
-
90
- public constructor(options: DocumentSessionOptions = {}) {
91
- this.autoSaveDelayMs = Math.max(0, options.autoSaveDelayMs ?? 500);
92
- this.autoSaveEnabled = options.autoSaveEnabled ?? true;
93
- this.autoSaveRetryDelaysMs = normalizeRetryDelays(
94
- options.autoSaveRetryDelaysMs ?? DEFAULT_AUTO_SAVE_RETRY_DELAYS_MS,
95
- );
96
- this.recoveryJournal = options.recoveryJournal ?? null;
97
- this.snapshot = this.createSnapshot();
98
- }
99
-
100
- public readonly subscribe = (listener: SessionListener): (() => void) => {
101
- this.listeners.add(listener);
102
- return () => this.listeners.delete(listener);
103
- };
104
-
105
- public readonly getSnapshot = (): DocumentSessionSnapshot => this.snapshot;
106
-
107
- /** Enable or disable automatic persistence without weakening explicit or lifecycle flushes. */
108
- public setAutoSaveEnabled(enabled: boolean): void {
109
- if (this.autoSaveEnabled === enabled) return;
110
- this.autoSaveEnabled = enabled;
111
- if (!enabled) {
112
- this.clearAutoSaveTimer();
113
- return;
114
- }
115
- if (
116
- this.target &&
117
- this.lifecycle === 'open' &&
118
- !this.frozen &&
119
- !this.conflict &&
120
- this.persistedRevision < this.revision
121
- ) {
122
- this.scheduleAutoSave();
123
- }
124
- }
125
-
126
- /**
127
- * Record a complete local snapshot and schedule its persistence.
128
- * Returns the assigned monotonic revision.
129
- */
130
- public edit(content: string, scope: DocumentSessionEditScope): number {
131
- if (!this.target || this.lifecycle !== 'open' || this.frozen) {
132
- throw new Error('Cannot edit a document session that is not open.');
133
- }
134
- if (scope.targetKey !== this.target.key || scope.generation !== this.generation) {
135
- throw new Error('Cannot apply an edit from an obsolete document instance.');
136
- }
137
- if (content === this.content) return this.revision;
138
-
139
- this.content = content;
140
- this.revision += 1;
141
- this.error = null;
142
- this.autoSaveRetryAttempt = 0;
143
- if (this.conflict) {
144
- this.conflict = {
145
- ...this.conflict,
146
- localContent: content,
147
- localRevision: this.revision,
148
- };
149
- }
150
- this.writeRecoverySnapshot();
151
- if (!this.conflict) this.scheduleAutoSave();
152
- this.emit();
153
- return this.revision;
154
- }
155
-
156
- /**
157
- * Persist the revision current when flush() is called. A later edit may
158
- * advance the session again, but commits remain strictly serialized.
159
- */
160
- public async flush(reason: DocumentSaveReason = 'manual'): Promise<DocumentSessionSnapshot> {
161
- this.clearAutoSaveTimer();
162
- if (!this.target) return this.snapshot;
163
- if (this.conflict) throw new DocumentSessionConflictError(this.conflict);
164
-
165
- const requiredRevision = this.revision;
166
- while (this.target && this.persistedRevision < requiredRevision) {
167
- if (this.conflict) throw new DocumentSessionConflictError(this.conflict);
168
- if (this.haltDrain) {
169
- throw new Error('Document persistence was cancelled before the requested revision saved.');
170
- }
171
- try {
172
- await this.ensureDrain(reason);
173
- } catch (error: unknown) {
174
- if (this.conflict) throw new DocumentSessionConflictError(this.conflict);
175
- throw error;
176
- }
177
- }
178
-
179
- if (this.conflict) throw new DocumentSessionConflictError(this.conflict);
180
- if (this.persistedRevision < requiredRevision) {
181
- throw new Error('The document target closed before the requested revision saved.');
182
- }
183
- return this.snapshot;
184
- }
185
-
186
- /**
187
- * Flush the old document, then replace it with a new clean baseline.
188
- * UI state should change only after this promise resolves.
189
- */
190
- public transitionTo(
191
- target: DocumentCommitTarget | null,
192
- content: string,
193
- ): Promise<DocumentSessionSnapshot> {
194
- return this.enqueueOperation(() =>
195
- this.performTransition(async () => ({ target, content })).then((snapshot) => {
196
- if (!snapshot) throw new Error('A fixed document transition cannot be cancelled.');
197
- return snapshot;
198
- }),
199
- );
200
- }
201
-
202
- /**
203
- * Freeze and flush the current document before reading the next one. The
204
- * loader may return null when a navigation request was superseded, leaving
205
- * the current target active and clean.
206
- */
207
- public transitionWithLoad(
208
- load: () => Promise<DocumentSessionTransition | null>,
209
- ): Promise<DocumentSessionSnapshot | null> {
210
- return this.enqueueOperation(() => this.performTransition(load));
211
- }
212
-
213
- /**
214
- * Flush the current path, run the storage move, and only then swap the
215
- * commit target. No edit can enter between those steps.
216
- */
217
- public retarget(
218
- nextTarget: DocumentCommitTarget,
219
- moveStorage: () => Promise<void>,
220
- ): Promise<DocumentSessionSnapshot> {
221
- return this.enqueueOperation(async () => {
222
- this.assertNotPreparingClose();
223
- if (!this.target) throw new Error('Cannot retarget a session without a document.');
224
-
225
- this.lifecycle = 'transitioning';
226
- this.frozen = true;
227
- this.emit();
228
- try {
229
- this.haltDrain = false;
230
- await this.flush('transition');
231
- await moveStorage();
232
- this.generation += 1;
233
- this.externalSequence = -1;
234
- this.target = nextTarget;
235
- this.error = null;
236
- this.conflict = null;
237
- this.lifecycle = 'open';
238
- this.frozen = false;
239
- this.emit();
240
- return this.snapshot;
241
- } catch (error: unknown) {
242
- if (isFileSystemMoveStateError(error)) {
243
- if (error.documentLocation === 'destination') {
244
- this.generation += 1;
245
- this.externalSequence = -1;
246
- this.target = nextTarget;
247
- }
248
- this.haltDrain = true;
249
- this.error = error;
250
- this.lifecycle = 'open';
251
- // The provider could not establish one authoritative compound move.
252
- // Keep editing frozen until navigation/recovery chooses a real path.
253
- this.frozen = true;
254
- this.emit();
255
- throw error;
256
- }
257
- this.lifecycle = 'open';
258
- this.frozen = false;
259
- this.emit();
260
- throw error;
261
- }
262
- });
263
- }
264
-
265
- /**
266
- * Cancel queued writes, wait for an already-running write, then delete.
267
- * The deletion is therefore the final storage operation for this target.
268
- */
269
- public delete(deleteStorage: () => Promise<void>): Promise<DocumentSessionSnapshot> {
270
- return this.enqueueOperation(async () => {
271
- this.assertNotPreparingClose();
272
- this.lifecycle = 'transitioning';
273
- this.frozen = true;
274
- this.haltDrain = true;
275
- this.clearAutoSaveTimer();
276
- this.emit();
277
-
278
- await this.awaitActiveDrain();
279
- try {
280
- await deleteStorage();
281
- } catch (error: unknown) {
282
- this.haltDrain = false;
283
- this.lifecycle = 'open';
284
- this.frozen = false;
285
- if (this.target && this.persistedRevision < this.revision && !this.conflict) {
286
- this.scheduleAutoSave();
287
- }
288
- this.emit();
289
- throw error;
290
- }
291
-
292
- this.discardRecoverySnapshot();
293
- this.detach();
294
- return this.snapshot;
295
- });
296
- }
297
-
298
- /**
299
- * Discard queued changes and detach without mutating storage. This is used
300
- * for explicitly abandoned sessions, never as an implicit effect cleanup.
301
- */
302
- public cancel(): Promise<DocumentSessionSnapshot> {
303
- return this.enqueueOperation(async () => {
304
- this.lifecycle = 'transitioning';
305
- this.frozen = true;
306
- this.haltDrain = true;
307
- this.clearAutoSaveTimer();
308
- this.emit();
309
- await this.awaitActiveDrain();
310
- this.discardRecoverySnapshot();
311
- this.detach();
312
- return this.snapshot;
313
- });
314
- }
315
-
316
- /**
317
- * Observe a filesystem/host change captured for targetKey. Clean sessions
318
- * adopt it. Dirty or saving sessions preserve local text and enter conflict.
319
- */
320
- public observeExternal(change: DocumentExternalSnapshot): DocumentExternalChangeResult {
321
- if (!this.target || change.targetKey !== this.target.key || this.lifecycle === 'closed') {
322
- return 'ignored';
323
- }
324
- if (change.sequence !== undefined) {
325
- if (!Number.isSafeInteger(change.sequence) || change.sequence <= this.externalSequence) {
326
- return 'ignored';
327
- }
328
- this.externalSequence = change.sequence;
329
- }
330
-
331
- // While conflicted, a current observation equal to the latest local
332
- // snapshot proves that revision durable without requiring another write.
333
- // Never acknowledge while a commit is still running; its eventual result
334
- // remains authoritative for the serialized drain.
335
- if (
336
- this.conflict &&
337
- change.content === this.content &&
338
- this.savingRevision === null &&
339
- this.lifecycle === 'open'
340
- ) {
341
- this.clearAutoSaveTimer();
342
- this.autoSaveRetryAttempt = 0;
343
- this.persistedRevision = this.revision;
344
- this.persistedContent = this.content;
345
- this.haltDrain = false;
346
- this.error = null;
347
- this.conflict = null;
348
- this.discardRecoverySnapshot();
349
- this.emit();
350
- return 'applied';
351
- }
352
-
353
- // Once a conflict exists, every later accepted observation refreshes its
354
- // external branch before ordinary duplicate suppression. In particular,
355
- // A -> local C -> external B -> external A must offer A, not stale B, when
356
- // the user chooses Reload external.
357
- if (this.conflict) {
358
- const externalVersion = change.version ?? null;
359
- if (
360
- this.conflict.localContent === this.content &&
361
- this.conflict.localRevision === this.revision &&
362
- this.conflict.externalContent === change.content &&
363
- this.conflict.externalVersion === externalVersion
364
- ) {
365
- return 'ignored';
366
- }
367
-
368
- this.clearAutoSaveTimer();
369
- this.haltDrain = true;
370
- this.conflict = {
371
- targetKey: change.targetKey,
372
- localContent: this.content,
373
- localRevision: this.revision,
374
- externalContent: change.content,
375
- externalVersion,
376
- };
377
- this.error = null;
378
- this.writeRecoverySnapshot();
379
- this.emit();
380
- return 'conflict';
381
- }
382
-
383
- if (
384
- change.content === this.content ||
385
- change.content === this.persistedContent ||
386
- (this.savingRevision !== null && change.content === this.savingContent)
387
- ) {
388
- return 'ignored';
389
- }
390
-
391
- const isClean =
392
- this.persistedRevision === this.revision &&
393
- this.savingRevision === null &&
394
- this.error === null &&
395
- this.conflict === null;
396
-
397
- if (isClean && change.content !== null) {
398
- this.content = change.content;
399
- this.revision += 1;
400
- this.persistedRevision = this.revision;
401
- this.persistedContent = change.content;
402
- this.generation += 1;
403
- this.discardRecoverySnapshot();
404
- this.emit();
405
- return 'applied';
406
- }
407
-
408
- this.clearAutoSaveTimer();
409
- this.haltDrain = true;
410
- this.conflict = {
411
- targetKey: change.targetKey,
412
- localContent: this.content,
413
- localRevision: this.revision,
414
- externalContent: change.content,
415
- externalVersion: change.version ?? null,
416
- };
417
- this.error = null;
418
- this.writeRecoverySnapshot();
419
- this.emit();
420
- return 'conflict';
421
- }
422
-
423
- public resolveConflict(strategy: DocumentConflictStrategy): Promise<DocumentSessionSnapshot> {
424
- return this.enqueueOperation(async () => {
425
- if (!this.conflict) return this.snapshot;
426
-
427
- this.clearAutoSaveTimer();
428
- this.haltDrain = true;
429
- await this.awaitActiveDrain();
430
- // A watcher may refresh or converge the conflict while an in-flight
431
- // commit settles. Resolve the branch that is current now, never the
432
- // snapshot captured before the await.
433
- const currentConflict = this.conflict;
434
- if (!currentConflict) return this.snapshot;
435
-
436
- if (strategy === 'use-external') {
437
- this.discardRecoverySnapshot();
438
- this.generation += 1;
439
- this.revision += 1;
440
- this.conflict = null;
441
- this.error = null;
442
- this.haltDrain = false;
443
-
444
- if (currentConflict.externalContent === null) {
445
- this.target = null;
446
- this.content = '';
447
- this.persistedContent = null;
448
- this.persistedRevision = this.revision;
449
- this.lifecycle = 'closed';
450
- this.frozen = true;
451
- } else {
452
- this.content = currentConflict.externalContent;
453
- this.persistedContent = currentConflict.externalContent;
454
- this.persistedRevision = this.revision;
455
- this.lifecycle = 'open';
456
- this.frozen = false;
457
- }
458
- this.emit();
459
- return this.snapshot;
460
- }
461
-
462
- // Keep the local branch, but make the observed external snapshot the
463
- // optimistic baseline for the next conditional commit.
464
- this.persistedContent = currentConflict.externalContent;
465
- // A clean external deletion/conflict has no unsaved local revision.
466
- // Keeping local still requires a fresh commit that recreates/overwrites
467
- // the target, so make that intent explicit with a new revision.
468
- this.revision += 1;
469
- this.conflict = null;
470
- this.error = null;
471
- this.haltDrain = false;
472
- this.lifecycle = 'open';
473
- this.frozen = false;
474
- this.writeRecoverySnapshot();
475
- this.emit();
476
- return this.flush('conflict');
477
- });
478
- }
479
-
480
- /**
481
- * Freeze edits and flush through the latest revision. A successful call
482
- * remains frozen until cancelClose(); the owning host may now destroy it.
483
- */
484
- public prepareClose(): Promise<DocumentSessionSnapshot> {
485
- return this.enqueueOperation(async () => {
486
- if (this.lifecycle === 'closing') return this.snapshot;
487
- this.lifecycle = 'closing';
488
- this.frozen = true;
489
- this.haltDrain = false;
490
- this.emit();
491
- try {
492
- await this.flush('close');
493
- return this.snapshot;
494
- } catch (error: unknown) {
495
- this.lifecycle = 'open';
496
- this.frozen = false;
497
- this.emit();
498
- throw error;
499
- }
500
- });
501
- }
502
-
503
- /** Resume editing when a host cancels a prepared close. */
504
- public cancelClose(): void {
505
- if (this.lifecycle !== 'closing') return;
506
- this.lifecycle = 'open';
507
- this.frozen = false;
508
- this.haltDrain = false;
509
- if (this.target && this.persistedRevision < this.revision && !this.conflict) {
510
- this.scheduleAutoSave();
511
- }
512
- this.emit();
513
- }
514
-
515
- private async performTransition(
516
- load: () => Promise<DocumentSessionTransition | null>,
517
- ): Promise<DocumentSessionSnapshot | null> {
518
- this.assertNotPreparingClose();
519
- this.lifecycle = 'transitioning';
520
- this.frozen = true;
521
- this.emit();
522
-
523
- try {
524
- this.haltDrain = false;
525
- await this.flush('transition');
526
- this.clearAutoSaveTimer();
527
- const next = await load();
528
- if (!next) {
529
- this.lifecycle = 'open';
530
- this.frozen = false;
531
- this.emit();
532
- return null;
533
- }
534
-
535
- this.generation += 1;
536
- this.externalSequence = -1;
537
- this.target = next.target;
538
- this.content = next.content;
539
- this.revision += 1;
540
- this.persistedRevision = this.revision;
541
- this.persistedContent = next.target ? next.content : null;
542
- this.savingRevision = null;
543
- this.savingContent = null;
544
- this.error = null;
545
- this.conflict = null;
546
- this.haltDrain = false;
547
- this.lifecycle = 'open';
548
- this.frozen = false;
549
- const recoveryState = this.restoreRecoverySnapshot();
550
- if (recoveryState === 'dirty') this.scheduleAutoSave();
551
- this.emit();
552
- return this.snapshot;
553
- } catch (error: unknown) {
554
- this.lifecycle = 'open';
555
- this.frozen = false;
556
- this.emit();
557
- throw error;
558
- }
559
- }
560
-
561
- private scheduleAutoSave(delayMs = this.autoSaveDelayMs): void {
562
- this.clearAutoSaveTimer();
563
- if (!this.autoSaveEnabled) return;
564
- this.autoSaveTimer = setTimeout(() => {
565
- this.autoSaveTimer = null;
566
- const target = this.target;
567
- const generation = this.generation;
568
- void this.flush('autosave').then(
569
- () => {
570
- this.autoSaveRetryAttempt = 0;
571
- },
572
- () => {
573
- // Preserve honest error/conflict state while making ordinary I/O
574
- // failures recoverable. A newer edit already owns a fresh debounce
575
- // timer, and lifecycle operations cancel this retry path.
576
- if (
577
- this.autoSaveTimer !== null ||
578
- !target ||
579
- target !== this.target ||
580
- generation !== this.generation ||
581
- this.lifecycle !== 'open' ||
582
- this.frozen ||
583
- this.haltDrain ||
584
- this.conflict ||
585
- this.persistedRevision >= this.revision
586
- ) {
587
- return;
588
- }
589
- const retryDelay = this.autoSaveRetryDelaysMs[this.autoSaveRetryAttempt];
590
- if (retryDelay === undefined) return;
591
- this.autoSaveRetryAttempt += 1;
592
- this.scheduleAutoSave(retryDelay);
593
- },
594
- );
595
- }, delayMs);
596
- }
597
-
598
- private clearAutoSaveTimer(): void {
599
- if (this.autoSaveTimer === null) return;
600
- clearTimeout(this.autoSaveTimer);
601
- this.autoSaveTimer = null;
602
- }
603
-
604
- private ensureDrain(reason: DocumentSaveReason): Promise<void> {
605
- if (this.drainPromise) return this.drainPromise;
606
- const running = this.drain(reason);
607
- this.drainPromise = running;
608
- const clear = () => {
609
- if (this.drainPromise === running) this.drainPromise = null;
610
- };
611
- void running.then(clear, clear);
612
- return running;
613
- }
614
-
615
- private async drain(reason: DocumentSaveReason): Promise<void> {
616
- while (
617
- this.target &&
618
- !this.haltDrain &&
619
- !this.conflict &&
620
- this.persistedRevision < this.revision
621
- ) {
622
- const target = this.target;
623
- const generation = this.generation;
624
- const saveRevision = this.revision;
625
- const saveContent = this.content;
626
- const previousPersistedRevision = this.persistedRevision;
627
- const previousPersistedContent = this.persistedContent;
628
-
629
- this.savingRevision = saveRevision;
630
- this.savingContent = saveContent;
631
- this.error = null;
632
- this.emit();
633
-
634
- try {
635
- await target.commit({
636
- targetKey: target.key,
637
- content: saveContent,
638
- revision: saveRevision,
639
- persistedRevision: previousPersistedRevision,
640
- persistedContent: previousPersistedContent,
641
- reason,
642
- });
643
- } catch (error: unknown) {
644
- this.savingRevision = null;
645
- this.savingContent = null;
646
- if (error instanceof DocumentCommitConflictError) {
647
- this.haltDrain = true;
648
- this.conflict = {
649
- targetKey: target.key,
650
- localContent: this.content,
651
- localRevision: this.revision,
652
- externalContent: error.externalContent,
653
- externalVersion: error.externalVersion,
654
- };
655
- this.error = null;
656
- } else {
657
- this.error = toError(error);
658
- }
659
- this.emit();
660
- throw error;
661
- }
662
-
663
- if (generation !== this.generation || target !== this.target) return;
664
- this.persistedRevision = Math.max(this.persistedRevision, saveRevision);
665
- this.persistedContent = saveContent;
666
- this.autoSaveRetryAttempt = 0;
667
- this.savingRevision = null;
668
- this.savingContent = null;
669
- this.error = null;
670
- const activeConflict = this.getSnapshot().conflict;
671
- if (activeConflict?.externalContent === saveContent && this.content === saveContent) {
672
- // An observation that matched the in-flight local snapshot could not
673
- // be acknowledged until the commit settled. Its successful return now
674
- // proves both branches converged on the same durable revision.
675
- this.conflict = null;
676
- this.haltDrain = false;
677
- }
678
- this.recoveryJournal?.acknowledge({
679
- targetKey: target.key,
680
- generation,
681
- persistedRevision: this.persistedRevision,
682
- });
683
- this.emit();
684
- }
685
- }
686
-
687
- private async awaitActiveDrain(): Promise<void> {
688
- const active = this.drainPromise;
689
- if (!active) return;
690
- try {
691
- await active;
692
- } catch {
693
- // A destructive cancellation still waits for the operation to settle,
694
- // but a failed obsolete write must not prevent the requested deletion.
695
- }
696
- }
697
-
698
- /**
699
- * Rehydrate a synchronous crash snapshot after the durable baseline has
700
- * been read. A matching baseline is safe to resume as dirty; a changed
701
- * baseline becomes an explicit conflict and is never auto-overwritten.
702
- */
703
- private restoreRecoverySnapshot(): 'none' | 'dirty' | 'conflict' {
704
- if (!this.target || !this.recoveryJournal) return 'none';
705
- const recovered = this.recoveryJournal.lookup(this.target.key);
706
- if (!recovered) return 'none';
707
-
708
- if (recovered.content === this.content) {
709
- this.recoveryJournal.acknowledge({
710
- targetKey: recovered.targetKey,
711
- generation: recovered.generation,
712
- persistedRevision: recovered.revision,
713
- });
714
- return 'none';
715
- }
716
-
717
- const externalContent = this.content;
718
- this.generation = Math.max(this.generation, recovered.generation) + 1;
719
- this.revision = Math.max(this.revision, recovered.revision) + 1;
720
- this.content = recovered.content;
721
-
722
- if (recovered.persistedContent === externalContent) {
723
- this.writeRecoverySnapshot();
724
- return 'dirty';
725
- }
726
-
727
- // Preserve the journal's prior baseline so a second crash continues to
728
- // reopen as a conflict until the user explicitly chooses a branch.
729
- this.persistedContent = recovered.persistedContent;
730
- this.haltDrain = true;
731
- this.conflict = {
732
- targetKey: this.target.key,
733
- localContent: recovered.content,
734
- localRevision: this.revision,
735
- externalContent,
736
- externalVersion: null,
737
- };
738
- this.writeRecoverySnapshot();
739
- return 'conflict';
740
- }
741
-
742
- private writeRecoverySnapshot(): void {
743
- if (!this.target || !this.recoveryJournal) return;
744
- if (this.persistedRevision >= this.revision && !this.conflict) return;
745
- this.recoveryJournal.write({
746
- targetKey: this.target.key,
747
- generation: this.generation,
748
- revision: this.revision,
749
- content: this.content,
750
- persistedContent: this.persistedContent,
751
- });
752
- }
753
-
754
- private discardRecoverySnapshot(): void {
755
- if (!this.target) return;
756
- this.recoveryJournal?.discard(this.target.key);
757
- }
758
-
759
- private detach(): void {
760
- this.clearAutoSaveTimer();
761
- this.autoSaveRetryAttempt = 0;
762
- this.generation += 1;
763
- this.externalSequence = -1;
764
- this.target = null;
765
- this.persistedContent = null;
766
- this.savingRevision = null;
767
- this.savingContent = null;
768
- this.error = null;
769
- this.conflict = null;
770
- this.haltDrain = false;
771
- this.lifecycle = 'closed';
772
- this.frozen = true;
773
- this.emit();
774
- }
775
-
776
- private assertNotPreparingClose(): void {
777
- if (this.lifecycle === 'closing') {
778
- throw new Error('The document is already prepared for close.');
779
- }
780
- }
781
-
782
- private enqueueOperation<T>(operation: () => Promise<T>): Promise<T> {
783
- const run = this.operationTail.then(operation, operation);
784
- this.operationTail = run.then(
785
- () => undefined,
786
- () => undefined,
787
- );
788
- return run;
789
- }
790
-
791
- private createSnapshot(): DocumentSessionSnapshot {
792
- return Object.freeze({
793
- targetKey: this.target?.key ?? null,
794
- content: this.content,
795
- revision: this.revision,
796
- persistedRevision: this.persistedRevision,
797
- persistedContent: this.persistedContent,
798
- savingRevision: this.savingRevision,
799
- generation: this.generation,
800
- status: this.computeStatus(),
801
- lifecycle: this.lifecycle,
802
- frozen: this.frozen,
803
- error: this.error,
804
- conflict: this.conflict,
805
- });
806
- }
807
-
808
- private computeStatus(): DocumentSessionStatus {
809
- if (this.lifecycle === 'closed') return 'closed';
810
- if (!this.target) return 'idle';
811
- if (this.conflict) return 'conflict';
812
- if (this.error) return 'error';
813
- if (this.savingRevision !== null) return 'saving';
814
- return this.persistedRevision < this.revision ? 'dirty' : 'saved';
815
- }
816
-
817
- private emit(): void {
818
- this.snapshot = this.createSnapshot();
819
- for (const listener of [...this.listeners]) listener();
820
- }
821
- }
822
-
823
- function toError(error: unknown): Error {
824
- return error instanceof Error ? error : new Error(String(error));
825
- }
826
-
827
- function normalizeRetryDelays(delays: readonly number[]): readonly number[] {
828
- return delays.map((delay) => {
829
- if (!Number.isFinite(delay) || delay < 0) {
830
- throw new TypeError('Autosave retry delays must be finite, non-negative numbers.');
831
- }
832
- return Math.floor(delay);
833
- });
834
- }