pushwork 1.0.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 (184) hide show
  1. package/README.md +460 -0
  2. package/dist/browser/browser-sync-engine.d.ts +64 -0
  3. package/dist/browser/browser-sync-engine.d.ts.map +1 -0
  4. package/dist/browser/browser-sync-engine.js +303 -0
  5. package/dist/browser/browser-sync-engine.js.map +1 -0
  6. package/dist/browser/filesystem-adapter.d.ts +84 -0
  7. package/dist/browser/filesystem-adapter.d.ts.map +1 -0
  8. package/dist/browser/filesystem-adapter.js +413 -0
  9. package/dist/browser/filesystem-adapter.js.map +1 -0
  10. package/dist/browser/index.d.ts +36 -0
  11. package/dist/browser/index.d.ts.map +1 -0
  12. package/dist/browser/index.js +90 -0
  13. package/dist/browser/index.js.map +1 -0
  14. package/dist/browser/types.d.ts +70 -0
  15. package/dist/browser/types.d.ts.map +1 -0
  16. package/dist/browser/types.js +6 -0
  17. package/dist/browser/types.js.map +1 -0
  18. package/dist/cli/commands.d.ts +71 -0
  19. package/dist/cli/commands.d.ts.map +1 -0
  20. package/dist/cli/commands.js +794 -0
  21. package/dist/cli/commands.js.map +1 -0
  22. package/dist/cli/index.d.ts +2 -0
  23. package/dist/cli/index.d.ts.map +1 -0
  24. package/dist/cli/index.js +19 -0
  25. package/dist/cli/index.js.map +1 -0
  26. package/dist/cli.d.ts +3 -0
  27. package/dist/cli.d.ts.map +1 -0
  28. package/dist/cli.js +199 -0
  29. package/dist/cli.js.map +1 -0
  30. package/dist/config/index.d.ts +71 -0
  31. package/dist/config/index.d.ts.map +1 -0
  32. package/dist/config/index.js +314 -0
  33. package/dist/config/index.js.map +1 -0
  34. package/dist/core/change-detection.d.ts +78 -0
  35. package/dist/core/change-detection.d.ts.map +1 -0
  36. package/dist/core/change-detection.js +370 -0
  37. package/dist/core/change-detection.js.map +1 -0
  38. package/dist/core/index.d.ts +5 -0
  39. package/dist/core/index.d.ts.map +1 -0
  40. package/dist/core/index.js +22 -0
  41. package/dist/core/index.js.map +1 -0
  42. package/dist/core/isomorphic-snapshot.d.ts +58 -0
  43. package/dist/core/isomorphic-snapshot.d.ts.map +1 -0
  44. package/dist/core/isomorphic-snapshot.js +204 -0
  45. package/dist/core/isomorphic-snapshot.js.map +1 -0
  46. package/dist/core/move-detection.d.ts +72 -0
  47. package/dist/core/move-detection.d.ts.map +1 -0
  48. package/dist/core/move-detection.js +200 -0
  49. package/dist/core/move-detection.js.map +1 -0
  50. package/dist/core/snapshot.d.ts +109 -0
  51. package/dist/core/snapshot.d.ts.map +1 -0
  52. package/dist/core/snapshot.js +263 -0
  53. package/dist/core/snapshot.js.map +1 -0
  54. package/dist/core/sync-engine.d.ts +110 -0
  55. package/dist/core/sync-engine.d.ts.map +1 -0
  56. package/dist/core/sync-engine.js +817 -0
  57. package/dist/core/sync-engine.js.map +1 -0
  58. package/dist/index.d.ts +6 -0
  59. package/dist/index.d.ts.map +1 -0
  60. package/dist/index.js +27 -0
  61. package/dist/index.js.map +1 -0
  62. package/dist/platform/browser-filesystem.d.ts +26 -0
  63. package/dist/platform/browser-filesystem.d.ts.map +1 -0
  64. package/dist/platform/browser-filesystem.js +91 -0
  65. package/dist/platform/browser-filesystem.js.map +1 -0
  66. package/dist/platform/filesystem.d.ts +29 -0
  67. package/dist/platform/filesystem.d.ts.map +1 -0
  68. package/dist/platform/filesystem.js +65 -0
  69. package/dist/platform/filesystem.js.map +1 -0
  70. package/dist/platform/node-filesystem.d.ts +21 -0
  71. package/dist/platform/node-filesystem.d.ts.map +1 -0
  72. package/dist/platform/node-filesystem.js +93 -0
  73. package/dist/platform/node-filesystem.js.map +1 -0
  74. package/dist/types/config.d.ts +119 -0
  75. package/dist/types/config.d.ts.map +1 -0
  76. package/dist/types/config.js +3 -0
  77. package/dist/types/config.js.map +1 -0
  78. package/dist/types/documents.d.ts +70 -0
  79. package/dist/types/documents.d.ts.map +1 -0
  80. package/dist/types/documents.js +23 -0
  81. package/dist/types/documents.js.map +1 -0
  82. package/dist/types/index.d.ts +4 -0
  83. package/dist/types/index.d.ts.map +1 -0
  84. package/dist/types/index.js +23 -0
  85. package/dist/types/index.js.map +1 -0
  86. package/dist/types/snapshot.d.ts +81 -0
  87. package/dist/types/snapshot.d.ts.map +1 -0
  88. package/dist/types/snapshot.js +17 -0
  89. package/dist/types/snapshot.js.map +1 -0
  90. package/dist/utils/content-similarity.d.ts +53 -0
  91. package/dist/utils/content-similarity.d.ts.map +1 -0
  92. package/dist/utils/content-similarity.js +155 -0
  93. package/dist/utils/content-similarity.js.map +1 -0
  94. package/dist/utils/content.d.ts +5 -0
  95. package/dist/utils/content.d.ts.map +1 -0
  96. package/dist/utils/content.js +30 -0
  97. package/dist/utils/content.js.map +1 -0
  98. package/dist/utils/fs-browser.d.ts +57 -0
  99. package/dist/utils/fs-browser.d.ts.map +1 -0
  100. package/dist/utils/fs-browser.js +311 -0
  101. package/dist/utils/fs-browser.js.map +1 -0
  102. package/dist/utils/fs-node.d.ts +53 -0
  103. package/dist/utils/fs-node.d.ts.map +1 -0
  104. package/dist/utils/fs-node.js +220 -0
  105. package/dist/utils/fs-node.js.map +1 -0
  106. package/dist/utils/fs.d.ts +62 -0
  107. package/dist/utils/fs.d.ts.map +1 -0
  108. package/dist/utils/fs.js +293 -0
  109. package/dist/utils/fs.js.map +1 -0
  110. package/dist/utils/index.d.ts +4 -0
  111. package/dist/utils/index.d.ts.map +1 -0
  112. package/dist/utils/index.js +23 -0
  113. package/dist/utils/index.js.map +1 -0
  114. package/dist/utils/isomorphic.d.ts +29 -0
  115. package/dist/utils/isomorphic.d.ts.map +1 -0
  116. package/dist/utils/isomorphic.js +139 -0
  117. package/dist/utils/isomorphic.js.map +1 -0
  118. package/dist/utils/mime-types.d.ts +13 -0
  119. package/dist/utils/mime-types.d.ts.map +1 -0
  120. package/dist/utils/mime-types.js +240 -0
  121. package/dist/utils/mime-types.js.map +1 -0
  122. package/dist/utils/network-sync.d.ts +12 -0
  123. package/dist/utils/network-sync.d.ts.map +1 -0
  124. package/dist/utils/network-sync.js +149 -0
  125. package/dist/utils/network-sync.js.map +1 -0
  126. package/dist/utils/pure.d.ts +25 -0
  127. package/dist/utils/pure.d.ts.map +1 -0
  128. package/dist/utils/pure.js +112 -0
  129. package/dist/utils/pure.js.map +1 -0
  130. package/dist/utils/repo-factory.d.ts +11 -0
  131. package/dist/utils/repo-factory.d.ts.map +1 -0
  132. package/dist/utils/repo-factory.js +77 -0
  133. package/dist/utils/repo-factory.js.map +1 -0
  134. package/package.json +83 -0
  135. package/src/cli/commands.ts +1053 -0
  136. package/src/cli/index.ts +2 -0
  137. package/src/cli.ts +287 -0
  138. package/src/config/index.ts +334 -0
  139. package/src/core/change-detection.ts +484 -0
  140. package/src/core/index.ts +5 -0
  141. package/src/core/move-detection.ts +269 -0
  142. package/src/core/snapshot.ts +285 -0
  143. package/src/core/sync-engine.ts +1167 -0
  144. package/src/index.ts +14 -0
  145. package/src/types/config.ts +130 -0
  146. package/src/types/documents.ts +72 -0
  147. package/src/types/index.ts +8 -0
  148. package/src/types/snapshot.ts +88 -0
  149. package/src/utils/content-similarity.ts +194 -0
  150. package/src/utils/content.ts +28 -0
  151. package/src/utils/fs.ts +289 -0
  152. package/src/utils/index.ts +8 -0
  153. package/src/utils/mime-types.ts +236 -0
  154. package/src/utils/network-sync.ts +153 -0
  155. package/src/utils/repo-factory.ts +58 -0
  156. package/test/README-TESTING-GAPS.md +174 -0
  157. package/test/integration/README.md +328 -0
  158. package/test/integration/clone-test.sh +310 -0
  159. package/test/integration/conflict-resolution-test.sh +309 -0
  160. package/test/integration/deletion-behavior-test.sh +487 -0
  161. package/test/integration/deletion-sync-test-simple.sh +193 -0
  162. package/test/integration/deletion-sync-test.sh +297 -0
  163. package/test/integration/exclude-patterns.test.ts +152 -0
  164. package/test/integration/full-integration-test.sh +363 -0
  165. package/test/integration/sync-deletion.test.ts +339 -0
  166. package/test/integration/sync-flow.test.ts +309 -0
  167. package/test/run-tests.sh +225 -0
  168. package/test/unit/content-similarity.test.ts +236 -0
  169. package/test/unit/deletion-behavior.test.ts +260 -0
  170. package/test/unit/enhanced-mime-detection.test.ts +266 -0
  171. package/test/unit/snapshot.test.ts +431 -0
  172. package/test/unit/sync-timing.test.ts +178 -0
  173. package/test/unit/utils.test.ts +368 -0
  174. package/tools/browser-sync/README.md +116 -0
  175. package/tools/browser-sync/package.json +44 -0
  176. package/tools/browser-sync/patchwork.json +1 -0
  177. package/tools/browser-sync/pnpm-lock.yaml +4202 -0
  178. package/tools/browser-sync/src/components/BrowserSyncTool.tsx +599 -0
  179. package/tools/browser-sync/src/index.ts +20 -0
  180. package/tools/browser-sync/src/polyfills.ts +31 -0
  181. package/tools/browser-sync/src/styles.css +290 -0
  182. package/tools/browser-sync/src/types.ts +27 -0
  183. package/tools/browser-sync/vite.config.ts +25 -0
  184. package/tsconfig.json +22 -0
@@ -0,0 +1,484 @@
1
+ import { AutomergeUrl, Repo, UrlHeads } from "@automerge/automerge-repo";
2
+ import * as A from "@automerge/automerge";
3
+ import {
4
+ ChangeType,
5
+ FileType,
6
+ SyncSnapshot,
7
+ SnapshotFileEntry,
8
+ SnapshotDirectoryEntry,
9
+ FileDocument,
10
+ DirectoryDocument,
11
+ } from "../types";
12
+ import {
13
+ readFileContent,
14
+ getFileSystemEntry,
15
+ listDirectory,
16
+ getRelativePath,
17
+ normalizePath,
18
+ } from "../utils";
19
+
20
+ // Re-export ChangeType for other modules
21
+ export { ChangeType } from "../types";
22
+
23
+ /**
24
+ * Represents a detected change
25
+ */
26
+ export interface DetectedChange {
27
+ path: string;
28
+ changeType: ChangeType;
29
+ fileType: FileType;
30
+ localContent: string | Uint8Array | null;
31
+ remoteContent: string | Uint8Array | null;
32
+ localHead?: UrlHeads;
33
+ remoteHead?: UrlHeads;
34
+ }
35
+
36
+ /**
37
+ * Change detection engine
38
+ */
39
+ export class ChangeDetector {
40
+ constructor(
41
+ private repo: Repo,
42
+ private rootPath: string,
43
+ private excludePatterns: string[] = []
44
+ ) {}
45
+
46
+ /**
47
+ * Detect all changes between local filesystem and snapshot
48
+ */
49
+ async detectChanges(snapshot: SyncSnapshot): Promise<DetectedChange[]> {
50
+ const changes: DetectedChange[] = [];
51
+
52
+ // Get current filesystem state
53
+ const currentFiles = await this.getCurrentFilesystemState();
54
+
55
+ // Check for local changes (new, modified, deleted files)
56
+ const localChanges = await this.detectLocalChanges(snapshot, currentFiles);
57
+ changes.push(...localChanges);
58
+
59
+ // Check for remote changes (changes in Automerge documents)
60
+ const remoteChanges = await this.detectRemoteChanges(snapshot);
61
+ changes.push(...remoteChanges);
62
+
63
+ // Check for new remote documents not in snapshot (critical for clone scenarios)
64
+ const newRemoteDocuments = await this.detectNewRemoteDocuments(snapshot);
65
+ changes.push(...newRemoteDocuments);
66
+
67
+ return changes;
68
+ }
69
+
70
+ /**
71
+ * Detect changes in local filesystem compared to snapshot
72
+ */
73
+ private async detectLocalChanges(
74
+ snapshot: SyncSnapshot,
75
+ currentFiles: Map<string, { content: string | Uint8Array; type: FileType }>
76
+ ): Promise<DetectedChange[]> {
77
+ const changes: DetectedChange[] = [];
78
+
79
+ // Check for new and modified files
80
+ for (const [relativePath, fileInfo] of currentFiles.entries()) {
81
+ const snapshotEntry = snapshot.files.get(relativePath);
82
+
83
+ if (!snapshotEntry) {
84
+ // New file
85
+ changes.push({
86
+ path: relativePath,
87
+ changeType: ChangeType.LOCAL_ONLY,
88
+ fileType: fileInfo.type,
89
+ localContent: fileInfo.content,
90
+ remoteContent: null,
91
+ });
92
+ } else {
93
+ // Check if content changed
94
+ const lastKnownContent = await this.getContentAtHead(
95
+ snapshotEntry.url,
96
+ snapshotEntry.head
97
+ );
98
+ const contentChanged = !this.isContentEqual(
99
+ fileInfo.content,
100
+ lastKnownContent
101
+ );
102
+
103
+ if (contentChanged) {
104
+ // Check remote state too
105
+ const currentRemoteContent = await this.getCurrentRemoteContent(
106
+ snapshotEntry.url
107
+ );
108
+ const remoteChanged = !this.isContentEqual(
109
+ lastKnownContent,
110
+ currentRemoteContent
111
+ );
112
+
113
+ const changeType = remoteChanged
114
+ ? ChangeType.BOTH_CHANGED
115
+ : ChangeType.LOCAL_ONLY;
116
+
117
+ changes.push({
118
+ path: relativePath,
119
+ changeType,
120
+ fileType: fileInfo.type,
121
+ localContent: fileInfo.content,
122
+ remoteContent: currentRemoteContent,
123
+ localHead: snapshotEntry.head,
124
+ remoteHead: await this.getCurrentRemoteHead(snapshotEntry.url),
125
+ });
126
+ }
127
+ }
128
+ }
129
+
130
+ // Check for deleted files
131
+ for (const [relativePath, snapshotEntry] of snapshot.files.entries()) {
132
+ if (!currentFiles.has(relativePath)) {
133
+ // File was deleted locally
134
+ const currentRemoteContent = await this.getCurrentRemoteContent(
135
+ snapshotEntry.url
136
+ );
137
+ const lastKnownContent = await this.getContentAtHead(
138
+ snapshotEntry.url,
139
+ snapshotEntry.head
140
+ );
141
+ const remoteChanged = !this.isContentEqual(
142
+ lastKnownContent,
143
+ currentRemoteContent
144
+ );
145
+
146
+ const changeType = remoteChanged
147
+ ? ChangeType.BOTH_CHANGED
148
+ : ChangeType.LOCAL_ONLY;
149
+
150
+ changes.push({
151
+ path: relativePath,
152
+ changeType,
153
+ fileType: FileType.TEXT, // Will be determined from document
154
+ localContent: null,
155
+ remoteContent: currentRemoteContent,
156
+ localHead: snapshotEntry.head,
157
+ remoteHead: await this.getCurrentRemoteHead(snapshotEntry.url),
158
+ });
159
+ }
160
+ }
161
+
162
+ return changes;
163
+ }
164
+
165
+ /**
166
+ * Detect changes in remote Automerge documents compared to snapshot
167
+ */
168
+ private async detectRemoteChanges(
169
+ snapshot: SyncSnapshot
170
+ ): Promise<DetectedChange[]> {
171
+ const changes: DetectedChange[] = [];
172
+
173
+ for (const [relativePath, snapshotEntry] of snapshot.files.entries()) {
174
+ const currentRemoteHead = await this.getCurrentRemoteHead(
175
+ snapshotEntry.url
176
+ );
177
+
178
+ if (!A.equals(currentRemoteHead, snapshotEntry.head)) {
179
+ // Remote document has changed
180
+ const currentRemoteContent = await this.getCurrentRemoteContent(
181
+ snapshotEntry.url
182
+ );
183
+ const localContent = await this.getLocalContent(relativePath);
184
+ const lastKnownContent = await this.getContentAtHead(
185
+ snapshotEntry.url,
186
+ snapshotEntry.head
187
+ );
188
+
189
+ const localChanged = localContent
190
+ ? !this.isContentEqual(localContent, lastKnownContent)
191
+ : false;
192
+
193
+ const changeType = localChanged
194
+ ? ChangeType.BOTH_CHANGED
195
+ : ChangeType.REMOTE_ONLY;
196
+
197
+ changes.push({
198
+ path: relativePath,
199
+ changeType,
200
+ fileType: await this.getFileTypeFromContent(currentRemoteContent),
201
+ localContent,
202
+ remoteContent: currentRemoteContent,
203
+ localHead: snapshotEntry.head,
204
+ remoteHead: currentRemoteHead,
205
+ });
206
+ }
207
+ }
208
+
209
+ return changes;
210
+ }
211
+
212
+ /**
213
+ * Detect new remote documents from directory hierarchy that aren't in snapshot
214
+ * This is critical for clone scenarios where local snapshot is empty
215
+ */
216
+ private async detectNewRemoteDocuments(
217
+ snapshot: SyncSnapshot
218
+ ): Promise<DetectedChange[]> {
219
+ const changes: DetectedChange[] = [];
220
+
221
+ // If no root directory URL, nothing to discover
222
+ if (!snapshot.rootDirectoryUrl) {
223
+ return changes;
224
+ }
225
+
226
+ try {
227
+ // Recursively traverse the directory hierarchy
228
+ await this.discoverRemoteDocumentsRecursive(
229
+ snapshot.rootDirectoryUrl,
230
+ "",
231
+ snapshot,
232
+ changes
233
+ );
234
+ } catch (error) {
235
+ console.warn(`❌ Failed to discover remote documents: ${error}`);
236
+ }
237
+
238
+ return changes;
239
+ }
240
+
241
+ /**
242
+ * Recursively discover remote documents in directory hierarchy
243
+ */
244
+ private async discoverRemoteDocumentsRecursive(
245
+ directoryUrl: AutomergeUrl,
246
+ currentPath: string,
247
+ snapshot: SyncSnapshot,
248
+ changes: DetectedChange[]
249
+ ): Promise<void> {
250
+ try {
251
+ const dirHandle = await this.repo.find<DirectoryDocument>(directoryUrl);
252
+ const dirDoc = await dirHandle.doc();
253
+
254
+ if (!dirDoc) {
255
+ return;
256
+ }
257
+
258
+ // Process each entry in the directory
259
+ for (const entry of dirDoc.docs) {
260
+ const entryPath = currentPath
261
+ ? `${currentPath}/${entry.name}`
262
+ : entry.name;
263
+
264
+ if (entry.type === "file") {
265
+ // Check if this file is already tracked in the snapshot
266
+ const existingEntry = snapshot.files.get(entryPath);
267
+
268
+ if (!existingEntry) {
269
+ // This is a remote file not in our snapshot
270
+ const localContent = await this.getLocalContent(entryPath);
271
+
272
+ // Only create changes for files that exist locally
273
+ // Files that don't exist locally AND aren't in snapshot should be ignored
274
+ // (they were likely deleted and directory documents haven't been cleaned up yet)
275
+ if (localContent) {
276
+ // File exists locally but not in snapshot - this is a new local file
277
+ const remoteContent = await this.getCurrentRemoteContent(
278
+ entry.url
279
+ );
280
+
281
+ changes.push({
282
+ path: entryPath,
283
+ changeType: ChangeType.BOTH_CHANGED,
284
+ fileType: await this.getFileTypeFromContent(remoteContent),
285
+ localContent,
286
+ remoteContent,
287
+ remoteHead: await this.getCurrentRemoteHead(entry.url),
288
+ });
289
+ }
290
+ // If file doesn't exist locally and isn't in snapshot, ignore it
291
+ // This prevents infinite sync loops with ghost entries from stale directory documents
292
+ }
293
+ } else if (entry.type === "folder") {
294
+ // Recursively process subdirectory
295
+ await this.discoverRemoteDocumentsRecursive(
296
+ entry.url,
297
+ entryPath,
298
+ snapshot,
299
+ changes
300
+ );
301
+ }
302
+ }
303
+ } catch (error) {
304
+ console.warn(`❌ Failed to process directory ${currentPath}: ${error}`);
305
+ }
306
+ }
307
+
308
+ /**
309
+ * Get current filesystem state as a map
310
+ */
311
+ private async getCurrentFilesystemState(): Promise<
312
+ Map<string, { content: string | Uint8Array; type: FileType }>
313
+ > {
314
+ const fileMap = new Map<
315
+ string,
316
+ { content: string | Uint8Array; type: FileType }
317
+ >();
318
+
319
+ try {
320
+ const entries = await listDirectory(
321
+ this.rootPath,
322
+ true,
323
+ this.excludePatterns
324
+ );
325
+
326
+ for (const entry of entries) {
327
+ if (entry.type !== FileType.DIRECTORY) {
328
+ const relativePath = getRelativePath(this.rootPath, entry.path);
329
+ const content = await readFileContent(entry.path);
330
+
331
+ fileMap.set(relativePath, {
332
+ content,
333
+ type: entry.type,
334
+ });
335
+ }
336
+ }
337
+ } catch (error) {
338
+ console.warn(`Failed to scan filesystem: ${error}`);
339
+ }
340
+
341
+ return fileMap;
342
+ }
343
+
344
+ /**
345
+ * Get local file content if it exists
346
+ */
347
+ private async getLocalContent(
348
+ relativePath: string
349
+ ): Promise<string | Uint8Array | null> {
350
+ try {
351
+ const fullPath = normalizePath(this.rootPath + "/" + relativePath);
352
+ return await readFileContent(fullPath);
353
+ } catch {
354
+ return null;
355
+ }
356
+ }
357
+
358
+ /**
359
+ * Get content from Automerge document at specific head
360
+ */
361
+ private async getContentAtHead(
362
+ url: AutomergeUrl,
363
+ heads: UrlHeads
364
+ ): Promise<string | Uint8Array | null> {
365
+ const handle = await this.repo.find<FileDocument>(url);
366
+ const doc = await handle.view(heads).doc();
367
+ return doc?.content as string | Uint8Array;
368
+ }
369
+
370
+ /**
371
+ * Get current content from Automerge document
372
+ */
373
+ private async getCurrentRemoteContent(
374
+ url: AutomergeUrl
375
+ ): Promise<string | Uint8Array | null> {
376
+ try {
377
+ const handle = await this.repo.find<FileDocument>(url);
378
+ const doc = await handle.doc();
379
+
380
+ if (!doc) return null;
381
+
382
+ const fileDoc = doc as FileDocument;
383
+ return fileDoc.content as string | Uint8Array;
384
+ } catch (error) {
385
+ console.warn(
386
+ `❌ Failed to get current remote content for ${url}: ${error}`
387
+ );
388
+ return null;
389
+ }
390
+ }
391
+
392
+ /**
393
+ * Get current head of Automerge document
394
+ */
395
+ private async getCurrentRemoteHead(url: AutomergeUrl): Promise<UrlHeads> {
396
+ return (await this.repo.find<FileDocument>(url)).heads();
397
+ }
398
+
399
+ /**
400
+ * Determine file type from content
401
+ */
402
+ private async getFileTypeFromContent(
403
+ content: string | Uint8Array | null
404
+ ): Promise<FileType> {
405
+ if (!content) return FileType.TEXT;
406
+
407
+ if (content instanceof Uint8Array) {
408
+ return FileType.BINARY;
409
+ } else {
410
+ return FileType.TEXT;
411
+ }
412
+ }
413
+
414
+ /**
415
+ * Compare two content pieces for equality
416
+ */
417
+ private isContentEqual(
418
+ content1: string | Uint8Array | null,
419
+ content2: string | Uint8Array | null
420
+ ): boolean {
421
+ if (content1 === content2) return true;
422
+ if (!content1 || !content2) return false;
423
+
424
+ if (typeof content1 !== typeof content2) return false;
425
+
426
+ if (typeof content1 === "string") {
427
+ return content1 === content2;
428
+ } else {
429
+ // Compare Uint8Array
430
+ const buf1 = content1 as Uint8Array;
431
+ const buf2 = content2 as Uint8Array;
432
+
433
+ if (buf1.length !== buf2.length) return false;
434
+
435
+ for (let i = 0; i < buf1.length; i++) {
436
+ if (buf1[i] !== buf2[i]) return false;
437
+ }
438
+
439
+ return true;
440
+ }
441
+ }
442
+
443
+ /**
444
+ * Classify change type for a path
445
+ */
446
+ async classifyChange(
447
+ relativePath: string,
448
+ snapshot: SyncSnapshot
449
+ ): Promise<ChangeType> {
450
+ const snapshotEntry = snapshot.files.get(relativePath);
451
+ const localContent = await this.getLocalContent(relativePath);
452
+
453
+ if (!snapshotEntry) {
454
+ // New file
455
+ return ChangeType.LOCAL_ONLY;
456
+ }
457
+
458
+ const lastKnownContent = await this.getContentAtHead(
459
+ snapshotEntry.url,
460
+ snapshotEntry.head
461
+ );
462
+ const currentRemoteContent = await this.getCurrentRemoteContent(
463
+ snapshotEntry.url
464
+ );
465
+
466
+ const localChanged = localContent
467
+ ? !this.isContentEqual(localContent, lastKnownContent)
468
+ : true;
469
+ const remoteChanged = !this.isContentEqual(
470
+ lastKnownContent,
471
+ currentRemoteContent
472
+ );
473
+
474
+ if (!localChanged && !remoteChanged) {
475
+ return ChangeType.NO_CHANGE;
476
+ } else if (localChanged && !remoteChanged) {
477
+ return ChangeType.LOCAL_ONLY;
478
+ } else if (!localChanged && remoteChanged) {
479
+ return ChangeType.REMOTE_ONLY;
480
+ } else {
481
+ return ChangeType.BOTH_CHANGED;
482
+ }
483
+ }
484
+ }
@@ -0,0 +1,5 @@
1
+ // Core sync components
2
+ export * from "./snapshot";
3
+ export * from "./change-detection";
4
+ export * from "./move-detection";
5
+ export * from "./sync-engine";