pushwork 1.1.3 → 2.0.0-a.sub.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 (116) hide show
  1. package/CLAUDE.md +9 -5
  2. package/dist/cli/commands.d.ts +71 -0
  3. package/dist/cli/commands.d.ts.map +1 -0
  4. package/dist/cli/commands.js +794 -0
  5. package/dist/cli/commands.js.map +1 -0
  6. package/dist/cli/index.d.ts +2 -0
  7. package/dist/cli/index.d.ts.map +1 -0
  8. package/dist/cli/index.js +19 -0
  9. package/dist/cli/index.js.map +1 -0
  10. package/dist/cli.js +48 -55
  11. package/dist/cli.js.map +1 -1
  12. package/dist/commands.d.ts +5 -1
  13. package/dist/commands.d.ts.map +1 -1
  14. package/dist/commands.js +262 -263
  15. package/dist/commands.js.map +1 -1
  16. package/dist/config/index.d.ts +71 -0
  17. package/dist/config/index.d.ts.map +1 -0
  18. package/dist/config/index.js +314 -0
  19. package/dist/config/index.js.map +1 -0
  20. package/dist/core/change-detection.d.ts +2 -2
  21. package/dist/core/change-detection.d.ts.map +1 -1
  22. package/dist/core/change-detection.js +82 -109
  23. package/dist/core/change-detection.js.map +1 -1
  24. package/dist/core/config.d.ts +1 -1
  25. package/dist/core/config.d.ts.map +1 -1
  26. package/dist/core/config.js +14 -57
  27. package/dist/core/config.js.map +1 -1
  28. package/dist/core/index.d.ts +5 -5
  29. package/dist/core/index.d.ts.map +1 -1
  30. package/dist/core/index.js +5 -21
  31. package/dist/core/index.js.map +1 -1
  32. package/dist/core/move-detection.d.ts +2 -2
  33. package/dist/core/move-detection.d.ts.map +1 -1
  34. package/dist/core/move-detection.js +9 -13
  35. package/dist/core/move-detection.js.map +1 -1
  36. package/dist/core/snapshot.d.ts +1 -1
  37. package/dist/core/snapshot.d.ts.map +1 -1
  38. package/dist/core/snapshot.js +9 -46
  39. package/dist/core/snapshot.js.map +1 -1
  40. package/dist/core/sync-engine.d.ts +1 -1
  41. package/dist/core/sync-engine.d.ts.map +1 -1
  42. package/dist/core/sync-engine.js +126 -151
  43. package/dist/core/sync-engine.js.map +1 -1
  44. package/dist/index.d.ts +4 -4
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +4 -20
  47. package/dist/index.js.map +1 -1
  48. package/dist/types/config.d.ts +7 -6
  49. package/dist/types/config.d.ts.map +1 -1
  50. package/dist/types/config.js +1 -5
  51. package/dist/types/config.js.map +1 -1
  52. package/dist/types/documents.js +4 -7
  53. package/dist/types/documents.js.map +1 -1
  54. package/dist/types/index.d.ts +3 -3
  55. package/dist/types/index.d.ts.map +1 -1
  56. package/dist/types/index.js +3 -19
  57. package/dist/types/index.js.map +1 -1
  58. package/dist/types/snapshot.js +1 -2
  59. package/dist/utils/content-similarity.d.ts +53 -0
  60. package/dist/utils/content-similarity.d.ts.map +1 -0
  61. package/dist/utils/content-similarity.js +155 -0
  62. package/dist/utils/content-similarity.js.map +1 -0
  63. package/dist/utils/content.js +4 -8
  64. package/dist/utils/content.js.map +1 -1
  65. package/dist/utils/directory.js +5 -9
  66. package/dist/utils/directory.js.map +1 -1
  67. package/dist/utils/fs.d.ts +1 -1
  68. package/dist/utils/fs.d.ts.map +1 -1
  69. package/dist/utils/fs.js +34 -84
  70. package/dist/utils/fs.js.map +1 -1
  71. package/dist/utils/index.d.ts +4 -4
  72. package/dist/utils/index.d.ts.map +1 -1
  73. package/dist/utils/index.js +4 -20
  74. package/dist/utils/index.js.map +1 -1
  75. package/dist/utils/mime-types.js +5 -43
  76. package/dist/utils/mime-types.js.map +1 -1
  77. package/dist/utils/network-sync.d.ts +13 -8
  78. package/dist/utils/network-sync.d.ts.map +1 -1
  79. package/dist/utils/network-sync.js +65 -137
  80. package/dist/utils/network-sync.js.map +1 -1
  81. package/dist/utils/node-polyfills.d.ts +9 -0
  82. package/dist/utils/node-polyfills.d.ts.map +1 -0
  83. package/dist/utils/node-polyfills.js +9 -0
  84. package/dist/utils/node-polyfills.js.map +1 -0
  85. package/dist/utils/output.js +32 -39
  86. package/dist/utils/output.js.map +1 -1
  87. package/dist/utils/repo-factory.d.ts +8 -2
  88. package/dist/utils/repo-factory.d.ts.map +1 -1
  89. package/dist/utils/repo-factory.js +38 -47
  90. package/dist/utils/repo-factory.js.map +1 -1
  91. package/dist/utils/string-similarity.js +1 -5
  92. package/dist/utils/string-similarity.js.map +1 -1
  93. package/dist/utils/text-diff.js +5 -43
  94. package/dist/utils/text-diff.js.map +1 -1
  95. package/dist/utils/trace.js +6 -11
  96. package/dist/utils/trace.js.map +1 -1
  97. package/package.json +7 -5
  98. package/src/cli.ts +25 -34
  99. package/src/commands.ts +75 -11
  100. package/src/core/change-detection.ts +25 -10
  101. package/src/core/config.ts +2 -12
  102. package/src/core/index.ts +5 -5
  103. package/src/core/move-detection.ts +4 -4
  104. package/src/core/snapshot.ts +3 -3
  105. package/src/core/sync-engine.ts +24 -17
  106. package/src/index.ts +4 -4
  107. package/src/types/config.ts +8 -8
  108. package/src/types/index.ts +3 -3
  109. package/src/utils/directory.ts +1 -1
  110. package/src/utils/fs.ts +6 -4
  111. package/src/utils/index.ts +4 -4
  112. package/src/utils/network-sync.ts +62 -115
  113. package/src/utils/node-polyfills.ts +8 -0
  114. package/src/utils/repo-factory.ts +55 -10
  115. package/src/utils/trace.ts +1 -1
  116. package/tsconfig.json +2 -1
@@ -1,13 +1,11 @@
1
1
  import {
2
2
  DocHandle,
3
- StorageId,
4
3
  Repo,
5
4
  AutomergeUrl,
6
5
  } from "@automerge/automerge-repo";
7
- import * as A from "@automerge/automerge";
8
- import { out } from "./output";
9
- import { DirectoryDocument } from "../types";
10
- import { getPlainUrl } from "./directory";
6
+ import { out } from "./output.js";
7
+ import { DirectoryDocument } from "../types/index.js";
8
+ import { getPlainUrl } from "./directory.js";
11
9
 
12
10
  const isDebug = !!process.env.DEBUG;
13
11
  function debug(...args: any[]) {
@@ -19,15 +17,16 @@ function debug(...args: any[]) {
19
17
  * This function waits until document heads stop changing, indicating that
20
18
  * both outgoing and incoming sync has completed.
21
19
  *
20
+ * With Subduction, sync is handled automatically by the transport layer.
21
+ * We poll heads until they stabilize to confirm sync completion.
22
+ *
22
23
  * @param repo - The Automerge repository
23
24
  * @param rootDirectoryUrl - The root directory URL to start traversal from
24
- * @param syncServerStorageId - The sync server storage ID
25
25
  * @param options - Configuration options
26
26
  */
27
27
  export async function waitForBidirectionalSync(
28
28
  repo: Repo,
29
29
  rootDirectoryUrl: AutomergeUrl | undefined,
30
- syncServerStorageId: StorageId | undefined,
31
30
  options: {
32
31
  timeoutMs?: number;
33
32
  pollIntervalMs?: number;
@@ -42,7 +41,7 @@ export async function waitForBidirectionalSync(
42
41
  handles,
43
42
  } = options;
44
43
 
45
- if (!syncServerStorageId || !rootDirectoryUrl) {
44
+ if (!rootDirectoryUrl) {
46
45
  return;
47
46
  }
48
47
 
@@ -217,129 +216,36 @@ export interface SyncWaitResult {
217
216
  const SYNC_BATCH_SIZE = 10;
218
217
 
219
218
  /**
220
- * Wait for a single document handle to sync to the server.
221
- * Resolves with the handle on success, rejects with the handle on timeout.
222
- */
223
- function waitForHandleSync(
224
- handle: DocHandle<unknown>,
225
- syncServerStorageId: StorageId,
226
- timeoutMs: number,
227
- startTime: number,
228
- ): Promise<DocHandle<unknown>> {
229
- return new Promise<DocHandle<unknown>>((resolve, reject) => {
230
- let pollInterval: NodeJS.Timeout;
231
-
232
- const cleanup = () => {
233
- clearTimeout(timeout);
234
- clearInterval(pollInterval);
235
- handle.off("remote-heads", onRemoteHeads);
236
- };
237
-
238
- const onConverged = () => {
239
- debug(`waitForSync: ${handle.url}... converged in ${Date.now() - startTime}ms`);
240
- cleanup();
241
- resolve(handle);
242
- };
243
-
244
- const timeout = setTimeout(() => {
245
- debug(`waitForSync: ${handle.url}... timed out after ${timeoutMs}ms`);
246
- cleanup();
247
- reject(handle);
248
- }, timeoutMs);
249
-
250
- const isConverged = () => {
251
- const localHeads = handle.heads();
252
- const info = handle.getSyncInfo(syncServerStorageId);
253
- return A.equals(localHeads, info?.lastHeads);
254
- };
255
-
256
- const onRemoteHeads = ({
257
- storageId,
258
- }: {
259
- storageId: StorageId;
260
- heads: any;
261
- }) => {
262
- if (storageId === syncServerStorageId && isConverged()) {
263
- onConverged();
264
- }
265
- };
266
-
267
- // Initial check
268
- if (isConverged()) {
269
- cleanup();
270
- resolve(handle);
271
- return;
272
- }
273
-
274
- // Start polling and event listening
275
- pollInterval = setInterval(() => {
276
- if (isConverged()) {
277
- onConverged();
278
- }
279
- }, 100);
280
-
281
- handle.on("remote-heads", onRemoteHeads);
282
- });
283
- }
284
-
285
- /**
286
- * Wait for documents to sync to the remote server.
287
- * Processes handles in batches to avoid flooding the server.
288
- * Returns a result with any failed handles instead of throwing,
289
- * so callers can attempt recovery (e.g. recreating documents).
219
+ * Wait for documents to sync by polling head stability.
220
+ *
221
+ * With Subduction, sync is automatic via the transport layer.
222
+ * We wait for each handle's heads to stabilize (stop changing),
223
+ * which indicates the sync layer has finished processing.
224
+ *
225
+ * Processes handles in batches to avoid overwhelming the system.
290
226
  */
291
227
  export async function waitForSync(
292
228
  handlesToWaitOn: DocHandle<unknown>[],
293
- syncServerStorageId?: StorageId,
294
229
  timeoutMs: number = 60000,
295
230
  ): Promise<SyncWaitResult> {
296
231
  const startTime = Date.now();
297
232
 
298
- if (!syncServerStorageId) {
299
- debug("waitForSync: no sync server storage ID, skipping");
300
- return { failed: [] };
301
- }
302
-
303
233
  if (handlesToWaitOn.length === 0) {
304
234
  debug("waitForSync: no documents to sync");
305
235
  return { failed: [] };
306
236
  }
307
237
 
308
238
  debug(`waitForSync: waiting for ${handlesToWaitOn.length} documents (timeout=${timeoutMs}ms, batchSize=${SYNC_BATCH_SIZE})`);
309
-
310
- // Separate already-synced from needs-sync
311
- const needsSync: DocHandle<unknown>[] = [];
312
- let alreadySynced = 0;
313
-
314
- for (const handle of handlesToWaitOn) {
315
- const heads = handle.heads();
316
- const syncInfo = handle.getSyncInfo(syncServerStorageId);
317
- const remoteHeads = syncInfo?.lastHeads;
318
- if (A.equals(heads, remoteHeads)) {
319
- alreadySynced++;
320
- debug(`waitForSync: ${handle.url}... already synced`);
321
- } else {
322
- debug(`waitForSync: ${handle.url}... needs sync (remoteHeads=${remoteHeads ? 'present' : 'missing'})`);
323
- needsSync.push(handle);
324
- }
325
- }
326
-
327
- if (needsSync.length > 0) {
328
- debug(`waitForSync: ${alreadySynced} already synced, ${needsSync.length} need sync`);
329
- out.taskLine(`Uploading: ${alreadySynced}/${handlesToWaitOn.length} already synced, waiting for ${needsSync.length} more`);
330
- } else {
331
- debug(`waitForSync: all ${handlesToWaitOn.length} already synced`);
332
- return { failed: [] };
333
- }
239
+ out.taskLine(`Uploading: waiting for ${handlesToWaitOn.length} documents to sync`);
334
240
 
335
241
  // Process in batches to avoid flooding the server
336
242
  const failed: DocHandle<unknown>[] = [];
337
- let synced = alreadySynced;
243
+ let synced = 0;
338
244
 
339
- for (let i = 0; i < needsSync.length; i += SYNC_BATCH_SIZE) {
340
- const batch = needsSync.slice(i, i + SYNC_BATCH_SIZE);
245
+ for (let i = 0; i < handlesToWaitOn.length; i += SYNC_BATCH_SIZE) {
246
+ const batch = handlesToWaitOn.slice(i, i + SYNC_BATCH_SIZE);
341
247
  const batchNum = Math.floor(i / SYNC_BATCH_SIZE) + 1;
342
- const totalBatches = Math.ceil(needsSync.length / SYNC_BATCH_SIZE);
248
+ const totalBatches = Math.ceil(handlesToWaitOn.length / SYNC_BATCH_SIZE);
343
249
 
344
250
  if (totalBatches > 1) {
345
251
  debug(`waitForSync: batch ${batchNum}/${totalBatches} (${batch.length} docs)`);
@@ -347,7 +253,7 @@ export async function waitForSync(
347
253
  }
348
254
 
349
255
  const results = await Promise.allSettled(
350
- batch.map(handle => waitForHandleSync(handle, syncServerStorageId, timeoutMs, startTime))
256
+ batch.map(handle => waitForHandleHeadStability(handle, timeoutMs, startTime))
351
257
  );
352
258
 
353
259
  for (const result of results) {
@@ -364,9 +270,50 @@ export async function waitForSync(
364
270
  debug(`waitForSync: ${failed.length} documents failed after ${elapsed}ms`);
365
271
  out.taskLine(`Upload: ${synced} synced, ${failed.length} failed after ${(elapsed / 1000).toFixed(1)}s`, true);
366
272
  } else {
367
- debug(`waitForSync: all ${handlesToWaitOn.length} documents synced in ${elapsed}ms (${alreadySynced} were already synced)`);
273
+ debug(`waitForSync: all ${handlesToWaitOn.length} documents synced in ${elapsed}ms`);
368
274
  out.taskLine(`All ${handlesToWaitOn.length} documents uploaded to server (${(elapsed / 1000).toFixed(1)}s)`);
369
275
  }
370
276
 
371
277
  return { failed };
372
278
  }
279
+
280
+ /**
281
+ * Wait for a single document handle's heads to stabilize.
282
+ * Polls the handle's heads and waits until they remain unchanged
283
+ * for several consecutive checks, indicating sync completion.
284
+ *
285
+ * Resolves with the handle on success, rejects with the handle on timeout.
286
+ */
287
+ function waitForHandleHeadStability(
288
+ handle: DocHandle<unknown>,
289
+ timeoutMs: number,
290
+ startTime: number,
291
+ ): Promise<DocHandle<unknown>> {
292
+ return new Promise<DocHandle<unknown>>((resolve, reject) => {
293
+ let lastHeads = JSON.stringify(handle.heads());
294
+ let stableCount = 0;
295
+ const stableRequired = 3;
296
+
297
+ const pollInterval = setInterval(() => {
298
+ const currentHeads = JSON.stringify(handle.heads());
299
+ if (currentHeads === lastHeads) {
300
+ stableCount++;
301
+ if (stableCount >= stableRequired) {
302
+ clearInterval(pollInterval);
303
+ clearTimeout(timeout);
304
+ debug(`waitForSync: ${handle.url}... converged in ${Date.now() - startTime}ms`);
305
+ resolve(handle);
306
+ }
307
+ } else {
308
+ stableCount = 0;
309
+ lastHeads = currentHeads;
310
+ }
311
+ }, 100);
312
+
313
+ const timeout = setTimeout(() => {
314
+ clearInterval(pollInterval);
315
+ debug(`waitForSync: ${handle.url}... timed out after ${timeoutMs}ms`);
316
+ reject(handle);
317
+ }, timeoutMs);
318
+ });
319
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Polyfills for browser APIs required by @automerge/automerge-subduction.
3
+ * Must be imported before any subduction code.
4
+ *
5
+ * The Subduction WASM module uses IndexedDB for key persistence
6
+ * (via WebCryptoSigner). In Node.js we provide a fake-indexeddb polyfill.
7
+ */
8
+ import "fake-indexeddb/auto";
@@ -1,28 +1,73 @@
1
+ import "./node-polyfills.js";
1
2
  import { Repo } from "@automerge/automerge-repo";
2
3
  import { NodeFSStorageAdapter } from "@automerge/automerge-repo-storage-nodefs";
3
- import { BrowserWebSocketClientAdapter } from "@automerge/automerge-repo-network-websocket";
4
+ import * as subductionModule from "@automerge/automerge-subduction";
5
+ import {
6
+ initSubductionModule,
7
+ SubductionStorageBridge,
8
+ } from "@automerge/automerge-repo-subduction-bridge";
4
9
  import * as path from "path";
5
- import { DirectoryConfig } from "../types";
10
+ import * as os from "os";
11
+ import { DirectoryConfig } from "../types/index.js";
12
+
13
+ const { WebCryptoSigner, Subduction } = subductionModule;
14
+
15
+ let subductionModuleInitialized = false;
16
+
17
+ function ensureSubductionModuleInit() {
18
+ if (!subductionModuleInitialized) {
19
+ initSubductionModule(subductionModule);
20
+ subductionModuleInitialized = true;
21
+ }
22
+ }
6
23
 
7
24
  /**
8
- * Create an Automerge repository with configuration-based setup
25
+ * Create an Automerge repository with Subduction-based setup
9
26
  */
10
27
  export async function createRepo(
11
28
  workingDir: string,
12
29
  config: DirectoryConfig
13
30
  ): Promise<Repo> {
31
+ ensureSubductionModuleInit();
32
+
14
33
  const syncToolDir = path.join(workingDir, ".pushwork");
15
- const storage = new NodeFSStorageAdapter(path.join(syncToolDir, "automerge"));
34
+ const nodeStorage = new NodeFSStorageAdapter(path.join(syncToolDir, "automerge"));
16
35
 
17
- const repoConfig: any = { storage };
36
+ const signer = await WebCryptoSigner.setup();
37
+ const storageBridge = new SubductionStorageBridge(nodeStorage);
38
+ const subduction = await Subduction.hydrate(signer, storageBridge);
18
39
 
19
- // Add network adapter only if sync is enabled and server is configured
40
+ // Connect to sync server if sync is enabled
20
41
  if (config.sync_enabled && config.sync_server) {
21
- const networkAdapter = new BrowserWebSocketClientAdapter(
22
- config.sync_server
42
+ await subduction.connectDiscover(
43
+ new URL(config.sync_server),
44
+ signer
23
45
  );
24
- repoConfig.network = [networkAdapter];
25
46
  }
26
47
 
27
- return new Repo(repoConfig);
48
+ return new Repo({ subduction } as any);
49
+ }
50
+
51
+ /**
52
+ * Create an ephemeral Automerge repository for remote reads.
53
+ * Uses a temporary directory for storage.
54
+ */
55
+ export async function createEphemeralRepo(
56
+ syncServer: string
57
+ ): Promise<Repo> {
58
+ ensureSubductionModuleInit();
59
+
60
+ const tmpDir = path.join(os.tmpdir(), `pushwork-ephemeral-${Date.now()}`);
61
+ const nodeStorage = new NodeFSStorageAdapter(tmpDir);
62
+
63
+ const signer = await WebCryptoSigner.setup();
64
+ const storageBridge = new SubductionStorageBridge(nodeStorage);
65
+ const subduction = await Subduction.hydrate(signer, storageBridge);
66
+
67
+ await subduction.connectDiscover(
68
+ new URL(syncServer),
69
+ signer
70
+ );
71
+
72
+ return new Repo({ subduction } as any);
28
73
  }
@@ -1,4 +1,4 @@
1
- import { out } from "./output";
1
+ import { out } from "./output.js";
2
2
 
3
3
  /**
4
4
  * Global tracing state
package/tsconfig.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "compilerOptions": {
3
3
  "target": "ES2020",
4
- "module": "commonjs",
4
+ "module": "nodenext",
5
+ "moduleResolution": "nodenext",
5
6
  "lib": ["ES2020"],
6
7
  "outDir": "./dist",
7
8
  "rootDir": "./src",