orgnote-api 0.20.2 → 0.40.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 (283) hide show
  1. package/api.d.ts +93 -98
  2. package/constants/{command-groups.contant.d.ts → command-groups.d.ts} +2 -1
  3. package/constants/{command-groups.contant.js → command-groups.js} +2 -0
  4. package/constants/extension-errors.d.ts +9 -0
  5. package/constants/extension-errors.js +18 -0
  6. package/constants/file-guard-errors.d.ts +8 -0
  7. package/constants/file-guard-errors.js +18 -0
  8. package/constants/git-errors.d.ts +9 -0
  9. package/constants/git-errors.js +18 -0
  10. package/constants/i18n-keys.d.ts +371 -0
  11. package/constants/i18n-keys.js +158 -0
  12. package/constants/index.d.ts +9 -1
  13. package/constants/index.js +9 -1
  14. package/constants/oauth-providers.d.ts +1 -0
  15. package/constants/oauth-providers.js +1 -0
  16. package/constants/route-names.d.ts +36 -0
  17. package/constants/route-names.js +37 -0
  18. package/constants/route-paths.d.ts +5 -0
  19. package/constants/route-paths.js +4 -0
  20. package/constants/style-sizes.d.ts +1 -0
  21. package/constants/style-sizes.js +1 -0
  22. package/encryption/__tests__/encryption.spec.js +4 -5
  23. package/encryption/__tests__/note-encryption.spec.js +46 -348
  24. package/encryption/encryption.d.ts +9 -4
  25. package/encryption/encryption.js +25 -5
  26. package/encryption/note-encryption.d.ts +1 -1
  27. package/encryption/note-encryption.js +6 -6
  28. package/files-api.d.ts +0 -1
  29. package/index.d.ts +4 -1
  30. package/index.js +4 -1
  31. package/mappers/orgnode-to-note.d.ts +2 -2
  32. package/mappers/orgnode-to-note.js +3 -2
  33. package/models/auth-store.d.ts +3 -3
  34. package/models/buffer-store.d.ts +14 -0
  35. package/models/buffer.d.ts +24 -0
  36. package/models/colors.d.ts +1 -0
  37. package/models/command.d.ts +13 -8
  38. package/models/commands-group-store.d.ts +11 -0
  39. package/models/commands-store.d.ts +13 -0
  40. package/models/completion-store.d.ts +14 -0
  41. package/models/completion.d.ts +16 -7
  42. package/models/config-store.d.ts +9 -0
  43. package/models/confirmation-modal.d.ts +11 -0
  44. package/models/context-menu-store.d.ts +10 -0
  45. package/models/cron-store.d.ts +21 -0
  46. package/models/cron-task.d.ts +30 -0
  47. package/models/css-utils.d.ts +17 -0
  48. package/models/default-commands.d.ts +52 -3
  49. package/models/default-commands.js +59 -2
  50. package/models/encryption-store.d.ts +10 -0
  51. package/models/encryption-store.js +1 -0
  52. package/models/encryption.d.ts +54 -11
  53. package/models/encryption.js +28 -1
  54. package/models/extension-registry-store.d.ts +9 -0
  55. package/models/extension-registry-store.js +1 -0
  56. package/models/extension-store.d.ts +18 -0
  57. package/models/extension-store.js +1 -0
  58. package/models/extension.d.ts +96 -24
  59. package/models/extension.js +88 -1
  60. package/models/file-guard-store.d.ts +14 -0
  61. package/models/file-guard-store.js +1 -0
  62. package/models/file-guard.d.ts +27 -0
  63. package/models/file-guard.js +1 -0
  64. package/models/{file-cache.d.ts → file-info.d.ts} +1 -1
  65. package/models/file-info.js +1 -0
  66. package/models/file-manager-store.d.ts +10 -12
  67. package/models/file-opener-store.d.ts +6 -4
  68. package/models/file-system-manager-store.d.ts +13 -0
  69. package/models/file-system-manager-store.js +1 -0
  70. package/models/file-system-store.d.ts +19 -0
  71. package/models/file-system-store.js +1 -0
  72. package/models/file-system.d.ts +32 -5
  73. package/models/file-system.js +2 -0
  74. package/models/file-upload.d.ts +6 -0
  75. package/models/file-upload.js +1 -0
  76. package/models/file-watcher-store.d.ts +18 -0
  77. package/models/file-watcher-store.js +1 -0
  78. package/models/files-store.d.ts +2 -2
  79. package/models/git-store.d.ts +12 -0
  80. package/models/git-store.js +1 -0
  81. package/models/git.d.ts +47 -0
  82. package/models/git.js +1 -0
  83. package/models/i18n-keys.d.ts +2 -0
  84. package/models/i18n-keys.js +1 -0
  85. package/models/index.d.ts +56 -2
  86. package/models/index.js +59 -2
  87. package/models/layout-snapshot-repository.d.ts +14 -0
  88. package/models/layout-snapshot-repository.js +1 -0
  89. package/models/layout-store.d.ts +17 -0
  90. package/models/layout-store.js +1 -0
  91. package/models/layout.d.ts +16 -0
  92. package/models/layout.js +1 -0
  93. package/models/log-repository.d.ts +17 -0
  94. package/models/log-repository.js +1 -0
  95. package/models/log-store.d.ts +15 -0
  96. package/models/log-store.js +1 -0
  97. package/models/log.d.ts +12 -0
  98. package/models/log.js +1 -0
  99. package/models/logger.d.ts +8 -0
  100. package/models/logger.js +1 -0
  101. package/models/menu-action.d.ts +18 -0
  102. package/models/menu-action.js +1 -0
  103. package/models/modal-store.d.ts +16 -0
  104. package/models/modal-store.js +1 -0
  105. package/models/modal.d.ts +10 -2
  106. package/models/note.d.ts +30 -13
  107. package/models/notification-config.d.ts +14 -0
  108. package/models/notification-config.js +1 -0
  109. package/models/notifications-store.d.ts +20 -0
  110. package/models/notifications-store.js +1 -0
  111. package/models/oauth-provider.d.ts +2 -1
  112. package/models/orgnote-config.d.ts +80 -0
  113. package/models/orgnote-config.js +42 -0
  114. package/models/orgnote-url.d.ts +6 -0
  115. package/models/orgnote-url.js +1 -0
  116. package/models/pane-snapshot-repository.d.ts +0 -0
  117. package/models/pane-snapshot-repository.js +0 -0
  118. package/models/pane.d.ts +38 -0
  119. package/models/pane.js +1 -0
  120. package/models/panes-store.d.ts +30 -0
  121. package/models/panes-store.js +1 -0
  122. package/models/platform-detection.d.ts +11 -0
  123. package/models/platform-detection.js +1 -0
  124. package/models/platform-specific.d.ts +1 -0
  125. package/models/platform-specific.js +1 -0
  126. package/models/platform.d.ts +2 -0
  127. package/models/platform.js +1 -0
  128. package/models/queue-store.d.ts +49 -0
  129. package/models/queue-store.js +1 -0
  130. package/models/queue-task.d.ts +15 -0
  131. package/models/queue-task.js +1 -0
  132. package/models/repositories.d.ts +58 -38
  133. package/models/screen-detection.d.ts +10 -0
  134. package/models/screen-detection.js +1 -0
  135. package/models/settings-store.d.ts +12 -0
  136. package/models/settings-store.js +1 -0
  137. package/models/settings-ui-store.d.ts +7 -0
  138. package/models/settings-ui-store.js +1 -0
  139. package/models/sidebar-store.d.ts +22 -0
  140. package/models/sidebar-store.js +1 -0
  141. package/models/splash-screen.d.ts +13 -0
  142. package/models/splash-screen.js +1 -0
  143. package/models/store.d.ts +1 -1
  144. package/models/style-size.d.ts +2 -0
  145. package/models/style-size.js +1 -0
  146. package/models/style-variant.d.ts +1 -0
  147. package/models/style-variant.js +1 -0
  148. package/models/sync-store.d.ts +8 -6
  149. package/models/sync.d.ts +0 -5
  150. package/models/system-info.d.ts +47 -0
  151. package/models/system-info.js +1 -0
  152. package/models/theme-store.d.ts +16 -0
  153. package/models/theme-store.js +1 -0
  154. package/models/theme-variables.d.ts +4 -189
  155. package/models/theme-variables.js +2 -191
  156. package/models/toolbar-store.d.ts +9 -0
  157. package/models/toolbar-store.js +1 -0
  158. package/models/ui-store.d.ts +6 -0
  159. package/models/ui-store.js +1 -0
  160. package/models/user.d.ts +3 -4
  161. package/models/vue-component.d.ts +4 -2
  162. package/package-lock.json +5553 -0
  163. package/package.json +37 -26
  164. package/remote-api/api.d.ts +288 -669
  165. package/remote-api/api.js +199 -485
  166. package/remote-api/base.js +1 -1
  167. package/remote-api/common.d.ts +1 -1
  168. package/sync/__tests__/memory-state.spec.d.ts +1 -0
  169. package/sync/__tests__/memory-state.spec.js +49 -0
  170. package/sync/__tests__/plan.spec.d.ts +1 -0
  171. package/sync/__tests__/plan.spec.js +116 -0
  172. package/sync/create-sync-plan.d.ts +2 -0
  173. package/sync/create-sync-plan.js +13 -0
  174. package/sync/fetch.d.ts +8 -0
  175. package/sync/fetch.js +32 -0
  176. package/sync/index.d.ts +10 -0
  177. package/sync/index.js +9 -0
  178. package/sync/memory-state.d.ts +2 -0
  179. package/sync/memory-state.js +22 -0
  180. package/sync/operations/conflict.d.ts +10 -0
  181. package/sync/operations/conflict.js +56 -0
  182. package/sync/operations/delete-local.d.ts +2 -0
  183. package/sync/operations/delete-local.js +17 -0
  184. package/sync/operations/delete-remote.d.ts +2 -0
  185. package/sync/operations/delete-remote.js +26 -0
  186. package/sync/operations/download.d.ts +2 -0
  187. package/sync/operations/download.js +20 -0
  188. package/sync/operations/index.d.ts +5 -0
  189. package/sync/operations/index.js +5 -0
  190. package/sync/operations/synced-file.d.ts +14 -0
  191. package/sync/operations/synced-file.js +5 -0
  192. package/sync/operations/upload.d.ts +2 -0
  193. package/sync/operations/upload.js +30 -0
  194. package/sync/plan.d.ts +9 -0
  195. package/sync/plan.js +57 -0
  196. package/sync/recovery.d.ts +2 -0
  197. package/sync/recovery.js +6 -0
  198. package/sync/scan.d.ts +4 -0
  199. package/sync/scan.js +40 -0
  200. package/sync/types.d.ts +74 -0
  201. package/sync/types.js +7 -0
  202. package/sync/utils/__tests__/oldest-synced-at.spec.d.ts +1 -0
  203. package/sync/utils/__tests__/oldest-synced-at.spec.js +38 -0
  204. package/sync/utils/oldest-synced-at.d.ts +2 -0
  205. package/sync/utils/oldest-synced-at.js +9 -0
  206. package/types/index.d.ts +0 -0
  207. package/types/index.js +0 -0
  208. package/utils/__tests__/find-files-diff.spec.d.ts +1 -0
  209. package/{tools → utils}/__tests__/find-files-diff.spec.js +3 -3
  210. package/utils/__tests__/find-note-files-diff.spec.d.ts +1 -0
  211. package/{tools → utils}/__tests__/find-note-files-diff.spec.js +5 -5
  212. package/utils/__tests__/get-file-name.spec.d.ts +1 -0
  213. package/utils/__tests__/get-string-path.spec.d.ts +1 -0
  214. package/utils/__tests__/is-gpg-encrypted.spec.d.ts +1 -0
  215. package/utils/__tests__/is-org-file.spec.d.ts +1 -0
  216. package/utils/__tests__/join.spec.d.ts +1 -0
  217. package/utils/__tests__/join.spec.js +32 -0
  218. package/utils/__tests__/nullable-guards.spec.d.ts +1 -0
  219. package/utils/__tests__/nullable-guards.spec.js +44 -0
  220. package/utils/__tests__/parent-folder.spec.d.ts +1 -0
  221. package/utils/__tests__/read-org-files-recursively.spec.d.ts +1 -0
  222. package/utils/__tests__/split-path.spec.d.ts +1 -0
  223. package/utils/__tests__/to-absolute-path.spec.d.ts +1 -0
  224. package/utils/__tests__/to-absolute-path.spec.js +26 -0
  225. package/utils/__tests__/to-error.spec.d.ts +1 -0
  226. package/utils/__tests__/to-error.spec.js +112 -0
  227. package/utils/__tests__/with-root.spec.d.ts +1 -0
  228. package/utils/__tests__/with-root.spec.js +20 -0
  229. package/{tools → utils}/find-notes-files-diff.js +6 -3
  230. package/{tools → utils}/index.d.ts +4 -1
  231. package/{tools → utils}/index.js +4 -1
  232. package/utils/join-path.d.ts +1 -0
  233. package/utils/join-path.js +13 -0
  234. package/utils/nullable-guards.d.ts +2 -0
  235. package/utils/nullable-guards.js +6 -0
  236. package/utils/to-absolute-path.d.ts +2 -0
  237. package/utils/to-absolute-path.js +2 -0
  238. package/utils/to-error.d.ts +6 -0
  239. package/utils/to-error.js +33 -0
  240. package/utils/toml.d.ts +3 -0
  241. package/utils/toml.js +31 -0
  242. package/utils/with-root.d.ts +1 -0
  243. package/utils/with-root.js +6 -0
  244. package/websocket/client.d.ts +24 -0
  245. package/websocket/client.js +83 -0
  246. package/models/file-tree.d.ts +0 -12
  247. package/tools/__tests__/join.spec.js +0 -24
  248. package/tools/join-path.d.ts +0 -1
  249. package/tools/join-path.js +0 -7
  250. package/tools/mock-server.d.ts +0 -1
  251. package/tools/mock-server.js +0 -12
  252. /package/models/{file-cache.js → buffer-store.js} +0 -0
  253. /package/models/{file-tree.js → buffer.js} +0 -0
  254. /package/{tools/__tests__/find-files-diff.spec.d.ts → models/colors.js} +0 -0
  255. /package/{tools/__tests__/find-note-files-diff.spec.d.ts → models/commands-group-store.js} +0 -0
  256. /package/{tools/__tests__/get-file-name.spec.d.ts → models/commands-store.js} +0 -0
  257. /package/{tools/__tests__/get-string-path.spec.d.ts → models/completion-store.js} +0 -0
  258. /package/{tools/__tests__/is-gpg-encrypted.spec.d.ts → models/config-store.js} +0 -0
  259. /package/{tools/__tests__/is-org-file.spec.d.ts → models/confirmation-modal.js} +0 -0
  260. /package/{tools/__tests__/join.spec.d.ts → models/context-menu-store.js} +0 -0
  261. /package/{tools/__tests__/parent-folder.spec.d.ts → models/cron-store.js} +0 -0
  262. /package/{tools/__tests__/read-org-files-recursively.spec.d.ts → models/cron-task.js} +0 -0
  263. /package/{tools/__tests__/split-path.spec.d.ts → models/css-utils.js} +0 -0
  264. /package/{tools → utils}/__tests__/get-file-name.spec.js +0 -0
  265. /package/{tools → utils}/__tests__/get-string-path.spec.js +0 -0
  266. /package/{tools → utils}/__tests__/is-gpg-encrypted.spec.js +0 -0
  267. /package/{tools → utils}/__tests__/is-org-file.spec.js +0 -0
  268. /package/{tools → utils}/__tests__/parent-folder.spec.js +0 -0
  269. /package/{tools → utils}/__tests__/read-org-files-recursively.spec.js +0 -0
  270. /package/{tools → utils}/__tests__/split-path.spec.js +0 -0
  271. /package/{tools → utils}/find-notes-files-diff.d.ts +0 -0
  272. /package/{tools → utils}/get-file-name.d.ts +0 -0
  273. /package/{tools → utils}/get-file-name.js +0 -0
  274. /package/{tools → utils}/get-parent-dir.d.ts +0 -0
  275. /package/{tools → utils}/get-parent-dir.js +0 -0
  276. /package/{tools → utils}/get-string-path.d.ts +0 -0
  277. /package/{tools → utils}/get-string-path.js +0 -0
  278. /package/{tools → utils}/is-gpg-encrypted.d.ts +0 -0
  279. /package/{tools → utils}/is-gpg-encrypted.js +0 -0
  280. /package/{tools → utils}/is-org-file.d.ts +0 -0
  281. /package/{tools → utils}/is-org-file.js +0 -0
  282. /package/{tools → utils}/split-path.d.ts +0 -0
  283. /package/{tools → utils}/split-path.js +0 -0
@@ -12,7 +12,7 @@
12
12
  * Do not edit the class manually.
13
13
  */
14
14
  import globalAxios from 'axios';
15
- export const BASE_PATH = "http://org-note.com/api/v1".replace(/\/+$/, "");
15
+ export const BASE_PATH = "http://localhost:8000/v1".replace(/\/+$/, "");
16
16
  /**
17
17
  *
18
18
  * @export
@@ -62,4 +62,4 @@ export declare const toPathString: (url: URL) => string;
62
62
  *
63
63
  * @export
64
64
  */
65
- export declare const createRequestFunction: (axiosArgs: RequestArgs, globalAxios: AxiosInstance, BASE_PATH: string, configuration?: Configuration) => <T = unknown, R = AxiosResponse<T, any>>(axios?: AxiosInstance, basePath?: string) => Promise<R>;
65
+ export declare const createRequestFunction: (axiosArgs: RequestArgs, globalAxios: AxiosInstance, BASE_PATH: string, configuration?: Configuration) => <T = unknown, R = AxiosResponse<T>>(axios?: AxiosInstance, basePath?: string) => Promise<R>;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,49 @@
1
+ import { test, expect } from 'vitest';
2
+ import { createMemorySyncState } from "../memory-state.js";
3
+ test('initial state empty', async () => {
4
+ const state = createMemorySyncState();
5
+ const data = await state.get();
6
+ expect(data.files).toEqual({});
7
+ });
8
+ test('initial with values', async () => {
9
+ const state = createMemorySyncState({
10
+ files: { 'a.org': { mtime: 1000, size: 100, status: 'synced' } },
11
+ });
12
+ const data = await state.get();
13
+ expect(data.files['a.org'].mtime).toBe(1000);
14
+ });
15
+ test('setFile and getFile', async () => {
16
+ const state = createMemorySyncState();
17
+ await state.setFile('b.org', { mtime: 2000, size: 200, status: 'dirty' });
18
+ const file = await state.getFile('b.org');
19
+ expect(file?.mtime).toBe(2000);
20
+ expect(file?.status).toBe('dirty');
21
+ });
22
+ test('getFile returns null for missing', async () => {
23
+ const state = createMemorySyncState();
24
+ const file = await state.getFile('missing.org');
25
+ expect(file).toBeNull();
26
+ });
27
+ test('removeFile', async () => {
28
+ const state = createMemorySyncState();
29
+ await state.setFile('c.org', { mtime: 1000, size: 100, status: 'synced' });
30
+ await state.removeFile('c.org');
31
+ const file = await state.getFile('c.org');
32
+ expect(file).toBeNull();
33
+ });
34
+ test('clear removes all', async () => {
35
+ const state = createMemorySyncState();
36
+ await state.setFile('d.org', { mtime: 1000, size: 100, status: 'synced' });
37
+ await state.setFile('e.org', { mtime: 2000, size: 200, status: 'synced' });
38
+ await state.clear();
39
+ const data = await state.get();
40
+ expect(data.files).toEqual({});
41
+ });
42
+ test('get returns copy', async () => {
43
+ const state = createMemorySyncState();
44
+ await state.setFile('f.org', { mtime: 1000, size: 100, status: 'synced' });
45
+ const data1 = await state.get();
46
+ data1.files['g.org'] = { mtime: 2000, size: 200, status: 'synced' };
47
+ const data2 = await state.get();
48
+ expect(data2.files['g.org']).toBeUndefined();
49
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,116 @@
1
+ import { test, expect } from 'vitest';
2
+ import { createPlan } from "../plan.js";
3
+ const emptyState = { files: {} };
4
+ const serverTime = '2024-01-01T00:00:00Z';
5
+ test('new local file → upload', () => {
6
+ const localFiles = [{ path: 'a.org', mtime: 1000, size: 100 }];
7
+ const plan = createPlan({ localFiles, deletedLocally: [], remoteFiles: [], stateData: emptyState, serverTime });
8
+ expect(plan.toUpload).toHaveLength(1);
9
+ expect(plan.toUpload[0].path).toBe('a.org');
10
+ });
11
+ test('new remote file → download', () => {
12
+ const remoteFiles = [
13
+ { path: 'b.org', version: 1, deleted: false, updatedAt: '' },
14
+ ];
15
+ const plan = createPlan({ localFiles: [], deletedLocally: [], remoteFiles, stateData: emptyState, serverTime });
16
+ expect(plan.toDownload).toHaveLength(1);
17
+ expect(plan.toDownload[0].path).toBe('b.org');
18
+ });
19
+ test('unchanged file → skip', () => {
20
+ const localFiles = [{ path: 'c.org', mtime: 1000, size: 100 }];
21
+ const remoteFiles = [
22
+ { path: 'c.org', version: 1, deleted: false, updatedAt: '' },
23
+ ];
24
+ const stateData = {
25
+ files: { 'c.org': { mtime: 1000, size: 100, version: 1, status: 'synced' } },
26
+ };
27
+ const plan = createPlan({ localFiles, deletedLocally: [], remoteFiles, stateData, serverTime });
28
+ expect(plan.toUpload).toHaveLength(0);
29
+ expect(plan.toDownload).toHaveLength(0);
30
+ });
31
+ test('local changed → upload', () => {
32
+ const localFiles = [{ path: 'd.org', mtime: 2000, size: 100 }];
33
+ const remoteFiles = [
34
+ { path: 'd.org', version: 1, deleted: false, updatedAt: '' },
35
+ ];
36
+ const stateData = {
37
+ files: { 'd.org': { mtime: 1000, size: 100, version: 1, status: 'synced' } },
38
+ };
39
+ const plan = createPlan({ localFiles, deletedLocally: [], remoteFiles, stateData, serverTime });
40
+ expect(plan.toUpload).toHaveLength(1);
41
+ });
42
+ test('remote changed → download', () => {
43
+ const localFiles = [{ path: 'e.org', mtime: 1000, size: 100 }];
44
+ const remoteFiles = [
45
+ { path: 'e.org', version: 2, deleted: false, updatedAt: '' },
46
+ ];
47
+ const stateData = {
48
+ files: { 'e.org': { mtime: 1000, size: 100, version: 1, status: 'synced' } },
49
+ };
50
+ const plan = createPlan({ localFiles, deletedLocally: [], remoteFiles, stateData, serverTime });
51
+ expect(plan.toDownload).toHaveLength(1);
52
+ });
53
+ test('both changed → upload (conflict handled by server)', () => {
54
+ const localFiles = [{ path: 'f.org', mtime: 3000, size: 100 }];
55
+ const remoteFiles = [
56
+ { path: 'f.org', version: 2, deleted: false, updatedAt: '1970-01-01T00:00:02.000Z' },
57
+ ];
58
+ const stateData = {
59
+ files: { 'f.org': { mtime: 1000, size: 100, version: 1, status: 'synced' } },
60
+ };
61
+ const plan = createPlan({ localFiles, deletedLocally: [], remoteFiles, stateData, serverTime });
62
+ expect(plan.toUpload).toHaveLength(1);
63
+ expect(plan.toDownload).toHaveLength(0);
64
+ });
65
+ test('deleted locally → delete remote', () => {
66
+ const deletedLocally = ['g.org'];
67
+ const stateData = {
68
+ files: { 'g.org': { mtime: 1000, size: 100, version: 1, status: 'synced' } },
69
+ };
70
+ const plan = createPlan({ localFiles: [], deletedLocally, remoteFiles: [], stateData, serverTime });
71
+ expect(plan.toDeleteRemote).toContain('g.org');
72
+ });
73
+ test('deleted remotely → delete local', () => {
74
+ const localFiles = [{ path: 'h.org', mtime: 1000, size: 100 }];
75
+ const remoteFiles = [
76
+ { path: 'h.org', version: 2, deleted: true, updatedAt: '' },
77
+ ];
78
+ const stateData = {
79
+ files: { 'h.org': { mtime: 1000, size: 100, version: 1, status: 'synced' } },
80
+ };
81
+ const plan = createPlan({ localFiles, deletedLocally: [], remoteFiles, stateData, serverTime });
82
+ expect(plan.toDeleteLocal).toContain('h.org');
83
+ });
84
+ test('deleted locally but modified remotely → download', () => {
85
+ const deletedLocally = ['i.org'];
86
+ const remoteFiles = [
87
+ { path: 'i.org', version: 2, deleted: false, updatedAt: '' },
88
+ ];
89
+ const stateData = {
90
+ files: { 'i.org': { mtime: 1000, size: 100, version: 1, status: 'synced' } },
91
+ };
92
+ const plan = createPlan({ localFiles: [], deletedLocally, remoteFiles, stateData, serverTime });
93
+ expect(plan.toDownload).toHaveLength(1);
94
+ expect(plan.toDeleteRemote).toHaveLength(0);
95
+ });
96
+ test('deleted remotely but modified locally → upload (local changes win)', () => {
97
+ const localFiles = [{ path: 'j.org', mtime: 3000, size: 100 }];
98
+ const remoteFiles = [
99
+ { path: 'j.org', version: 2, deleted: true, updatedAt: '1970-01-01T00:00:02.000Z' },
100
+ ];
101
+ const stateData = {
102
+ files: { 'j.org': { mtime: 1000, size: 100, version: 1, status: 'synced' } },
103
+ };
104
+ const plan = createPlan({ localFiles, deletedLocally: [], remoteFiles, stateData, serverTime });
105
+ expect(plan.toUpload).toHaveLength(1);
106
+ expect(plan.toDeleteLocal).toHaveLength(0);
107
+ });
108
+ test('file with error status → retry upload', () => {
109
+ const localFiles = [{ path: 'k.org', mtime: 1000, size: 100 }];
110
+ const stateData = {
111
+ files: { 'k.org': { mtime: 1000, size: 100, version: 1, status: 'error', errorMessage: 'some error' } },
112
+ };
113
+ const plan = createPlan({ localFiles, deletedLocally: [], remoteFiles: [], stateData, serverTime });
114
+ expect(plan.toUpload).toHaveLength(1);
115
+ expect(plan.toUpload[0].path).toBe('k.org');
116
+ });
@@ -0,0 +1,2 @@
1
+ import type { CreateSyncPlanParams, SyncPlan } from './types.js';
2
+ export declare function createSyncPlan(params: CreateSyncPlanParams): Promise<SyncPlan>;
@@ -0,0 +1,13 @@
1
+ import { scanLocalFiles, findDeletedLocally } from "./scan.js";
2
+ import { fetchRemoteChanges } from "./fetch.js";
3
+ import { createPlan } from "./plan.js";
4
+ import { getOldestSyncedAt } from "./utils/oldest-synced-at.js";
5
+ export async function createSyncPlan(params) {
6
+ const { fs, api, state, rootPath, ignorePatterns } = params;
7
+ const stateData = await state.get();
8
+ const localFiles = await scanLocalFiles(fs, rootPath, ignorePatterns);
9
+ const deletedLocally = findDeletedLocally(localFiles, stateData);
10
+ const since = getOldestSyncedAt(stateData);
11
+ const { files: remoteFiles, serverTime } = await fetchRemoteChanges(api, since);
12
+ return createPlan({ localFiles, deletedLocally, remoteFiles, stateData, serverTime });
13
+ }
@@ -0,0 +1,8 @@
1
+ import type { SyncChangesResponse } from '../remote-api/index.js';
2
+ import type { SyncApi, RemoteFile } from './types.js';
3
+ type ServerFields = Pick<SyncChangesResponse, 'serverTime' | 'cursor'>;
4
+ export interface FetchResult extends ServerFields {
5
+ files: RemoteFile[];
6
+ }
7
+ export declare const fetchRemoteChanges: (api: SyncApi, since?: string) => Promise<FetchResult>;
8
+ export {};
package/sync/fetch.js ADDED
@@ -0,0 +1,32 @@
1
+ const DEFAULT_LIMIT = 100;
2
+ export const fetchRemoteChanges = async (api, since) => fetchAllPages(api, since);
3
+ const fetchAllPages = async (api, since, cursor, accumulated = []) => {
4
+ const page = await fetchPage(api, since, cursor);
5
+ const files = [...accumulated, ...page.files];
6
+ if (!page.cursor) {
7
+ return { files, serverTime: page.serverTime };
8
+ }
9
+ return fetchAllPages(api, since, page.cursor, files);
10
+ };
11
+ const toTimestamp = (isoString) => {
12
+ if (!isoString)
13
+ return undefined;
14
+ return new Date(isoString).getTime();
15
+ };
16
+ const fetchPage = async (api, since, cursor) => {
17
+ const sinceMs = toTimestamp(since);
18
+ const response = await api.syncChangesGet(sinceMs, DEFAULT_LIMIT, cursor);
19
+ const data = response.data.data;
20
+ return {
21
+ files: mapChangesToFiles(data.changes),
22
+ serverTime: data.serverTime,
23
+ cursor: data.hasMore ? data.cursor : undefined,
24
+ };
25
+ };
26
+ const mapChangesToFiles = (changes) => changes.map(toRemoteFile);
27
+ const toRemoteFile = (change) => ({
28
+ path: change.path,
29
+ version: change.version,
30
+ deleted: change.deleted,
31
+ updatedAt: change.updatedAt,
32
+ });
@@ -0,0 +1,10 @@
1
+ export { createSyncPlan } from './create-sync-plan.js';
2
+ export { createPlan } from './plan.js';
3
+ export { createMemorySyncState } from './memory-state.js';
4
+ export { scanLocalFiles, findDeletedLocally } from './scan.js';
5
+ export { fetchRemoteChanges } from './fetch.js';
6
+ export { recoverState } from './recovery.js';
7
+ export { getOldestSyncedAt } from './utils/oldest-synced-at.js';
8
+ export { processUpload, processDownload, processDeleteLocal, processDeleteRemote, handleConflict, generateConflictPath, hasConflict, } from './operations/index.js';
9
+ export { SyncOperationType } from './types.js';
10
+ export type { SyncState, SyncStateData, SyncedFile, SyncStatus, LocalFile, RemoteFile, UploadResult, SyncPlan, SyncTask, SyncExecutor, SyncContext, CreateSyncPlanParams, } from './types.js';
package/sync/index.js ADDED
@@ -0,0 +1,9 @@
1
+ export { createSyncPlan } from "./create-sync-plan.js";
2
+ export { createPlan } from "./plan.js";
3
+ export { createMemorySyncState } from "./memory-state.js";
4
+ export { scanLocalFiles, findDeletedLocally } from "./scan.js";
5
+ export { fetchRemoteChanges } from "./fetch.js";
6
+ export { recoverState } from "./recovery.js";
7
+ export { getOldestSyncedAt } from "./utils/oldest-synced-at.js";
8
+ export { processUpload, processDownload, processDeleteLocal, processDeleteRemote, handleConflict, generateConflictPath, hasConflict, } from "./operations/index.js";
9
+ export { SyncOperationType } from "./types.js";
@@ -0,0 +1,2 @@
1
+ import type { SyncState, SyncStateData } from './types.js';
2
+ export declare function createMemorySyncState(initial?: Partial<SyncStateData>): SyncState;
@@ -0,0 +1,22 @@
1
+ export function createMemorySyncState(initial) {
2
+ const data = {
3
+ files: { ...initial?.files },
4
+ };
5
+ return {
6
+ async get() {
7
+ return { files: { ...data.files } };
8
+ },
9
+ async getFile(path) {
10
+ return data.files[path] ?? null;
11
+ },
12
+ async setFile(path, file) {
13
+ data.files[path] = { ...file };
14
+ },
15
+ async removeFile(path) {
16
+ delete data.files[path];
17
+ },
18
+ async clear() {
19
+ data.files = {};
20
+ },
21
+ };
22
+ }
@@ -0,0 +1,10 @@
1
+ import type { SyncContext, UploadResult } from '../types.js';
2
+ type ConflictUploadResult = Extract<UploadResult, {
3
+ status: 'conflict';
4
+ }>;
5
+ export declare const generateConflictPath: (path: string, deviceName?: string) => string;
6
+ export declare const handleConflict: (path: string, conflictResult: ConflictUploadResult, ctx: SyncContext) => Promise<void>;
7
+ export declare const hasConflict: (file: {
8
+ conflictPath?: string;
9
+ }) => boolean;
10
+ export {};
@@ -0,0 +1,56 @@
1
+ import { createSyncedFile } from "./synced-file.js";
2
+ export const generateConflictPath = (path, deviceName = 'device') => {
3
+ const lastDot = path.lastIndexOf('.');
4
+ const ext = lastDot >= 0 ? path.substring(lastDot) : '';
5
+ const base = lastDot >= 0 ? path.substring(0, lastDot) : path;
6
+ const timestamp = Date.now();
7
+ return `${base}.sync-conflict-${timestamp}-${deviceName}${ext}`;
8
+ };
9
+ const copyFile = async (fs, src, dest) => {
10
+ if (fs.copyFile) {
11
+ await fs.copyFile(src, dest);
12
+ return;
13
+ }
14
+ const content = await fs.readFile(src, 'binary');
15
+ await fs.writeFile(dest, content);
16
+ };
17
+ const isNotFoundError = (error) => {
18
+ if (typeof error !== 'object' || error === null)
19
+ return false;
20
+ const axiosError = error;
21
+ return axiosError.response?.status === 404;
22
+ };
23
+ const tryDownloadServerVersion = async (path, serverVersion, ctx) => {
24
+ try {
25
+ await ctx.executor.download({
26
+ path,
27
+ version: serverVersion,
28
+ deleted: false,
29
+ updatedAt: new Date().toISOString(),
30
+ });
31
+ return true;
32
+ }
33
+ catch (error) {
34
+ if (isNotFoundError(error))
35
+ return false;
36
+ throw error;
37
+ }
38
+ };
39
+ export const handleConflict = async (path, conflictResult, ctx) => {
40
+ const conflictPath = generateConflictPath(path, ctx.deviceName);
41
+ await copyFile(ctx.fs, path, conflictPath);
42
+ const downloaded = await tryDownloadServerVersion(path, conflictResult.serverVersion, ctx);
43
+ if (!downloaded) {
44
+ await ctx.fs.deleteFile(path);
45
+ await ctx.state.removeFile(path);
46
+ return;
47
+ }
48
+ const fileInfo = await ctx.fs.fileInfo(path);
49
+ const meta = { mtime: fileInfo?.mtime ?? 0, size: fileInfo?.size ?? 0 };
50
+ await ctx.state.setFile(path, createSyncedFile(meta, {
51
+ version: conflictResult.serverVersion,
52
+ status: 'synced',
53
+ conflictPath,
54
+ }));
55
+ };
56
+ export const hasConflict = (file) => file.conflictPath !== undefined;
@@ -0,0 +1,2 @@
1
+ import type { SyncContext } from '../types.js';
2
+ export declare const processDeleteLocal: (path: string, ctx: SyncContext) => Promise<void>;
@@ -0,0 +1,17 @@
1
+ export const processDeleteLocal = async (path, ctx) => {
2
+ try {
3
+ await ctx.fs.deleteFile(path);
4
+ await ctx.state.removeFile(path);
5
+ }
6
+ catch (error) {
7
+ const stored = await ctx.state.getFile(path);
8
+ if (stored) {
9
+ await ctx.state.setFile(path, {
10
+ ...stored,
11
+ status: 'error',
12
+ errorMessage: String(error),
13
+ });
14
+ }
15
+ throw error;
16
+ }
17
+ };
@@ -0,0 +1,2 @@
1
+ import type { SyncContext } from '../types.js';
2
+ export declare const processDeleteRemote: (path: string, ctx: SyncContext) => Promise<void>;
@@ -0,0 +1,26 @@
1
+ const isNotFoundError = (error) => {
2
+ if (!(error instanceof Error))
3
+ return false;
4
+ return error.message.includes('404') || error.message.includes('not found');
5
+ };
6
+ export const processDeleteRemote = async (path, ctx) => {
7
+ const stored = await ctx.state.getFile(path);
8
+ try {
9
+ await ctx.executor.deleteRemote(path, stored?.version ?? 0);
10
+ await ctx.state.removeFile(path);
11
+ }
12
+ catch (error) {
13
+ if (isNotFoundError(error)) {
14
+ await ctx.state.removeFile(path);
15
+ return;
16
+ }
17
+ if (stored) {
18
+ await ctx.state.setFile(path, {
19
+ ...stored,
20
+ status: 'error',
21
+ errorMessage: String(error),
22
+ });
23
+ }
24
+ throw error;
25
+ }
26
+ };
@@ -0,0 +1,2 @@
1
+ import type { RemoteFile, SyncContext } from '../types.js';
2
+ export declare const processDownload: (file: RemoteFile, ctx: SyncContext) => Promise<void>;
@@ -0,0 +1,20 @@
1
+ import { createSyncedFile } from "./synced-file.js";
2
+ const storedMeta = (stored) => ({
3
+ mtime: stored?.mtime ?? 0,
4
+ size: stored?.size ?? 0,
5
+ });
6
+ export const processDownload = async (file, ctx) => {
7
+ const stored = await ctx.state.getFile(file.path);
8
+ const meta = storedMeta(stored);
9
+ await ctx.state.setFile(file.path, createSyncedFile(meta, { version: stored?.version, status: 'downloading' }));
10
+ try {
11
+ await ctx.executor.download(file);
12
+ const fileInfo = await ctx.fs.fileInfo(file.path);
13
+ const downloadedMeta = { mtime: fileInfo?.mtime ?? 0, size: fileInfo?.size ?? 0 };
14
+ await ctx.state.setFile(file.path, createSyncedFile(downloadedMeta, { version: file.version, status: 'synced', syncedAt: ctx.serverTime }));
15
+ }
16
+ catch (error) {
17
+ await ctx.state.setFile(file.path, createSyncedFile(meta, { version: stored?.version, status: 'error', errorMessage: String(error) }));
18
+ throw error;
19
+ }
20
+ };
@@ -0,0 +1,5 @@
1
+ export { processUpload } from './upload.js';
2
+ export { processDownload } from './download.js';
3
+ export { processDeleteLocal } from './delete-local.js';
4
+ export { processDeleteRemote } from './delete-remote.js';
5
+ export { handleConflict, generateConflictPath, hasConflict } from './conflict.js';
@@ -0,0 +1,5 @@
1
+ export { processUpload } from "./upload.js";
2
+ export { processDownload } from "./download.js";
3
+ export { processDeleteLocal } from "./delete-local.js";
4
+ export { processDeleteRemote } from "./delete-remote.js";
5
+ export { handleConflict, generateConflictPath, hasConflict } from "./conflict.js";
@@ -0,0 +1,14 @@
1
+ import type { SyncedFile, SyncStatus } from '../types.js';
2
+ interface FileMeta {
3
+ mtime: number;
4
+ size: number;
5
+ }
6
+ export interface SyncedFileOptions {
7
+ version?: number;
8
+ status: SyncStatus;
9
+ syncedAt?: string;
10
+ errorMessage?: string;
11
+ conflictPath?: string;
12
+ }
13
+ export declare const createSyncedFile: (meta: FileMeta, options: SyncedFileOptions) => SyncedFile;
14
+ export {};
@@ -0,0 +1,5 @@
1
+ export const createSyncedFile = (meta, options) => ({
2
+ mtime: meta.mtime,
3
+ size: meta.size,
4
+ ...options,
5
+ });
@@ -0,0 +1,2 @@
1
+ import type { LocalFile, SyncContext } from '../types.js';
2
+ export declare const processUpload: (file: LocalFile, ctx: SyncContext) => Promise<void>;
@@ -0,0 +1,30 @@
1
+ import { handleConflict } from "./conflict.js";
2
+ import { createSyncedFile } from "./synced-file.js";
3
+ const executeUpload = async (file, expectedVersion, ctx) => {
4
+ const result = await ctx.executor.upload(file, expectedVersion);
5
+ if (result.status === 'ok') {
6
+ await ctx.state.setFile(file.path, createSyncedFile(file, {
7
+ version: result.version,
8
+ status: 'synced',
9
+ syncedAt: ctx.serverTime,
10
+ }));
11
+ return;
12
+ }
13
+ await handleConflict(file.path, result, ctx);
14
+ };
15
+ export const processUpload = async (file, ctx) => {
16
+ const stored = await ctx.state.getFile(file.path);
17
+ const expectedVersion = stored?.version;
18
+ await ctx.state.setFile(file.path, createSyncedFile(file, { version: expectedVersion, status: 'uploading' }));
19
+ try {
20
+ await executeUpload(file, expectedVersion, ctx);
21
+ }
22
+ catch (error) {
23
+ await ctx.state.setFile(file.path, createSyncedFile(file, {
24
+ version: expectedVersion,
25
+ status: 'error',
26
+ errorMessage: String(error),
27
+ }));
28
+ throw error;
29
+ }
30
+ };
package/sync/plan.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ import type { LocalFile, RemoteFile, SyncPlan, SyncStateData } from './types.js';
2
+ export interface CreatePlanParams {
3
+ localFiles: LocalFile[];
4
+ deletedLocally: string[];
5
+ remoteFiles: RemoteFile[];
6
+ stateData: SyncStateData;
7
+ serverTime: string;
8
+ }
9
+ export declare const createPlan: ({ localFiles, deletedLocally, remoteFiles, stateData, serverTime, }: CreatePlanParams) => SyncPlan;
package/sync/plan.js ADDED
@@ -0,0 +1,57 @@
1
+ import { SyncOperationType } from "./types.js";
2
+ export const createPlan = ({ localFiles, deletedLocally, remoteFiles, stateData, serverTime, }) => {
3
+ const index = buildFileIndex(localFiles, deletedLocally, remoteFiles);
4
+ const localActions = localFiles.map(local => resolveLocalFile(local, index.remoteByPath.get(local.path), stateData.files[local.path]));
5
+ const deletedActions = deletedLocally.map(path => resolveDeletedLocally(path, index.remoteByPath.get(path), stateData.files[path]));
6
+ const remoteActions = remoteFiles
7
+ .filter(remote => isNewRemote(remote, index))
8
+ .map(resolveNewRemote);
9
+ return buildPlanFromActions([...localActions, ...deletedActions, ...remoteActions], serverTime);
10
+ };
11
+ const buildFileIndex = (localFiles, deletedLocally, remoteFiles) => ({
12
+ remoteByPath: new Map(remoteFiles.map(f => [f.path, f])),
13
+ localByPath: new Map(localFiles.map(f => [f.path, f])),
14
+ deletedSet: new Set(deletedLocally),
15
+ });
16
+ const isNewRemote = (remote, index) => !index.localByPath.has(remote.path) && !index.deletedSet.has(remote.path);
17
+ const buildPlanFromActions = (actions, serverTime) => actions.reduce((plan, action) => applyAction(plan, action), { toUpload: [], toDownload: [], toDeleteLocal: [], toDeleteRemote: [], serverTime });
18
+ const actionHandlers = {
19
+ [SyncOperationType.Upload]: (plan, action) => ({ ...plan, toUpload: [...plan.toUpload, action.file] }),
20
+ [SyncOperationType.Download]: (plan, action) => ({ ...plan, toDownload: [...plan.toDownload, action.file] }),
21
+ [SyncOperationType.DeleteLocal]: (plan, action) => ({ ...plan, toDeleteLocal: [...plan.toDeleteLocal, action.path] }),
22
+ [SyncOperationType.DeleteRemote]: (plan, action) => ({ ...plan, toDeleteRemote: [...plan.toDeleteRemote, action.path] }),
23
+ none: (plan) => plan,
24
+ };
25
+ const applyAction = (plan, action) => actionHandlers[action.type](plan, action);
26
+ const resolveLocalFile = (local, remote, stored) => {
27
+ const localChanged = isLocalChanged(local, stored);
28
+ if (!remote) {
29
+ return localChanged ? upload(local) : none();
30
+ }
31
+ if (remote.deleted) {
32
+ return localChanged ? upload(local) : deleteLocal(local.path);
33
+ }
34
+ const remoteChanged = isRemoteChanged(remote, stored);
35
+ if (localChanged && remoteChanged) {
36
+ return upload(local);
37
+ }
38
+ if (localChanged)
39
+ return upload(local);
40
+ if (remoteChanged)
41
+ return download(remote);
42
+ return none();
43
+ };
44
+ const resolveDeletedLocally = (path, remote, stored) => {
45
+ if (!remote || remote.deleted) {
46
+ return deleteRemote(path);
47
+ }
48
+ return isRemoteChanged(remote, stored) ? download(remote) : deleteRemote(path);
49
+ };
50
+ const resolveNewRemote = (remote) => remote.deleted ? none() : download(remote);
51
+ const isLocalChanged = (local, stored) => !stored || local.mtime !== stored.mtime || stored.status === 'error';
52
+ const isRemoteChanged = (remote, stored) => !stored || remote.version > (stored.version ?? 0);
53
+ const upload = (file) => ({ type: SyncOperationType.Upload, file });
54
+ const download = (file) => ({ type: SyncOperationType.Download, file });
55
+ const deleteLocal = (path) => ({ type: SyncOperationType.DeleteLocal, path });
56
+ const deleteRemote = (path) => ({ type: SyncOperationType.DeleteRemote, path });
57
+ const none = () => ({ type: 'none' });
@@ -0,0 +1,2 @@
1
+ import type { SyncState } from './types.js';
2
+ export declare const recoverState: (state: SyncState) => Promise<void>;
@@ -0,0 +1,6 @@
1
+ const isInterruptedStatus = (status) => status === 'uploading' || status === 'downloading';
2
+ export const recoverState = async (state) => {
3
+ const data = await state.get();
4
+ const interruptedFiles = Object.entries(data.files).filter(([, file]) => isInterruptedStatus(file.status));
5
+ await Promise.all(interruptedFiles.map(([path, file]) => state.setFile(path, { ...file, status: 'dirty' })));
6
+ };
package/sync/scan.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ import type { FileSystem } from '../models/file-system.js';
2
+ import type { LocalFile, SyncStateData } from './types.js';
3
+ export declare function scanLocalFiles(fs: FileSystem, rootPath: string, ignorePatterns?: string[]): Promise<LocalFile[]>;
4
+ export declare const findDeletedLocally: (localFiles: LocalFile[], stateData: SyncStateData) => string[];
package/sync/scan.js ADDED
@@ -0,0 +1,40 @@
1
+ import { toAbsolutePath } from "../utils/to-absolute-path.js";
2
+ const DEFAULT_IGNORE = [
3
+ '.git',
4
+ '.DS_Store',
5
+ 'node_modules',
6
+ '.sync-state',
7
+ '.Trash',
8
+ ];
9
+ export async function scanLocalFiles(fs, rootPath, ignorePatterns = []) {
10
+ const ignore = [...DEFAULT_IGNORE, ...ignorePatterns];
11
+ return scanDir(fs, rootPath, ignore);
12
+ }
13
+ async function scanDir(fs, path, ignore) {
14
+ const entries = await fs.readDir(path);
15
+ const filteredEntries = entries.filter((e) => !shouldIgnore(e.name, ignore));
16
+ const nestedResults = await Promise.all(filteredEntries.map((entry) => processEntry(fs, entry, ignore)));
17
+ return nestedResults.flat();
18
+ }
19
+ async function processEntry(fs, entry, ignore) {
20
+ if (entry.type === 'directory') {
21
+ return scanDir(fs, entry.path, ignore);
22
+ }
23
+ return [toLocalFile(entry)];
24
+ }
25
+ const toLocalFile = (entry) => ({
26
+ path: toAbsolutePath(entry.path),
27
+ mtime: entry.mtime,
28
+ size: entry.size,
29
+ });
30
+ const shouldIgnore = (name, patterns) => patterns.some((pattern) => matchPattern(pattern, name));
31
+ const matchPattern = (pattern, name) => {
32
+ if (!pattern.includes('*'))
33
+ return name === pattern;
34
+ const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
35
+ return regex.test(name);
36
+ };
37
+ export const findDeletedLocally = (localFiles, stateData) => {
38
+ const localPaths = new Set(localFiles.map((f) => f.path));
39
+ return Object.keys(stateData.files).filter((path) => !localPaths.has(path));
40
+ };