@gadgetinc/ggt 0.2.1 → 0.2.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.
Files changed (45) hide show
  1. package/README.md +6 -6
  2. package/bin/dev.js +2 -2
  3. package/bin/run.js +1 -1
  4. package/lib/commands/help.js +2 -2
  5. package/lib/commands/help.js.map +1 -1
  6. package/lib/commands/list.js +3 -3
  7. package/lib/commands/list.js.map +1 -1
  8. package/lib/commands/login.js +2 -2
  9. package/lib/commands/login.js.map +1 -1
  10. package/lib/commands/logout.js +2 -2
  11. package/lib/commands/logout.js.map +1 -1
  12. package/lib/commands/sync.js +233 -88
  13. package/lib/commands/sync.js.map +1 -1
  14. package/lib/commands/whoami.js +2 -2
  15. package/lib/commands/whoami.js.map +1 -1
  16. package/lib/{utils → services}/base-command.js +23 -8
  17. package/lib/services/base-command.js.map +1 -0
  18. package/lib/{utils → services}/client.js +70 -21
  19. package/lib/services/client.js.map +1 -0
  20. package/lib/{utils → services}/context.js +42 -27
  21. package/lib/services/context.js.map +1 -0
  22. package/lib/{utils → services}/errors.js +8 -8
  23. package/lib/services/errors.js.map +1 -0
  24. package/lib/{utils → services}/flags.js +4 -3
  25. package/lib/services/flags.js.map +1 -0
  26. package/lib/{utils → services}/fs-utils.js +2 -2
  27. package/lib/services/fs-utils.js.map +1 -0
  28. package/lib/{utils → services}/help.js +1 -1
  29. package/lib/services/help.js.map +1 -0
  30. package/lib/services/promise.js.map +1 -0
  31. package/lib/{utils → services}/sleep.js +6 -2
  32. package/lib/services/sleep.js.map +1 -0
  33. package/npm-shrinkwrap.json +2188 -2630
  34. package/oclif.manifest.json +47 -7
  35. package/package.json +29 -28
  36. package/lib/utils/base-command.js.map +0 -1
  37. package/lib/utils/client.js.map +0 -1
  38. package/lib/utils/context.js.map +0 -1
  39. package/lib/utils/errors.js.map +0 -1
  40. package/lib/utils/flags.js.map +0 -1
  41. package/lib/utils/fs-utils.js.map +0 -1
  42. package/lib/utils/help.js.map +0 -1
  43. package/lib/utils/promise.js.map +0 -1
  44. package/lib/utils/sleep.js.map +0 -1
  45. /package/lib/{utils → services}/promise.js +0 -0
@@ -5,7 +5,6 @@ import { _ as _define_property } from "@swc/helpers/_/_define_property";
5
5
  import { Args, Flags } from "@oclif/core";
6
6
  import assert from "assert";
7
7
  import chalkTemplate from "chalk-template";
8
- import { default as FSWatcher } from "watcher";
9
8
  import { format } from "date-fns";
10
9
  import { execa } from "execa";
11
10
  import fs from "fs-extra";
@@ -17,24 +16,17 @@ import PQueue from "p-queue";
17
16
  import path from "path";
18
17
  import pluralize from "pluralize";
19
18
  import { dedent } from "ts-dedent";
19
+ import FSWatcher from "watcher";
20
20
  import which from "which";
21
21
  import { FileSyncEncoding } from "../__generated__/graphql.js";
22
- import { BaseCommand } from "../utils/base-command.js";
23
- import { Client } from "../utils/client.js";
24
- import { context } from "../utils/context.js";
25
- import { InvalidSyncAppFlagError, InvalidSyncFileError, YarnNotFoundError } from "../utils/errors.js";
26
- import { app } from "../utils/flags.js";
27
- import { FSIgnorer, ignoreEnoent, isEmptyDir, walkDir } from "../utils/fs-utils.js";
28
- import { PromiseSignal } from "../utils/promise.js";
22
+ import { BaseCommand } from "../services/base-command.js";
23
+ import { Client } from "../services/client.js";
24
+ import { context } from "../services/context.js";
25
+ import { InvalidSyncAppFlagError, InvalidSyncFileError, YarnNotFoundError } from "../services/errors.js";
26
+ import { app } from "../services/flags.js";
27
+ import { FSIgnorer, ignoreEnoent, isEmptyDir, walkDir } from "../services/fs-utils.js";
28
+ import { PromiseSignal } from "../services/promise.js";
29
29
  class Sync extends BaseCommand {
30
- static fileStats(path) {
31
- try {
32
- return fs.statSync(path);
33
- } catch (error) {
34
- ignoreEnoent(error);
35
- return undefined;
36
- }
37
- }
38
30
  /**
39
31
  * Turns an absolute filepath into a relative one from {@linkcode dir}.
40
32
  */ relative(to) {
@@ -77,8 +69,8 @@ class Sync extends BaseCommand {
77
69
  * @param options.limit The maximum number of lines to print. Defaults to 10. If debug is enabled, this is ignored.
78
70
  */ logPaths(prefix, changed, deleted, { limit = 10 } = {}) {
79
71
  const lines = _.sortBy([
80
- ...changed.map((normalizedPath)=>chalkTemplate`{green ${prefix}} ${normalizedPath} {gray (changed)}`),
81
- ...deleted.map((normalizedPath)=>chalkTemplate`{red ${prefix}} ${normalizedPath} {gray (deleted)}`)
72
+ ..._.map(changed, (normalizedPath)=>chalkTemplate`{green ${prefix}} ${normalizedPath} {gray (changed)}`),
73
+ ..._.map(deleted, (normalizedPath)=>chalkTemplate`{red ${prefix}} ${normalizedPath} {gray (deleted)}`)
82
74
  ], (line)=>line.slice(line.indexOf(" ") + 1));
83
75
  let logged = 0;
84
76
  for (const line of lines){
@@ -100,16 +92,21 @@ class Sync extends BaseCommand {
100
92
  * - Prompts the user how to resolve conflicts if the local filesystem has changed since the last sync.
101
93
  */ async init() {
102
94
  await super.init();
103
- assert(_.isString(this.args["directory"]));
104
- this.dir = this.config.windows && this.args["directory"].startsWith("~/") ? path.join(this.config.home, this.args["directory"].slice(2)) : path.resolve(this.args["directory"]);
95
+ this.dir = this.config.windows && _.startsWith(this.args.directory, "~/") ? path.join(this.config.home, this.args.directory.slice(2)) : path.resolve(this.args.directory);
105
96
  const getApp = async ()=>{
106
- if (this.flags.app) return this.flags.app;
107
- if (this.state?.app) return this.state.app;
97
+ if (this.flags.app) {
98
+ return this.flags.app;
99
+ }
100
+ // this.state can be undefined if the user is running `ggt sync` for the first time
101
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
102
+ if (this.state?.app) {
103
+ return this.state.app;
104
+ }
108
105
  const selected = await inquirer.prompt({
109
106
  type: "list",
110
107
  name: "app",
111
108
  message: "Please select the app to sync to.",
112
- choices: await context.getAvailableApps().then((apps)=>apps.map((app)=>app.slug))
109
+ choices: await context.getAvailableApps().then((apps)=>_.map(apps, "slug"))
113
110
  });
114
111
  return selected.app;
115
112
  };
@@ -142,7 +139,6 @@ class Sync extends BaseCommand {
142
139
  ".gadget",
143
140
  ".git"
144
141
  ]);
145
- this.debug("starting");
146
142
  if (!which.sync("yarn", {
147
143
  nothrow: true
148
144
  })) {
@@ -167,7 +163,7 @@ class Sync extends BaseCommand {
167
163
  files.delete("/");
168
164
  return files;
169
165
  };
170
- const changedFiles = await getChangedFiles();
166
+ let changedFiles = await getChangedFiles();
171
167
  const hasLocalChanges = changedFiles.size > 0;
172
168
  if (hasLocalChanges) {
173
169
  this.log("Local files have changed since you last synced");
@@ -176,11 +172,16 @@ class Sync extends BaseCommand {
176
172
  });
177
173
  this.log();
178
174
  }
179
- this.debug("init %O", {
180
- metadata: this.state,
181
- remoteFilesVersion,
182
- hasRemoteChanges,
183
- hasLocalChanges
175
+ context.addBreadcrumb({
176
+ category: "sync",
177
+ message: "Initializing",
178
+ data: {
179
+ state: this.state,
180
+ remoteFilesVersion,
181
+ hasRemoteChanges,
182
+ hasLocalChanges,
183
+ changed: Array.from(changedFiles.keys())
184
+ }
184
185
  });
185
186
  let action;
186
187
  if (hasLocalChanges) {
@@ -195,11 +196,20 @@ class Sync extends BaseCommand {
195
196
  message: hasRemoteChanges ? "Remote files have also changed. How would you like to proceed?" : "How would you like to proceed?"
196
197
  }));
197
198
  }
199
+ // get all the changed files again in case more changed
200
+ changedFiles = await getChangedFiles();
198
201
  switch(action){
199
202
  case Action.MERGE:
200
203
  {
201
- // get all the changed files again in case more changed
202
- const files = await getChangedFiles();
204
+ context.addBreadcrumb({
205
+ category: "sync",
206
+ message: "Merging local changes",
207
+ data: {
208
+ state: this.state,
209
+ remoteFilesVersion,
210
+ changed: Array.from(changedFiles.keys())
211
+ }
212
+ });
203
213
  // We purposefully don't set the returned remoteFilesVersion here because we haven't received the remote changes
204
214
  // yet. This will cause us to receive the local files that we just published + the remote files that were
205
215
  // changed since the last sync.
@@ -208,7 +218,7 @@ class Sync extends BaseCommand {
208
218
  variables: {
209
219
  input: {
210
220
  expectedRemoteFilesVersion: remoteFilesVersion,
211
- changed: await pMap(files, async ([normalizedPath, stats])=>{
221
+ changed: await pMap(changedFiles, async ([normalizedPath, stats])=>{
212
222
  if (stats.mtime.getTime() > this.state.mtime) {
213
223
  this.state.mtime = stats.mtime.getTime();
214
224
  }
@@ -227,6 +237,15 @@ class Sync extends BaseCommand {
227
237
  }
228
238
  case Action.RESET:
229
239
  {
240
+ context.addBreadcrumb({
241
+ category: "sync",
242
+ message: "Resetting local changes",
243
+ data: {
244
+ state: this.state,
245
+ remoteFilesVersion,
246
+ changed: Array.from(changedFiles.keys())
247
+ }
248
+ });
230
249
  // delete all the local files that have changed since the last sync and set the files version to 0 so we receive
231
250
  // all the remote files again, including any files that we just deleted that still exist
232
251
  await pMap(changedFiles.keys(), (normalizedPath)=>this.softDelete(normalizedPath));
@@ -238,27 +257,32 @@ class Sync extends BaseCommand {
238
257
  process.exit(0);
239
258
  }
240
259
  }
241
- this.debug("started");
260
+ context.addBreadcrumb({
261
+ category: "sync",
262
+ message: "Initialized",
263
+ data: {
264
+ state: this.state
265
+ }
266
+ });
242
267
  }
243
268
  /**
244
269
  * Runs the sync process until it is stopped or an error occurs.
245
270
  */ async run() {
246
271
  let error;
247
272
  const stopped = new PromiseSignal();
248
- this.watcher = new FSWatcher(this.dir, {
249
- ignore: (filepath)=>{
250
- return this.ignorer.ignores(filepath);
251
- },
252
- // don't emit an event for every watched file on boot
253
- ignoreInitial: true,
254
- renameDetection: true,
255
- recursive: true
256
- });
257
273
  this.stop = async (e)=>{
258
274
  if (this.status != SyncStatus.RUNNING) return;
259
- error = e;
260
- this.debug("stopping");
261
275
  this.status = SyncStatus.STOPPING;
276
+ error = e;
277
+ context.addBreadcrumb({
278
+ category: "sync",
279
+ message: "Stopping",
280
+ level: error ? "error" : undefined,
281
+ data: {
282
+ state: this.state,
283
+ error
284
+ }
285
+ });
262
286
  try {
263
287
  unsubscribe();
264
288
  this.watcher.removeAllListeners();
@@ -270,9 +294,15 @@ class Sync extends BaseCommand {
270
294
  this.watcher.close(),
271
295
  this.client.dispose()
272
296
  ]);
273
- this.debug("stopped");
274
297
  this.status = SyncStatus.STOPPED;
275
298
  stopped.resolve();
299
+ context.addBreadcrumb({
300
+ category: "sync",
301
+ message: "Stopped",
302
+ data: {
303
+ state: this.state
304
+ }
305
+ });
276
306
  }
277
307
  };
278
308
  for (const signal of [
@@ -301,22 +331,49 @@ class Sync extends BaseCommand {
301
331
  }, {
302
332
  error: (error)=>void this.stop(error),
303
333
  next: ({ remoteFileSyncEvents })=>{
334
+ context.addBreadcrumb({
335
+ category: "sync",
336
+ message: "Received file sync events",
337
+ data: {
338
+ state: this.state,
339
+ remoteFilesVersion: remoteFileSyncEvents.remoteFilesVersion,
340
+ changed: _.map(remoteFileSyncEvents.changed, "path"),
341
+ deleted: _.map(remoteFileSyncEvents.deleted, "path")
342
+ }
343
+ });
304
344
  const remoteFilesVersion = remoteFileSyncEvents.remoteFilesVersion;
305
345
  // we always ignore .gadget/ files so that we don't publish them (they're managed by gadget), but we still want to receive them
306
- const filter = (event)=>event.path.startsWith(".gadget/") || !this.ignorer.ignores(event.path);
307
- const changed = remoteFileSyncEvents.changed.filter(filter);
308
- const deleted = remoteFileSyncEvents.deleted.filter(filter);
346
+ const filter = (event)=>_.startsWith(event.path, ".gadget/") || !this.ignorer.ignores(event.path);
347
+ const changed = _.filter(remoteFileSyncEvents.changed, filter);
348
+ const deleted = _.filter(remoteFileSyncEvents.deleted, filter);
309
349
  this._enqueue(async ()=>{
350
+ context.addBreadcrumb({
351
+ category: "sync",
352
+ message: "Processing received file sync events",
353
+ data: {
354
+ state: this.state,
355
+ remoteFilesVersion: remoteFileSyncEvents.remoteFilesVersion,
356
+ changed: _.map(remoteFileSyncEvents.changed, "path"),
357
+ deleted: _.map(remoteFileSyncEvents.deleted, "path")
358
+ }
359
+ });
310
360
  if (!changed.length && !deleted.length) {
311
361
  if (BigInt(remoteFilesVersion) > this.state.filesVersion) {
312
362
  // we still need to update filesVersion, otherwise our expectedFilesVersion will be behind the next time we publish
313
- this.debug("updated local files version from %s to %s", this.state.filesVersion, remoteFilesVersion);
314
363
  this.state.filesVersion = remoteFilesVersion;
364
+ context.addBreadcrumb({
365
+ category: "sync",
366
+ message: "Received empty file sync events",
367
+ data: {
368
+ state: this.state,
369
+ remoteFilesVersion: remoteFileSyncEvents.remoteFilesVersion
370
+ }
371
+ });
315
372
  }
316
373
  return;
317
374
  }
318
375
  this.log(chalkTemplate`Received {gray ${format(new Date(), "pp")}}`);
319
- this.logPaths("←", changed.map((x)=>x.path), deleted.map((x)=>x.path));
376
+ this.logPaths("←", _.map(changed, "path"), _.map(deleted, "path"));
320
377
  // we need to processed deleted files first as we may delete an empty directory after a file has been put
321
378
  // into it. if processed out of order the new file will be deleted as well
322
379
  await pMap(deleted, async (file)=>{
@@ -326,14 +383,16 @@ class Sync extends BaseCommand {
326
383
  await pMap(changed, async (file)=>{
327
384
  this.recentRemoteChanges.add(file.path);
328
385
  const absolutePath = this.absolute(file.path);
329
- if (file.path.endsWith("/")) {
386
+ if (_.endsWith(file.path, "/")) {
330
387
  await fs.ensureDir(absolutePath, {
331
388
  mode: 0o755
332
389
  });
333
390
  return;
334
391
  }
335
- // we need to add the parent directory to recentRemoteChanges so that we don't try to publish it
336
- this.recentRemoteChanges.add(path.dirname(file.path) + "/");
392
+ // we need to add all parent directories to recentRemoteChanges so that we don't re-publish them
393
+ for (const dir of _.split(path.dirname(file.path), "/")){
394
+ this.recentRemoteChanges.add(dir + "/");
395
+ }
337
396
  await fs.ensureDir(path.dirname(absolutePath), {
338
397
  mode: 0o755
339
398
  });
@@ -345,16 +404,22 @@ class Sync extends BaseCommand {
345
404
  "install"
346
405
  ], {
347
406
  cwd: this.dir
348
- }).catch((err)=>{
349
- this.debug("yarn install failed");
350
- this.debug(err.message);
407
+ }).catch((error)=>{
408
+ context.addBreadcrumb({
409
+ category: "sync",
410
+ message: "Yarn install failed",
411
+ level: "error",
412
+ data: {
413
+ state: this.state,
414
+ error
415
+ }
416
+ });
351
417
  });
352
418
  }
353
419
  if (absolutePath == this.ignorer.filepath) {
354
420
  this.ignorer.reload();
355
421
  }
356
422
  });
357
- this.debug("updated local files version from %s to %s", this.state.filesVersion, remoteFilesVersion);
358
423
  this.state.filesVersion = remoteFilesVersion;
359
424
  // always remove the root directory from recentRemoteChanges
360
425
  this.recentRemoteChanges.delete("./");
@@ -364,6 +429,17 @@ class Sync extends BaseCommand {
364
429
  this.recentRemoteChanges.delete(filepath);
365
430
  }
366
431
  }
432
+ context.addBreadcrumb({
433
+ category: "sync",
434
+ message: "Processed received file sync events",
435
+ data: {
436
+ state: this.state,
437
+ remoteFilesVersion: remoteFileSyncEvents.remoteFilesVersion,
438
+ changed: _.map(remoteFileSyncEvents.changed, "path"),
439
+ deleted: _.map(remoteFileSyncEvents.deleted, "path"),
440
+ recentRemoteChanges: Array.from(this.recentRemoteChanges.keys())
441
+ }
442
+ });
367
443
  });
368
444
  }
369
445
  });
@@ -372,6 +448,14 @@ class Sync extends BaseCommand {
372
448
  const localFiles = new Map(localFilesBuffer.entries());
373
449
  localFilesBuffer.clear();
374
450
  this._enqueue(async ()=>{
451
+ context.addBreadcrumb({
452
+ category: "sync",
453
+ message: "Publishing file sync events",
454
+ data: {
455
+ state: this.state,
456
+ localFiles: Array.from(localFiles.keys())
457
+ }
458
+ });
375
459
  const changed = [];
376
460
  const deleted = [];
377
461
  await pMap(localFiles, async ([normalizedPath, file])=>{
@@ -381,16 +465,13 @@ class Sync extends BaseCommand {
381
465
  });
382
466
  return;
383
467
  }
384
- const isRename = "oldPath" in file;
385
468
  try {
386
469
  changed.push({
387
470
  path: normalizedPath,
471
+ oldPath: "oldPath" in file ? file.oldPath : undefined,
388
472
  mode: file.mode,
389
- content: file.isDirectory ? "" : await fs.readFile(this.absolute(normalizedPath), "base64"),
390
- encoding: FileSyncEncoding.Base64,
391
- ...isRename ? {
392
- oldPath: file.oldPath
393
- } : undefined
473
+ content: file.isDirectory ? "" : await fs.readFile(this.absolute(normalizedPath), FileSyncEncoding.Base64),
474
+ encoding: FileSyncEncoding.Base64
394
475
  });
395
476
  } catch (error) {
396
477
  // A file could have been changed and then deleted before we process the change event, so the readFile
@@ -401,7 +482,7 @@ class Sync extends BaseCommand {
401
482
  if (!changed.length && !deleted.length) {
402
483
  return;
403
484
  }
404
- const data = await this.client.queryUnwrap({
485
+ const { publishFileSyncEvents } = await this.client.queryUnwrap({
405
486
  query: PUBLISH_FILE_SYNC_EVENTS_MUTATION,
406
487
  variables: {
407
488
  input: {
@@ -411,32 +492,52 @@ class Sync extends BaseCommand {
411
492
  }
412
493
  }
413
494
  });
414
- this.log(chalkTemplate`Sent {gray ${format(new Date(), "pp")}}`);
415
- this.logPaths("", changed.map((x)=>x.path), deleted.map((x)=>x.path));
416
- const { remoteFilesVersion } = data.publishFileSyncEvents;
417
- this.debug("remote files version after publishing %s", remoteFilesVersion);
418
- if (BigInt(remoteFilesVersion) > this.state.filesVersion) {
419
- this.debug("updated local files version from %s to %s", this.state.filesVersion, remoteFilesVersion);
420
- this.state.filesVersion = remoteFilesVersion;
495
+ context.addBreadcrumb({
496
+ category: "sync",
497
+ message: "Published file sync events",
498
+ data: {
499
+ state: this.state,
500
+ remoteFilesVersion: publishFileSyncEvents.remoteFilesVersion,
501
+ changed: _.map(changed, "path"),
502
+ deleted: _.map(deleted, "path")
503
+ }
504
+ });
505
+ if (BigInt(publishFileSyncEvents.remoteFilesVersion) > this.state.filesVersion) {
506
+ this.state.filesVersion = publishFileSyncEvents.remoteFilesVersion;
421
507
  }
508
+ this.log(chalkTemplate`Sent {gray ${format(new Date(), "pp")}}`);
509
+ this.logPaths("→", _.map(changed, "path"), _.map(deleted, "path"));
422
510
  });
423
511
  }, this.flags["file-push-delay"]);
424
- this.on("error", (error)=>void this.stop(error)).on("all", (event, absolutePath, renamedPath)=>{
425
- const filePath = event === "rename" || event === "renameDir" ? renamedPath : absolutePath;
512
+ this.watcher = new FSWatcher(this.dir, {
513
+ // paths that we never want to publish
514
+ ignore: /(\.gadget|\.git|node_modules)/,
515
+ // don't emit an event for every watched file on boot
516
+ ignoreInitial: true,
517
+ renameDetection: true,
518
+ recursive: true,
519
+ debounce: this.flags["file-watch-debounce"],
520
+ pollingInterval: this.flags["file-watch-poll-interval"],
521
+ pollingTimeout: this.flags["file-watch-poll-timeout"],
522
+ renameTimeout: this.flags["file-watch-rename-timeout"]
523
+ });
524
+ this.watcher.once("error", (error)=>void this.stop(error));
525
+ this.watcher.on("all", (event, absolutePath, renamedPath)=>{
526
+ const filepath = event === "rename" || event === "renameDir" ? renamedPath : absolutePath;
426
527
  const isDirectory = event === "renameDir" || event === "addDir" || event === "unlinkDir";
427
- const normalizedPath = this.normalize(filePath, isDirectory);
428
- const stats = Sync.fileStats(filePath);
429
- // this shouldn't ever be the case using watcher since it doesn't support symlinks but we'll keep it here just in case we ever switch to another lib that does support it
430
- if (stats?.isSymbolicLink?.()) {
431
- this.debug("skipping event caused by symlink %s", normalizedPath);
432
- return;
433
- }
434
- if (filePath == this.ignorer.filepath) {
528
+ const normalizedPath = this.normalize(filepath, isDirectory);
529
+ if (filepath == this.ignorer.filepath) {
435
530
  this.ignorer.reload();
436
- } else if (this.ignorer.ignores(filePath)) {
531
+ } else if (this.ignorer.ignores(filepath)) {
437
532
  this.debug("skipping event caused by ignored file %s", normalizedPath);
438
533
  return;
439
534
  }
535
+ let stats;
536
+ try {
537
+ stats = fs.statSync(filepath);
538
+ } catch (error) {
539
+ ignoreEnoent(error);
540
+ }
440
541
  // we only update the mtime if the file is not ignored, because if we restart and the mtime is set to an ignored
441
542
  // file, then it could be greater than the mtime of all non ignored files and we'll think that local files have
442
543
  // changed when only an ignored one has
@@ -447,7 +548,7 @@ class Sync extends BaseCommand {
447
548
  this.debug("skipping event caused by recent write %s", normalizedPath);
448
549
  return;
449
550
  }
450
- this.debug("file changed %s", normalizedPath, event);
551
+ this.debug("%s %s", event, normalizedPath);
451
552
  switch(event){
452
553
  case "add":
453
554
  case "change":
@@ -515,10 +616,6 @@ class Sync extends BaseCommand {
515
616
  this.log("Goodbye!");
516
617
  }
517
618
  }
518
- on(eventName, listener) {
519
- this.watcher.on(eventName, listener);
520
- return this;
521
- }
522
619
  /**
523
620
  * Enqueues a function that handles file-sync events onto the {@linkcode queue}.
524
621
  *
@@ -612,6 +709,35 @@ _define_property(Sync, "flags", {
612
709
  helpValue: "ms",
613
710
  default: 100,
614
711
  hidden: true
712
+ }),
713
+ // The following flags are passed to FSWatcher (https://github.com/fabiospampinato/watcher)
714
+ "file-watch-debounce": Flags.integer({
715
+ summary: "Amount of milliseconds to debounce file changed events",
716
+ helpGroup: "file",
717
+ helpValue: "ms",
718
+ default: 300,
719
+ hidden: true
720
+ }),
721
+ "file-watch-poll-interval": Flags.integer({
722
+ summary: "Polling is used as a last resort measure when watching non-existent paths inside non-existent directories, this controls how often polling is performed, in milliseconds. You can set it to a lower value to make the app detect events much more quickly, but don't set it too low if you are watching many paths that require polling as polling is expensive.",
723
+ helpGroup: "file",
724
+ helpValue: "ms",
725
+ default: 3_000,
726
+ hidden: true
727
+ }),
728
+ "file-watch-poll-timeout": Flags.integer({
729
+ summary: "Sometimes polling will fail, for example if there are too many file descriptors currently open, usually eventually polling will succeed after a few tries though, this controls the amount of milliseconds the library should keep retrying for.",
730
+ helpGroup: "file",
731
+ helpValue: "ms",
732
+ default: 20_000,
733
+ hidden: true
734
+ }),
735
+ "file-watch-rename-timeout": Flags.integer({
736
+ summary: "Amount of milliseconds to wait for a potential rename/renameDir event to be detected. The higher this value is the more reliably renames will be detected, but don't set this too high, or the emission of some events could be delayed by that amount. The higher this value is the longer the library will take to emit add/addDir/unlink/unlinkDir events.",
737
+ helpGroup: "file",
738
+ helpValue: "ms",
739
+ default: 1_250,
740
+ hidden: true
615
741
  })
616
742
  });
617
743
  _define_property(Sync, "examples", [
@@ -700,6 +826,13 @@ var /**
700
826
  * @returns The SyncFile instance.
701
827
  */ static load(rootDir) {
702
828
  const state = fs.readJsonSync(path.join(rootDir, ".gadget/sync.json"));
829
+ context.addBreadcrumb({
830
+ category: "sync",
831
+ message: "Loaded sync state",
832
+ data: {
833
+ state
834
+ }
835
+ });
703
836
  assert(_.isString(state.app), "missing or invalid app");
704
837
  assert(_.isString(state.filesVersion), "missing or invalid filesVersion");
705
838
  assert(_.isNumber(state.mtime), "missing or invalid mtime");
@@ -714,6 +847,11 @@ var /**
714
847
  */ flush() {
715
848
  _class_private_field_get(this, _save).flush();
716
849
  }
850
+ /**
851
+ * @returns The JSON representation of this instance.
852
+ */ toJSON() {
853
+ return this._inner;
854
+ }
717
855
  constructor(_rootDir, inner){
718
856
  _define_property(this, "_rootDir", void 0);
719
857
  _define_property(this, "_inner", void 0);
@@ -726,6 +864,13 @@ var /**
726
864
  fs.outputJSONSync(path.join(this._rootDir, ".gadget/sync.json"), this._inner, {
727
865
  spaces: 2
728
866
  });
867
+ context.addBreadcrumb({
868
+ category: "sync",
869
+ message: "Saved sync state",
870
+ data: {
871
+ state: this._inner
872
+ }
873
+ });
729
874
  }, 100));
730
875
  this._inner = inner;
731
876
  }