@fizzyflow/wdoublesync_cli 1.0.2

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.
@@ -0,0 +1,692 @@
1
+ import { mkdir, writeFile, readFile, readdir, rm } from 'node:fs/promises';
2
+ import { watch as fsWatch } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { createHash } from 'node:crypto';
5
+ import { homedir } from 'node:os';
6
+ import { SuiMaster } from 'suidouble';
7
+ import EndlessVector from '@fizzyflow/endless-vector';
8
+ import { WDoubleSync } from '@fizzyflow/wdoublesync';
9
+ import { DoubleSync, CDCStore, DoubleSyncSnapshot } from '@fizzyflow/doublesync';
10
+ import { WalrusSealClient } from 'walrus-seal-client-with-local';
11
+ import { DoubleSyncFSFolder } from './DoubleSyncFSFolder.js';
12
+
13
+ async function makeWalrusSealClient(args) {
14
+ const params = { client: args.chain };
15
+ const key = args.key || process.env.WDOUBLESYNC_KEY;
16
+ if (key) {
17
+ params.privateKey = key;
18
+ } else if (args.phrase) {
19
+ params.phrase = args.phrase;
20
+ } else {
21
+ throw new Error('No signing key. Use --key, --phrase, or WDOUBLESYNC_KEY env var');
22
+ }
23
+
24
+ const suiMaster = new SuiMaster(params);
25
+ await suiMaster.initialize();
26
+
27
+ const client = new WalrusSealClient({ network: args.chain, suiMaster });
28
+ await client.initialize();
29
+ return client;
30
+ }
31
+
32
+ async function makeReadOnlyWalrusSealClient(chain) {
33
+ const client = new WalrusSealClient({ network: chain });
34
+ await client.initialize();
35
+ return client;
36
+ }
37
+
38
+ async function makeClientForRead(args) {
39
+ const key = args.key || process.env.WDOUBLESYNC_KEY;
40
+ if (key || args.phrase) {
41
+ return makeWalrusSealClient(args);
42
+ }
43
+ return makeReadOnlyWalrusSealClient(args.chain);
44
+ }
45
+
46
+ function formatBytes(bytes) {
47
+ if (bytes < 1024) return bytes + ' B';
48
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
49
+ return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
50
+ }
51
+
52
+ function formatPath(path) {
53
+ const home = homedir();
54
+ if (path.startsWith(home)) {
55
+ return '~' + path.slice(home.length);
56
+ }
57
+ return path;
58
+ }
59
+
60
+ async function countTree(folder, prefix = []) {
61
+ let files = 0;
62
+ let folders = 0;
63
+ const children = await folder.list();
64
+ for (const child of children) {
65
+ if (child.getContent) {
66
+ files++;
67
+ } else {
68
+ folders++;
69
+ const sub = await countTree(child, [...prefix, child.name]);
70
+ files += sub.files;
71
+ folders += sub.folders;
72
+ }
73
+ }
74
+ return { files, folders };
75
+ }
76
+
77
+ export async function push(args) {
78
+ let vectorId = args.vectorId;
79
+ const chain = args.chain;
80
+
81
+ const excludes = args.exclude
82
+ ? args.exclude.split(',').map(s => s.trim())
83
+ : undefined;
84
+ const destPath = process.cwd();
85
+ const fsFolder = new DoubleSyncFSFolder(destPath, excludes);
86
+
87
+ const counts = await countTree(fsFolder);
88
+ console.log(' scanning...', counts.files, 'files,', counts.folders, 'folders');
89
+
90
+ if (args.manifest) {
91
+ const manifest = await readManifest(destPath);
92
+ const localFiles = await hashLocalTree(fsFolder);
93
+ if (vectorId && manifest && manifest.vectorId === vectorId && manifest.files) {
94
+ if (hashMapsEqual(localFiles, manifest.files)) {
95
+ console.log(' no changes since last pull, nothing to push');
96
+ return;
97
+ }
98
+ }
99
+ }
100
+
101
+ const wsc = await makeWalrusSealClient(args);
102
+ const evParams = {
103
+ suiClient: wsc.suiClient,
104
+ packageId: chain,
105
+ signAndExecuteTransaction: (tx) => wsc.signAndExecuteTransaction(tx),
106
+ walrusClient: wsc.walrusClient,
107
+ sealClient: wsc.sealClient,
108
+ aggregatorUrl: wsc.aggregatorUrl,
109
+ senderAddress: wsc.suiMaster.address,
110
+ signer: wsc.suiMaster._keypair,
111
+ };
112
+
113
+ if (!vectorId) {
114
+ console.log('creating new EndlessVector on', chain, '...');
115
+ const ev = await EndlessVector.create(evParams);
116
+ vectorId = ev.id;
117
+ console.log(' created:', vectorId);
118
+ }
119
+
120
+ console.log('syncing ./ →', vectorId);
121
+
122
+ const ev = new EndlessVector({ ...evParams, id: vectorId });
123
+
124
+ const wdsync = new WDoubleSync({
125
+ endlessVector: ev,
126
+ compress: args.compress || 'gzip',
127
+ });
128
+
129
+ let existingVersions = 0;
130
+ if (args.forceSnapshot) {
131
+ // Bypass chain replay — chain may be corrupt. Force a full snapshot at the
132
+ // current position so restore() can use it as a recovery point.
133
+ console.log(' skipping chain replay — pushing full snapshot...');
134
+ await ev.initialize();
135
+ wdsync._isInitialized = true;
136
+ wdsync._lastSnapshot = null;
137
+ wdsync._replayedCount = ev.length;
138
+ } else {
139
+ await wdsync.initialize();
140
+ existingVersions = await wdsync.length();
141
+
142
+ if (vectorId && existingVersions > 0 && !args.manifest) {
143
+ console.log(' comparing tree hashes...');
144
+ const localHash = await localTreeHash(destPath);
145
+ const remoteHash = await wdsync.getTreeHash(existingVersions);
146
+ if (localHash && remoteHash.length === localHash.length &&
147
+ localHash.every((b, i) => b === remoteHash[i])) {
148
+ console.log(' no changes, nothing to push');
149
+ return;
150
+ }
151
+ }
152
+ }
153
+
154
+ console.log(' building patch + pushing to chain...');
155
+ const result = await wdsync.push(fsFolder);
156
+
157
+ const patchType = args.forceSnapshot || existingVersions === 0 ? 'full snapshot' : 'diff';
158
+ console.log(' version', result.version, 'pushed (' + patchType + ', gzip compressed)');
159
+ console.log(' vector:', vectorId);
160
+
161
+ if (args.manifest) {
162
+ const localFiles = await hashLocalTree(fsFolder);
163
+ await writeManifest(destPath, { vectorId, version: result.version, files: localFiles });
164
+ }
165
+ }
166
+
167
+ export async function pull(args) {
168
+ if (!args.vectorId) {
169
+ throw new Error('vector-id is required for pull');
170
+ }
171
+
172
+ const wsc = await makeClientForRead(args);
173
+
174
+ const evParams = {
175
+ suiClient: wsc.suiClient,
176
+ id: args.vectorId,
177
+ packageId: args.chain,
178
+ walrusClient: wsc.walrusClient,
179
+ aggregatorUrl: wsc.aggregatorUrl,
180
+ };
181
+ if (wsc.sealClient && wsc.suiMaster) {
182
+ evParams.sealClient = wsc.sealClient;
183
+ evParams.signAndExecuteTransaction = (tx) => wsc.signAndExecuteTransaction(tx);
184
+ evParams.senderAddress = wsc.suiMaster.address;
185
+ evParams.signer = wsc.suiMaster._keypair;
186
+ }
187
+
188
+ const ev = new EndlessVector(evParams);
189
+
190
+ const wdsync = new WDoubleSync({ endlessVector: ev });
191
+
192
+ const version = args.version != null ? Number(args.version) : undefined;
193
+ console.log('restoring', args.vectorId, version != null ? 'at version ' + version : '(latest)', '...');
194
+
195
+ let folder;
196
+ try {
197
+ folder = await wdsync.restore(version);
198
+ } catch (err) {
199
+ if (err.message?.includes('sealClient not configured') || err.message?.includes('signer or sessionKey is required')) {
200
+ throw new Error('This vector is Seal-encrypted. Provide --key or --phrase to decrypt.');
201
+ }
202
+ throw err;
203
+ }
204
+
205
+ const destPath = process.cwd();
206
+ const oldFiles = await hashDiskTree(destPath);
207
+
208
+ const { stats, newFiles } = await syncMemoryFolderToDisk(folder, destPath, oldFiles);
209
+
210
+ if (args.manifest) {
211
+ await ev.initialize();
212
+ const pulledVersion = version ?? ev.length;
213
+ await writeManifest(destPath, { vectorId: args.vectorId, version: pulledVersion, files: newFiles });
214
+ }
215
+
216
+ console.log(' ', stats.written, 'written,', stats.skipped, 'unchanged,',
217
+ stats.deleted, 'deleted', '(' + formatBytes(stats.bytes) + ')');
218
+ }
219
+
220
+ export async function info(args) {
221
+ if (!args.vectorId) {
222
+ throw new Error('vector-id is required for info');
223
+ }
224
+
225
+ const wsc = await makeClientForRead(args);
226
+
227
+ const evParams = {
228
+ suiClient: wsc.suiClient,
229
+ id: args.vectorId,
230
+ packageId: args.chain,
231
+ walrusClient: wsc.walrusClient,
232
+ aggregatorUrl: wsc.aggregatorUrl,
233
+ };
234
+ if (wsc.sealClient && wsc.suiMaster) {
235
+ evParams.sealClient = wsc.sealClient;
236
+ evParams.signAndExecuteTransaction = (tx) => wsc.signAndExecuteTransaction(tx);
237
+ evParams.senderAddress = wsc.suiMaster.address;
238
+ evParams.signer = wsc.suiMaster._keypair;
239
+ }
240
+
241
+ const ev = new EndlessVector(evParams);
242
+
243
+ await ev.initialize();
244
+
245
+ const totalVersions = ev.length;
246
+
247
+ console.log('EndlessVector:', args.vectorId);
248
+ console.log(' chain: ', args.chain);
249
+ console.log(' versions: ', totalVersions);
250
+ console.log(' binary size: ', formatBytes(ev.binaryLength));
251
+ console.log(' history items: ', ev.historyItemsCount);
252
+ console.log(' archives: ', ev.archiveItemsCount);
253
+
254
+ const destPath = process.cwd();
255
+ const localHash = await localTreeHash(destPath);
256
+
257
+ if (!localHash) {
258
+ console.log('\n local folder: empty (run pull to fetch)');
259
+ return;
260
+ }
261
+
262
+ const localHex = Buffer.from(localHash).toString('hex').slice(0, 12);
263
+ console.log('\n local tree hash: ', localHex + '...');
264
+ console.log(' detecting local version...');
265
+
266
+ const wdsync = new WDoubleSync({ endlessVector: ev });
267
+ let matchedVersion = null;
268
+
269
+ for (let v = totalVersions; v >= 1; v--) {
270
+ process.stdout.write(' checking version ' + v + '/' + totalVersions + '... \r');
271
+ let remoteHash;
272
+ try {
273
+ remoteHash = await wdsync.getTreeHash(v);
274
+ } catch (err) {
275
+ if (err.message?.includes('sealClient not configured')) {
276
+ throw new Error('This vector is Seal-encrypted. Provide --key or --phrase to decrypt.');
277
+ }
278
+ throw err;
279
+ }
280
+ if (localHash.length === remoteHash.length && localHash.every((b, i) => b === remoteHash[i])) {
281
+ matchedVersion = v;
282
+ break;
283
+ }
284
+ }
285
+
286
+ process.stdout.write(' \r');
287
+ if (matchedVersion !== null) {
288
+ console.log(' local matches: version', matchedVersion, 'of', totalVersions);
289
+ if (matchedVersion < totalVersions) {
290
+ console.log(' behind by: ', totalVersions - matchedVersion, 'version(s)');
291
+ } else {
292
+ console.log(' status: up to date');
293
+ }
294
+ } else {
295
+ console.log(' local matches: no known version (local modifications?)');
296
+ console.log(' latest remote: version', totalVersions);
297
+ }
298
+ }
299
+
300
+ function hashContent(bytes) {
301
+ return createHash('sha256').update(bytes).digest('hex');
302
+ }
303
+
304
+ async function readManifest(destPath) {
305
+ try {
306
+ const raw = await readFile(join(destPath, '.wdoublesync'), 'utf8');
307
+ return JSON.parse(raw);
308
+ } catch { return null; }
309
+ }
310
+
311
+ async function writeManifest(destPath, manifest) {
312
+ await writeFile(join(destPath, '.wdoublesync'), JSON.stringify(manifest, null, 2));
313
+ }
314
+
315
+ const DISK_EXCLUDES = new Set(['.wdoublesync', '.git', '.DS_Store', 'node_modules']);
316
+
317
+ async function hashDiskTree(dirPath, prefix = []) {
318
+ const files = {};
319
+ let entries;
320
+ try { entries = await readdir(dirPath, { withFileTypes: true }); } catch { return files; }
321
+ for (const entry of entries) {
322
+ if (DISK_EXCLUDES.has(entry.name) || entry.name.startsWith('.')) continue;
323
+ const relParts = [...prefix, entry.name];
324
+ const relPath = relParts.join('/');
325
+ const fullPath = join(dirPath, entry.name);
326
+ if (entry.isFile()) {
327
+ const buf = await readFile(fullPath);
328
+ files[relPath] = hashContent(buf);
329
+ } else if (entry.isDirectory()) {
330
+ Object.assign(files, await hashDiskTree(fullPath, relParts));
331
+ }
332
+ }
333
+ return files;
334
+ }
335
+
336
+ async function localTreeHash(dirPath) {
337
+ const fsFolder = new DoubleSyncFSFolder(dirPath);
338
+ const children = await fsFolder.list();
339
+ if (children.length === 0) return null;
340
+
341
+ const sync = new DoubleSync();
342
+ const store = new CDCStore();
343
+ const snapshotBytes = await sync.buildSnapshot({ root: fsFolder, store });
344
+ return new DoubleSyncSnapshot(snapshotBytes).treeHash;
345
+ }
346
+
347
+ async function syncMemoryFolderToDisk(folder, destPath, oldFiles) {
348
+ const newFiles = {};
349
+ const newDirs = new Set();
350
+ const stats = { written: 0, skipped: 0, deleted: 0, folders: 0, bytes: 0 };
351
+
352
+ await walkAndSync(folder, destPath, [], oldFiles, newFiles, newDirs, stats);
353
+
354
+ if (oldFiles) {
355
+ for (const relPath of Object.keys(oldFiles)) {
356
+ if (!(relPath in newFiles)) {
357
+ const absPath = join(destPath, ...relPath.split('/'));
358
+ await rm(absPath, { force: true });
359
+ stats.deleted++;
360
+ }
361
+ }
362
+ }
363
+
364
+ await removeStaleDirectories(destPath, newDirs);
365
+
366
+ return { stats, newFiles };
367
+ }
368
+
369
+ function hashMapsEqual(a, b) {
370
+ const keysA = Object.keys(a).sort();
371
+ const keysB = Object.keys(b).sort();
372
+ if (keysA.length !== keysB.length) return false;
373
+ return keysA.every((k, i) => k === keysB[i] && a[k] === b[k]);
374
+ }
375
+
376
+ async function hashLocalTree(fsFolder, prefix = []) {
377
+ const files = {};
378
+ const children = await fsFolder.list();
379
+ for (const child of children) {
380
+ const relParts = [...prefix, child.name];
381
+ const relPath = relParts.join('/');
382
+ if (child.getContent) {
383
+ const content = await child.getContent();
384
+ files[relPath] = hashContent(content);
385
+ } else {
386
+ Object.assign(files, await hashLocalTree(child, relParts));
387
+ }
388
+ }
389
+ return files;
390
+ }
391
+
392
+ async function walkAndSync(folder, destPath, prefix, oldFiles, newFiles, newDirs, stats) {
393
+ const children = await folder.list();
394
+
395
+ for (const child of children) {
396
+ const relParts = [...prefix, child.name];
397
+ const relPath = relParts.join('/');
398
+ const absPath = join(destPath, child.name);
399
+
400
+ if (child.getContent) {
401
+ const content = await child.getContent();
402
+ const hash = hashContent(content);
403
+ newFiles[relPath] = hash;
404
+
405
+ if (oldFiles && oldFiles[relPath] === hash) {
406
+ stats.skipped++;
407
+ } else {
408
+ await writeFile(absPath, content);
409
+ stats.written++;
410
+ stats.bytes += content.length;
411
+ }
412
+ } else {
413
+ await mkdir(absPath, { recursive: true });
414
+ newDirs.add(relPath);
415
+ stats.folders++;
416
+ await walkAndSync(child, absPath, relParts, oldFiles, newFiles, newDirs, stats);
417
+ }
418
+ }
419
+ }
420
+
421
+ async function removeStaleDirectories(destPath, newDirs) {
422
+ let entries;
423
+ try { entries = await readdir(destPath, { withFileTypes: true }); } catch { return; }
424
+ for (const entry of entries) {
425
+ if (!entry.isDirectory()) continue;
426
+ if (DISK_EXCLUDES.has(entry.name) || entry.name.startsWith('.')) continue;
427
+ const fullPath = join(destPath, entry.name);
428
+ if (!newDirs.has(entry.name)) {
429
+ await rm(fullPath, { recursive: true, force: true });
430
+ } else {
431
+ await removeStaleSubdirs(fullPath, entry.name, newDirs);
432
+ }
433
+ }
434
+ }
435
+
436
+ async function removeStaleSubdirs(dirPath, prefix, newDirs) {
437
+ let entries;
438
+ try { entries = await readdir(dirPath, { withFileTypes: true }); } catch { return; }
439
+ for (const entry of entries) {
440
+ if (!entry.isDirectory()) continue;
441
+ const relPath = prefix + '/' + entry.name;
442
+ const fullPath = join(dirPath, entry.name);
443
+ if (!newDirs.has(relPath)) {
444
+ await rm(fullPath, { recursive: true, force: true });
445
+ } else {
446
+ await removeStaleSubdirs(fullPath, relPath, newDirs);
447
+ }
448
+ }
449
+ }
450
+
451
+ export async function watch(args) {
452
+ if (!args.vectorId) throw new Error('vector-id is required for watch');
453
+
454
+ const cwd = process.cwd();
455
+ const chain = args.chain;
456
+ const debounceMs = args.debounce ?? 5000;
457
+ const pollIntervalMs = (args.pollInterval ?? 2) * 1000;
458
+ const pushEnabled = !args.pullOnly;
459
+ const pullEnabled = !args.pushOnly;
460
+
461
+ const excludes = args.exclude ? args.exclude.split(',').map(s => s.trim()) : undefined;
462
+
463
+ const wsc = pushEnabled
464
+ ? await makeWalrusSealClient(args)
465
+ : await makeClientForRead(args);
466
+
467
+ const evParams = {
468
+ suiClient: wsc.suiClient,
469
+ id: args.vectorId,
470
+ packageId: chain,
471
+ walrusClient: wsc.walrusClient,
472
+ aggregatorUrl: wsc.aggregatorUrl,
473
+ };
474
+ if (wsc.sealClient && wsc.suiMaster) {
475
+ evParams.sealClient = wsc.sealClient;
476
+ evParams.senderAddress = wsc.suiMaster.address;
477
+ evParams.signer = wsc.suiMaster._keypair;
478
+ if (pushEnabled) {
479
+ evParams.signAndExecuteTransaction = (tx) => wsc.signAndExecuteTransaction(tx);
480
+ }
481
+ }
482
+
483
+ const ev = new EndlessVector({ ...evParams });
484
+ const wdsync = new WDoubleSync({
485
+ endlessVector: ev,
486
+ compress: args.compress || 'gzip',
487
+ });
488
+
489
+ await wdsync.initialize();
490
+
491
+ let remoteVersion = ev.length;
492
+ let lastSyncedHash = await localTreeHash(cwd);
493
+ let pushInFlight = false;
494
+ let debounceTimer = null;
495
+ let dirty = false;
496
+
497
+ console.log(`watching ${formatPath(cwd)}`);
498
+ console.log(` vector: ${args.vectorId}`);
499
+ console.log(` remote version: ${remoteVersion}`);
500
+ console.log(` push: ${pushEnabled ? `enabled (debounce ${debounceMs}ms)` : 'disabled'}`);
501
+ console.log(` pull: ${pullEnabled ? `enabled (poll every ${pollIntervalMs / 1000}s)` : 'disabled'}`);
502
+ console.log('press Ctrl+C to stop\n');
503
+
504
+ async function runPush() {
505
+ if (!pushEnabled || pushInFlight) return;
506
+ dirty = false;
507
+ const newHash = await localTreeHash(cwd);
508
+ if (hashesEqual(newHash, lastSyncedHash)) return;
509
+
510
+ pushInFlight = true;
511
+ try {
512
+ const fsFolder = new DoubleSyncFSFolder(cwd, excludes);
513
+ console.log(`[${timestamp()}] change detected — pushing...`);
514
+ const result = await wdsync.push(fsFolder);
515
+ lastSyncedHash = newHash;
516
+ remoteVersion = result.version;
517
+ console.log(`[${timestamp()}] pushed version ${result.version}`);
518
+ } catch (err) {
519
+ if (isEnsureLengthError(err)) {
520
+ console.log(`[${timestamp()}] conflict: remote was updated while pushing — skipping push, will pull on next poll`);
521
+ dirty = true; // retry push after next pull
522
+ } else {
523
+ console.error(`[${timestamp()}] push error:`, err.message);
524
+ }
525
+ } finally {
526
+ pushInFlight = false;
527
+ }
528
+ }
529
+
530
+ async function runPoll() {
531
+ if (!pullEnabled || pushInFlight) return;
532
+ try {
533
+ ev.reInitialize();
534
+ await ev.initialize();
535
+ if (ev.length <= remoteVersion) return;
536
+
537
+ const newRemoteVersion = ev.length;
538
+
539
+ // Always resync wdsync when remote has advanced. Without this,
540
+ // wdsync._lastSnapshot stays at the old version while ev.length has
541
+ // advanced — the next push would build a diff against the wrong base
542
+ // snapshot, corrupting the chain.
543
+ wdsync.reInitialize();
544
+
545
+ const localNow = await localTreeHash(cwd);
546
+
547
+ if (!hashesEqual(localNow, lastSyncedHash)) {
548
+ console.log(`[${timestamp()}] remote has ${newRemoteVersion - remoteVersion} new version(s) but local has unpushed changes — skipping pull`);
549
+ return;
550
+ }
551
+
552
+ console.log(`[${timestamp()}] remote advanced to version ${newRemoteVersion} — pulling...`);
553
+ let folder;
554
+ try {
555
+ folder = await wdsync.restore();
556
+ } catch (err) {
557
+ // Unrestorable chain (e.g. corrupt patch). Skip past it so we don't
558
+ // retry forever, and let the user pull manually to diagnose.
559
+ console.error(`[${timestamp()}] pull failed (${err.message}) — skipping to version ${newRemoteVersion}; run \`pull\` manually`);
560
+ remoteVersion = newRemoteVersion;
561
+ return;
562
+ }
563
+ const oldFiles = await hashDiskTree(cwd);
564
+ const { stats } = await syncMemoryFolderToDisk(folder, cwd, oldFiles);
565
+ lastSyncedHash = await localTreeHash(cwd);
566
+ remoteVersion = newRemoteVersion;
567
+ console.log(`[${timestamp()}] pulled version ${remoteVersion} (${stats.written} written, ${stats.skipped} unchanged, ${stats.deleted} deleted)`);
568
+ } catch (err) {
569
+ console.error(`[${timestamp()}] poll error:`, err.message);
570
+ }
571
+ }
572
+
573
+ // FS watcher
574
+ const watcher = fsWatch(cwd, { recursive: true }, (eventType, filename) => {
575
+ if (!pushEnabled) return;
576
+ if (!filename) return;
577
+ const base = filename.split('/')[0];
578
+ if (DISK_EXCLUDES.has(base) || base.startsWith('.')) return;
579
+ dirty = true;
580
+ clearTimeout(debounceTimer);
581
+ debounceTimer = setTimeout(runPush, debounceMs);
582
+ });
583
+
584
+ // Poll interval
585
+ const poller = setInterval(runPoll, pollIntervalMs);
586
+
587
+ // Graceful shutdown
588
+ let shutdownResolve;
589
+ const shutdownPromise = new Promise((resolve) => {
590
+ shutdownResolve = resolve;
591
+ });
592
+
593
+ function shutdown() {
594
+ console.log('\nstopping watch...');
595
+ clearTimeout(debounceTimer);
596
+ clearInterval(poller);
597
+ watcher.close();
598
+ shutdownResolve();
599
+ }
600
+ process.once('SIGINT', shutdown);
601
+ process.once('SIGTERM', shutdown);
602
+
603
+ await shutdownPromise;
604
+ }
605
+
606
+ function timestamp() {
607
+ return new Date().toTimeString().slice(0, 8);
608
+ }
609
+
610
+ function hashesEqual(a, b) {
611
+ if (a === null && b === null) return true;
612
+ if (a === null || b === null) return false;
613
+ return a.length === b.length && a.every((byte, i) => byte === b[i]);
614
+ }
615
+
616
+ function isEnsureLengthError(err) {
617
+ return err?.message?.includes('abort code: 98') ||
618
+ err?.message?.includes('ensure_length') ||
619
+ err?.message?.includes('EUnexpectedLength');
620
+ }
621
+
622
+ export async function rebate(args) {
623
+ if (!args.vectorId) {
624
+ throw new Error('vector-id is required for rebate');
625
+ }
626
+
627
+ const destPath = process.cwd();
628
+ const excludes = args.exclude
629
+ ? args.exclude.split(',').map(s => s.trim())
630
+ : undefined;
631
+ const fsFolder = new DoubleSyncFSFolder(destPath, excludes);
632
+
633
+ const counts = await countTree(fsFolder);
634
+ console.log(' scanning...', counts.files, 'files,', counts.folders, 'folders');
635
+
636
+ const wsc = await makeWalrusSealClient(args);
637
+
638
+ const evParams = {
639
+ suiClient: wsc.suiClient,
640
+ id: args.vectorId,
641
+ packageId: args.chain,
642
+ signAndExecuteTransaction: (tx) => wsc.signAndExecuteTransaction(tx),
643
+ walrusClient: wsc.walrusClient,
644
+ aggregatorUrl: wsc.aggregatorUrl,
645
+ senderAddress: wsc.suiMaster.address,
646
+ signer: wsc.suiMaster._keypair,
647
+ };
648
+ if (wsc.sealClient && wsc.suiMaster) {
649
+ evParams.sealClient = wsc.sealClient;
650
+ }
651
+
652
+ const ev = new EndlessVector(evParams);
653
+ await ev.initialize();
654
+
655
+ const beforeHistoryItems = ev.historyItemsCount;
656
+ const beforeArchiveItems = ev.archiveItemsCount;
657
+
658
+ console.log('step 1: archiving history...');
659
+ await ev.archive();
660
+ console.log(' archived');
661
+
662
+ console.log('step 2: burning archives...');
663
+ await ev.burnArchive();
664
+ console.log(' burned');
665
+
666
+ console.log('step 3: pushing local state as fresh snapshot...');
667
+
668
+ const wdsync = new WDoubleSync({
669
+ endlessVector: ev,
670
+ compress: args.compress || 'gzip',
671
+ });
672
+
673
+ // Force fresh snapshot by resetting internal state to prevent replay of broken diffs
674
+ wdsync._senderStore = new CDCStore({ copyBytes: false });
675
+ wdsync._receiverMirror = new CDCStore();
676
+ wdsync._lastSnapshot = null;
677
+ wdsync._replayedCount = 0;
678
+ wdsync._isInitialized = false;
679
+
680
+ const result = await wdsync.push(fsFolder);
681
+
682
+ // Reinitialize to get fresh state and updated length
683
+ ev.reInitialize();
684
+ await ev.initialize();
685
+
686
+ console.log(' pushed version', result.version);
687
+ console.log(' [debug] after push: length=', ev.length, 'burned=', ev.burnedArchiveCount, 'version=', result.version);
688
+
689
+ console.log('\nrebate complete:');
690
+ console.log(' new version:', result.version);
691
+ console.log(' new on-chain size:', formatBytes(ev.binaryLength));
692
+ }