pushwork 2.0.0-preview.6 → 2.0.0-preview.8
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/dist/branches.d.ts +1 -2
- package/dist/branches.d.ts.map +1 -1
- package/dist/branches.js +4 -23
- package/dist/branches.js.map +1 -1
- 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 +56 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands.d.ts +58 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/commands.js +975 -0
- package/dist/commands.js.map +1 -0
- 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 +80 -0
- package/dist/core/change-detection.d.ts.map +1 -0
- package/dist/core/change-detection.js +560 -0
- package/dist/core/change-detection.js.map +1 -0
- package/dist/core/config.d.ts +81 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +304 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/index.d.ts +6 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +22 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/move-detection.d.ts +34 -0
- package/dist/core/move-detection.d.ts.map +1 -0
- package/dist/core/move-detection.js +128 -0
- package/dist/core/move-detection.js.map +1 -0
- package/dist/core/snapshot.d.ts +105 -0
- package/dist/core/snapshot.d.ts.map +1 -0
- package/dist/core/snapshot.js +254 -0
- package/dist/core/snapshot.js.map +1 -0
- package/dist/core/sync-engine.d.ts +177 -0
- package/dist/core/sync-engine.d.ts.map +1 -0
- package/dist/core/sync-engine.js +1471 -0
- package/dist/core/sync-engine.js.map +1 -0
- package/dist/pushwork.d.ts +4 -0
- package/dist/pushwork.d.ts.map +1 -1
- package/dist/pushwork.js +74 -2
- package/dist/pushwork.js.map +1 -1
- package/dist/stash.d.ts +0 -2
- package/dist/stash.d.ts.map +1 -1
- package/dist/stash.js +0 -1
- package/dist/stash.js.map +1 -1
- package/dist/types/config.d.ts +102 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/config.js +10 -0
- package/dist/types/config.js.map +1 -0
- package/dist/types/documents.d.ts +88 -0
- package/dist/types/documents.d.ts.map +1 -0
- package/dist/types/documents.js +23 -0
- package/dist/types/documents.js.map +1 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +20 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/snapshot.d.ts +64 -0
- package/dist/types/snapshot.d.ts.map +1 -0
- package/dist/types/snapshot.js +3 -0
- package/dist/types/snapshot.js.map +1 -0
- 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.d.ts +10 -0
- package/dist/utils/content.d.ts.map +1 -0
- package/dist/utils/content.js +35 -0
- package/dist/utils/content.js.map +1 -0
- package/dist/utils/directory.d.ts +24 -0
- package/dist/utils/directory.d.ts.map +1 -0
- package/dist/utils/directory.js +56 -0
- package/dist/utils/directory.js.map +1 -0
- package/dist/utils/fs.d.ts +74 -0
- package/dist/utils/fs.d.ts.map +1 -0
- package/dist/utils/fs.js +298 -0
- package/dist/utils/fs.js.map +1 -0
- package/dist/utils/index.d.ts +5 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +21 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/mime-types.d.ts +13 -0
- package/dist/utils/mime-types.d.ts.map +1 -0
- package/dist/utils/mime-types.js +247 -0
- package/dist/utils/mime-types.js.map +1 -0
- package/dist/utils/network-sync.d.ts +30 -0
- package/dist/utils/network-sync.d.ts.map +1 -0
- package/dist/utils/network-sync.js +391 -0
- package/dist/utils/network-sync.js.map +1 -0
- 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.d.ts +129 -0
- package/dist/utils/output.d.ts.map +1 -0
- package/dist/utils/output.js +375 -0
- package/dist/utils/output.js.map +1 -0
- package/dist/utils/repo-factory.d.ts +15 -0
- package/dist/utils/repo-factory.d.ts.map +1 -0
- package/dist/utils/repo-factory.js +156 -0
- package/dist/utils/repo-factory.js.map +1 -0
- package/dist/utils/string-similarity.d.ts +14 -0
- package/dist/utils/string-similarity.d.ts.map +1 -0
- package/dist/utils/string-similarity.js +43 -0
- package/dist/utils/string-similarity.js.map +1 -0
- package/dist/utils/text-diff.d.ts +37 -0
- package/dist/utils/text-diff.d.ts.map +1 -0
- package/dist/utils/text-diff.js +131 -0
- package/dist/utils/text-diff.js.map +1 -0
- package/dist/utils/trace.d.ts +19 -0
- package/dist/utils/trace.d.ts.map +1 -0
- package/dist/utils/trace.js +68 -0
- package/dist/utils/trace.js.map +1 -0
- package/package.json +5 -5
- package/dist/checkpoints.d.ts +0 -41
- package/dist/checkpoints.d.ts.map +0 -1
- package/dist/checkpoints.js +0 -210
- package/dist/checkpoints.js.map +0 -1
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.getEnhancedMimeType = getEnhancedMimeType;
|
|
37
|
+
exports.shouldForceAsText = shouldForceAsText;
|
|
38
|
+
exports.isEnhancedTextFile = isEnhancedTextFile;
|
|
39
|
+
const mimeTypes = __importStar(require("mime-types"));
|
|
40
|
+
/**
|
|
41
|
+
* Custom MIME type definitions for developer files
|
|
42
|
+
* Based on patchwork-cli's approach
|
|
43
|
+
*/
|
|
44
|
+
const CUSTOM_MIME_TYPES = {
|
|
45
|
+
// TypeScript files - override the incorrect video/mp2t detection
|
|
46
|
+
".ts": "text/typescript",
|
|
47
|
+
".tsx": "text/tsx",
|
|
48
|
+
// Config file formats
|
|
49
|
+
".json": "application/json",
|
|
50
|
+
".yaml": "text/yaml",
|
|
51
|
+
".yml": "text/yaml",
|
|
52
|
+
".toml": "application/toml",
|
|
53
|
+
".ini": "text/plain",
|
|
54
|
+
".conf": "text/plain",
|
|
55
|
+
".config": "text/plain",
|
|
56
|
+
// Vue.js single file components
|
|
57
|
+
".vue": "text/vue",
|
|
58
|
+
// Modern CSS preprocessors
|
|
59
|
+
".scss": "text/scss",
|
|
60
|
+
".sass": "text/sass",
|
|
61
|
+
".less": "text/less",
|
|
62
|
+
".styl": "text/stylus",
|
|
63
|
+
// Modern JavaScript variants
|
|
64
|
+
".mjs": "application/javascript",
|
|
65
|
+
".cjs": "application/javascript",
|
|
66
|
+
// React JSX
|
|
67
|
+
".jsx": "text/jsx",
|
|
68
|
+
// Svelte components
|
|
69
|
+
".svelte": "text/svelte",
|
|
70
|
+
// Web assembly
|
|
71
|
+
".wasm": "application/wasm",
|
|
72
|
+
// Other common dev files
|
|
73
|
+
".d.ts": "text/typescript",
|
|
74
|
+
".map": "application/json", // Source maps
|
|
75
|
+
".env": "text/plain",
|
|
76
|
+
".gitignore": "text/plain",
|
|
77
|
+
".gitattributes": "text/plain",
|
|
78
|
+
".editorconfig": "text/plain",
|
|
79
|
+
".prettierrc": "application/json",
|
|
80
|
+
".eslintrc": "application/json",
|
|
81
|
+
".babelrc": "application/json",
|
|
82
|
+
// Documentation formats
|
|
83
|
+
".mdx": "text/markdown",
|
|
84
|
+
".rst": "text/x-rst",
|
|
85
|
+
// Docker files
|
|
86
|
+
Dockerfile: "text/plain",
|
|
87
|
+
".dockerignore": "text/plain",
|
|
88
|
+
// Package manager files
|
|
89
|
+
"package.json": "application/json",
|
|
90
|
+
"package-lock.json": "application/json",
|
|
91
|
+
"yarn.lock": "text/plain",
|
|
92
|
+
"pnpm-lock.yaml": "text/yaml",
|
|
93
|
+
"composer.json": "application/json",
|
|
94
|
+
Pipfile: "text/plain",
|
|
95
|
+
"requirements.txt": "text/plain",
|
|
96
|
+
// Build tool configs
|
|
97
|
+
"webpack.config.js": "application/javascript",
|
|
98
|
+
"vite.config.js": "application/javascript",
|
|
99
|
+
"rollup.config.js": "application/javascript",
|
|
100
|
+
"tsconfig.json": "application/json",
|
|
101
|
+
"jsconfig.json": "application/json",
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* File extensions that should always be treated as text
|
|
105
|
+
* regardless of MIME type detection
|
|
106
|
+
*/
|
|
107
|
+
const FORCE_TEXT_EXTENSIONS = new Set([
|
|
108
|
+
".ts",
|
|
109
|
+
".tsx",
|
|
110
|
+
".jsx",
|
|
111
|
+
".vue",
|
|
112
|
+
".svelte",
|
|
113
|
+
".scss",
|
|
114
|
+
".sass",
|
|
115
|
+
".less",
|
|
116
|
+
".styl",
|
|
117
|
+
".env",
|
|
118
|
+
".gitignore",
|
|
119
|
+
".gitattributes",
|
|
120
|
+
".editorconfig",
|
|
121
|
+
".d.ts",
|
|
122
|
+
".map",
|
|
123
|
+
".mdx",
|
|
124
|
+
".rst",
|
|
125
|
+
".toml",
|
|
126
|
+
".ini",
|
|
127
|
+
".conf",
|
|
128
|
+
".config",
|
|
129
|
+
".lock",
|
|
130
|
+
]);
|
|
131
|
+
/**
|
|
132
|
+
* Get enhanced MIME type for file with custom dev file support
|
|
133
|
+
*/
|
|
134
|
+
function getEnhancedMimeType(filePath) {
|
|
135
|
+
const normalized = normalizePathSeparators(filePath);
|
|
136
|
+
const filename = normalized.split("/").pop() || "";
|
|
137
|
+
const extension = getFileExtension(normalized);
|
|
138
|
+
// Check custom definitions first (by extension)
|
|
139
|
+
if (extension && CUSTOM_MIME_TYPES[extension]) {
|
|
140
|
+
return CUSTOM_MIME_TYPES[extension];
|
|
141
|
+
}
|
|
142
|
+
// Check custom definitions by full filename
|
|
143
|
+
if (CUSTOM_MIME_TYPES[filename]) {
|
|
144
|
+
return CUSTOM_MIME_TYPES[filename];
|
|
145
|
+
}
|
|
146
|
+
// Fall back to standard mime-types library
|
|
147
|
+
const standardMime = mimeTypes.lookup(normalized);
|
|
148
|
+
if (standardMime) {
|
|
149
|
+
return standardMime;
|
|
150
|
+
}
|
|
151
|
+
// Final fallback
|
|
152
|
+
return "application/octet-stream";
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Check if file extension should be forced to text type
|
|
156
|
+
*/
|
|
157
|
+
function shouldForceAsText(filePath) {
|
|
158
|
+
const extension = getFileExtension(filePath);
|
|
159
|
+
return extension ? FORCE_TEXT_EXTENSIONS.has(extension) : false;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Get file extension including the dot (internal helper)
|
|
163
|
+
*/
|
|
164
|
+
function getFileExtension(filePath) {
|
|
165
|
+
const match = filePath.match(/\.[^.]*$/);
|
|
166
|
+
return match ? match[0].toLowerCase() : "";
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Normalize path separators to forward slashes for cross-platform consistency
|
|
170
|
+
*/
|
|
171
|
+
function normalizePathSeparators(p) {
|
|
172
|
+
return p.replace(/\\/g, "/");
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Enhanced text file detection with developer file support
|
|
176
|
+
*/
|
|
177
|
+
async function isEnhancedTextFile(filePath) {
|
|
178
|
+
// Force certain extensions to be treated as text
|
|
179
|
+
if (shouldForceAsText(filePath)) {
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
// Check MIME type
|
|
183
|
+
const mimeType = getEnhancedMimeType(filePath);
|
|
184
|
+
if (isTextMimeType(mimeType)) {
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
// If it's a known binary type (but not the generic fallback), don't fall back to content detection
|
|
188
|
+
if (isBinaryMimeType(mimeType) && mimeType !== "application/octet-stream") {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
// For generic octet-stream or unknown types, use content-based detection
|
|
192
|
+
return isTextByContent(filePath);
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Check if MIME type indicates text content
|
|
196
|
+
*/
|
|
197
|
+
function isTextMimeType(mimeType) {
|
|
198
|
+
return (mimeType.startsWith("text/") ||
|
|
199
|
+
mimeType === "application/json" ||
|
|
200
|
+
mimeType === "application/xml" ||
|
|
201
|
+
mimeType === "application/javascript" ||
|
|
202
|
+
mimeType === "application/typescript" ||
|
|
203
|
+
mimeType === "application/toml" ||
|
|
204
|
+
mimeType.includes("javascript") ||
|
|
205
|
+
mimeType.includes("typescript") ||
|
|
206
|
+
mimeType.includes("json") ||
|
|
207
|
+
mimeType.includes("xml"));
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Check if MIME type indicates binary content
|
|
211
|
+
*/
|
|
212
|
+
function isBinaryMimeType(mimeType) {
|
|
213
|
+
return (mimeType.startsWith("image/") ||
|
|
214
|
+
mimeType.startsWith("video/") ||
|
|
215
|
+
mimeType.startsWith("audio/") ||
|
|
216
|
+
mimeType.startsWith("font/") ||
|
|
217
|
+
mimeType === "application/zip" ||
|
|
218
|
+
mimeType === "application/pdf" ||
|
|
219
|
+
mimeType === "application/octet-stream" ||
|
|
220
|
+
mimeType === "application/wasm" ||
|
|
221
|
+
mimeType.includes("binary"));
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Content-based text detection (fallback method)
|
|
225
|
+
*/
|
|
226
|
+
async function isTextByContent(filePath) {
|
|
227
|
+
try {
|
|
228
|
+
const fs = await Promise.resolve().then(() => __importStar(require("fs/promises")));
|
|
229
|
+
// Sample first 8KB to detect binary content
|
|
230
|
+
const handle = await fs.open(filePath, "r");
|
|
231
|
+
const stats = await handle.stat();
|
|
232
|
+
const sampleSize = Math.min(8192, stats.size);
|
|
233
|
+
if (sampleSize === 0) {
|
|
234
|
+
await handle.close();
|
|
235
|
+
return true; // Empty file is text
|
|
236
|
+
}
|
|
237
|
+
const buffer = Buffer.alloc(sampleSize);
|
|
238
|
+
await handle.read(buffer, 0, sampleSize, 0);
|
|
239
|
+
await handle.close();
|
|
240
|
+
// Check for null bytes which indicate binary content
|
|
241
|
+
return !buffer.includes(0);
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
//# sourceMappingURL=mime-types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mime-types.js","sourceRoot":"","sources":["../../src/utils/mime-types.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { DocHandle, StorageId, Repo, AutomergeUrl } from "@automerge/automerge-repo";
|
|
2
|
+
/**
|
|
3
|
+
* Wait for bidirectional sync to stabilize.
|
|
4
|
+
* This function waits until document heads stop changing, indicating that
|
|
5
|
+
* both outgoing and incoming sync has completed.
|
|
6
|
+
*
|
|
7
|
+
* @param repo - The Automerge repository
|
|
8
|
+
* @param rootDirectoryUrl - The root directory URL to start traversal from
|
|
9
|
+
* @param options - Configuration options
|
|
10
|
+
*/
|
|
11
|
+
export declare function waitForBidirectionalSync(repo: Repo, rootDirectoryUrl: AutomergeUrl | undefined, options?: {
|
|
12
|
+
timeoutMs?: number;
|
|
13
|
+
pollIntervalMs?: number;
|
|
14
|
+
stableChecksRequired?: number;
|
|
15
|
+
handles?: DocHandle<unknown>[];
|
|
16
|
+
}): Promise<void>;
|
|
17
|
+
/**
|
|
18
|
+
* Result of waitForSync — lists which handles failed to sync.
|
|
19
|
+
*/
|
|
20
|
+
export interface SyncWaitResult {
|
|
21
|
+
failed: DocHandle<unknown>[];
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Wait for documents to sync to the remote server.
|
|
25
|
+
* Processes handles in batches to avoid flooding the server.
|
|
26
|
+
* Returns a result with any failed handles instead of throwing,
|
|
27
|
+
* so callers can attempt recovery (e.g. recreating documents).
|
|
28
|
+
*/
|
|
29
|
+
export declare function waitForSync(handlesToWaitOn: DocHandle<unknown>[], syncServerStorageId?: StorageId, timeoutMs?: number): Promise<SyncWaitResult>;
|
|
30
|
+
//# sourceMappingURL=network-sync.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"network-sync.d.ts","sourceRoot":"","sources":["../../src/utils/network-sync.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,SAAS,EACT,SAAS,EACT,IAAI,EACJ,YAAY,EACb,MAAM,2BAA2B,CAAC;AAWnC;;;;;;;;GAQG;AACH,wBAAsB,wBAAwB,CAC5C,IAAI,EAAE,IAAI,EACV,gBAAgB,EAAE,YAAY,GAAG,SAAS,EAC1C,OAAO,GAAE;IACP,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,OAAO,CAAC,EAAE,SAAS,CAAC,OAAO,CAAC,EAAE,CAAC;CAC3B,GACL,OAAO,CAAC,IAAI,CAAC,CAiFf;AA2FD;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,SAAS,CAAC,OAAO,CAAC,EAAE,CAAC;CAC9B;AAuED;;;;;GAKG;AACH,wBAAsB,WAAW,CAC/B,eAAe,EAAE,SAAS,CAAC,OAAO,CAAC,EAAE,EACrC,mBAAmB,CAAC,EAAE,SAAS,EAC/B,SAAS,GAAE,MAAc,GACxB,OAAO,CAAC,cAAc,CAAC,CAiFzB"}
|
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.waitForBidirectionalSync = waitForBidirectionalSync;
|
|
37
|
+
exports.waitForSync = waitForSync;
|
|
38
|
+
const A = __importStar(require("@automerge/automerge"));
|
|
39
|
+
const output_1 = require("./output");
|
|
40
|
+
const directory_1 = require("./directory");
|
|
41
|
+
const isDebug = !!process.env.DEBUG;
|
|
42
|
+
function debug(...args) {
|
|
43
|
+
if (isDebug)
|
|
44
|
+
console.error("[pushwork:sync]", ...args);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Wait for bidirectional sync to stabilize.
|
|
48
|
+
* This function waits until document heads stop changing, indicating that
|
|
49
|
+
* both outgoing and incoming sync has completed.
|
|
50
|
+
*
|
|
51
|
+
* @param repo - The Automerge repository
|
|
52
|
+
* @param rootDirectoryUrl - The root directory URL to start traversal from
|
|
53
|
+
* @param options - Configuration options
|
|
54
|
+
*/
|
|
55
|
+
async function waitForBidirectionalSync(repo, rootDirectoryUrl, options = {}) {
|
|
56
|
+
const { timeoutMs = 10000, pollIntervalMs = 100, stableChecksRequired = 3, handles, } = options;
|
|
57
|
+
if (!rootDirectoryUrl) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const startTime = Date.now();
|
|
61
|
+
let lastSeenHeads = new Map();
|
|
62
|
+
let stableCount = 0;
|
|
63
|
+
let pollCount = 0;
|
|
64
|
+
let dynamicTimeoutMs = timeoutMs;
|
|
65
|
+
debug(`waitForBidirectionalSync: starting (timeout=${timeoutMs}ms, stableChecks=${stableChecksRequired}${handles ? `, tracking ${handles.length} handles` : ', full tree scan'})`);
|
|
66
|
+
while (Date.now() - startTime < dynamicTimeoutMs) {
|
|
67
|
+
pollCount++;
|
|
68
|
+
// Get current heads: use provided handles if available, otherwise full tree scan
|
|
69
|
+
const currentHeads = handles
|
|
70
|
+
? getHandleHeads(handles)
|
|
71
|
+
: await getAllDocumentHeads(repo, rootDirectoryUrl);
|
|
72
|
+
// After first scan: scale timeout to tree size and reset the clock.
|
|
73
|
+
// The first scan is just establishing a baseline — its duration
|
|
74
|
+
// shouldn't count against the stability-wait timeout.
|
|
75
|
+
if (pollCount === 1) {
|
|
76
|
+
const scanDuration = Date.now() - startTime;
|
|
77
|
+
dynamicTimeoutMs = Math.max(timeoutMs, 5000 + currentHeads.size * 50) + scanDuration;
|
|
78
|
+
debug(`waitForBidirectionalSync: first scan took ${scanDuration}ms, timeout now ${dynamicTimeoutMs}ms for ${currentHeads.size} docs`);
|
|
79
|
+
}
|
|
80
|
+
// Check if heads are stable (no changes since last check)
|
|
81
|
+
const isStable = headsMapEqual(lastSeenHeads, currentHeads);
|
|
82
|
+
if (isStable) {
|
|
83
|
+
stableCount++;
|
|
84
|
+
debug(`waitForBidirectionalSync: stable check ${stableCount}/${stableChecksRequired} (${currentHeads.size} docs, poll #${pollCount})`);
|
|
85
|
+
if (stableCount >= stableChecksRequired) {
|
|
86
|
+
const elapsed = Date.now() - startTime;
|
|
87
|
+
debug(`waitForBidirectionalSync: converged in ${elapsed}ms after ${pollCount} polls (${currentHeads.size} docs)`);
|
|
88
|
+
output_1.out.taskLine(`Bidirectional sync converged (${currentHeads.size} docs, ${elapsed}ms)`);
|
|
89
|
+
return; // Converged!
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
// Find which docs changed
|
|
94
|
+
if (lastSeenHeads.size > 0) {
|
|
95
|
+
const changedDocs = [];
|
|
96
|
+
for (const [url, heads] of currentHeads) {
|
|
97
|
+
if (lastSeenHeads.get(url) !== heads) {
|
|
98
|
+
changedDocs.push(url);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const newDocs = currentHeads.size - lastSeenHeads.size;
|
|
102
|
+
if (newDocs > 0) {
|
|
103
|
+
debug(`waitForBidirectionalSync: ${newDocs} new docs discovered, ${changedDocs.length} docs changed heads (poll #${pollCount})`);
|
|
104
|
+
}
|
|
105
|
+
else if (changedDocs.length > 0) {
|
|
106
|
+
debug(`waitForBidirectionalSync: ${changedDocs.length} docs changed heads: ${changedDocs.slice(0, 5).join(", ")}${changedDocs.length > 5 ? ` ...and ${changedDocs.length - 5} more` : ""} (poll #${pollCount})`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
debug(`waitForBidirectionalSync: initial scan found ${currentHeads.size} docs (poll #${pollCount})`);
|
|
111
|
+
}
|
|
112
|
+
if (stableCount > 0) {
|
|
113
|
+
debug(`waitForBidirectionalSync: heads changed after ${stableCount} stable checks, resetting`);
|
|
114
|
+
}
|
|
115
|
+
stableCount = 0;
|
|
116
|
+
lastSeenHeads = currentHeads;
|
|
117
|
+
}
|
|
118
|
+
await new Promise((r) => setTimeout(r, pollIntervalMs));
|
|
119
|
+
}
|
|
120
|
+
// Timeout - but don't throw, just log a warning
|
|
121
|
+
// The sync may still work, we just couldn't confirm stability
|
|
122
|
+
const elapsed = Date.now() - startTime;
|
|
123
|
+
debug(`waitForBidirectionalSync: timed out after ${elapsed}ms (${pollCount} polls, ${lastSeenHeads.size} docs tracked, reached ${stableCount}/${stableChecksRequired} stable checks)`);
|
|
124
|
+
output_1.out.taskLine(`Bidirectional sync timed out after ${(elapsed / 1000).toFixed(1)}s - document heads were still changing after ${pollCount} checks across ${lastSeenHeads.size} docs (reached ${stableCount}/${stableChecksRequired} stability checks). This may mean another peer is actively editing, or the sync server is slow to relay changes. The sync will continue but some remote changes may not be reflected yet.`, true);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Get heads from a pre-collected set of handles (cheap, synchronous reads).
|
|
128
|
+
* Used for post-push stabilization where we already know which documents changed.
|
|
129
|
+
*/
|
|
130
|
+
function getHandleHeads(handles) {
|
|
131
|
+
const heads = new Map();
|
|
132
|
+
for (const handle of handles) {
|
|
133
|
+
heads.set((0, directory_1.getPlainUrl)(handle.url), JSON.stringify(handle.heads()));
|
|
134
|
+
}
|
|
135
|
+
return heads;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Get all document heads in the directory hierarchy.
|
|
139
|
+
* Returns a map of document URL -> serialized heads.
|
|
140
|
+
* Uses plain URLs (without heads) to ensure we see current document state.
|
|
141
|
+
*/
|
|
142
|
+
async function getAllDocumentHeads(repo, rootDirectoryUrl) {
|
|
143
|
+
const heads = new Map();
|
|
144
|
+
// Pass URL as-is; collectHeadsRecursive will strip heads
|
|
145
|
+
await collectHeadsRecursive(repo, rootDirectoryUrl, heads);
|
|
146
|
+
return heads;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Recursively collect document heads from the directory hierarchy.
|
|
150
|
+
* Uses getPlainUrl to strip heads and always see the CURRENT state of documents.
|
|
151
|
+
*/
|
|
152
|
+
async function collectHeadsRecursive(repo, directoryUrl, heads) {
|
|
153
|
+
try {
|
|
154
|
+
const plainUrl = (0, directory_1.getPlainUrl)(directoryUrl);
|
|
155
|
+
const handle = await repo.find(plainUrl);
|
|
156
|
+
const doc = await handle.doc();
|
|
157
|
+
// Record this directory's heads (use plain URL as key for consistency)
|
|
158
|
+
heads.set(plainUrl, JSON.stringify(handle.heads()));
|
|
159
|
+
if (!doc || !doc.docs) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
// Process all entries in the directory concurrently
|
|
163
|
+
await Promise.all(doc.docs.map(async (entry) => {
|
|
164
|
+
if (entry.type === "folder") {
|
|
165
|
+
// Recurse into subdirectory (entry.url may have stale heads)
|
|
166
|
+
await collectHeadsRecursive(repo, entry.url, heads);
|
|
167
|
+
}
|
|
168
|
+
else if (entry.type === "file") {
|
|
169
|
+
// Get file document heads (strip heads from entry.url)
|
|
170
|
+
try {
|
|
171
|
+
const fileUrl = (0, directory_1.getPlainUrl)(entry.url);
|
|
172
|
+
const fileHandle = await repo.find(fileUrl);
|
|
173
|
+
heads.set(fileUrl, JSON.stringify(fileHandle.heads()));
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
// File document may not exist yet
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}));
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
// Directory may not exist yet
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Compare two heads maps for equality.
|
|
187
|
+
*/
|
|
188
|
+
function headsMapEqual(a, b) {
|
|
189
|
+
if (a.size !== b.size) {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
for (const [key, value] of a) {
|
|
193
|
+
if (b.get(key) !== value) {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
/** Maximum documents to sync concurrently to avoid flooding the server */
|
|
200
|
+
const SYNC_BATCH_SIZE = 10;
|
|
201
|
+
/**
|
|
202
|
+
* Wait for a single document handle to sync to the server.
|
|
203
|
+
* Resolves with the handle on success, rejects with the handle on timeout.
|
|
204
|
+
*/
|
|
205
|
+
function waitForHandleSync(handle, syncServerStorageId, timeoutMs, startTime) {
|
|
206
|
+
return new Promise((resolve, reject) => {
|
|
207
|
+
let pollInterval;
|
|
208
|
+
const cleanup = () => {
|
|
209
|
+
clearTimeout(timeout);
|
|
210
|
+
clearInterval(pollInterval);
|
|
211
|
+
handle.off("remote-heads", onRemoteHeads);
|
|
212
|
+
};
|
|
213
|
+
const onConverged = () => {
|
|
214
|
+
debug(`waitForSync: ${handle.url}... converged in ${Date.now() - startTime}ms`);
|
|
215
|
+
cleanup();
|
|
216
|
+
resolve(handle);
|
|
217
|
+
};
|
|
218
|
+
const timeout = setTimeout(() => {
|
|
219
|
+
debug(`waitForSync: ${handle.url}... timed out after ${timeoutMs}ms`);
|
|
220
|
+
cleanup();
|
|
221
|
+
reject(handle);
|
|
222
|
+
}, timeoutMs);
|
|
223
|
+
const isConverged = () => {
|
|
224
|
+
const localHeads = handle.heads();
|
|
225
|
+
const info = handle.getSyncInfo(syncServerStorageId);
|
|
226
|
+
return A.equals(localHeads, info?.lastHeads);
|
|
227
|
+
};
|
|
228
|
+
const onRemoteHeads = ({ storageId, }) => {
|
|
229
|
+
if (storageId === syncServerStorageId && isConverged()) {
|
|
230
|
+
onConverged();
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
// Initial check
|
|
234
|
+
if (isConverged()) {
|
|
235
|
+
cleanup();
|
|
236
|
+
resolve(handle);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
// Start polling and event listening
|
|
240
|
+
pollInterval = setInterval(() => {
|
|
241
|
+
if (isConverged()) {
|
|
242
|
+
onConverged();
|
|
243
|
+
}
|
|
244
|
+
}, 100);
|
|
245
|
+
handle.on("remote-heads", onRemoteHeads);
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Wait for documents to sync to the remote server.
|
|
250
|
+
* Processes handles in batches to avoid flooding the server.
|
|
251
|
+
* Returns a result with any failed handles instead of throwing,
|
|
252
|
+
* so callers can attempt recovery (e.g. recreating documents).
|
|
253
|
+
*/
|
|
254
|
+
async function waitForSync(handlesToWaitOn, syncServerStorageId, timeoutMs = 60000) {
|
|
255
|
+
const startTime = Date.now();
|
|
256
|
+
if (handlesToWaitOn.length === 0) {
|
|
257
|
+
debug("waitForSync: no documents to sync");
|
|
258
|
+
return { failed: [] };
|
|
259
|
+
}
|
|
260
|
+
// When no StorageId is available (Subduction mode), use head-stability
|
|
261
|
+
// polling. The SubductionSource handles sync internally — we just wait
|
|
262
|
+
// for each handle's heads to stop changing.
|
|
263
|
+
if (!syncServerStorageId) {
|
|
264
|
+
debug(`waitForSync: no storage ID, using head-stability polling for ${handlesToWaitOn.length} documents`);
|
|
265
|
+
output_1.out.taskLine(`Waiting for ${handlesToWaitOn.length} documents to sync`);
|
|
266
|
+
return waitForSyncViaHeadStability(handlesToWaitOn, timeoutMs, startTime);
|
|
267
|
+
}
|
|
268
|
+
debug(`waitForSync: waiting for ${handlesToWaitOn.length} documents (timeout=${timeoutMs}ms, batchSize=${SYNC_BATCH_SIZE})`);
|
|
269
|
+
// Separate already-synced from needs-sync
|
|
270
|
+
const needsSync = [];
|
|
271
|
+
let alreadySynced = 0;
|
|
272
|
+
for (const handle of handlesToWaitOn) {
|
|
273
|
+
const heads = handle.heads();
|
|
274
|
+
const syncInfo = handle.getSyncInfo(syncServerStorageId);
|
|
275
|
+
const remoteHeads = syncInfo?.lastHeads;
|
|
276
|
+
if (A.equals(heads, remoteHeads)) {
|
|
277
|
+
alreadySynced++;
|
|
278
|
+
debug(`waitForSync: ${handle.url}... already synced`);
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
debug(`waitForSync: ${handle.url}... needs sync (remoteHeads=${remoteHeads ? 'present' : 'missing'})`);
|
|
282
|
+
needsSync.push(handle);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
if (needsSync.length > 0) {
|
|
286
|
+
debug(`waitForSync: ${alreadySynced} already synced, ${needsSync.length} need sync`);
|
|
287
|
+
output_1.out.taskLine(`Uploading: ${alreadySynced}/${handlesToWaitOn.length} already synced, waiting for ${needsSync.length} more`);
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
debug(`waitForSync: all ${handlesToWaitOn.length} already synced`);
|
|
291
|
+
return { failed: [] };
|
|
292
|
+
}
|
|
293
|
+
// Process in batches to avoid flooding the server
|
|
294
|
+
const failed = [];
|
|
295
|
+
let synced = alreadySynced;
|
|
296
|
+
for (let i = 0; i < needsSync.length; i += SYNC_BATCH_SIZE) {
|
|
297
|
+
const batch = needsSync.slice(i, i + SYNC_BATCH_SIZE);
|
|
298
|
+
const batchNum = Math.floor(i / SYNC_BATCH_SIZE) + 1;
|
|
299
|
+
const totalBatches = Math.ceil(needsSync.length / SYNC_BATCH_SIZE);
|
|
300
|
+
if (totalBatches > 1) {
|
|
301
|
+
debug(`waitForSync: batch ${batchNum}/${totalBatches} (${batch.length} docs)`);
|
|
302
|
+
output_1.out.update(`Uploading batch ${batchNum}/${totalBatches} (${synced}/${handlesToWaitOn.length} done)`);
|
|
303
|
+
}
|
|
304
|
+
const results = await Promise.allSettled(batch.map(handle => waitForHandleSync(handle, syncServerStorageId, timeoutMs, startTime)));
|
|
305
|
+
for (const result of results) {
|
|
306
|
+
if (result.status === "rejected") {
|
|
307
|
+
failed.push(result.reason);
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
synced++;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
const elapsed = Date.now() - startTime;
|
|
315
|
+
if (failed.length > 0) {
|
|
316
|
+
debug(`waitForSync: ${failed.length} documents failed after ${elapsed}ms`);
|
|
317
|
+
output_1.out.taskLine(`Upload: ${synced} synced, ${failed.length} failed after ${(elapsed / 1000).toFixed(1)}s`, true);
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
debug(`waitForSync: all ${handlesToWaitOn.length} documents synced in ${elapsed}ms (${alreadySynced} were already synced)`);
|
|
321
|
+
output_1.out.taskLine(`All ${handlesToWaitOn.length} documents uploaded to server (${(elapsed / 1000).toFixed(1)}s)`);
|
|
322
|
+
}
|
|
323
|
+
return { failed };
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Wait for sync by polling head stability (Subduction mode).
|
|
327
|
+
* Each handle's heads are polled until they remain unchanged for
|
|
328
|
+
* several consecutive checks, indicating the SubductionSource has
|
|
329
|
+
* finished syncing.
|
|
330
|
+
*/
|
|
331
|
+
async function waitForSyncViaHeadStability(handles, timeoutMs, startTime) {
|
|
332
|
+
const failed = [];
|
|
333
|
+
let synced = 0;
|
|
334
|
+
// Process in batches
|
|
335
|
+
for (let i = 0; i < handles.length; i += SYNC_BATCH_SIZE) {
|
|
336
|
+
const batch = handles.slice(i, i + SYNC_BATCH_SIZE);
|
|
337
|
+
const results = await Promise.allSettled(batch.map(handle => waitForHandleHeadStability(handle, timeoutMs, startTime)));
|
|
338
|
+
for (const result of results) {
|
|
339
|
+
if (result.status === "rejected") {
|
|
340
|
+
failed.push(result.reason);
|
|
341
|
+
}
|
|
342
|
+
else {
|
|
343
|
+
synced++;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
const elapsed = Date.now() - startTime;
|
|
348
|
+
if (failed.length > 0) {
|
|
349
|
+
debug(`waitForSync(heads): ${failed.length} documents failed after ${elapsed}ms`);
|
|
350
|
+
output_1.out.taskLine(`Sync: ${synced} synced, ${failed.length} timed out after ${(elapsed / 1000).toFixed(1)}s`, true);
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
debug(`waitForSync(heads): all ${handles.length} documents synced in ${elapsed}ms`);
|
|
354
|
+
output_1.out.taskLine(`All ${handles.length} documents synced (${(elapsed / 1000).toFixed(1)}s)`);
|
|
355
|
+
}
|
|
356
|
+
return { failed };
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Wait for a single handle's heads to stabilize.
|
|
360
|
+
* Polls heads at 100ms intervals; resolves after 3 consecutive stable
|
|
361
|
+
* checks, rejects on timeout.
|
|
362
|
+
*/
|
|
363
|
+
function waitForHandleHeadStability(handle, timeoutMs, startTime) {
|
|
364
|
+
return new Promise((resolve, reject) => {
|
|
365
|
+
let lastHeads = JSON.stringify(handle.heads());
|
|
366
|
+
let stableCount = 0;
|
|
367
|
+
const stableRequired = 3;
|
|
368
|
+
const pollInterval = setInterval(() => {
|
|
369
|
+
const currentHeads = JSON.stringify(handle.heads());
|
|
370
|
+
if (currentHeads === lastHeads) {
|
|
371
|
+
stableCount++;
|
|
372
|
+
if (stableCount >= stableRequired) {
|
|
373
|
+
clearInterval(pollInterval);
|
|
374
|
+
clearTimeout(timeout);
|
|
375
|
+
debug(`waitForSync(heads): ${handle.url}... converged in ${Date.now() - startTime}ms`);
|
|
376
|
+
resolve(handle);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
else {
|
|
380
|
+
stableCount = 0;
|
|
381
|
+
lastHeads = currentHeads;
|
|
382
|
+
}
|
|
383
|
+
}, 100);
|
|
384
|
+
const timeout = setTimeout(() => {
|
|
385
|
+
clearInterval(pollInterval);
|
|
386
|
+
debug(`waitForSync(heads): ${handle.url}... timed out after ${timeoutMs}ms`);
|
|
387
|
+
reject(handle);
|
|
388
|
+
}, timeoutMs);
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
//# sourceMappingURL=network-sync.js.map
|