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.
- package/CLAUDE.md +9 -5
- package/dist/cli/commands.d.ts +71 -0
- package/dist/cli/commands.d.ts.map +1 -0
- package/dist/cli/commands.js +794 -0
- package/dist/cli/commands.js.map +1 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +19 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli.js +48 -55
- package/dist/cli.js.map +1 -1
- package/dist/commands.d.ts +5 -1
- package/dist/commands.d.ts.map +1 -1
- package/dist/commands.js +262 -263
- package/dist/commands.js.map +1 -1
- package/dist/config/index.d.ts +71 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +314 -0
- package/dist/config/index.js.map +1 -0
- package/dist/core/change-detection.d.ts +2 -2
- package/dist/core/change-detection.d.ts.map +1 -1
- package/dist/core/change-detection.js +82 -109
- package/dist/core/change-detection.js.map +1 -1
- package/dist/core/config.d.ts +1 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +14 -57
- package/dist/core/config.js.map +1 -1
- package/dist/core/index.d.ts +5 -5
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +5 -21
- package/dist/core/index.js.map +1 -1
- package/dist/core/move-detection.d.ts +2 -2
- package/dist/core/move-detection.d.ts.map +1 -1
- package/dist/core/move-detection.js +9 -13
- package/dist/core/move-detection.js.map +1 -1
- package/dist/core/snapshot.d.ts +1 -1
- package/dist/core/snapshot.d.ts.map +1 -1
- package/dist/core/snapshot.js +9 -46
- package/dist/core/snapshot.js.map +1 -1
- package/dist/core/sync-engine.d.ts +1 -1
- package/dist/core/sync-engine.d.ts.map +1 -1
- package/dist/core/sync-engine.js +126 -151
- package/dist/core/sync-engine.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -20
- package/dist/index.js.map +1 -1
- package/dist/types/config.d.ts +7 -6
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/config.js +1 -5
- package/dist/types/config.js.map +1 -1
- package/dist/types/documents.js +4 -7
- package/dist/types/documents.js.map +1 -1
- package/dist/types/index.d.ts +3 -3
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +3 -19
- package/dist/types/index.js.map +1 -1
- package/dist/types/snapshot.js +1 -2
- package/dist/utils/content-similarity.d.ts +53 -0
- package/dist/utils/content-similarity.d.ts.map +1 -0
- package/dist/utils/content-similarity.js +155 -0
- package/dist/utils/content-similarity.js.map +1 -0
- package/dist/utils/content.js +4 -8
- package/dist/utils/content.js.map +1 -1
- package/dist/utils/directory.js +5 -9
- package/dist/utils/directory.js.map +1 -1
- package/dist/utils/fs.d.ts +1 -1
- package/dist/utils/fs.d.ts.map +1 -1
- package/dist/utils/fs.js +34 -84
- package/dist/utils/fs.js.map +1 -1
- package/dist/utils/index.d.ts +4 -4
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +4 -20
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/mime-types.js +5 -43
- package/dist/utils/mime-types.js.map +1 -1
- package/dist/utils/network-sync.d.ts +13 -8
- package/dist/utils/network-sync.d.ts.map +1 -1
- package/dist/utils/network-sync.js +65 -137
- package/dist/utils/network-sync.js.map +1 -1
- package/dist/utils/node-polyfills.d.ts +9 -0
- package/dist/utils/node-polyfills.d.ts.map +1 -0
- package/dist/utils/node-polyfills.js +9 -0
- package/dist/utils/node-polyfills.js.map +1 -0
- package/dist/utils/output.js +32 -39
- package/dist/utils/output.js.map +1 -1
- package/dist/utils/repo-factory.d.ts +8 -2
- package/dist/utils/repo-factory.d.ts.map +1 -1
- package/dist/utils/repo-factory.js +38 -47
- package/dist/utils/repo-factory.js.map +1 -1
- package/dist/utils/string-similarity.js +1 -5
- package/dist/utils/string-similarity.js.map +1 -1
- package/dist/utils/text-diff.js +5 -43
- package/dist/utils/text-diff.js.map +1 -1
- package/dist/utils/trace.js +6 -11
- package/dist/utils/trace.js.map +1 -1
- package/package.json +7 -5
- package/src/cli.ts +25 -34
- package/src/commands.ts +75 -11
- package/src/core/change-detection.ts +25 -10
- package/src/core/config.ts +2 -12
- package/src/core/index.ts +5 -5
- package/src/core/move-detection.ts +4 -4
- package/src/core/snapshot.ts +3 -3
- package/src/core/sync-engine.ts +24 -17
- package/src/index.ts +4 -4
- package/src/types/config.ts +8 -8
- package/src/types/index.ts +3 -3
- package/src/utils/directory.ts +1 -1
- package/src/utils/fs.ts +6 -4
- package/src/utils/index.ts +4 -4
- package/src/utils/network-sync.ts +62 -115
- package/src/utils/node-polyfills.ts +8 -0
- package/src/utils/repo-factory.ts +55 -10
- package/src/utils/trace.ts +1 -1
- 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
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
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 (!
|
|
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
|
|
221
|
-
*
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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 =
|
|
243
|
+
let synced = 0;
|
|
338
244
|
|
|
339
|
-
for (let i = 0; i <
|
|
340
|
-
const batch =
|
|
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(
|
|
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 =>
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
34
|
+
const nodeStorage = new NodeFSStorageAdapter(path.join(syncToolDir, "automerge"));
|
|
16
35
|
|
|
17
|
-
const
|
|
36
|
+
const signer = await WebCryptoSigner.setup();
|
|
37
|
+
const storageBridge = new SubductionStorageBridge(nodeStorage);
|
|
38
|
+
const subduction = await Subduction.hydrate(signer, storageBridge);
|
|
18
39
|
|
|
19
|
-
//
|
|
40
|
+
// Connect to sync server if sync is enabled
|
|
20
41
|
if (config.sync_enabled && config.sync_server) {
|
|
21
|
-
|
|
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(
|
|
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
|
}
|
package/src/utils/trace.ts
CHANGED