@kjerneverk/riotplan-cloud 1.0.0-dev.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/dist/index.js ADDED
@@ -0,0 +1,1113 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFile, writeFile, rename, mkdir, stat, rm, readdir } from "node:fs/promises";
3
+ import { join, resolve, dirname } from "node:path";
4
+ const SYNC_MANIFEST_VERSION = 1;
5
+ const SYNC_MANIFEST_FILE = ".riotplan-sync-manifest.json";
6
+ const REMOTE_INDEX_SCHEMA_VERSION = 1;
7
+ const REMOTE_INDEX_FILE = ".riotplan-sync-index-v1.json";
8
+ const REMOTE_INDEX_CACHE_FILE = ".riotplan-remote-index-cache-v1.json";
9
+ const REMOTE_INDEX_DOWNLOAD_TMP = ".riotplan-remote-index-download.tmp.json";
10
+ function buildSyncDiff(remoteObjects, manifestObjects, localFiles) {
11
+ const added = [];
12
+ const changed = [];
13
+ const unchanged = [];
14
+ const remotePaths = new Set(Object.keys(remoteObjects));
15
+ for (const relativePath of remotePaths) {
16
+ const remote = remoteObjects[relativePath];
17
+ const localExists = localFiles.has(relativePath);
18
+ const manifest = manifestObjects?.[relativePath];
19
+ if (!localExists) {
20
+ added.push(relativePath);
21
+ continue;
22
+ }
23
+ if (!manifest) {
24
+ changed.push(relativePath);
25
+ continue;
26
+ }
27
+ const byGeneration = manifest.generation && remote.generation && manifest.generation === remote.generation;
28
+ const byEtag = manifest.etag && remote.etag && manifest.etag === remote.etag;
29
+ const byMd5 = manifest.md5Hash && remote.md5Hash && manifest.md5Hash === remote.md5Hash;
30
+ if (byGeneration || byEtag || byMd5) {
31
+ unchanged.push(relativePath);
32
+ } else {
33
+ changed.push(relativePath);
34
+ }
35
+ }
36
+ const deletedLocal = [];
37
+ for (const localRelative of localFiles) {
38
+ if (!remotePaths.has(localRelative)) {
39
+ deletedLocal.push(localRelative);
40
+ }
41
+ }
42
+ return { added, changed, unchanged, deletedLocal };
43
+ }
44
+ function isRetryableError(error) {
45
+ if (!error || typeof error !== "object") return false;
46
+ const code = error.code;
47
+ const message = String(error.message || "").toLowerCase();
48
+ if (typeof code === "number" && [408, 429, 500, 502, 503, 504].includes(code)) return true;
49
+ if (typeof code === "string" && ["ETIMEDOUT", "ECONNRESET", "EAI_AGAIN"].includes(code)) return true;
50
+ return message.includes("timeout") || message.includes("temporar") || message.includes("rate");
51
+ }
52
+ async function withRetry(fn, retries = 2) {
53
+ let attempt = 0;
54
+ let lastError;
55
+ while (attempt <= retries) {
56
+ try {
57
+ return await fn();
58
+ } catch (error) {
59
+ lastError = error;
60
+ if (attempt >= retries || !isRetryableError(error)) {
61
+ throw error;
62
+ }
63
+ const backoffMs = 100 * Math.pow(2, attempt);
64
+ await new Promise((resolve2) => setTimeout(resolve2, backoffMs));
65
+ }
66
+ attempt += 1;
67
+ }
68
+ throw lastError instanceof Error ? lastError : new Error(String(lastError));
69
+ }
70
+ function normalizePrefix(prefix) {
71
+ if (!prefix) {
72
+ return "";
73
+ }
74
+ return prefix.replace(/^\/+/, "").replace(/\/+$/, "");
75
+ }
76
+ function toObjectName(prefix, relativePath) {
77
+ const normalizedRelative = relativePath.split("\\").join("/");
78
+ return prefix ? `${prefix}/${normalizedRelative}` : normalizedRelative;
79
+ }
80
+ function toRelativePath(prefix, objectName) {
81
+ if (!prefix) {
82
+ return objectName;
83
+ }
84
+ if (objectName.startsWith(`${prefix}/`)) {
85
+ return objectName.slice(prefix.length + 1);
86
+ }
87
+ return objectName;
88
+ }
89
+ function isInternalSyncControlFile(relativePath) {
90
+ const normalized = relativePath.split("\\").join("/");
91
+ const base = normalized.split("/").pop() || normalized;
92
+ return base === SYNC_MANIFEST_FILE || base === REMOTE_INDEX_FILE || base === REMOTE_INDEX_CACHE_FILE || base === REMOTE_INDEX_DOWNLOAD_TMP;
93
+ }
94
+ async function loadRemoteSyncIndexCache(localDirectory) {
95
+ const cachePath = join(localDirectory, REMOTE_INDEX_CACHE_FILE);
96
+ try {
97
+ const raw = await readFile(cachePath, "utf8");
98
+ const parsed = JSON.parse(raw);
99
+ if (!parsed || typeof parsed !== "object" || parsed.version !== REMOTE_INDEX_SCHEMA_VERSION || typeof parsed.indexObject !== "string" || !parsed.objects || typeof parsed.objects !== "object") {
100
+ return null;
101
+ }
102
+ return parsed;
103
+ } catch {
104
+ return null;
105
+ }
106
+ }
107
+ async function saveRemoteSyncIndexCache(localDirectory, cache) {
108
+ const path = join(localDirectory, REMOTE_INDEX_CACHE_FILE);
109
+ const tempPath = `${path}.tmp`;
110
+ await writeFile(tempPath, `${JSON.stringify(cache, null, 2)}
111
+ `, "utf8");
112
+ await rename(tempPath, path);
113
+ }
114
+ function metadataStringValue(value) {
115
+ return typeof value === "string" && value.trim() ? value : void 0;
116
+ }
117
+ function metadataNumberValue(value) {
118
+ if (typeof value === "number" && Number.isFinite(value)) {
119
+ return value;
120
+ }
121
+ if (typeof value === "string" && value.trim()) {
122
+ const parsed = Number.parseInt(value, 10);
123
+ if (Number.isFinite(parsed)) {
124
+ return parsed;
125
+ }
126
+ }
127
+ return void 0;
128
+ }
129
+ function parseRemoteMetadata(metadata) {
130
+ return {
131
+ md5Hash: metadataStringValue(metadata?.md5Hash),
132
+ generation: metadataStringValue(metadata?.generation),
133
+ etag: metadataStringValue(metadata?.etag),
134
+ size: metadataNumberValue(metadata?.size),
135
+ updatedAt: metadataStringValue(metadata?.updated)
136
+ };
137
+ }
138
+ function normalizeRemoteIndexObjects(objects) {
139
+ if (!objects || typeof objects !== "object") {
140
+ return {};
141
+ }
142
+ const normalized = {};
143
+ for (const [rawPath, rawState] of Object.entries(objects)) {
144
+ const path = typeof rawPath === "string" ? rawPath : "";
145
+ if (!path || isInternalSyncControlFile(path)) {
146
+ continue;
147
+ }
148
+ const state = rawState;
149
+ normalized[path] = {
150
+ path,
151
+ generation: metadataStringValue(state?.generation),
152
+ etag: metadataStringValue(state?.etag),
153
+ md5Hash: metadataStringValue(state?.md5Hash),
154
+ size: metadataNumberValue(state?.size),
155
+ updatedAt: metadataStringValue(state?.updatedAt)
156
+ };
157
+ }
158
+ return normalized;
159
+ }
160
+ async function collectLocalFiles(rootDir) {
161
+ const entries = await readdir(rootDir, { withFileTypes: true }).catch(() => []);
162
+ const files = [];
163
+ for (const entry of entries) {
164
+ const fullPath = join(rootDir, entry.name);
165
+ if (entry.isDirectory()) {
166
+ const nested = await collectLocalFiles(fullPath);
167
+ for (const child of nested) {
168
+ files.push(join(entry.name, child));
169
+ }
170
+ continue;
171
+ }
172
+ files.push(entry.name);
173
+ }
174
+ return files;
175
+ }
176
+ async function fileMd5Base64(path) {
177
+ const buffer = await readFile(path);
178
+ return createHash("md5").update(buffer).digest("base64");
179
+ }
180
+ async function fileExists(path) {
181
+ try {
182
+ await stat(path);
183
+ return true;
184
+ } catch {
185
+ return false;
186
+ }
187
+ }
188
+ async function resolveRemoteMd5(file) {
189
+ if (file.metadata?.md5Hash) {
190
+ return file.metadata.md5Hash;
191
+ }
192
+ if (typeof file.getMetadata === "function") {
193
+ try {
194
+ const [metadata] = await file.getMetadata();
195
+ const md5Hash = typeof metadata?.md5Hash === "string" ? metadata.md5Hash : void 0;
196
+ if (md5Hash) {
197
+ file.metadata = { ...file.metadata || {}, md5Hash };
198
+ }
199
+ return md5Hash;
200
+ } catch {
201
+ return void 0;
202
+ }
203
+ }
204
+ return void 0;
205
+ }
206
+ async function resolveRemoteMetadata(file) {
207
+ const fromInline = parseRemoteMetadata(file.metadata);
208
+ if (fromInline.md5Hash || fromInline.generation || fromInline.etag || fromInline.size !== void 0 || fromInline.updatedAt) {
209
+ return fromInline;
210
+ }
211
+ if (typeof file.getMetadata !== "function") {
212
+ return fromInline;
213
+ }
214
+ try {
215
+ const [metadata] = await file.getMetadata();
216
+ const parsed = parseRemoteMetadata(metadata);
217
+ const md5Hash = parsed.md5Hash || file.metadata?.md5Hash;
218
+ const generation = parsed.generation;
219
+ const etag = parsed.etag;
220
+ const size = parsed.size;
221
+ const updatedAt = parsed.updatedAt;
222
+ if (md5Hash) {
223
+ file.metadata = { ...file.metadata || {}, md5Hash };
224
+ }
225
+ return { md5Hash, generation, etag, size, updatedAt };
226
+ } catch {
227
+ return {
228
+ md5Hash: file.metadata?.md5Hash
229
+ };
230
+ }
231
+ }
232
+ async function loadRemoteSyncIndex(bucket, prefix, localDirectory, onDebugEvent) {
233
+ const indexObject = toObjectName(prefix, REMOTE_INDEX_FILE);
234
+ const remoteIndexFile = bucket.file(indexObject);
235
+ if (!remoteIndexFile || typeof remoteIndexFile.getMetadata !== "function") {
236
+ return { objects: null, usedCachedIndex: false };
237
+ }
238
+ let metadata;
239
+ try {
240
+ const [resolved] = await withRetry(() => remoteIndexFile.getMetadata());
241
+ metadata = resolved;
242
+ } catch (error) {
243
+ if (error?.code === 404) {
244
+ return { objects: null, usedCachedIndex: false };
245
+ }
246
+ onDebugEvent?.("sync_down.index.metadata_failed", {
247
+ indexObject,
248
+ error: error instanceof Error ? error.message : String(error)
249
+ });
250
+ return { objects: null, usedCachedIndex: false };
251
+ }
252
+ const indexEtag = metadataStringValue(metadata?.etag);
253
+ const indexGeneration = metadataStringValue(metadata?.generation);
254
+ const cache = await loadRemoteSyncIndexCache(localDirectory);
255
+ if (cache && cache.indexObject === indexObject && (indexEtag && cache.etag === indexEtag || indexGeneration && cache.generation === indexGeneration)) {
256
+ onDebugEvent?.("sync_down.index.cache_hit", {
257
+ indexObject,
258
+ objectCount: Object.keys(cache.objects || {}).length
259
+ });
260
+ return {
261
+ objects: normalizeRemoteIndexObjects(cache.objects),
262
+ indexEtag,
263
+ indexGeneration,
264
+ usedCachedIndex: true
265
+ };
266
+ }
267
+ const downloadPath = join(localDirectory, REMOTE_INDEX_DOWNLOAD_TMP);
268
+ try {
269
+ await withRetry(() => remoteIndexFile.download({ destination: downloadPath }));
270
+ const raw = await readFile(downloadPath, "utf8");
271
+ const parsed = JSON.parse(raw);
272
+ if (parsed.version !== REMOTE_INDEX_SCHEMA_VERSION || !parsed.objects || typeof parsed.objects !== "object") {
273
+ onDebugEvent?.("sync_down.index.invalid_schema", {
274
+ indexObject,
275
+ version: parsed.version ?? null
276
+ });
277
+ return { objects: null, indexEtag, indexGeneration, usedCachedIndex: false };
278
+ }
279
+ const normalizedObjects = normalizeRemoteIndexObjects(parsed.objects);
280
+ await saveRemoteSyncIndexCache(localDirectory, {
281
+ version: REMOTE_INDEX_SCHEMA_VERSION,
282
+ indexObject,
283
+ etag: indexEtag,
284
+ generation: indexGeneration,
285
+ updatedAt: metadataStringValue(metadata?.updated),
286
+ objects: normalizedObjects
287
+ });
288
+ onDebugEvent?.("sync_down.index.downloaded", {
289
+ indexObject,
290
+ objectCount: Object.keys(normalizedObjects).length
291
+ });
292
+ return {
293
+ objects: normalizedObjects,
294
+ indexEtag,
295
+ indexGeneration,
296
+ usedCachedIndex: false
297
+ };
298
+ } catch (error) {
299
+ onDebugEvent?.("sync_down.index.download_failed", {
300
+ indexObject,
301
+ error: error instanceof Error ? error.message : String(error)
302
+ });
303
+ return { objects: null, indexEtag, indexGeneration, usedCachedIndex: false };
304
+ } finally {
305
+ await rm(downloadPath, { force: true }).catch(() => void 0);
306
+ }
307
+ }
308
+ async function writeRemoteSyncIndex(bucket, prefix, localDirectory, objects, onDebugEvent) {
309
+ const indexObject = toObjectName(prefix, REMOTE_INDEX_FILE);
310
+ const tempPath = join(localDirectory, `${REMOTE_INDEX_FILE}.tmp`);
311
+ const payload = {
312
+ version: REMOTE_INDEX_SCHEMA_VERSION,
313
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
314
+ objects
315
+ };
316
+ try {
317
+ await writeFile(tempPath, `${JSON.stringify(payload, null, 2)}
318
+ `, "utf8");
319
+ await withRetry(() => bucket.upload(tempPath, { destination: indexObject }));
320
+ onDebugEvent?.("sync_up.index.written", {
321
+ indexObject,
322
+ objectCount: Object.keys(objects).length
323
+ });
324
+ } finally {
325
+ await rm(tempPath, { force: true }).catch(() => void 0);
326
+ }
327
+ }
328
+ async function loadSyncManifest(localDirectory) {
329
+ const manifestPath = join(localDirectory, SYNC_MANIFEST_FILE);
330
+ try {
331
+ const raw = await readFile(manifestPath, "utf8");
332
+ const parsed = JSON.parse(raw);
333
+ if (!parsed || typeof parsed !== "object" || parsed.version !== SYNC_MANIFEST_VERSION || !parsed.objects) {
334
+ return { manifest: null, invalidated: true };
335
+ }
336
+ return { manifest: parsed, invalidated: false };
337
+ } catch (error) {
338
+ if (error?.code === "ENOENT") {
339
+ return { manifest: null, invalidated: false };
340
+ }
341
+ return { manifest: null, invalidated: true };
342
+ }
343
+ }
344
+ async function writeSyncManifest(localDirectory, objects) {
345
+ const manifestPath = join(localDirectory, SYNC_MANIFEST_FILE);
346
+ const tempManifestPath = `${manifestPath}.tmp`;
347
+ const now = (/* @__PURE__ */ new Date()).toISOString();
348
+ const payload = {
349
+ version: SYNC_MANIFEST_VERSION,
350
+ createdAt: now,
351
+ updatedAt: now,
352
+ objects
353
+ };
354
+ await writeFile(tempManifestPath, `${JSON.stringify(payload, null, 2)}
355
+ `, "utf8");
356
+ await rename(tempManifestPath, manifestPath);
357
+ }
358
+ async function createStorageClient(auth) {
359
+ let credentials;
360
+ if (auth.credentialsJson) {
361
+ try {
362
+ credentials = JSON.parse(auth.credentialsJson);
363
+ } catch (error) {
364
+ throw new Error(`Invalid cloud.credentialsJson JSON: ${error instanceof Error ? error.message : String(error)}`);
365
+ }
366
+ }
367
+ const moduleName = "@google-cloud/storage";
368
+ const storageModule = await import(moduleName);
369
+ return new storageModule.Storage({
370
+ projectId: auth.projectId,
371
+ keyFilename: auth.keyFilename,
372
+ credentials
373
+ });
374
+ }
375
+ class GcsMirror {
376
+ auth;
377
+ bucketName;
378
+ prefix;
379
+ localDirectory;
380
+ includeFile;
381
+ incrementalSyncEnabled;
382
+ onDebugEvent;
383
+ constructor(options) {
384
+ this.auth = options.auth;
385
+ this.bucketName = options.location.bucket;
386
+ this.prefix = normalizePrefix(options.location.prefix);
387
+ this.localDirectory = resolve(options.localDirectory);
388
+ this.includeFile = options.includeFile || (() => true);
389
+ this.incrementalSyncEnabled = options.incrementalSyncEnabled !== false;
390
+ this.onDebugEvent = options.onDebugEvent;
391
+ }
392
+ async syncDown() {
393
+ const startedAt = Date.now();
394
+ this.onDebugEvent?.("sync_down.start", {
395
+ bucket: this.bucketName,
396
+ prefix: this.prefix,
397
+ localDirectory: this.localDirectory
398
+ });
399
+ const mkdirStartedAt = Date.now();
400
+ await mkdir(this.localDirectory, { recursive: true });
401
+ const mkdirMs = Date.now() - mkdirStartedAt;
402
+ this.onDebugEvent?.("sync_down.phase.mkdir", { elapsedMs: mkdirMs });
403
+ const clientStartedAt = Date.now();
404
+ const storage = await createStorageClient(this.auth);
405
+ const createClientMs = Date.now() - clientStartedAt;
406
+ this.onDebugEvent?.("sync_down.phase.create_client", { elapsedMs: createClientMs });
407
+ const bucket = storage.bucket(this.bucketName);
408
+ const listRemoteStartedAt = Date.now();
409
+ const remoteIndex = await loadRemoteSyncIndex(bucket, this.prefix, this.localDirectory, this.onDebugEvent);
410
+ let files = [];
411
+ let usedRemoteIndex = false;
412
+ if (remoteIndex.objects) {
413
+ usedRemoteIndex = true;
414
+ } else {
415
+ [files] = await withRetry(() => bucket.getFiles({ prefix: this.prefix || void 0 }));
416
+ }
417
+ const listRemoteMs = Date.now() - listRemoteStartedAt;
418
+ this.onDebugEvent?.("sync_down.phase.list_remote", {
419
+ elapsedMs: listRemoteMs,
420
+ listedCount: usedRemoteIndex ? Object.keys(remoteIndex.objects || {}).length : files.length,
421
+ source: usedRemoteIndex ? "gcs-index" : "bucket-list",
422
+ usedCachedIndex: remoteIndex.usedCachedIndex
423
+ });
424
+ const { manifest, invalidated } = this.incrementalSyncEnabled ? await loadSyncManifest(this.localDirectory) : { manifest: null, invalidated: false };
425
+ if (invalidated) {
426
+ this.onDebugEvent?.("sync_down.manifest.invalidated", {
427
+ localDirectory: this.localDirectory,
428
+ reason: "unsupported_or_corrupt_manifest"
429
+ });
430
+ }
431
+ const localFilesAtStart = await collectLocalFiles(this.localDirectory);
432
+ const localTrackedAtStart = new Set(
433
+ localFilesAtStart.filter((relativePath) => this.includeFile(relativePath))
434
+ );
435
+ const remoteStateByPath = {};
436
+ const remoteFileByPath = /* @__PURE__ */ new Map();
437
+ if (usedRemoteIndex && remoteIndex.objects) {
438
+ for (const [relativePath, objectState] of Object.entries(remoteIndex.objects)) {
439
+ if (!relativePath || isInternalSyncControlFile(relativePath) || !this.includeFile(relativePath)) {
440
+ continue;
441
+ }
442
+ remoteStateByPath[relativePath] = objectState;
443
+ remoteFileByPath.set(relativePath, bucket.file(toObjectName(this.prefix, relativePath)));
444
+ }
445
+ } else {
446
+ for (const file of files) {
447
+ const objectName = file.name;
448
+ if (objectName.endsWith("/")) {
449
+ continue;
450
+ }
451
+ const relativePath = toRelativePath(this.prefix, objectName);
452
+ if (!relativePath || isInternalSyncControlFile(relativePath) || !this.includeFile(relativePath)) {
453
+ continue;
454
+ }
455
+ remoteFileByPath.set(relativePath, file);
456
+ const remoteMeta = await resolveRemoteMetadata(file);
457
+ remoteStateByPath[relativePath] = {
458
+ path: relativePath,
459
+ generation: remoteMeta.generation,
460
+ etag: remoteMeta.etag,
461
+ md5Hash: remoteMeta.md5Hash,
462
+ size: remoteMeta.size,
463
+ updatedAt: remoteMeta.updatedAt
464
+ };
465
+ }
466
+ }
467
+ const remoteSet = new Set(Object.keys(remoteStateByPath));
468
+ const remoteIncludedCount = remoteSet.size;
469
+ let diff;
470
+ if (!this.incrementalSyncEnabled) {
471
+ diff = {
472
+ added: [...remoteSet],
473
+ changed: [],
474
+ unchanged: [],
475
+ deletedLocal: [...localTrackedAtStart].filter((path) => !remoteSet.has(path))
476
+ };
477
+ } else {
478
+ try {
479
+ diff = buildSyncDiff(remoteStateByPath, manifest?.objects, localTrackedAtStart);
480
+ } catch (error) {
481
+ this.onDebugEvent?.("sync_down.diff_recovery", {
482
+ reason: error instanceof Error ? error.message : String(error),
483
+ strategy: "full_resync"
484
+ });
485
+ diff = {
486
+ added: [],
487
+ changed: [...remoteSet],
488
+ unchanged: [],
489
+ deletedLocal: [...localTrackedAtStart].filter((path) => !remoteSet.has(path))
490
+ };
491
+ }
492
+ }
493
+ let changedCount = diff.added.length + diff.changed.length;
494
+ let skippedUnchangedCount = diff.unchanged.length;
495
+ let downloadedCount = 0;
496
+ let downloadedBytes = 0;
497
+ const manifestObjects = {};
498
+ const downloadStartedAt = Date.now();
499
+ const downloadTargets = [...diff.added, ...diff.changed];
500
+ for (const relativePath of downloadTargets) {
501
+ const file = remoteFileByPath.get(relativePath);
502
+ if (!file) {
503
+ continue;
504
+ }
505
+ const localPath = join(this.localDirectory, relativePath);
506
+ let shouldDownload = true;
507
+ const remoteMeta = remoteStateByPath[relativePath];
508
+ const existingManifestEntry = manifest?.objects?.[relativePath];
509
+ const existsLocally = await fileExists(localPath);
510
+ if (this.incrementalSyncEnabled && existsLocally && existingManifestEntry) {
511
+ const unchangedByManifest = existingManifestEntry.generation && remoteMeta?.generation && existingManifestEntry.generation === remoteMeta.generation;
512
+ if (unchangedByManifest) {
513
+ shouldDownload = false;
514
+ }
515
+ }
516
+ if (this.incrementalSyncEnabled && existsLocally && shouldDownload) {
517
+ const remoteMd5 = remoteMeta?.md5Hash || await resolveRemoteMd5(file);
518
+ if (remoteMd5) {
519
+ const localMd5 = await fileMd5Base64(localPath);
520
+ shouldDownload = localMd5 !== remoteMd5;
521
+ }
522
+ }
523
+ if (!shouldDownload) {
524
+ skippedUnchangedCount += 1;
525
+ changedCount -= 1;
526
+ continue;
527
+ }
528
+ await mkdir(dirname(localPath), { recursive: true });
529
+ const downloadFileStartedAt = Date.now();
530
+ await withRetry(() => file.download({ destination: localPath }));
531
+ const fileElapsedMs = Date.now() - downloadFileStartedAt;
532
+ downloadedCount += 1;
533
+ try {
534
+ downloadedBytes += (await stat(localPath)).size;
535
+ } catch {
536
+ }
537
+ if (fileElapsedMs >= 250) {
538
+ this.onDebugEvent?.("sync_down.file_download", {
539
+ path: relativePath,
540
+ elapsedMs: fileElapsedMs
541
+ });
542
+ }
543
+ manifestObjects[relativePath] = {
544
+ path: relativePath,
545
+ generation: remoteMeta?.generation,
546
+ etag: remoteMeta?.etag,
547
+ md5Hash: remoteMeta?.md5Hash,
548
+ size: remoteMeta?.size,
549
+ updatedAt: remoteMeta?.updatedAt,
550
+ localPath: relativePath,
551
+ lastSyncedAt: (/* @__PURE__ */ new Date()).toISOString()
552
+ };
553
+ }
554
+ for (const relativePath of remoteSet) {
555
+ if (!manifestObjects[relativePath] && manifest?.objects?.[relativePath]) {
556
+ manifestObjects[relativePath] = {
557
+ ...manifest.objects[relativePath],
558
+ lastSyncedAt: (/* @__PURE__ */ new Date()).toISOString()
559
+ };
560
+ }
561
+ }
562
+ const downloadMs = Date.now() - downloadStartedAt;
563
+ this.onDebugEvent?.("sync_down.phase.download", {
564
+ elapsedMs: downloadMs,
565
+ downloadedCount,
566
+ includedCount: remoteIncludedCount,
567
+ changedCount,
568
+ skippedUnchangedCount,
569
+ downloadedBytes
570
+ });
571
+ const listLocalStartedAt = Date.now();
572
+ const localFiles = await collectLocalFiles(this.localDirectory);
573
+ const listLocalMs = Date.now() - listLocalStartedAt;
574
+ this.onDebugEvent?.("sync_down.phase.list_local", { elapsedMs: listLocalMs, scannedCount: localFiles.length });
575
+ const cleanupStartedAt = Date.now();
576
+ let removedCount = 0;
577
+ this.onDebugEvent?.("sync_down.gc.start", {
578
+ localDirectory: this.localDirectory,
579
+ deletedLocalCandidates: diff.deletedLocal.length
580
+ });
581
+ for (const localRelative of diff.deletedLocal) {
582
+ if (!remoteSet.has(localRelative)) {
583
+ await rm(join(this.localDirectory, localRelative), { force: true });
584
+ removedCount += 1;
585
+ this.onDebugEvent?.("sync_down.gc.remove_local", {
586
+ path: localRelative
587
+ });
588
+ }
589
+ }
590
+ const cleanupMs = Date.now() - cleanupStartedAt;
591
+ this.onDebugEvent?.("sync_down.gc.complete", {
592
+ removedCount,
593
+ elapsedMs: cleanupMs
594
+ });
595
+ if (this.incrementalSyncEnabled) {
596
+ const manifestWriteStartedAt = Date.now();
597
+ await writeSyncManifest(this.localDirectory, manifestObjects);
598
+ this.onDebugEvent?.("sync_down.manifest.write_complete", {
599
+ objectCount: Object.keys(manifestObjects).length,
600
+ elapsedMs: Date.now() - manifestWriteStartedAt
601
+ });
602
+ }
603
+ const elapsedMs = Date.now() - startedAt;
604
+ const stats = {
605
+ bucket: this.bucketName,
606
+ prefix: this.prefix,
607
+ localDirectory: this.localDirectory,
608
+ remoteListedCount: usedRemoteIndex ? remoteIncludedCount : files.length,
609
+ remoteIncludedCount,
610
+ changedCount,
611
+ skippedUnchangedCount,
612
+ downloadedCount,
613
+ downloadedBytes,
614
+ localScannedCount: localFiles.length,
615
+ removedCount,
616
+ elapsedMs,
617
+ phases: {
618
+ mkdirMs,
619
+ createClientMs,
620
+ listRemoteMs,
621
+ downloadMs,
622
+ listLocalMs,
623
+ cleanupMs
624
+ }
625
+ };
626
+ this.onDebugEvent?.("sync_down.complete", stats);
627
+ return stats;
628
+ }
629
+ async syncUp() {
630
+ const startedAt = Date.now();
631
+ this.onDebugEvent?.("sync_up.start", {
632
+ bucket: this.bucketName,
633
+ prefix: this.prefix,
634
+ localDirectory: this.localDirectory
635
+ });
636
+ const mkdirStartedAt = Date.now();
637
+ await mkdir(this.localDirectory, { recursive: true });
638
+ const mkdirMs = Date.now() - mkdirStartedAt;
639
+ const clientStartedAt = Date.now();
640
+ const storage = await createStorageClient(this.auth);
641
+ const createClientMs = Date.now() - clientStartedAt;
642
+ const bucket = storage.bucket(this.bucketName);
643
+ const listRemoteStartedAt = Date.now();
644
+ const [remoteFiles] = await withRetry(() => bucket.getFiles({ prefix: this.prefix || void 0 }));
645
+ const listRemoteMs = Date.now() - listRemoteStartedAt;
646
+ const remoteByRelative = /* @__PURE__ */ new Map();
647
+ for (const remote of remoteFiles) {
648
+ if (remote.name.endsWith("/")) {
649
+ continue;
650
+ }
651
+ const relativePath = toRelativePath(this.prefix, remote.name);
652
+ if (!relativePath || isInternalSyncControlFile(relativePath) || !this.includeFile(relativePath)) {
653
+ continue;
654
+ }
655
+ remoteByRelative.set(relativePath, remote);
656
+ }
657
+ const listLocalStartedAt = Date.now();
658
+ const localFiles = await collectLocalFiles(this.localDirectory);
659
+ const listLocalMs = Date.now() - listLocalStartedAt;
660
+ const localSet = /* @__PURE__ */ new Set();
661
+ let localIncludedCount = 0;
662
+ let uploadedCount = 0;
663
+ const uploadStartedAt = Date.now();
664
+ for (const localRelative of localFiles) {
665
+ if (isInternalSyncControlFile(localRelative) || !this.includeFile(localRelative)) {
666
+ continue;
667
+ }
668
+ localIncludedCount += 1;
669
+ localSet.add(localRelative);
670
+ const localPath = join(this.localDirectory, localRelative);
671
+ const remoteFile = remoteByRelative.get(localRelative);
672
+ let shouldUpload = true;
673
+ if (remoteFile) {
674
+ const remoteMd5 = await resolveRemoteMd5(remoteFile);
675
+ if (remoteMd5) {
676
+ const localMd5 = await fileMd5Base64(localPath);
677
+ shouldUpload = localMd5 !== remoteMd5;
678
+ }
679
+ }
680
+ if (!shouldUpload) {
681
+ continue;
682
+ }
683
+ const objectName = toObjectName(this.prefix, localRelative);
684
+ const uploadFileStartedAt = Date.now();
685
+ await withRetry(
686
+ () => bucket.upload(localPath, {
687
+ destination: objectName
688
+ })
689
+ );
690
+ const fileElapsedMs = Date.now() - uploadFileStartedAt;
691
+ uploadedCount += 1;
692
+ if (fileElapsedMs >= 250) {
693
+ this.onDebugEvent?.("sync_up.file_upload", {
694
+ path: localRelative,
695
+ elapsedMs: fileElapsedMs
696
+ });
697
+ }
698
+ }
699
+ const uploadMs = Date.now() - uploadStartedAt;
700
+ let removedRemoteCount = 0;
701
+ const cleanupStartedAt = Date.now();
702
+ this.onDebugEvent?.("sync_up.gc.start", {
703
+ bucket: this.bucketName,
704
+ prefix: this.prefix,
705
+ remoteCandidates: remoteFiles.length
706
+ });
707
+ for (const remote of remoteFiles) {
708
+ if (remote.name.endsWith("/")) {
709
+ continue;
710
+ }
711
+ const relativePath = toRelativePath(this.prefix, remote.name);
712
+ if (!relativePath || isInternalSyncControlFile(relativePath) || !this.includeFile(relativePath)) {
713
+ continue;
714
+ }
715
+ if (!localSet.has(relativePath)) {
716
+ await withRetry(() => remote.delete({ ignoreNotFound: true }));
717
+ removedRemoteCount += 1;
718
+ this.onDebugEvent?.("sync_up.gc.remove_remote", {
719
+ path: relativePath
720
+ });
721
+ }
722
+ }
723
+ const cleanupMs = Date.now() - cleanupStartedAt;
724
+ this.onDebugEvent?.("sync_up.gc.complete", {
725
+ removedRemoteCount,
726
+ elapsedMs: cleanupMs
727
+ });
728
+ const indexStartedAt = Date.now();
729
+ const indexObjects = {};
730
+ for (const localRelative of localSet) {
731
+ const localPath = join(this.localDirectory, localRelative);
732
+ const localStats = await stat(localPath).catch(() => null);
733
+ const md5Hash = await fileMd5Base64(localPath).catch(() => void 0);
734
+ indexObjects[localRelative] = {
735
+ path: localRelative,
736
+ md5Hash,
737
+ size: localStats?.size,
738
+ updatedAt: localStats?.mtime ? localStats.mtime.toISOString() : void 0
739
+ };
740
+ }
741
+ await writeRemoteSyncIndex(bucket, this.prefix, this.localDirectory, indexObjects, this.onDebugEvent);
742
+ this.onDebugEvent?.("sync_up.phase.write_index", {
743
+ elapsedMs: Date.now() - indexStartedAt,
744
+ objectCount: Object.keys(indexObjects).length
745
+ });
746
+ const elapsedMs = Date.now() - startedAt;
747
+ const stats = {
748
+ bucket: this.bucketName,
749
+ prefix: this.prefix,
750
+ localDirectory: this.localDirectory,
751
+ localScannedCount: localFiles.length,
752
+ localIncludedCount,
753
+ uploadedCount,
754
+ remoteListedCount: remoteFiles.length,
755
+ removedRemoteCount,
756
+ elapsedMs,
757
+ phases: {
758
+ mkdirMs,
759
+ createClientMs,
760
+ listLocalMs,
761
+ uploadMs,
762
+ listRemoteMs,
763
+ cleanupMs
764
+ }
765
+ };
766
+ this.onDebugEvent?.("sync_up.complete", stats);
767
+ return stats;
768
+ }
769
+ }
770
+ function isTruthy(value) {
771
+ if (typeof value === "boolean") {
772
+ return value;
773
+ }
774
+ if (typeof value === "string") {
775
+ return /^(1|true|yes|on)$/i.test(value);
776
+ }
777
+ return false;
778
+ }
779
+ function firstNonEmpty(...values) {
780
+ for (const value of values) {
781
+ if (typeof value === "string" && value.trim()) {
782
+ return value.trim();
783
+ }
784
+ }
785
+ return void 0;
786
+ }
787
+ const inFlightOperations = /* @__PURE__ */ new Map();
788
+ const debouncedOperations = /* @__PURE__ */ new Map();
789
+ const lastSuccessfulSyncAtByScope = /* @__PURE__ */ new Map();
790
+ function toInt(value) {
791
+ if (!value) return void 0;
792
+ const parsed = Number.parseInt(value, 10);
793
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : void 0;
794
+ }
795
+ function resolveFreshnessTtlMs(config) {
796
+ const fromConfig = config?.cloud?.syncFreshnessTtlMs;
797
+ if (typeof fromConfig === "number" && Number.isFinite(fromConfig) && fromConfig >= 0) {
798
+ return fromConfig;
799
+ }
800
+ return toInt(process.env.RIOTPLAN_CLOUD_SYNC_FRESHNESS_TTL_MS) || 5e3;
801
+ }
802
+ function resolveSyncTimeoutMs(config) {
803
+ const fromConfig = config?.cloud?.syncTimeoutMs;
804
+ if (typeof fromConfig === "number" && Number.isFinite(fromConfig) && fromConfig > 0) {
805
+ return fromConfig;
806
+ }
807
+ return toInt(process.env.RIOTPLAN_CLOUD_SYNC_TIMEOUT_MS) || 12e4;
808
+ }
809
+ function resolveSyncUpDebounceMs(_config) {
810
+ const fromEnv = toInt(process.env.RIOTPLAN_CLOUD_SYNC_UP_DEBOUNCE_MS);
811
+ if (typeof fromEnv === "number") {
812
+ return fromEnv;
813
+ }
814
+ return 400;
815
+ }
816
+ async function runCoalescedOperation(key, operation, options) {
817
+ const existing = inFlightOperations.get(key);
818
+ if (existing) {
819
+ existing.waiterCount += 1;
820
+ const result2 = await existing.promise;
821
+ return {
822
+ result: result2,
823
+ coalesced: true,
824
+ waiterCount: existing.waiterCount
825
+ };
826
+ }
827
+ const timeoutMs = options?.timeoutMs && options.timeoutMs > 0 ? options.timeoutMs : 0;
828
+ const entry = {
829
+ waiterCount: 0,
830
+ promise: (async () => {
831
+ try {
832
+ if (!timeoutMs) {
833
+ return await operation();
834
+ }
835
+ return await Promise.race([
836
+ operation(),
837
+ new Promise((_, reject) => {
838
+ setTimeout(() => {
839
+ reject(new Error(`Coalesced operation timed out after ${timeoutMs}ms`));
840
+ }, timeoutMs);
841
+ })
842
+ ]);
843
+ } finally {
844
+ inFlightOperations.delete(key);
845
+ }
846
+ })()
847
+ };
848
+ inFlightOperations.set(key, entry);
849
+ const result = await entry.promise;
850
+ return {
851
+ result,
852
+ coalesced: false,
853
+ waiterCount: entry.waiterCount
854
+ };
855
+ }
856
+ async function runDebouncedCoalescedOperation(key, operation, options) {
857
+ const existing = debouncedOperations.get(key);
858
+ if (existing) {
859
+ existing.waiterCount += 1;
860
+ const result = await existing.promise;
861
+ return {
862
+ result,
863
+ coalesced: true,
864
+ waiterCount: existing.waiterCount
865
+ };
866
+ }
867
+ const debounceMs = options?.debounceMs && options.debounceMs > 0 ? options.debounceMs : 0;
868
+ const timeoutMs = options?.timeoutMs && options.timeoutMs > 0 ? options.timeoutMs : 0;
869
+ let timer;
870
+ const promise = new Promise((resolve2, reject) => {
871
+ const execute = async () => {
872
+ const run = async () => operation();
873
+ const task = timeoutMs ? Promise.race([
874
+ run(),
875
+ new Promise((_, timeoutReject) => {
876
+ setTimeout(() => {
877
+ timeoutReject(new Error(`Debounced operation timed out after ${timeoutMs}ms`));
878
+ }, timeoutMs);
879
+ })
880
+ ]) : run();
881
+ try {
882
+ resolve2(await task);
883
+ } catch (error) {
884
+ reject(error);
885
+ } finally {
886
+ debouncedOperations.delete(key);
887
+ }
888
+ };
889
+ if (debounceMs > 0) {
890
+ timer = setTimeout(() => {
891
+ void execute();
892
+ }, debounceMs);
893
+ return;
894
+ }
895
+ void execute();
896
+ });
897
+ const state = {
898
+ promise,
899
+ waiterCount: 0
900
+ };
901
+ debouncedOperations.set(key, state);
902
+ try {
903
+ const result = await promise;
904
+ return {
905
+ result,
906
+ coalesced: false,
907
+ waiterCount: state.waiterCount
908
+ };
909
+ } catch (error) {
910
+ if (timer) {
911
+ clearTimeout(timer);
912
+ }
913
+ throw error;
914
+ }
915
+ }
916
+ async function createCloudRuntime(config, localPlanDirectory, diagnostics) {
917
+ const cloudConfig = config?.cloud;
918
+ const incrementalSyncEnabled = cloudConfig?.incrementalSyncEnabled !== false;
919
+ const freshnessTtlMs = incrementalSyncEnabled ? resolveFreshnessTtlMs(config) : 0;
920
+ const syncTimeoutMs = resolveSyncTimeoutMs(config);
921
+ const syncUpDebounceMs = resolveSyncUpDebounceMs();
922
+ const enabled = isTruthy(cloudConfig?.enabled) || isTruthy(process.env.RIOTPLAN_CLOUD_ENABLED) || isTruthy(process.env.RIOTPLAN_GCS_ENABLED);
923
+ if (!enabled) {
924
+ return {
925
+ enabled: false,
926
+ workingDirectory: localPlanDirectory,
927
+ contextDirectory: localPlanDirectory,
928
+ syncDown: async () => ({
929
+ plan: null,
930
+ context: null,
931
+ syncFreshHit: false,
932
+ coalescedWaiterCount: 0
933
+ }),
934
+ syncUpPlans: async () => null,
935
+ syncUpContext: async () => null
936
+ };
937
+ }
938
+ const planBucket = firstNonEmpty(
939
+ cloudConfig?.planBucket,
940
+ process.env.RIOTPLAN_PLAN_BUCKET,
941
+ process.env.RIOTPLAN_GCS_PLAN_BUCKET
942
+ );
943
+ const contextBucket = firstNonEmpty(
944
+ cloudConfig?.contextBucket,
945
+ process.env.RIOTPLAN_CONTEXT_BUCKET,
946
+ process.env.RIOTPLAN_GCS_CONTEXT_BUCKET
947
+ );
948
+ if (!planBucket || !contextBucket) {
949
+ throw new Error(
950
+ "Cloud mode enabled but missing bucket config. Set cloud.planBucket + cloud.contextBucket or RIOTPLAN_PLAN_BUCKET + RIOTPLAN_CONTEXT_BUCKET."
951
+ );
952
+ }
953
+ const cacheRoot = resolve(
954
+ firstNonEmpty(cloudConfig?.cacheDirectory, process.env.RIOTPLAN_CLOUD_CACHE_DIR) || join(localPlanDirectory, ".cloud-cache")
955
+ );
956
+ const planMirrorDir = join(cacheRoot, "plans");
957
+ const contextMirrorDir = join(cacheRoot, "context");
958
+ await mkdir(planMirrorDir, { recursive: true });
959
+ await mkdir(contextMirrorDir, { recursive: true });
960
+ const auth = {
961
+ projectId: firstNonEmpty(cloudConfig?.projectId, process.env.GOOGLE_CLOUD_PROJECT),
962
+ keyFilename: firstNonEmpty(cloudConfig?.keyFilename, process.env.GOOGLE_APPLICATION_CREDENTIALS),
963
+ credentialsJson: firstNonEmpty(cloudConfig?.credentialsJson, process.env.GOOGLE_CREDENTIALS_JSON)
964
+ };
965
+ const planMirror = new GcsMirror({
966
+ auth,
967
+ location: {
968
+ bucket: planBucket,
969
+ prefix: cloudConfig?.planPrefix
970
+ },
971
+ localDirectory: planMirrorDir,
972
+ includeFile: (relativePath) => relativePath.endsWith(".plan"),
973
+ incrementalSyncEnabled,
974
+ onDebugEvent: (event, details) => {
975
+ diagnostics?.debug?.(`plan.${event}`, details);
976
+ }
977
+ });
978
+ const contextMirror = new GcsMirror({
979
+ auth,
980
+ location: {
981
+ bucket: contextBucket,
982
+ prefix: cloudConfig?.contextPrefix
983
+ },
984
+ localDirectory: contextMirrorDir,
985
+ incrementalSyncEnabled,
986
+ onDebugEvent: (event, details) => {
987
+ diagnostics?.debug?.(`context.${event}`, details);
988
+ }
989
+ });
990
+ return {
991
+ enabled: true,
992
+ workingDirectory: planMirrorDir,
993
+ contextDirectory: contextMirrorDir,
994
+ syncDown: async (options) => {
995
+ const startedAt = Date.now();
996
+ const planScope = `sync_down:${planMirrorDir}:plan`;
997
+ const contextScope = `sync_down:${contextMirrorDir}:context`;
998
+ const now = Date.now();
999
+ const planFresh = freshnessTtlMs > 0 && now - (lastSuccessfulSyncAtByScope.get(planScope) || 0) <= freshnessTtlMs;
1000
+ const contextFresh = freshnessTtlMs > 0 && now - (lastSuccessfulSyncAtByScope.get(contextScope) || 0) <= freshnessTtlMs;
1001
+ if (!options?.forceRefresh && planFresh && contextFresh) {
1002
+ diagnostics?.debug?.("sync_down.fresh_hit", {
1003
+ freshnessTtlMs,
1004
+ planScope,
1005
+ contextScope
1006
+ });
1007
+ return {
1008
+ plan: null,
1009
+ context: null,
1010
+ syncFreshHit: true,
1011
+ coalescedWaiterCount: 0
1012
+ };
1013
+ }
1014
+ diagnostics?.debug?.("sync_down.start", {
1015
+ planBucket,
1016
+ contextBucket,
1017
+ planMirrorDir,
1018
+ contextMirrorDir,
1019
+ incrementalSyncEnabled,
1020
+ freshnessTtlMs,
1021
+ syncTimeoutMs
1022
+ });
1023
+ const [planSync, contextSync] = await Promise.all([
1024
+ runCoalescedOperation(planScope, () => planMirror.syncDown(), { timeoutMs: syncTimeoutMs }),
1025
+ runCoalescedOperation(contextScope, () => contextMirror.syncDown(), { timeoutMs: syncTimeoutMs })
1026
+ ]);
1027
+ const planStats = planSync.result;
1028
+ const contextStats = contextSync.result;
1029
+ lastSuccessfulSyncAtByScope.set(planScope, Date.now());
1030
+ lastSuccessfulSyncAtByScope.set(contextScope, Date.now());
1031
+ const coalescedWaiterCount = planSync.waiterCount + contextSync.waiterCount;
1032
+ const syncFreshHit = planStats.downloadedCount === 0 && contextStats.downloadedCount === 0;
1033
+ diagnostics?.debug?.("sync_down.complete", {
1034
+ elapsedMs: Date.now() - startedAt,
1035
+ syncFreshHit,
1036
+ coalescedWaiterCount,
1037
+ syncOutcome: syncFreshHit ? "fresh-hit" : "full-or-incremental",
1038
+ plan: planStats,
1039
+ context: contextStats
1040
+ });
1041
+ return {
1042
+ plan: planStats,
1043
+ context: contextStats,
1044
+ syncFreshHit,
1045
+ coalescedWaiterCount
1046
+ };
1047
+ },
1048
+ syncUpPlans: async () => {
1049
+ const startedAt = Date.now();
1050
+ diagnostics?.debug?.("sync_up_plans.start", { planBucket, planMirrorDir });
1051
+ const syncUpScope = `sync_up:${planMirrorDir}:plan`;
1052
+ const syncResult = await runDebouncedCoalescedOperation(
1053
+ syncUpScope,
1054
+ () => planMirror.syncUp(),
1055
+ { debounceMs: syncUpDebounceMs, timeoutMs: syncTimeoutMs }
1056
+ );
1057
+ const stats = syncResult.result;
1058
+ if (syncResult.coalesced || syncResult.waiterCount > 0) {
1059
+ diagnostics?.debug?.("sync_up_plans.coalesced", {
1060
+ scope: syncUpScope,
1061
+ coalesced: syncResult.coalesced,
1062
+ coalescedWaiterCount: syncResult.waiterCount,
1063
+ debounceMs: syncUpDebounceMs
1064
+ });
1065
+ }
1066
+ diagnostics?.debug?.("sync_up_plans.complete", {
1067
+ elapsedMs: Date.now() - startedAt,
1068
+ plan: stats,
1069
+ coalesced: syncResult.coalesced,
1070
+ coalescedWaiterCount: syncResult.waiterCount,
1071
+ debounceMs: syncUpDebounceMs
1072
+ });
1073
+ return stats;
1074
+ },
1075
+ syncUpContext: async () => {
1076
+ const startedAt = Date.now();
1077
+ diagnostics?.debug?.("sync_up_context.start", { contextBucket, contextMirrorDir });
1078
+ const syncUpScope = `sync_up:${contextMirrorDir}:context`;
1079
+ const syncResult = await runDebouncedCoalescedOperation(
1080
+ syncUpScope,
1081
+ () => contextMirror.syncUp(),
1082
+ { debounceMs: syncUpDebounceMs, timeoutMs: syncTimeoutMs }
1083
+ );
1084
+ const stats = syncResult.result;
1085
+ if (syncResult.coalesced || syncResult.waiterCount > 0) {
1086
+ diagnostics?.debug?.("sync_up_context.coalesced", {
1087
+ scope: syncUpScope,
1088
+ coalesced: syncResult.coalesced,
1089
+ coalescedWaiterCount: syncResult.waiterCount,
1090
+ debounceMs: syncUpDebounceMs
1091
+ });
1092
+ }
1093
+ diagnostics?.debug?.("sync_up_context.complete", {
1094
+ elapsedMs: Date.now() - startedAt,
1095
+ context: stats,
1096
+ coalesced: syncResult.coalesced,
1097
+ coalescedWaiterCount: syncResult.waiterCount,
1098
+ debounceMs: syncUpDebounceMs
1099
+ });
1100
+ return stats;
1101
+ }
1102
+ };
1103
+ }
1104
+ export {
1105
+ GcsMirror,
1106
+ buildSyncDiff,
1107
+ createCloudRuntime,
1108
+ loadSyncManifest,
1109
+ runCoalescedOperation,
1110
+ runDebouncedCoalescedOperation,
1111
+ writeSyncManifest
1112
+ };
1113
+ //# sourceMappingURL=index.js.map