velocious 1.0.695 → 1.0.697

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 (62) hide show
  1. package/README.md +1 -1
  2. package/build/background-jobs/adapter.js +1 -1
  3. package/build/background-jobs/client.js +1 -1
  4. package/build/background-jobs/job-record.js +23 -0
  5. package/build/background-jobs/local-adapter.js +1 -1
  6. package/build/background-jobs/local-store.js +10 -5
  7. package/build/background-jobs/main.js +6 -1
  8. package/build/background-jobs/owner-liveness.js +92 -0
  9. package/build/background-jobs/pooled-runner-child.js +5 -1
  10. package/build/background-jobs/status-reporter.js +7 -4
  11. package/build/background-jobs/store.js +13 -5
  12. package/build/background-jobs/types.js +2 -1
  13. package/build/background-jobs/worker.js +4 -2
  14. package/build/database/migrations-ledger.js +96 -8
  15. package/build/src/background-jobs/adapter.d.ts +2 -1
  16. package/build/src/background-jobs/adapter.d.ts.map +1 -1
  17. package/build/src/background-jobs/adapter.js +2 -2
  18. package/build/src/background-jobs/client.js +2 -2
  19. package/build/src/background-jobs/job-record.d.ts +16 -0
  20. package/build/src/background-jobs/job-record.d.ts.map +1 -1
  21. package/build/src/background-jobs/job-record.js +23 -1
  22. package/build/src/background-jobs/local-adapter.d.ts +2 -1
  23. package/build/src/background-jobs/local-adapter.d.ts.map +1 -1
  24. package/build/src/background-jobs/local-adapter.js +2 -2
  25. package/build/src/background-jobs/local-store.d.ts +5 -3
  26. package/build/src/background-jobs/local-store.d.ts.map +1 -1
  27. package/build/src/background-jobs/local-store.js +12 -6
  28. package/build/src/background-jobs/main.d.ts.map +1 -1
  29. package/build/src/background-jobs/main.js +7 -2
  30. package/build/src/background-jobs/owner-liveness.d.ts +71 -0
  31. package/build/src/background-jobs/owner-liveness.d.ts.map +1 -0
  32. package/build/src/background-jobs/owner-liveness.js +78 -0
  33. package/build/src/background-jobs/pooled-runner-child.js +6 -2
  34. package/build/src/background-jobs/status-reporter.d.ts +6 -2
  35. package/build/src/background-jobs/status-reporter.d.ts.map +1 -1
  36. package/build/src/background-jobs/status-reporter.js +8 -5
  37. package/build/src/background-jobs/store.d.ts +5 -3
  38. package/build/src/background-jobs/store.d.ts.map +1 -1
  39. package/build/src/background-jobs/store.js +15 -6
  40. package/build/src/background-jobs/types.d.ts +7 -1
  41. package/build/src/background-jobs/types.d.ts.map +1 -1
  42. package/build/src/background-jobs/types.js +3 -2
  43. package/build/src/background-jobs/worker.d.ts +2 -1
  44. package/build/src/background-jobs/worker.d.ts.map +1 -1
  45. package/build/src/background-jobs/worker.js +6 -4
  46. package/build/src/database/migrations-ledger.d.ts +34 -1
  47. package/build/src/database/migrations-ledger.d.ts.map +1 -1
  48. package/build/src/database/migrations-ledger.js +82 -9
  49. package/package.json +1 -1
  50. package/src/background-jobs/adapter.js +1 -1
  51. package/src/background-jobs/client.js +1 -1
  52. package/src/background-jobs/job-record.js +23 -0
  53. package/src/background-jobs/local-adapter.js +1 -1
  54. package/src/background-jobs/local-store.js +10 -5
  55. package/src/background-jobs/main.js +6 -1
  56. package/src/background-jobs/owner-liveness.js +92 -0
  57. package/src/background-jobs/pooled-runner-child.js +5 -1
  58. package/src/background-jobs/status-reporter.js +7 -4
  59. package/src/background-jobs/store.js +13 -5
  60. package/src/background-jobs/types.js +2 -1
  61. package/src/background-jobs/worker.js +4 -2
  62. package/src/database/migrations-ledger.js +96 -8
@@ -20,13 +20,46 @@ export default class MigrationsLedger {
20
20
  * @returns {Promise<boolean>} - Whether the ledger table exists.
21
21
  */
22
22
  static tableExists(db: import("./drivers/base.js").default): Promise<boolean>;
23
+ /**
24
+ * Whether the error means a concurrent session already created the ledger table.
25
+ *
26
+ * Several processes bootstrap the same database at the same time (for example every
27
+ * CI test shard), and the MSSQL `IF NOT EXISTS (...) BEGIN CREATE TABLE ... END` guard
28
+ * is NOT atomic: two sessions can both evaluate the check as false and then both
29
+ * create, so the loser is rejected. Walks the wrapped-error cause chain because
30
+ * `db.query` re-wraps the driver's `RequestError` in a plain `Error`, and matches the
31
+ * stable numeric error number instead of the localized message text.
32
+ * @param {unknown} error - Error thrown while creating the ledger table.
33
+ * @returns {boolean} - True if the table was created by a concurrent session.
34
+ */
35
+ static isConcurrentCreationError(error: unknown): boolean;
36
+ /**
37
+ * Whether the error is a transient deadlock that should be retried.
38
+ *
39
+ * Concurrent boots also deadlock on the `INFORMATION_SCHEMA` metadata scan that
40
+ * `getTableByName` performs on SQL Server; the victim transaction rolled back cleanly,
41
+ * so retrying it is safe. Matched by numeric error number, never by message text.
42
+ * @param {unknown} error - Error thrown while inspecting or creating the ledger table.
43
+ * @returns {boolean} - True if the error is a retryable deadlock.
44
+ */
45
+ static isDeadlockError(error: unknown): boolean;
23
46
  /**
24
47
  * Creates the ledger table if it does not exist. This is the single definition of
25
48
  * the `schema_migrations` table shape.
49
+ *
50
+ * Safe under concurrency: a session that loses the create race (or deadlocks on the
51
+ * catalog read) converges on the existing table instead of failing its caller. That
52
+ * matters because this runs on every boot of a database configured for migrations
53
+ * (see `Migrator#createMigrationsTableForDatabase` and
54
+ * `migrateRequireContextForDatabase`), so a crash here takes down a whole test shard
55
+ * or deployment over a table that provably already exists.
26
56
  * @param {import("./drivers/base.js").default} db - Database that should contain the ledger table.
57
+ * @param {{maxAttempts?: number}} [args] - Retry budget for transient catalog deadlocks.
27
58
  * @returns {Promise<void>}
28
59
  */
29
- static ensureTable(db: import("./drivers/base.js").default): Promise<void>;
60
+ static ensureTable(db: import("./drivers/base.js").default, { maxAttempts }?: {
61
+ maxAttempts?: number;
62
+ }): Promise<void>;
30
63
  /**
31
64
  * Every applied migration version recorded in the ledger.
32
65
  * @param {import("./drivers/base.js").default} db - Database whose applied versions are loaded.
@@ -1 +1 @@
1
- {"version":3,"file":"migrations-ledger.d.ts","sourceRoot":"","sources":["../../../src/database/migrations-ledger.js"],"names":[],"mappings":"AAOA;;;;;;;;;GASG;AACH,MAAM,CAAC,OAAO,OAAO,gBAAgB;IACnC;;;OAGG;IACH,MAAM,CAAC,SAAS,IAFH,MAAM,CAIlB;IAED;;;;OAIG;IACH,OAAa,WAAW,CAAC,EAAE,EAHhB,OAAO,mBAAmB,EAAE,OAGZ,GAFd,OAAO,CAAC,OAAO,CAAC,CAM5B;IAED;;;;;OAKG;IACH,OAAa,WAAW,CAAC,EAAE,EAHhB,OAAO,mBAAmB,EAAE,OAGZ,GAFd,OAAO,CAAC,IAAI,CAAC,CAczB;IAED;;;;OAIG;IACH,OAAa,eAAe,CAAC,EAAE,EAHpB,OAAO,mBAAmB,EAAE,OAGR,GAFlB,OAAO,CAAC,MAAM,EAAE,CAAC,CAM7B;IAED;;;;;OAKG;IACH,OAAa,UAAU,CAAC,EAAE,EAJf,OAAO,mBAAmB,EAAE,OAIb,EAAE,OAAO,EAHxB,MAGwB,GAFtB,OAAO,CAAC,OAAO,CAAC,CAS5B;IAED;;;;;;OAMG;IACH,OAAa,aAAa,CAAC,EAAE,EAJlB,OAAO,mBAAmB,EAAE,OAIV,EAAE,OAAO,EAH3B,MAG2B,GAFzB,OAAO,CAAC,IAAI,CAAC,CAMzB;IAED;;;;;OAKG;IACH,OAAa,aAAa,CAAC,EAAE,EAJlB,OAAO,mBAAmB,EAAE,OAIV,EAAE,OAAO,EAH3B,MAG2B,GAFzB,OAAO,CAAC,IAAI,CAAC,CAIzB;IAED;;;;;;;OAOG;IACH,OAAa,WAAW,CAAC,EAAE,EAJhB,OAAO,mBAAmB,EAAE,OAIZ,EAAE,QAAQ,EAH1B,MAAM,EAGoB,GAFxB,OAAO,CAAC,MAAM,EAAE,CAAC,CAmB7B;IAED;;;;;;;OAOG;IACH,OAAa,oBAAoB,CAAC,EAAC,QAAQ,EAAE,QAAQ,EAAC,EAH3C;QAAC,QAAQ,EAAE,OAAO,mBAAmB,EAAE,OAAO,CAAC;QAAC,QAAQ,EAAE,OAAO,mBAAmB,EAAE,OAAO,CAAA;KAGlD,GAFzC,OAAO,CAAC,MAAM,EAAE,CAAC,CAM7B;CACF"}
1
+ {"version":3,"file":"migrations-ledger.d.ts","sourceRoot":"","sources":["../../../src/database/migrations-ledger.js"],"names":[],"mappings":"AAYA;;;;;;;;;GASG;AACH,MAAM,CAAC,OAAO,OAAO,gBAAgB;IACnC;;;OAGG;IACH,MAAM,CAAC,SAAS,IAFH,MAAM,CAIlB;IAED;;;;OAIG;IACH,OAAa,WAAW,CAAC,EAAE,EAHhB,OAAO,mBAAmB,EAAE,OAGZ,GAFd,OAAO,CAAC,OAAO,CAAC,CAM5B;IAED;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,yBAAyB,CAAC,KAAK,EAH3B,OAG2B,GAFzB,OAAO,CAcnB;IAED;;;;;;;;OAQG;IACH,MAAM,CAAC,eAAe,CAAC,KAAK,EAHjB,OAGiB,GAFf,OAAO,CAcnB;IAED;;;;;;;;;;;;;OAaG;IACH,OAAa,WAAW,CAAC,EAAE,EAJhB,OAAO,mBAAmB,EAAE,OAIZ,EAAE,EAAC,WAAe,EAAC,AAH3C,CACA,EADQ;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAGmB,GAFtC,OAAO,CAAC,IAAI,CAAC,CAwCzB;IAED;;;;OAIG;IACH,OAAa,eAAe,CAAC,EAAE,EAHpB,OAAO,mBAAmB,EAAE,OAGR,GAFlB,OAAO,CAAC,MAAM,EAAE,CAAC,CAM7B;IAED;;;;;OAKG;IACH,OAAa,UAAU,CAAC,EAAE,EAJf,OAAO,mBAAmB,EAAE,OAIb,EAAE,OAAO,EAHxB,MAGwB,GAFtB,OAAO,CAAC,OAAO,CAAC,CAS5B;IAED;;;;;;OAMG;IACH,OAAa,aAAa,CAAC,EAAE,EAJlB,OAAO,mBAAmB,EAAE,OAIV,EAAE,OAAO,EAH3B,MAG2B,GAFzB,OAAO,CAAC,IAAI,CAAC,CAMzB;IAED;;;;;OAKG;IACH,OAAa,aAAa,CAAC,EAAE,EAJlB,OAAO,mBAAmB,EAAE,OAIV,EAAE,OAAO,EAH3B,MAG2B,GAFzB,OAAO,CAAC,IAAI,CAAC,CAIzB;IAED;;;;;;;OAOG;IACH,OAAa,WAAW,CAAC,EAAE,EAJhB,OAAO,mBAAmB,EAAE,OAIZ,EAAE,QAAQ,EAH1B,MAAM,EAGoB,GAFxB,OAAO,CAAC,MAAM,EAAE,CAAC,CAmB7B;IAED;;;;;;;OAOG;IACH,OAAa,oBAAoB,CAAC,EAAC,QAAQ,EAAE,QAAQ,EAAC,EAH3C;QAAC,QAAQ,EAAE,OAAO,mBAAmB,EAAE,OAAO,CAAC;QAAC,QAAQ,EAAE,OAAO,mBAAmB,EAAE,OAAO,CAAA;KAGlD,GAFzC,OAAO,CAAC,MAAM,EAAE,CAAC,CAM7B;CACF"}
@@ -2,6 +2,10 @@
2
2
  import { digg } from "diggerize";
3
3
  import TableData from "./table-data/index.js";
4
4
  const TABLE_NAME = "schema_migrations";
5
+ /** SQL Server "There is already an object named ..." — raised when two sessions race to create the same table. */
6
+ const MSSQL_OBJECT_ALREADY_EXISTS_ERROR_NUMBER = 2714;
7
+ /** SQL Server deadlock victim — raised when concurrent sessions deadlock on the same catalog reads. */
8
+ const MSSQL_DEADLOCK_VICTIM_ERROR_NUMBER = 1205;
5
9
  /**
6
10
  * Single owner of the `schema_migrations` ledger shape and the only place that reads
7
11
  * or writes applied migration versions for a database connection. The migrator uses
@@ -29,21 +33,90 @@ export default class MigrationsLedger {
29
33
  const table = await db.getTableByName(TABLE_NAME, { throwError: false });
30
34
  return Boolean(table);
31
35
  }
36
+ /**
37
+ * Whether the error means a concurrent session already created the ledger table.
38
+ *
39
+ * Several processes bootstrap the same database at the same time (for example every
40
+ * CI test shard), and the MSSQL `IF NOT EXISTS (...) BEGIN CREATE TABLE ... END` guard
41
+ * is NOT atomic: two sessions can both evaluate the check as false and then both
42
+ * create, so the loser is rejected. Walks the wrapped-error cause chain because
43
+ * `db.query` re-wraps the driver's `RequestError` in a plain `Error`, and matches the
44
+ * stable numeric error number instead of the localized message text.
45
+ * @param {unknown} error - Error thrown while creating the ledger table.
46
+ * @returns {boolean} - True if the table was created by a concurrent session.
47
+ */
48
+ static isConcurrentCreationError(error) {
49
+ let current = error;
50
+ while (current instanceof Error) {
51
+ const errorNumber = /** @type {{number?: unknown}} */ (current).number;
52
+ if (typeof errorNumber === "number" && errorNumber === MSSQL_OBJECT_ALREADY_EXISTS_ERROR_NUMBER)
53
+ return true;
54
+ current = current.cause;
55
+ }
56
+ return false;
57
+ }
58
+ /**
59
+ * Whether the error is a transient deadlock that should be retried.
60
+ *
61
+ * Concurrent boots also deadlock on the `INFORMATION_SCHEMA` metadata scan that
62
+ * `getTableByName` performs on SQL Server; the victim transaction rolled back cleanly,
63
+ * so retrying it is safe. Matched by numeric error number, never by message text.
64
+ * @param {unknown} error - Error thrown while inspecting or creating the ledger table.
65
+ * @returns {boolean} - True if the error is a retryable deadlock.
66
+ */
67
+ static isDeadlockError(error) {
68
+ let current = error;
69
+ while (current instanceof Error) {
70
+ const errorNumber = /** @type {{number?: unknown}} */ (current).number;
71
+ if (typeof errorNumber === "number" && errorNumber === MSSQL_DEADLOCK_VICTIM_ERROR_NUMBER)
72
+ return true;
73
+ current = current.cause;
74
+ }
75
+ return false;
76
+ }
32
77
  /**
33
78
  * Creates the ledger table if it does not exist. This is the single definition of
34
79
  * the `schema_migrations` table shape.
80
+ *
81
+ * Safe under concurrency: a session that loses the create race (or deadlocks on the
82
+ * catalog read) converges on the existing table instead of failing its caller. That
83
+ * matters because this runs on every boot of a database configured for migrations
84
+ * (see `Migrator#createMigrationsTableForDatabase` and
85
+ * `migrateRequireContextForDatabase`), so a crash here takes down a whole test shard
86
+ * or deployment over a table that provably already exists.
35
87
  * @param {import("./drivers/base.js").default} db - Database that should contain the ledger table.
88
+ * @param {{maxAttempts?: number}} [args] - Retry budget for transient catalog deadlocks.
36
89
  * @returns {Promise<void>}
37
90
  */
38
- static async ensureTable(db) {
39
- if (await MigrationsLedger.tableExists(db))
40
- return;
41
- const tableData = new TableData(TABLE_NAME, { ifNotExists: true });
42
- tableData.string("version", { null: false, primaryKey: true });
43
- for (const sql of await db.createTableSql(tableData)) {
44
- await db.query(sql);
91
+ static async ensureTable(db, { maxAttempts = 3 } = {}) {
92
+ let attempt = 0;
93
+ while (true) {
94
+ attempt++;
95
+ try {
96
+ if (await MigrationsLedger.tableExists(db))
97
+ return;
98
+ const tableData = new TableData(TABLE_NAME, { ifNotExists: true });
99
+ tableData.string("version", { null: false, primaryKey: true });
100
+ for (const sql of await db.createTableSql(tableData)) {
101
+ await db.query(sql);
102
+ }
103
+ db.clearSchemaCache();
104
+ return;
105
+ }
106
+ catch (error) {
107
+ if (MigrationsLedger.isConcurrentCreationError(error)) {
108
+ // Another session created the ledger between our check and our create. The
109
+ // table exists with exactly this shape, so the goal state is already reached.
110
+ db.clearSchemaCache();
111
+ return;
112
+ }
113
+ if (MigrationsLedger.isDeadlockError(error) && attempt < maxAttempts) {
114
+ db.clearSchemaCache();
115
+ continue;
116
+ }
117
+ throw error;
118
+ }
45
119
  }
46
- db.clearSchemaCache();
47
120
  }
48
121
  /**
49
122
  * Every applied migration version recorded in the ledger.
@@ -123,4 +196,4 @@ export default class MigrationsLedger {
123
196
  return await MigrationsLedger.markApplied(targetDb, sourceVersions);
124
197
  }
125
198
  }
126
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibWlncmF0aW9ucy1sZWRnZXIuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvZGF0YWJhc2UvbWlncmF0aW9ucy1sZWRnZXIuanMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsWUFBWTtBQUVaLE9BQU8sRUFBQyxJQUFJLEVBQUMsTUFBTSxXQUFXLENBQUE7QUFDOUIsT0FBTyxTQUFTLE1BQU0sdUJBQXVCLENBQUE7QUFFN0MsTUFBTSxVQUFVLEdBQUcsbUJBQW1CLENBQUE7QUFFdEM7Ozs7Ozs7OztHQVNHO0FBQ0gsTUFBTSxDQUFDLE9BQU8sT0FBTyxnQkFBZ0I7SUFDbkM7OztPQUdHO0lBQ0gsTUFBTSxDQUFDLFNBQVM7UUFDZCxPQUFPLFVBQVUsQ0FBQTtJQUNuQixDQUFDO0lBRUQ7Ozs7T0FJRztJQUNILE1BQU0sQ0FBQyxLQUFLLENBQUMsV0FBVyxDQUFDLEVBQUU7UUFDekIsTUFBTSxLQUFLLEdBQUcsTUFBTSxFQUFFLENBQUMsY0FBYyxDQUFDLFVBQVUsRUFBRSxFQUFDLFVBQVUsRUFBRSxLQUFLLEVBQUMsQ0FBQyxDQUFBO1FBRXRFLE9BQU8sT0FBTyxDQUFDLEtBQUssQ0FBQyxDQUFBO0lBQ3ZCLENBQUM7SUFFRDs7Ozs7T0FLRztJQUNILE1BQU0sQ0FBQyxLQUFLLENBQUMsV0FBVyxDQUFDLEVBQUU7UUFDekIsSUFBSSxNQUFNLGdCQUFnQixDQUFDLFdBQVcsQ0FBQyxFQUFFLENBQUM7WUFBRSxPQUFNO1FBRWxELE1BQU0sU0FBUyxHQUFHLElBQUksU0FBUyxDQUFDLFVBQVUsRUFBRSxFQUFDLFdBQVcsRUFBRSxJQUFJLEVBQUMsQ0FBQyxDQUFBO1FBRWhFLFNBQVMsQ0FBQyxNQUFNLENBQUMsU0FBUyxFQUFFLEVBQUMsSUFBSSxFQUFFLEtBQUssRUFBRSxVQUFVLEVBQUUsSUFBSSxFQUFDLENBQUMsQ0FBQTtRQUU1RCxLQUFLLE1BQU0sR0FBRyxJQUFJLE1BQU0sRUFBRSxDQUFDLGNBQWMsQ0FBQyxTQUFT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QUMsQ0FBQTtJQUNyRSxDQUFDO0NBQ0YiLCJzb3VyY2VzQ29udGVudCI6WyIvLyBAdHMtY2hlY2tcblxuaW1wb3J0IHtkaWdnfSBmcm9tIFwiZGlnZ2VyaXplXCJcbmltcG9ydCBUYWJsZURhdGEgZnJvbSBcIi4vdGFibGUtZGF0YS9pbmRleC5qc1wiXG5cbmNvbnN0IFRBQkxFX05BTUUgPSBcInNjaGVtYV9taWdyYXRpb25zXCJcblxuLyoqXG4gKiBTaW5nbGUgb3duZXIgb2YgdGhlIGBzY2hlbWFfbWlncmF0aW9uc2AgbGVkZ2VyIHNoYXBlIGFuZCB0aGUgb25seSBwbGFjZSB0aGF0IHJlYWRzXG4gKiBvciB3cml0ZXMgYXBwbGllZCBtaWdyYXRpb24gdmVyc2lvbnMgZm9yIGEgZGF0YWJhc2UgY29ubmVjdGlvbi4gVGhlIG1pZ3JhdG9yIHVzZXNcbiAqIGl0IHRvIHJlY29yZCB2ZXJzaW9ucyBhcyBpdCBydW5zIG1pZ3JhdGlvbnM7IHByb3Zpc2lvbmluZyAvIHNjaGVtYS1jbG9uZSBwYXRocyB1c2VcbiAqIGBtYXJrQXBwbGllZGAgLyBgYmFzZWxpbmVGcm9tRGF0YWJhc2VgIHRvIHJlY29yZCB2ZXJzaW9ucyBhcyBhcHBsaWVkIFdJVEhPVVRcbiAqIHJlLXJ1bm5pbmcgdGhlbSAodGhlIFJhaWxzIGBzY2hlbWE6bG9hZGAgLyBGbHl3YXkgYGJhc2VsaW5lYCBpZGVhKS4gVGhhdCBrZWVwcyB0aGVcbiAqIGxlZGdlciBob25lc3Qgd2hlbiBhIGRhdGFiYXNlJ3Mgc2NoZW1hIHdhcyBhZHZhbmNlZCBvdXQgb2YgYmFuZCDigJQgZS5nLiBieSBjbG9uaW5nXG4gKiB0YWJsZSBzdHJ1Y3R1cmUgYmV0d2VlbiBkYXRhYmFzZXMg4oCUIHNvIHRoZSBtaWdyYXRvciBkb2VzIG5vdCBsYXRlciByZS1ydW4gYVxuICogbWlncmF0aW9uIHdob3NlIHNjaGVtYSBvYmplY3QgYWxyZWFkeSBleGlzdHMuXG4gKi9cbmV4cG9ydCBkZWZhdWx0IGNsYXNzIE1pZ3JhdGlvbnNMZWRnZXIge1xuICAvKipcbiAgICogVGhlIGxlZGdlciB0YWJsZSBuYW1lLlxuICAgKiBAcmV0dXJucyB7c3RyaW5nfSAtIExlZGdlciB0YWJsZSBuYW1lLlxuICAgKi9cbiAgc3RhdGljIHRhYmxlTmFtZSgpIHtcbiAgICByZXR1cm4gVEFCTEVfTkFNRVxuICB9XG5cbiAgLyoqXG4gICAqIFdoZXRoZXIgdGhlIGxlZGdlciB0YWJsZSBleGlzdHMgb24gdGhlIGdpdmVuIGRhdGFiYXNlLlxuICAgKiBAcGFyYW0ge2ltcG9ydChcIi4vZHJpdmVycy9iYXNlLmpzXCIpLmRlZmF1bHR9IGRiIC0gRGF0YWJhc2Ugd2hvc2UgbWlncmF0aW9uIGxlZGdlciBpcyBpbnNwZWN0ZWQuXG4gICAqIEByZXR1cm5zIHtQcm9taXNlPGJvb2xlYW4+fSAtIFdoZXRoZXIgdGhlIGxlZGdlciB0YWJsZSBleGlzdHMuXG4gICAqL1xuICBzdGF0aWMgYXN5bmMgdGFibGVFeGlzdHMoZGIpIHtcbiAgICBjb25zdCB0YWJsZSA9IGF3YWl0IGRiLmdldFRhYmxlQnlOYW1lKFRBQkxFX05BTUUsIHt0aHJvd0Vycm9yOiBmYWxzZX0pXG5cbiAgICByZXR1cm4gQm9vbGVhbih0YWJsZSlcbiAgfVxuXG4gIC8qKlxuICAgKiBDcmVhdGVzIHRoZSBsZWRnZXIgdGFibGUgaWYgaXQgZG9lcyBub3QgZXhpc3QuIFRoaXMgaXMgdGhlIHNpbmdsZSBkZWZpbml0aW9uIG9mXG4gICAqIHRoZSBgc2NoZW1hX21pZ3JhdGlvbnNgIHRhYmxlIHNoYXBlLlxuICAgKiBAcGFyYW0ge2ltcG9ydChcIi4vZHJpdmVycy9iYXNlLmpzXCIpLmRlZmF1bHR9IGRiIC0gRGF0YWJhc2UgdGhhdCBzaG91bGQgY29udGFpbiB0aGUgbGVkZ2VyIHRhYmxlLlxuICAgKiBAcmV0dXJucyB7UHJvbWlzZTx2b2lkPn1cbiAgICovXG4gIHN0YXRpYyBhc3luYyBlbnN1cmVUYWJsZShkYikge1xuICAgIGlmIChhd2FpdCBNaWdyYXRpb25zTGVkZ2VyLnRhYmxlRXhpc3RzKGRiKSkgcmV0dXJuXG5cbiAgICBjb25zdCB0YWJsZURhdGEgPSBuZXcgVGFibGVEYXRhKFRBQkxFX05BTUUsIHtpZk5vdEV4aXN0czogdHJ1ZX0pXG5cbiAgICB0YWJsZURhdGEuc3RyaW5nKFwidmVyc2lvblwiLCB7bnVsbDogZmFsc2UsIHByaW1hcnlLZXk6IHRydWV9KVxuXG4gICAgZm9yIChjb25zdCBzcWwgb2YgYXdhaXQgZGIuY3JlYXRlVGFibGVTcWwodGFibGVEYXRhKSkge1xuICAgICAgYXdhaXQgZGIucXVlcnkoc3FsKVxuICAgIH1cblxuICAgIGRiLmNsZWFyU2NoZW1hQ2FjaGUoKVxuICB9XG5cbiAgLyoqXG4gICAqIEV2ZXJ5IGFwcGxpZWQgbWlncmF0aW9uIHZlcnNpb24gcmVjb3JkZWQgaW4gdGhlIGxlZGdlci5cbiAgICogQHBhcmFtIHtpbXBvcnQoXCIuL2RyaXZlcnMvYmFzZS5qc1wiKS5kZWZhdWx0fSBkYiAtIERhdGFiYXNlIHdob3NlIGFwcGxpZWQgdmVyc2lvbnMgYXJlIGxvYWRlZC5cbiAgICogQHJldHVybnMge1Byb21pc2U8c3RyaW5nW10+fSAtIEFwcGxpZWQgbWlncmF0aW9uIHZlcnNpb25zLlxuICAgKi9cbiAgc3RhdGljIGFzeW5jIGFwcGxpZWRWZXJzaW9ucyhkYikge1xuICAgIGNvbnN0IHJvd3MgPSBhd2FpdCBkYi5zZWxlY3QoVEFCTEVfTkFNRSlcblxuICAgIHJldHVybiByb3dzLm1hcCgocm93KSA9PiBgJHtkaWdnKHJvdywgXCJ2ZXJzaW9uXCIpfWApXG4gIH1cblxuICAvKipcbiAgICogV2hldGhlciB0aGUgZ2l2ZW4gdmVyc2lvbiBpcyByZWNvcmRlZCBhcyBhcHBsaWVkLlxuICAgKiBAcGFyYW0ge2ltcG9ydChcIi4vZHJpdmVycy9iYXNlLmpzXCIpLmRlZmF1bHR9IGRiIC0gRGF0YWJhc2Ugd2hvc2UgbGVkZ2VyIGlzIHF1ZXJpZWQuXG4gICAqIEBwYXJhbSB7c3RyaW5nfSB2ZXJzaW9uIC0gTWlncmF0aW9uIHZlcnNpb24gdG8gbG9vayB1cC5cbiAgICogQHJldHVybnMge1Byb21pc2U8Ym9vbGVhbj59IC0gV2hldGhlciB0aGUgbWlncmF0aW9uIHZlcnNpb24gaXMgYXBwbGllZC5cbiAgICovXG4gIHN0YXRpYyBhc3luYyBoYXNWZXJzaW9uKGRiLCB2ZXJzaW9uKSB7XG4gICAgY29uc3Qgcm93cyA9IGF3YWl0IGRiLm5ld1F1ZXJ5KClcbiAgICAgIC5mcm9tKFRBQkxFX05BTUUpXG4gICAgICAud2hlcmUoe3ZlcnNpb259KVxuICAgICAgLnJlc3VsdHMoKVxuXG4gICAgcmV0dXJuIHJvd3MubGVuZ3RoID4gMFxuICB9XG5cbiAgLyoqXG4gICAqIFJlY29yZHMgYSBzaW5nbGUgdmVyc2lvbiBhcyBhcHBsaWVkLiBUaGUgdGFyZ2V0ZWQgZXhpc3RlbmNlIGNoZWNrIGtlZXBzIHRoZVxuICAgKiBtaWdyYXRvcidzIHBlci1taWdyYXRpb24gaG90IHBhdGggY2hlYXAgKG5vIGZ1bGwtdGFibGUgbG9hZCBwZXIgbWlncmF0aW9uKS5cbiAgICogQHBhcmFtIHtpbXBvcnQoXCIuL2RyaXZlcnMvYmFzZS5qc1wiKS5kZWZhdWx0fSBkYiAtIERhdGFiYXNlIHdob3NlIGxlZGdlciByZWNlaXZlcyB0aGUgdmVyc2lvbi5cbiAgICogQHBhcmFtIHtzdHJpbmd9IHZlcnNpb24gLSBNaWdyYXRpb24gdmVyc2lvbiB0byByZWNvcmQgYXMgYXBwbGllZC5cbiAgICogQHJldHVybnMge1Byb21pc2U8dm9pZD59XG4gICAqL1xuICBzdGF0aWMgYXN5bmMgcmVjb3JkVmVyc2lvbihkYiwgdmVyc2lvbikge1xuICAgIGlmIChhd2FpdCBNaWdyYXRpb25zTGVkZ2VyLmhhc1ZlcnNpb24oZGIsIHZlcnNpb24pKSByZXR1cm5cblxuICAgIGF3YWl0IGRiLmluc2VydCh7dGFibGVOYW1lOiBUQUJMRV9OQU1FLCBkYXRhOiB7dmVyc2lvbn19KVxuICB9XG5cbiAgLyoqXG4gICAqIFJlbW92ZXMgYSB2ZXJzaW9uIGZyb20gdGhlIGxlZGdlciAodXNlZCB3aGVuIG1pZ3JhdGluZyBkb3duKS5cbiAgICogQHBhcmFtIHtpbXBvcnQoXCIuL2RyaXZlcnMvYmFzZS5qc1wiKS5kZWZhdWx0fSBkYiAtIERhdGFiYXNlIHdob3NlIGxlZGdlciBsb3NlcyB0aGUgdmVyc2lvbi5cbiAgICogQHBhcmFtIHtzdHJpbmd9IHZlcnNpb24gLSBNaWdyYXRpb24gdmVyc2lvbiB0byBtYXJrIGFzIHVuYXBwbGllZC5cbiAgICogQHJldHVybnMge1Byb21pc2U8dm9pZD59XG4gICAqL1xuICBzdGF0aWMgYXN5bmMgcmVtb3ZlVmVyc2lvbihkYiwgdmVyc2lvbikge1xuICAgIGF3YWl0IGRiLmRlbGV0ZSh7dGFibGVOYW1lOiBUQUJMRV9OQU1FLCBjb25kaXRpb25zOiB7dmVyc2lvbn19KVxuICB9XG5cbiAgLyoqXG4gICAqIEJhc2VsaW5lcyBhIGRhdGFiYXNlJ3MgbGVkZ2VyOiByZWNvcmRzIGVhY2ggdmVyc2lvbiBhcyBhcHBsaWVkIHdpdGhvdXQgcnVubmluZ1xuICAgKiBpdHMgbWlncmF0aW9uLiBJZGVtcG90ZW50IOKAlCBhbHJlYWR5LXJlY29yZGVkIHZlcnNpb25zIGFyZSBza2lwcGVkLiBFbnN1cmVzIHRoZVxuICAgKiBsZWRnZXIgdGFibGUgZXhpc3RzIGZpcnN0LCB0aGVuIGxvYWRzIHRoZSBleGlzdGluZyBzZXQgb25jZSBmb3IgdGhlIHdob2xlIGJhdGNoLlxuICAgKiBAcGFyYW0ge2ltcG9ydChcIi4vZHJpdmVycy9iYXNlLmpzXCIpLmRlZmF1bHR9IGRiIC0gRGF0YWJhc2Ugd2hvc2UgbGVkZ2VyIHNob3VsZCBiZSBiYXNlbGluZWQuXG4gICAqIEBwYXJhbSB7c3RyaW5nW119IHZlcnNpb25zIC0gTWlncmF0aW9uIHZlcnNpb25zIHRvIHJlY29yZCB3aXRob3V0IHJ1bm5pbmcgdGhlbS5cbiAgICogQHJldHVybnMge1Byb21pc2U8c3RyaW5nW10+fSBUaGUgdmVyc2lvbnMgdGhhdCB3ZXJlIG5ld2x5IHJlY29yZGVkLlxuICAgKi9cbiAgc3RhdGljIGFzeW5jIG1hcmtBcHBsaWVkKGRiLCB2ZXJzaW9ucykge1xuICAgIGF3YWl0IE1pZ3JhdGlvbnNMZWRnZXIuZW5zdXJlVGFibGUoZGIpXG5cbiAgICBjb25zdCBleGlzdGluZyA9IG5ldyBTZXQoYXdhaXQgTWlncmF0aW9uc0xlZGdlci5hcHBsaWVkVmVyc2lvbnMoZGIpKVxuICAgIGNvbnN0IHJlY29yZGVkID0gW11cblxuICAgIGZvciAoY29uc3QgdmVyc2lvbiBvZiB2ZXJzaW9ucykge1xuICAgICAgY29uc3Qgbm9ybWFsaXplZFZlcnNpb24gPSBgJHt2ZXJzaW9ufWBcblxuICAgICAgaWYgKGV4aXN0aW5nLmhhcyhub3JtYWxpemVkVmVyc2lvbikpIGNvbnRpbnVlXG5cbiAgICAgIGF3YWl0IGRiLmluc2VydCh7dGFibGVOYW1lOiBUQUJMRV9OQU1FLCBkYXRhOiB7dmVyc2lvbjogbm9ybWFsaXplZFZlcnNpb259fSlcbiAgICAgIGV4aXN0aW5nLmFkZChub3JtYWxpemVkVmVyc2lvbilcbiAgICAgIHJlY29yZGVkLnB1c2gobm9ybWFsaXplZFZlcnNpb24pXG4gICAgfVxuXG4gICAgcmV0dXJuIHJlY29yZGVkXG4gIH1cblxuICAvKipcbiAgICogQmFzZWxpbmVzIGB0YXJnZXREYmAgdG8gbWF0Y2ggdGhlIGFwcGxpZWQgdmVyc2lvbnMgb2YgYHNvdXJjZURiYC4gVXNlIHdoZW4gYVxuICAgKiBwcm92aXNpb25pbmcgcGF0aCBhZHZhbmNlZCBgdGFyZ2V0RGJgJ3Mgc2NoZW1hIHRvIG1hdGNoIGBzb3VyY2VEYmAgb3V0IG9mIGJhbmRcbiAgICogKGUuZy4gY2xvbmluZyB0YWJsZSBzdHJ1Y3R1cmUgYmV0d2VlbiBkYXRhYmFzZXMpOiB0aGUgbWlncmF0aW9ucyBhcmUsIGJ5XG4gICAqIGNvbnN0cnVjdGlvbiwgYWxyZWFkeSBhcHBsaWVkIG9uIHRoZSB0YXJnZXQsIHNvIHJlY29yZCB0aGVtIHdpdGhvdXQgcmUtcnVubmluZy5cbiAgICogQHBhcmFtIHt7c291cmNlRGI6IGltcG9ydChcIi4vZHJpdmVycy9iYXNlLmpzXCIpLmRlZmF1bHQsIHRhcmdldERiOiBpbXBvcnQoXCIuL2RyaXZlcnMvYmFzZS5qc1wiKS5kZWZhdWx0fX0gYXJncyAtIFNvdXJjZSBsZWRnZXIgYW5kIHRhcmdldCBkYXRhYmFzZSB0byBiYXNlbGluZS5cbiAgICogQHJldHVybnMge1Byb21pc2U8c3RyaW5nW10+fSBUaGUgdmVyc2lvbnMgdGhhdCB3ZXJlIG5ld2x5IHJlY29yZGVkIG9uIHRoZSB0YXJnZXQuXG4gICAqL1xuICBzdGF0aWMgYXN5bmMgYmFzZWxpbmVGcm9tRGF0YWJhc2Uoe3NvdXJjZURiLCB0YXJnZXREYn0pIHtcbiAgICBjb25zdCBzb3VyY2VWZXJzaW9ucyA9IGF3YWl0IE1pZ3JhdGlvbnNMZWRnZXIuYXBwbGllZFZlcnNpb25zKHNvdXJjZURiKVxuXG4gICAgcmV0dXJuIGF3YWl0IE1pZ3JhdGlvbnNMZWRnZXIubWFya0FwcGxpZWQodGFyZ2V0RGIsIHNvdXJjZVZlcnNpb25zKVxuICB9XG59XG4iXX0=
199
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibWlncmF0aW9ucy1sZWRnZXIuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvZGF0YWJhc2UvbWlncmF0aW9ucy1sZWRnZXIuanMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsWUFBWTtBQUVaLE9BQU8sRUFBQyxJQUFJLEVBQUMsTUFBTSxXQUFXLENBQUE7QUFDOUIsT0FBTyxTQUFTLE1BQU0sdUJBQXVCLENBQUE7QUFFN0MsTUFBTSxVQUFVLEdBQUcsbUJBQW1CLENBQUE7QUFDdEMsa0hBQWtIO0FBQ2xILE1BQU0sd0NBQXdDLEdBQUcsSUFBSSxDQUFBO0FBQ3JELHVHQUF1RztBQUN2RyxNQUFNLGtDQUFrQyxHQUFHLElBQUksQ0FBQTtBQUcvQzs7Ozs7Ozs7O0dBU0c7QUFDSCxNQUFNLENBQUMsT0FBTyxPQUFPLGdCQUFnQjtJQUNuQzs7O09BR0c7SUFDSCxNQUFNLENBQUMsU0FBUztRQUNkLE9BQU8sVUFBVSxDQUFBO0lBQ25CLENBQUM7SUFFRDs7OztPQUlHO0lBQ0gsTUFBTSxDQUFDLEtBQUssQ0FBQyxXQUFXLENBQUMsRUFBRTtRQUN6QixNQUFNLEtBQUssR0FBRyxNQUFNLEVBQUUsQ0FBQyxjQUFjLENBQUMsVUFBVSxFQUFFLEVBQUMsVUFBVSxFQUFFLEtBQUssRUFBQyxDQUFDLENBQUE7UUFFdEUsT0FBTyxPQUFPLENBQUMsS0FBSyxDQUFDLENBQUE7SUFDdkIsQ0FBQztJQUVEOzs7Ozs7Ozs7OztPQVdHO0lBQ0gsTUFBTSxDQUFDLHlCQUF5QixDQUFDLEtBQUs7UUFDcEMsSUFBSSxPQUFPLEdBQUcsS0FBSyxDQUFBO1FBRW5CLE9BQU8sT0FBTyxZQUFZLEtBQUssRUFBRSxDQUFDO1lBQ2hDLE1BQU0sV0FBVyxHQUFHLGlDQUFpQyxDQUFDLENBQUMsT0FBTyxDQUFDLENBQUMsTUFBTSxDQUFBO1lBRXRFLElBQUksT0FBTyxXQUFXLEtBQUssUUFBUSxJQUFJLFdBQVcsS0FBSyx3Q0FBd0M7Z0JBQUUsT0FBTyxJ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RUFBRSxpQkFBaUIsRUFBQyxFQUFDLENBQUMsQ0FBQTtZQUM1RSxRQUFRLENBQUMsR0FBRyxDQUFDLGlCQUFpQixDQUFDLENBQUE7WUFDL0IsUUFBUSxDQUFDLElBQUksQ0FBQyxpQkFBaUIsQ0FBQyxDQUFBO1FBQ2xDLENBQUM7UUFFRCxPQUFPLFFBQVEsQ0FBQTtJQUNqQixDQUFDO0lBRUQ7Ozs7Ozs7T0FPRztJQUNILE1BQU0sQ0FBQyxLQUFLLENBQUMsb0JBQW9CLENBQUMsRUFBQyxRQUFRLEVBQUUsUUFBUSxFQUFDO1FBQ3BELE1BQU0sY0FBYyxHQUFHLE1BQU0sZ0JBQWdCLENBQUMsZUFBZSxDQUFDLFFBQVEsQ0FBQyxDQUFBO1FBRXZFLE9BQU8sTUFBTSxnQkFBZ0IsQ0FBQyxXQUFXLENBQUMsUUFBUSxFQUFFLGNBQWMsQ0FBQyxDQUFBO0lBQ3JFLENBQUM7Q0FDRiIsInNvdXJjZXNDb250ZW50IjpbIi8vIEB0cy1jaGVja1xuXG5pbXBvcnQge2RpZ2d9IGZyb20gXCJkaWdnZXJpemVcIlxuaW1wb3J0IFRhYmxlRGF0YSBmcm9tIFwiLi90YWJsZS1kYXRhL2luZGV4LmpzXCJcblxuY29uc3QgVEFCTEVfTkFNRSA9IFwic2NoZW1hX21pZ3JhdGlvbnNcIlxuLyoqIFNRTCBTZXJ2ZXIgXCJUaGVyZSBpcyBhbHJlYWR5IGFuIG9iamVjdCBuYW1lZCAuLi5cIiDigJQgcmFpc2VkIHdoZW4gdHdvIHNlc3Npb25zIHJhY2UgdG8gY3JlYXRlIHRoZSBzYW1lIHRhYmxlLiAqL1xuY29uc3QgTVNTUUxfT0JKRUNUX0FMUkVBRFlfRVhJU1RTX0VSUk9SX05VTUJFUiA9IDI3MTRcbi8qKiBTUUwgU2VydmVyIGRlYWRsb2NrIHZpY3RpbSDigJQgcmFpc2VkIHdoZW4gY29uY3VycmVudCBzZXNzaW9ucyBkZWFkbG9jayBvbiB0aGUgc2FtZSBjYXRhbG9nIHJlYWRzLiAqL1xuY29uc3QgTVNTUUxfREVBRExPQ0tfVklDVElNX0VSUk9SX05VTUJFUiA9IDEyMDVcblxuXG4vKipcbiAqIFNpbmdsZSBvd25lciBvZiB0aGUgYHNjaGVtYV9taWdyYXRpb25zYCBsZWRnZXIgc2hhcGUgYW5kIHRoZSBvbmx5IHBsYWNlIHRoYXQgcmVhZHNcbiAqIG9yIHdyaXRlcyBhcHBsaWVkIG1pZ3JhdGlvbiB2ZXJzaW9ucyBmb3IgYSBkYXRhYmFzZSBjb25uZWN0aW9uLiBUaGUgbWlncmF0b3IgdXNlc1xuICogaXQgdG8gcmVjb3JkIHZlcnNpb25zIGFzIGl0IHJ1bnMgbWlncmF0aW9uczsgcHJvdmlzaW9uaW5nIC8gc2NoZW1hLWNsb25lIHBhdGhzIHVzZVxuICogYG1hcmtBcHBsaWVkYCAvIGBiYXNlbGluZUZyb21EYXRhYmFzZWAgdG8gcmVjb3JkIHZlcnNpb25zIGFzIGFwcGxpZWQgV0lUSE9VVFxuICogcmUtcnVubmluZyB0aGVtICh0aGUgUmFpbHMgYHNjaGVtYTpsb2FkYCAvIEZseXdheSBgYmFzZWxpbmVgIGlkZWEpLiBUaGF0IGtlZXBzIHRoZVxuICogbGVkZ2VyIGhvbmVzdCB3aGVuIGEgZGF0YWJhc2UncyBzY2hlbWEgd2FzIGFkdmFuY2VkIG91dCBvZiBiYW5kIOKAlCBlLmcuIGJ5IGNsb25pbmdcbiAqIHRhYmxlIHN0cnVjdHVyZSBiZXR3ZWVuIGRhdGFiYXNlcyDigJQgc28gdGhlIG1pZ3JhdG9yIGRvZXMgbm90IGxhdGVyIHJlLXJ1biBhXG4gKiBtaWdyYXRpb24gd2hvc2Ugc2NoZW1hIG9iamVjdCBhbHJlYWR5IGV4aXN0cy5cbiAqL1xuZXhwb3J0IGRlZmF1bHQgY2xhc3MgTWlncmF0aW9uc0xlZGdlciB7XG4gIC8qKlxuICAgKiBUaGUgbGVkZ2VyIHRhYmxlIG5hbWUuXG4gICAqIEByZXR1cm5zIHtzdHJpbmd9IC0gTGVkZ2VyIHRhYmxlIG5hbWUuXG4gICAqL1xuICBzdGF0aWMgdGFibGVOYW1lKCkge1xuICAgIHJldHVybiBUQUJMRV9OQU1FXG4gIH1cblxuICAvKipcbiAgICogV2hldGhlciB0aGUgbGVkZ2VyIHRhYmxlIGV4aXN0cyBvbiB0aGUgZ2l2ZW4gZGF0YWJhc2UuXG4gICAqIEBwYXJhbSB7aW1wb3J0KFwiLi9kcml2ZXJzL2Jhc2UuanNcIikuZGVmYXVsdH0gZGIgLSBEYXRhYmFzZSB3aG9zZSBtaWdyYXRpb24gbGVkZ2VyIGlzIGluc3BlY3RlZC5cbiAgICogQHJldHVybnMge1Byb21pc2U8Ym9vbGVhbj59IC0gV2hldGhlciB0aGUgbGVkZ2VyIHRhYmxlIGV4aXN0cy5cbiAgICovXG4gIHN0YXRpYyBhc3luYyB0YWJsZUV4aXN0cyhkYikge1xuICAgIGNvbnN0IHRhYmxlID0gYXdhaXQgZGIuZ2V0VGFibGVCeU5hbWUoVEFCTEVfTkFNRSwge3Rocm93RXJyb3I6IGZhbHNlfSlcblxuICAgIHJldHVybiBCb29sZWFuKHRhYmxlKVxuICB9XG5cbiAgLyoqXG4gICAqIFdoZXRoZXIgdGhlIGVycm9yIG1lYW5zIGEgY29uY3VycmVudCBzZXNzaW9uIGFscmVhZHkgY3JlYXRlZCB0aGUgbGVkZ2VyIHRhYmxlLlxuICAgKlxuICAgKiBTZXZlcmFsIHByb2Nlc3NlcyBib290c3RyYXAgdGhlIHNhbWUgZGF0YWJhc2UgYXQgdGhlIHNhbWUgdGltZSAoZm9yIGV4YW1wbGUgZXZlcnlcbiAgICogQ0kgdGVzdCBzaGFyZCksIGFuZCB0aGUgTVNTUUwgYElGIE5PVCBFWElTVFMgKC4uLikgQkVHSU4gQ1JFQVRFIFRBQkxFIC4uLiBFTkRgIGd1YXJkXG4gICAqIGlzIE5PVCBhdG9taWM6IHR3byBzZXNzaW9ucyBjYW4gYm90aCBldmFsdWF0ZSB0aGUgY2hlY2sgYXMgZmFsc2UgYW5kIHRoZW4gYm90aFxuICAgKiBjcmVhdGUsIHNvIHRoZSBsb3NlciBpcyByZWplY3RlZC4gV2Fsa3MgdGhlIHdyYXBwZWQtZXJyb3IgY2F1c2UgY2hhaW4gYmVjYXVzZVxuICAgKiBgZGIucXVlcnlgIHJlLXdyYXBzIHRoZSBkcml2ZXIncyBgUmVxdWVzdEVycm9yYCBpbiBhIHBsYWluIGBFcnJvcmAsIGFuZCBtYXRjaGVzIHRoZVxuICAgKiBzdGFibGUgbnVtZXJpYyBlcnJvciBudW1iZXIgaW5zdGVhZCBvZiB0aGUgbG9jYWxpemVkIG1lc3NhZ2UgdGV4dC5cbiAgICogQHBhcmFtIHt1bmtub3dufSBlcnJvciAtIEVycm9yIHRocm93biB3aGlsZSBjcmVhdGluZyB0aGUgbGVkZ2VyIHRhYmxlLlxuICAgKiBAcmV0dXJucyB7Ym9vbGVhbn0gLSBUcnVlIGlmIHRoZSB0YWJsZSB3YXMgY3JlYXRlZCBieSBhIGNvbmN1cnJlbnQgc2Vzc2lvbi5cbiAgICovXG4gIHN0YXRpYyBpc0NvbmN1cnJlbnRDcmVhdGlvbkVycm9yKGVycm9yKSB7XG4gICAgbGV0IGN1cnJlbnQgPSBlcnJvclxuXG4gICAgd2hpbGUgKGN1cnJlbnQgaW5zdGFuY2VvZiBFcnJvcikge1xuICAgICAgY29uc3QgZXJyb3JOdW1iZXIgPSAvKiogQHR5cGUge3tudW1iZXI/OiB1bmtub3dufX0gKi8gKGN1cnJlbnQpLm51bWJlclxuXG4gICAgICBpZiAodHlwZW9mIGVycm9yTnVtYmVyID09PSBcIm51bWJlclwiICYmIGVycm9yTnVtYmVyID09PSBNU1NRTF9PQkpFQ1RfQUxSRUFEWV9FWElTVFNfRVJST1JfTlVNQkVSKSByZXR1cm4gdHJ1ZVxuXG4gICAgICBjdXJyZW50ID0gY3VycmVudC5jYXVzZVxuICAgIH1cblxuICAgIHJldHVybiBmYWxzZVxuICB9XG5cbiAgLyoqXG4gICAqIFdoZXRoZXIgdGhlIGVycm9yIGlzIGEgdHJhbnNpZW50IGRlYWRsb2NrIHRoYXQgc2hvdWxkIGJlIHJldHJpZWQuXG4gICAqXG4gICAqIENvbmN1cnJlbnQgYm9vdHMgYWxzbyBkZWFkbG9jayBvbiB0aGUgYElORk9STUFUSU9OX1NDSEVNQWAgbWV0YWRhdGEgc2NhbiB0aGF0XG4gICAqIGBnZXRUYWJsZUJ5TmFtZWAgcGVyZm9ybXMgb24gU1FMIFNlcnZlcjsgdGhlIHZpY3RpbSB0cmFuc2FjdGlvbiByb2xsZWQgYmFjayBjbGVhbmx5LFxuICAgKiBzbyByZXRyeWluZyBpdCBpcyBzYWZlLiBNYXRjaGVkIGJ5IG51bWVyaWMgZXJyb3IgbnVtYmVyLCBuZXZlciBieSBtZXNzYWdlIHRleHQuXG4gICAqIEBwYXJhbSB7dW5rbm93bn0gZXJyb3IgLSBFcnJvciB0aHJvd24gd2hpbGUgaW5zcGVjdGluZyBvciBjcmVhdGluZyB0aGUgbGVkZ2VyIHRhYmxlLlxuICAgKiBAcmV0dXJucyB7Ym9vbGVhbn0gLSBUcnVlIGlmIHRoZSBlcnJvciBpcyBhIHJldHJ5YWJsZSBkZWFkbG9jay5cbiAgICovXG4gIHN0YXRpYyBpc0RlYWRsb2NrRXJyb3IoZXJyb3IpIHtcbiAgICBsZXQgY3VycmVudCA9IGVycm9yXG5cbiAgICB3aGlsZSAoY3VycmVudCBpbnN0YW5jZW9mIEVycm9yKSB7XG4gICAgICBjb25zdCBlcnJvck51bWJlciA9IC8qKiBAdHlwZSB7e251bWJlcj86IHVua25vd259fSAqLyAoY3VycmVudCkubnVtYmVyXG5cbiAgICAgIGlmICh0eXBlb2YgZXJyb3JOdW1iZXIgPT09IFwibnVtYmVyXCIgJiYgZXJyb3JOdW1iZXIgPT09IE1TU1FMX0RFQURMT0NLX1ZJQ1RJTV9FUlJPUl9OVU1CRVIpIHJldHVybiB0cnVlXG5cbiAgICAgIGN1cnJlbnQgPSBjdXJyZW50LmNhdXNlXG4gICAgfVxuXG4gICAgcmV0dXJuIGZhbHNlXG4gIH1cblxuICAvKipcbiAgICogQ3JlYXRlcyB0aGUgbGVkZ2VyIHRhYmxlIGlmIGl0IGRvZXMgbm90IGV4aXN0LiBUaGlzIGlzIHRoZSBzaW5nbGUgZGVmaW5pdGlvbiBvZlxuICAgKiB0aGUgYHNjaGVtYV9taWdyYXRpb25zYCB0YWJsZSBzaGFwZS5cbiAgICpcbiAgICogU2FmZSB1bmRlciBjb25jdXJyZW5jeTogYSBzZXNzaW9uIHRoYXQgbG9zZXMgdGhlIGNyZWF0ZSByYWNlIChvciBkZWFkbG9ja3Mgb24gdGhlXG4gICAqIGNhdGFsb2cgcmVhZCkgY29udmVyZ2VzIG9uIHRoZSBleGlzdGluZyB0YWJsZSBpbnN0ZWFkIG9mIGZhaWxpbmcgaXRzIGNhbGxlci4gVGhhdFxuICAgKiBtYXR0ZXJzIGJlY2F1c2UgdGhpcyBydW5zIG9uIGV2ZXJ5IGJvb3Qgb2YgYSBkYXRhYmFzZSBjb25maWd1cmVkIGZvciBtaWdyYXRpb25zXG4gICAqIChzZWUgYE1pZ3JhdG9yI2NyZWF0ZU1pZ3JhdGlvbnNUYWJsZUZvckRhdGFiYXNlYCBhbmRcbiAgICogYG1pZ3JhdGVSZXF1aXJlQ29udGV4dEZvckRhdGFiYXNlYCksIHNvIGEgY3Jhc2ggaGVyZSB0YWtlcyBkb3duIGEgd2hvbGUgdGVzdCBzaGFyZFxuICAgKiBvciBkZXBsb3ltZW50IG92ZXIgYSB0YWJsZSB0aGF0IHByb3ZhYmx5IGFscmVhZHkgZXhpc3RzLlxuICAgKiBAcGFyYW0ge2ltcG9ydChcIi4vZHJpdmVycy9iYXNlLmpzXCIpLmRlZmF1bHR9IGRiIC0gRGF0YWJhc2UgdGhhdCBzaG91bGQgY29udGFpbiB0aGUgbGVkZ2VyIHRhYmxlLlxuICAgKiBAcGFyYW0ge3ttYXhBdHRlbXB0cz86IG51bWJlcn19IFthcmdzXSAtIFJldHJ5IGJ1ZGdldCBmb3IgdHJhbnNpZW50IGNhdGFsb2cgZGVhZGxvY2tzLlxuICAgKiBAcmV0dXJucyB7UHJvbWlzZTx2b2lkPn1cbiAgICovXG4gIHN0YXRpYyBhc3luYyBlbnN1cmVUYWJsZShkYiwge21heEF0dGVtcHRzID0gM30gPSB7fSkge1xuICAgIGxldCBhdHRlbXB0ID0gMFxuXG4gICAgd2hpbGUgKHRydWUpIHtcbiAgICAgIGF0dGVtcHQrK1xuXG4gICAgICB0cnkge1xuICAgICAgICBpZiAoYXdhaXQgTWlncmF0aW9uc0xlZGdlci50YWJsZUV4aXN0cyhkYikpIHJldHVyblxuXG4gICAgICAgIGNvbnN0IHRhYmxlRGF0YSA9IG5ldyBUYWJsZURhdGEoVEFCTEVfTkFNRSwge2lmTm90RXhpc3RzOiB0cnVlfSlcblxuICAgICAgICB0YWJsZURhdGEuc3RyaW5nKFwidmVyc2lvblwiLCB7bnVsbDogZmFsc2UsIHByaW1hcnlLZXk6IHRydWV9KVxuXG4gICAgICAgIGZvciAoY29uc3Qgc3FsIG9mIGF3YWl0IGRiLmNyZWF0ZVRhYmxlU3FsKHRhYmxlRGF0YSkpIHtcbiAgICAgICAgICBhd2FpdCBkYi5xdWVyeShzcWwpXG4gICAgICAgIH1cblxuICAgICAgICBkYi5jbGVhclNjaGVtYUNhY2hlKClcblxuICAgICAgICByZXR1cm5cbiAgICAgIH0gY2F0Y2ggKGVycm9yKSB7XG4gICAgICAgIGlmIChNaWdyYXRpb25zTGVkZ2VyLmlzQ29uY3VycmVudENyZWF0aW9uRXJyb3IoZXJyb3IpKSB7XG4gICAgICAgICAgLy8gQW5vdGhlciBzZXNzaW9uIGNyZWF0ZWQgdGhlIGxlZGdlciBiZXR3ZWVuIG91ciBjaGVjayBhbmQgb3VyIGNyZWF0ZS4gVGhlXG4gICAgICAgICAgLy8gdGFibGUgZXhpc3RzIHdpdGggZXhhY3RseSB0aGlzIHNoYXBlLCBzbyB0aGUgZ29hbCBzdGF0ZSBpcyBhbHJlYWR5IHJlYWNoZWQuXG4gICAgICAgICAgZGIuY2xlYXJTY2hlbWFDYWNoZSgpXG5cbiAgICAgICAgICByZXR1cm5cbiAgICAgICAgfVxuXG4gICAgICAgIGlmIChNaWdyYXRpb25zTGVkZ2VyLmlzRGVhZGxvY2tFcnJvcihlcnJvcikgJiYgYXR0ZW1wdCA8IG1heEF0dGVtcHRzKSB7XG4gICAgICAgICAgZGIuY2xlYXJTY2hlbWFDYWNoZSgpXG5cbiAgICAgICAgICBjb250aW51ZVxuICAgICAgICB9XG5cbiAgICAgICAgdGhyb3cgZXJyb3JcbiAgICAgIH1cbiAgICB9XG4gIH1cblxuICAvKipcbiAgICogRXZlcnkgYXBwbGllZCBtaWdyYXRpb24gdmVyc2lvbiByZWNvcmRlZCBpbiB0aGUgbGVkZ2VyLlxuICAgKiBAcGFyYW0ge2ltcG9ydChcIi4vZHJpdmVycy9iYXNlLmpzXCIpLmRlZmF1bHR9IGRiIC0gRGF0YWJhc2Ugd2hvc2UgYXBwbGllZCB2ZXJzaW9ucyBhcmUgbG9hZGVkLlxuICAgKiBAcmV0dXJucyB7UHJvbWlzZTxzdHJpbmdbXT59IC0gQXBwbGllZCBtaWdyYXRpb24gdmVyc2lvbnMuXG4gICAqL1xuICBzdGF0aWMgYXN5bmMgYXBwbGllZFZlcnNpb25zKGRiKSB7XG4gICAgY29uc3Qgcm93cyA9IGF3YWl0IGRiLnNlbGVjdChUQUJMRV9OQU1FKVxuXG4gICAgcmV0dXJuIHJvd3MubWFwKChyb3cpID0+IGAke2RpZ2cocm93LCBcInZlcnNpb25cIil9YClcbiAgfVxuXG4gIC8qKlxuICAgKiBXaGV0aGVyIHRoZSBnaXZlbiB2ZXJzaW9uIGlzIHJlY29yZGVkIGFzIGFwcGxpZWQuXG4gICAqIEBwYXJhbSB7aW1wb3J0KFwiLi9kcml2ZXJzL2Jhc2UuanNcIikuZGVmYXVsdH0gZGIgLSBEYXRhYmFzZSB3aG9zZSBsZWRnZXIgaXMgcXVlcmllZC5cbiAgICogQHBhcmFtIHtzdHJpbmd9IHZlcnNpb24gLSBNaWdyYXRpb24gdmVyc2lvbiB0byBsb29rIHVwLlxuICAgKiBAcmV0dXJucyB7UHJvbWlzZTxib29sZWFuPn0gLSBXaGV0aGVyIHRoZSBtaWdyYXRpb24gdmVyc2lvbiBpcyBhcHBsaWVkLlxuICAgKi9cbiAgc3RhdGljIGFzeW5jIGhhc1ZlcnNpb24oZGIsIHZlcnNpb24pIHtcbiAgICBjb25zdCByb3dzID0gYXdhaXQgZGIubmV3UXVlcnkoKVxuICAgICAgLmZyb20oVEFCTEVfTkFNRSlcbiAgICAgIC53aGVyZSh7dmVyc2lvbn0pXG4gICAgICAucmVzdWx0cygpXG5cbiAgICByZXR1cm4gcm93cy5sZW5ndGggPiAwXG4gIH1cblxuICAvKipcbiAgICogUmVjb3JkcyBhIHNpbmdsZSB2ZXJzaW9uIGFzIGFwcGxpZWQuIFRoZSB0YXJnZXRlZCBleGlzdGVuY2UgY2hlY2sga2VlcHMgdGhlXG4gICAqIG1pZ3JhdG9yJ3MgcGVyLW1pZ3JhdGlvbiBob3QgcGF0aCBjaGVhcCAobm8gZnVsbC10YWJsZSBsb2FkIHBlciBtaWdyYXRpb24pLlxuICAgKiBAcGFyYW0ge2ltcG9ydChcIi4vZHJpdmVycy9iYXNlLmpzXCIpLmRlZmF1bHR9IGRiIC0gRGF0YWJhc2Ugd2hvc2UgbGVkZ2VyIHJlY2VpdmVzIHRoZSB2ZXJzaW9uLlxuICAgKiBAcGFyYW0ge3N0cmluZ30gdmVyc2lvbiAtIE1pZ3JhdGlvbiB2ZXJzaW9uIHRvIHJlY29yZCBhcyBhcHBsaWVkLlxuICAgKiBAcmV0dXJucyB7UHJvbWlzZTx2b2lkPn1cbiAgICovXG4gIHN0YXRpYyBhc3luYyByZWNvcmRWZXJzaW9uKGRiLCB2ZXJzaW9uKSB7XG4gICAgaWYgKGF3YWl0IE1pZ3JhdGlvbnNMZWRnZXIuaGFzVmVyc2lvbihkYiwgdmVyc2lvbikpIHJldHVyblxuXG4gICAgYXdhaXQgZGIuaW5zZXJ0KHt0YWJsZU5hbWU6IFRBQkxFX05BTUUsIGRhdGE6IHt2ZXJzaW9ufX0pXG4gIH1cblxuICAvKipcbiAgICogUmVtb3ZlcyBhIHZlcnNpb24gZnJvbSB0aGUgbGVkZ2VyICh1c2VkIHdoZW4gbWlncmF0aW5nIGRvd24pLlxuICAgKiBAcGFyYW0ge2ltcG9ydChcIi4vZHJpdmVycy9iYXNlLmpzXCIpLmRlZmF1bHR9IGRiIC0gRGF0YWJhc2Ugd2hvc2UgbGVkZ2VyIGxvc2VzIHRoZSB2ZXJzaW9uLlxuICAgKiBAcGFyYW0ge3N0cmluZ30gdmVyc2lvbiAtIE1pZ3JhdGlvbiB2ZXJzaW9uIHRvIG1hcmsgYXMgdW5hcHBsaWVkLlxuICAgKiBAcmV0dXJucyB7UHJvbWlzZTx2b2lkPn1cbiAgICovXG4gIHN0YXRpYyBhc3luYyByZW1vdmVWZXJzaW9uKGRiLCB2ZXJzaW9uKSB7XG4gICAgYXdhaXQgZGIuZGVsZXRlKHt0YWJsZU5hbWU6IFRBQkxFX05BTUUsIGNvbmRpdGlvbnM6IHt2ZXJzaW9ufX0pXG4gIH1cblxuICAvKipcbiAgICogQmFzZWxpbmVzIGEgZGF0YWJhc2UncyBsZWRnZXI6IHJlY29yZHMgZWFjaCB2ZXJzaW9uIGFzIGFwcGxpZWQgd2l0aG91dCBydW5uaW5nXG4gICAqIGl0cyBtaWdyYXRpb24uIElkZW1wb3RlbnQg4oCUIGFscmVhZHktcmVjb3JkZWQgdmVyc2lvbnMgYXJlIHNraXBwZWQuIEVuc3VyZXMgdGhlXG4gICAqIGxlZGdlciB0YWJsZSBleGlzdHMgZmlyc3QsIHRoZW4gbG9hZHMgdGhlIGV4aXN0aW5nIHNldCBvbmNlIGZvciB0aGUgd2hvbGUgYmF0Y2guXG4gICAqIEBwYXJhbSB7aW1wb3J0KFwiLi9kcml2ZXJzL2Jhc2UuanNcIikuZGVmYXVsdH0gZGIgLSBEYXRhYmFzZSB3aG9zZSBsZWRnZXIgc2hvdWxkIGJlIGJhc2VsaW5lZC5cbiAgICogQHBhcmFtIHtzdHJpbmdbXX0gdmVyc2lvbnMgLSBNaWdyYXRpb24gdmVyc2lvbnMgdG8gcmVjb3JkIHdpdGhvdXQgcnVubmluZyB0aGVtLlxuICAgKiBAcmV0dXJucyB7UHJvbWlzZTxzdHJpbmdbXT59IFRoZSB2ZXJzaW9ucyB0aGF0IHdlcmUgbmV3bHkgcmVjb3JkZWQuXG4gICAqL1xuICBzdGF0aWMgYXN5bmMgbWFya0FwcGxpZWQoZGIsIHZlcnNpb25zKSB7XG4gICAgYXdhaXQgTWlncmF0aW9uc0xlZGdlci5lbnN1cmVUYWJsZShkYilcblxuICAgIGNvbnN0IGV4aXN0aW5nID0gbmV3IFNldChhd2FpdCBNaWdyYXRpb25zTGVkZ2VyLmFwcGxpZWRWZXJzaW9ucyhkYikpXG4gICAgY29uc3QgcmVjb3JkZWQgPSBbXVxuXG4gICAgZm9yIChjb25zdCB2ZXJzaW9uIG9mIHZlcnNpb25zKSB7XG4gICAgICBjb25zdCBub3JtYWxpemVkVmVyc2lvbiA9IGAke3ZlcnNpb259YFxuXG4gICAgICBpZiAoZXhpc3RpbmcuaGFzKG5vcm1hbGl6ZWRWZXJzaW9uKSkgY29udGludWVcblxuICAgICAgYXdhaXQgZGIuaW5zZXJ0KHt0YWJsZU5hbWU6IFRBQkxFX05BTUUsIGRhdGE6IHt2ZXJzaW9uOiBub3JtYWxpemVkVmVyc2lvbn19KVxuICAgICAgZXhpc3RpbmcuYWRkKG5vcm1hbGl6ZWRWZXJzaW9uKVxuICAgICAgcmVjb3JkZWQucHVzaChub3JtYWxpemVkVmVyc2lvbilcbiAgICB9XG5cbiAgICByZXR1cm4gcmVjb3JkZWRcbiAgfVxuXG4gIC8qKlxuICAgKiBCYXNlbGluZXMgYHRhcmdldERiYCB0byBtYXRjaCB0aGUgYXBwbGllZCB2ZXJzaW9ucyBvZiBgc291cmNlRGJgLiBVc2Ugd2hlbiBhXG4gICAqIHByb3Zpc2lvbmluZyBwYXRoIGFkdmFuY2VkIGB0YXJnZXREYmAncyBzY2hlbWEgdG8gbWF0Y2ggYHNvdXJjZURiYCBvdXQgb2YgYmFuZFxuICAgKiAoZS5nLiBjbG9uaW5nIHRhYmxlIHN0cnVjdHVyZSBiZXR3ZWVuIGRhdGFiYXNlcyk6IHRoZSBtaWdyYXRpb25zIGFyZSwgYnlcbiAgICogY29uc3RydWN0aW9uLCBhbHJlYWR5IGFwcGxpZWQgb24gdGhlIHRhcmdldCwgc28gcmVjb3JkIHRoZW0gd2l0aG91dCByZS1ydW5uaW5nLlxuICAgKiBAcGFyYW0ge3tzb3VyY2VEYjogaW1wb3J0KFwiLi9kcml2ZXJzL2Jhc2UuanNcIikuZGVmYXVsdCwgdGFyZ2V0RGI6IGltcG9ydChcIi4vZHJpdmVycy9iYXNlLmpzXCIpLmRlZmF1bHR9fSBhcmdzIC0gU291cmNlIGxlZGdlciBhbmQgdGFyZ2V0IGRhdGFiYXNlIHRvIGJhc2VsaW5lLlxuICAgKiBAcmV0dXJucyB7UHJvbWlzZTxzdHJpbmdbXT59IFRoZSB2ZXJzaW9ucyB0aGF0IHdlcmUgbmV3bHkgcmVjb3JkZWQgb24gdGhlIHRhcmdldC5cbiAgICovXG4gIHN0YXRpYyBhc3luYyBiYXNlbGluZUZyb21EYXRhYmFzZSh7c291cmNlRGIsIHRhcmdldERifSkge1xuICAgIGNvbnN0IHNvdXJjZVZlcnNpb25zID0gYXdhaXQgTWlncmF0aW9uc0xlZGdlci5hcHBsaWVkVmVyc2lvbnMoc291cmNlRGIpXG5cbiAgICByZXR1cm4gYXdhaXQgTWlncmF0aW9uc0xlZGdlci5tYXJrQXBwbGllZCh0YXJnZXREYiwgc291cmNlVmVyc2lvbnMpXG4gIH1cbn1cbiJdfQ==
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "velocious": "build/bin/velocious.js"
4
4
  },
5
5
  "name": "velocious",
6
- "version": "1.0.695",
6
+ "version": "1.0.697",
7
7
  "main": "build/index.js",
8
8
  "types": "build/index.d.ts",
9
9
  "files": [
@@ -148,7 +148,7 @@ export default class BackgroundJobsAdapter {
148
148
  * Records pooled-child acceptance evidence (received/started timestamps plus
149
149
  * runner identity) for a handed-off job, fenced by its active handoff lease.
150
150
  * Only the fields supplied are written.
151
- * @param {{jobId: string, handoffId?: string, workerId?: string, handedOffAtMs?: number, receivedAtMs?: number, startedAtMs?: number, childInstanceId?: string, childPid?: number}} _args - Acceptance report.
151
+ * @param {{jobId: string, handoffId?: string, workerId?: string, handedOffAtMs?: number, receivedAtMs?: number, startedAtMs?: number, childInstanceId?: string, childPid?: number, runnerHostname?: string}} _args - Acceptance report.
152
152
  * @returns {Promise<boolean>} - Whether the fenced report was accepted.
153
153
  */
154
154
  async markChildAccepted(_args) { throw new Error("BackgroundJobsAdapter#markChildAccepted is not implemented") }
@@ -26,7 +26,7 @@ const BACKGROUND_JOB_NULLABLE_NUMBER_FIELDS = [
26
26
  "scheduledAtMs",
27
27
  "timeoutMs"
28
28
  ]
29
- const BACKGROUND_JOB_NULLABLE_STRING_FIELDS = ["childInstanceId", "concurrencyKey", "handoffId", "lastError", "scheduleKey", "workerId"]
29
+ const BACKGROUND_JOB_NULLABLE_STRING_FIELDS = ["childInstanceId", "concurrencyKey", "handoffId", "lastError", "runnerHostname", "scheduleKey", "workerId"]
30
30
 
31
31
  /**
32
32
  * Checks a required nullable number from a normalized wire row.
@@ -1,6 +1,7 @@
1
1
  // @ts-check
2
2
 
3
3
  import DatabaseRecord from "../database/record/index.js"
4
+ import { evaluateBackgroundJobOwnerLiveness } from "./owner-liveness.js"
4
5
 
5
6
  export default class BackgroundJobRecord extends DatabaseRecord {
6
7
  static tableName() {
@@ -10,4 +11,26 @@ export default class BackgroundJobRecord extends DatabaseRecord {
10
11
  static primaryKey() {
11
12
  return "id"
12
13
  }
14
+
15
+ /**
16
+ * Evaluates whether the process that accepted this job is still alive.
17
+ *
18
+ * Local ownership is answered from the recorded runner hostname and OS pid.
19
+ * Jobs accepted on another host, or jobs recorded before runner hostnames
20
+ * existed, answer `unknown`: remote liveness needs a heartbeat or instance
21
+ * registry, which this helper deliberately does not invent.
22
+ * @param {object} [args] - Options.
23
+ * @param {string} [args.hostname] - Hostname to compare against, defaults to this process's hostname.
24
+ * @param {(pid: number) => boolean} [args.probe] - Process probe, injectable for tests.
25
+ * @returns {import("./owner-liveness.js").BackgroundJobOwnerLivenessResult} - Liveness verdict.
26
+ */
27
+ ownerIsAlive({hostname, probe} = {}) {
28
+ return evaluateBackgroundJobOwnerLiveness({
29
+ childInstanceId: /** @type {string | null} */ (this.readAttribute("childInstanceId")),
30
+ childPid: /** @type {number | null} */ (this.readAttribute("childPid")),
31
+ currentHostname: hostname,
32
+ probe,
33
+ runnerHostname: /** @type {string | null} */ (this.readAttribute("runnerHostname"))
34
+ })
35
+ }
13
36
  }
@@ -163,7 +163,7 @@ export default class LocalBackgroundJobsAdapter extends BackgroundJobsAdapter {
163
163
 
164
164
  /**
165
165
  * Records pooled-child acceptance evidence for a local handoff.
166
- * @param {{jobId: string, handoffId?: string, receivedAtMs?: number, startedAtMs?: number, childInstanceId?: string, childPid?: number}} args - Acceptance report.
166
+ * @param {{jobId: string, handoffId?: string, receivedAtMs?: number, startedAtMs?: number, childInstanceId?: string, childPid?: number, runnerHostname?: string}} args - Acceptance report.
167
167
  * @returns {Promise<boolean>} - Whether accepted.
168
168
  */
169
169
  async markChildAccepted(args) { return await this.store.markChildAccepted(args) }
@@ -63,7 +63,8 @@ const EXPECTED_JOB_COLUMNS = [
63
63
  "child_received_at_ms",
64
64
  "child_started_at_ms",
65
65
  "child_instance_id",
66
- "child_pid"
66
+ "child_pid",
67
+ "runner_hostname"
67
68
  ]
68
69
  const EXPECTED_CONCURRENCY_COLUMNS = ["concurrency_key", "max_concurrency", "active_count"]
69
70
  const EXPECTED_SCHEDULE_KEY_COLUMNS = ["schedule_key", "job_id"]
@@ -333,6 +334,7 @@ export default class LocalBackgroundJobsStore {
333
334
  table.bigint("child_started_at_ms", {null: true})
334
335
  table.string("child_instance_id", {null: true})
335
336
  table.integer("child_pid", {null: true})
337
+ table.string("runner_hostname", {null: true})
336
338
  table.addIndex(new TableIndex(["status", "scheduled_at_ms", "created_at_ms", "id"], {name: LOCAL_BACKGROUND_JOBS_INDEX_NAMES[0]}))
337
339
  table.addIndex(new TableIndex(["queue", "status", "created_at_ms"], {name: LOCAL_BACKGROUND_JOBS_INDEX_NAMES[1]}))
338
340
  table.addIndex(new TableIndex(["args_digest"], {name: LOCAL_BACKGROUND_JOBS_INDEX_NAMES[2]}))
@@ -1098,9 +1100,10 @@ export default class LocalBackgroundJobsStore {
1098
1100
  * @param {number} [args.startedAtMs] - Epoch ms the job's perform started in the child.
1099
1101
  * @param {string} [args.childInstanceId] - Stable pooled child identity.
1100
1102
  * @param {number} [args.childPid] - Pooled child OS pid.
1103
+ * @param {string} [args.runnerHostname] - Hostname of the machine running the pooled child.
1101
1104
  * @returns {Promise<boolean>} - Whether the lease won.
1102
1105
  */
1103
- async markChildAccepted({jobId, handoffId, receivedAtMs, startedAtMs, childInstanceId, childPid}) {
1106
+ async markChildAccepted({jobId, handoffId, receivedAtMs, startedAtMs, childInstanceId, childPid, runnerHostname}) {
1104
1107
  await this.ensureReady()
1105
1108
 
1106
1109
  return await this._withDb(async (connection) => await this._mutate(connection, async (db) => {
@@ -1113,6 +1116,7 @@ export default class LocalBackgroundJobsStore {
1113
1116
  if (typeof startedAtMs === "number") data.child_started_at_ms = startedAtMs
1114
1117
  if (typeof childInstanceId === "string") data.child_instance_id = childInstanceId
1115
1118
  if (typeof childPid === "number") data.child_pid = childPid
1119
+ if (typeof runnerHostname === "string") data.runner_hostname = runnerHostname
1116
1120
  if (Object.keys(data).length === 0) return false
1117
1121
 
1118
1122
  const affectedRows = await this._updateAffectedRows(db, {
@@ -1279,15 +1283,15 @@ export default class LocalBackgroundJobsStore {
1279
1283
  * @returns {Record<string, ReturnType<typeof JSON.parse>>} - Cleared acceptance columns.
1280
1284
  */
1281
1285
  _clearedChildAcceptanceData() {
1282
- return {child_instance_id: null, child_pid: null, child_received_at_ms: null, child_started_at_ms: null}
1286
+ return {child_instance_id: null, child_pid: null, child_received_at_ms: null, child_started_at_ms: null, runner_hostname: null}
1283
1287
  }
1284
1288
 
1285
1289
  /**
1286
1290
  * Returns the row-shape counterpart of the cleared acceptance columns.
1287
- * @returns {Pick<import("./types.js").BackgroundJobRow, "childInstanceId" | "childPid" | "childReceivedAtMs" | "childStartedAtMs">} - Cleared acceptance fields.
1291
+ * @returns {Pick<import("./types.js").BackgroundJobRow, "childInstanceId" | "childPid" | "childReceivedAtMs" | "childStartedAtMs" | "runnerHostname">} - Cleared acceptance fields.
1288
1292
  */
1289
1293
  _clearedChildAcceptanceRow() {
1290
- return {childInstanceId: null, childPid: null, childReceivedAtMs: null, childStartedAtMs: null}
1294
+ return {childInstanceId: null, childPid: null, childReceivedAtMs: null, childStartedAtMs: null, runnerHostname: null}
1291
1295
  }
1292
1296
 
1293
1297
  /**
@@ -1583,6 +1587,7 @@ export default class LocalBackgroundJobsStore {
1583
1587
  args: parsedArgs,
1584
1588
  attempts: this._numberOrNull(row.attempts),
1585
1589
  childInstanceId: row.child_instance_id === null || row.child_instance_id === undefined ? null : String(row.child_instance_id),
1590
+ runnerHostname: row.runner_hostname === null || row.runner_hostname === undefined ? null : String(row.runner_hostname),
1586
1591
  childPid: this._numberOrNull(row.child_pid),
1587
1592
  childReceivedAtMs: this._numberOrNull(row.child_received_at_ms),
1588
1593
  childStartedAtMs: this._numberOrNull(row.child_started_at_ms),
@@ -1272,6 +1272,7 @@ export default class BackgroundJobsMain {
1272
1272
  handoffId: message.handoffId,
1273
1273
  jobId: message.jobId,
1274
1274
  receivedAtMs: message.receivedAtMs,
1275
+ runnerHostname: message.runnerHostname,
1275
1276
  startedAtMs: message.startedAtMs,
1276
1277
  workerId: message.workerId
1277
1278
  })
@@ -1862,14 +1863,18 @@ export default class BackgroundJobsMain {
1862
1863
  const payload = {
1863
1864
  context: {
1864
1865
  attempts: job.attempts,
1866
+ childInstanceId: job.childInstanceId,
1867
+ childPid: job.childPid,
1865
1868
  jobArgs: job.args,
1866
1869
  jobId: job.id,
1867
1870
  jobName: job.jobName,
1868
1871
  maxRetries: job.maxRetries,
1872
+ runnerHostname: job.runnerHostname,
1869
1873
  stage: "background-job-orphaned",
1870
1874
  status: job.status,
1871
1875
  terminal: job.status === "failed" || job.status === "orphaned",
1872
- willRetry: job.status === "queued"
1876
+ willRetry: job.status === "queued",
1877
+ workerId: job.workerId
1873
1878
  },
1874
1879
  error: normalizedError
1875
1880
  }
@@ -0,0 +1,92 @@
1
+ // @ts-check
2
+
3
+ import os from "node:os"
4
+
5
+ /**
6
+ * Liveness verdict for the process recorded on a background job.
7
+ * @typedef {"alive" | "dead" | "unknown"} BackgroundJobOwnerLivenessStatus
8
+ */
9
+
10
+ /**
11
+ * Verdict describing whether the process that accepted a background job can
12
+ * still be running.
13
+ * @typedef {object} BackgroundJobOwnerLivenessResult
14
+ * @property {string | null} childInstanceId - Recorded pooled-child identity, when available.
15
+ * @property {number | null} childPid - Recorded OS pid, when available.
16
+ * @property {string} reason - Machine-readable reason for the verdict.
17
+ * @property {string | null} runnerHostname - Recorded runner hostname, when available.
18
+ * @property {BackgroundJobOwnerLivenessStatus} status - Liveness verdict.
19
+ */
20
+
21
+ /**
22
+ * Probes a local pid without signalling the target process. `EPERM` means the
23
+ * process exists but belongs to another user, so it counts as alive; `ESRCH`
24
+ * means it is gone. Any other error is unexpected and is rethrown rather than
25
+ * silently reported as "dead".
26
+ * @param {number} pid - Process id to probe.
27
+ * @returns {boolean} - Whether a process with that pid exists.
28
+ */
29
+ export function isProcessAlive(pid) {
30
+ if (!Number.isInteger(pid) || pid <= 0) return false
31
+
32
+ try {
33
+ process.kill(pid, 0)
34
+ return true
35
+ } catch (error) {
36
+ const code = /** @type {{code?: string}} */ (error).code
37
+
38
+ if (code === "EPERM") return true
39
+ if (code === "ESRCH") return false
40
+
41
+ throw error
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Evaluates whether the process recorded on a background job is still alive.
47
+ *
48
+ * Only the local host can be answered from a PID: when the job was accepted on
49
+ * another machine the result is `unknown`, because liveness there needs a
50
+ * heartbeat or instance registry rather than a local probe. A pid alone can be
51
+ * recycled by the OS after a process exits, so treat `alive` as "a process with
52
+ * that pid exists now", not proof that it is the same process; pair it with
53
+ * `childStartedAtMs` when that distinction matters.
54
+ * @param {object} [args] - Options.
55
+ * @param {string | null} [args.childInstanceId] - Recorded pooled-child identity.
56
+ * @param {number | null} [args.childPid] - Recorded OS pid.
57
+ * @param {string} [args.currentHostname] - Hostname of this process, defaults to `os.hostname()`.
58
+ * @param {(pid: number) => boolean} [args.probe] - Process probe, injectable for tests.
59
+ * @param {string | null} [args.runnerHostname] - Hostname recorded with the job.
60
+ * @returns {BackgroundJobOwnerLivenessResult} - Liveness verdict with its reason.
61
+ */
62
+ export function evaluateBackgroundJobOwnerLiveness({
63
+ childInstanceId = null,
64
+ childPid = null,
65
+ currentHostname = os.hostname(),
66
+ probe = isProcessAlive,
67
+ runnerHostname = null
68
+ } = {}) {
69
+ const owner = {
70
+ childInstanceId: typeof childInstanceId === "string" ? childInstanceId : null,
71
+ childPid: typeof childPid === "number" ? childPid : null,
72
+ runnerHostname: typeof runnerHostname === "string" ? runnerHostname : null
73
+ }
74
+
75
+ if (!owner.runnerHostname) {
76
+ return {...owner, reason: "runner_hostname_unrecorded", status: "unknown"}
77
+ }
78
+
79
+ if (owner.runnerHostname !== currentHostname) {
80
+ return {...owner, reason: "remote_runner_host", status: "unknown"}
81
+ }
82
+
83
+ if (!Number.isInteger(owner.childPid) || owner.childPid === null || owner.childPid <= 0) {
84
+ return {...owner, reason: "child_pid_unrecorded", status: "unknown"}
85
+ }
86
+
87
+ const probedPid = /** @type {number} */ (owner.childPid)
88
+
89
+ return probe(probedPid)
90
+ ? {...owner, reason: "local_pid_alive", status: "alive"}
91
+ : {...owner, reason: "local_pid_missing", status: "dead"}
92
+ }
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { randomUUID } from "node:crypto"
4
4
  import { readFileSync } from "node:fs"
5
+ import os from "node:os"
5
6
  import v8 from "node:v8"
6
7
  import timeout from "awaitery/build/timeout.js"
7
8
  import runJobPayload, { BackgroundJobPerformedFailure } from "./job-runner.js"
@@ -16,6 +17,8 @@ const BASE_PROCESS_TITLE = "velocious background-jobs-runner"
16
17
  const SHUTDOWN_OBSERVATION_SEND_TIMEOUT_MS = 100
17
18
  /** Stable identity of this pooled child process for the life of the process. */
18
19
  const childInstanceId = randomUUID()
20
+ /** Hostname (the container id under Docker) of the machine running this pooled child. */
21
+ const runnerHostname = os.hostname()
19
22
  /** Sampling cadence for the memory observation sampler. */
20
23
  const MEMORY_OBSERVATION_INTERVAL_MS = 10000
21
24
  /** Bound on in-flight job ids carried in a memory observation. */
@@ -167,6 +170,7 @@ function sendChildAcceptance(type, payload, observedAtMs) {
167
170
  handedOffAtMs: payload.handedOffAtMs,
168
171
  handoffId: payload.handoffId,
169
172
  jobId: payload.id,
173
+ runnerHostname,
170
174
  type,
171
175
  workerId: payload.workerId,
172
176
  ...(type === "job-received" ? {receivedAtMs: observedAtMs} : {startedAtMs: observedAtMs})
@@ -399,4 +403,4 @@ process.on("message", (message) => handleMessage(message))
399
403
  process.once("disconnect", () => void shutdownRunner({exitCode: 0, reason: "ipc_disconnect"}))
400
404
  process.once("SIGTERM", () => void shutdownRunner({exitCode: 1, reason: "signal_sigterm", signal: "SIGTERM"}))
401
405
  process.once("SIGINT", () => void shutdownRunner({exitCode: 1, reason: "signal_sigint", signal: "SIGINT"}))
402
- if (process.send) process.send({childInstanceId, childPid: process.pid, type: "ready"})
406
+ if (process.send) process.send({childInstanceId, childPid: process.pid, runnerHostname, type: "ready"})
@@ -103,9 +103,10 @@ export default class BackgroundJobsStatusReporter {
103
103
  * @param {number} [args.startedAtMs] - Epoch ms the job's perform started in the child.
104
104
  * @param {string} [args.childInstanceId] - Stable pooled child identity.
105
105
  * @param {number} [args.childPid] - Pooled child OS pid.
106
+ * @param {string} [args.runnerHostname] - Hostname of the machine running the pooled child.
106
107
  * @returns {Promise<void>} - Resolves when reported.
107
108
  */
108
- async reportChildAccepted({jobId, handoffId, workerId, handedOffAtMs, receivedAtMs, startedAtMs, childInstanceId, childPid}) {
109
+ async reportChildAccepted({jobId, handoffId, workerId, handedOffAtMs, receivedAtMs, startedAtMs, childInstanceId, childPid, runnerHostname}) {
109
110
  const config = this.configuration.getBackgroundJobsConfig()
110
111
  const host = this.host || config.host
111
112
  const port = typeof this.port === "number" ? this.port : config.port
@@ -131,7 +132,8 @@ export default class BackgroundJobsStatusReporter {
131
132
  receivedAtMs,
132
133
  startedAtMs,
133
134
  childInstanceId,
134
- childPid
135
+ childPid,
136
+ runnerHostname
135
137
  })
136
138
  },
137
139
  onMessage: ({message, resolve, reject}) => {
@@ -161,16 +163,17 @@ export default class BackgroundJobsStatusReporter {
161
163
  * @param {number} [args.startedAtMs] - Epoch ms the job's perform started in the child.
162
164
  * @param {string} [args.childInstanceId] - Stable pooled child identity.
163
165
  * @param {number} [args.childPid] - Pooled child OS pid.
166
+ * @param {string} [args.runnerHostname] - Hostname of the machine running the pooled child.
164
167
  * @param {number} args.maxDurationMs - Max duration for retries.
165
168
  * @returns {Promise<void>} - Resolves when reported or the budget elapses.
166
169
  */
167
- async reportChildAcceptedWithRetry({jobId, handoffId, workerId, handedOffAtMs, receivedAtMs, startedAtMs, childInstanceId, childPid, maxDurationMs}) {
170
+ async reportChildAcceptedWithRetry({jobId, handoffId, workerId, handedOffAtMs, receivedAtMs, startedAtMs, childInstanceId, childPid, runnerHostname, maxDurationMs}) {
168
171
  let attempt = 0
169
172
  const startTime = Date.now()
170
173
 
171
174
  while (true) {
172
175
  try {
173
- await this.reportChildAccepted({jobId, handoffId, workerId, handedOffAtMs, receivedAtMs, startedAtMs, childInstanceId, childPid})
176
+ await this.reportChildAccepted({jobId, handoffId, workerId, handedOffAtMs, receivedAtMs, startedAtMs, childInstanceId, childPid, runnerHostname})
174
177
  return
175
178
  } catch (error) {
176
179
  attempt += 1
@@ -1437,9 +1437,10 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
1437
1437
  * @param {number} [args.startedAtMs] - Epoch ms the job's perform started in the child.
1438
1438
  * @param {string} [args.childInstanceId] - Stable pooled child identity.
1439
1439
  * @param {number} [args.childPid] - Pooled child OS pid.
1440
+ * @param {string} [args.runnerHostname] - Hostname of the machine running the pooled child.
1440
1441
  * @returns {Promise<boolean>} - Whether the fenced report was accepted.
1441
1442
  */
1442
- async markChildAccepted({jobId, handoffId, workerId, handedOffAtMs, receivedAtMs, startedAtMs, childInstanceId, childPid}) {
1443
+ async markChildAccepted({jobId, handoffId, workerId, handedOffAtMs, receivedAtMs, startedAtMs, childInstanceId, childPid, runnerHostname}) {
1443
1444
  await this.ensureReady()
1444
1445
 
1445
1446
  return await this._serializedConnectionMutation(async (db) => {
@@ -1453,6 +1454,7 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
1453
1454
  if (typeof startedAtMs === "number") data.child_started_at_ms = startedAtMs
1454
1455
  if (typeof childInstanceId === "string") data.child_instance_id = childInstanceId
1455
1456
  if (typeof childPid === "number") data.child_pid = childPid
1457
+ if (typeof runnerHostname === "string") data.runner_hostname = runnerHostname
1456
1458
  if (Object.keys(data).length === 0) return false
1457
1459
 
1458
1460
  const affectedRows = await this._updateAffectedRows(db, {
@@ -1470,15 +1472,15 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
1470
1472
  * @returns {Record<string, ReturnType<typeof JSON.parse>>} - Cleared acceptance columns.
1471
1473
  */
1472
1474
  _clearedChildAcceptanceData() {
1473
- return {child_instance_id: null, child_pid: null, child_received_at_ms: null, child_started_at_ms: null}
1475
+ return {child_instance_id: null, child_pid: null, child_received_at_ms: null, child_started_at_ms: null, runner_hostname: null}
1474
1476
  }
1475
1477
 
1476
1478
  /**
1477
1479
  * Returns the row-shape counterpart of the cleared acceptance columns.
1478
- * @returns {Pick<import("./types.js").BackgroundJobRow, "childInstanceId" | "childPid" | "childReceivedAtMs" | "childStartedAtMs">} - Cleared acceptance fields.
1480
+ * @returns {Pick<import("./types.js").BackgroundJobRow, "childInstanceId" | "childPid" | "childReceivedAtMs" | "childStartedAtMs" | "runnerHostname">} - Cleared acceptance fields.
1479
1481
  */
1480
1482
  _clearedChildAcceptanceRow() {
1481
- return {childInstanceId: null, childPid: null, childReceivedAtMs: null, childStartedAtMs: null}
1483
+ return {childInstanceId: null, childPid: null, childReceivedAtMs: null, childStartedAtMs: null, runnerHostname: null}
1482
1484
  }
1483
1485
 
1484
1486
  /**
@@ -2275,6 +2277,7 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
2275
2277
  table.bigint("child_started_at_ms", {null: true})
2276
2278
  table.string("child_instance_id", {null: true})
2277
2279
  table.integer("child_pid", {null: true})
2280
+ table.string("runner_hostname", {null: true})
2278
2281
 
2279
2282
  await db.createTable(table)
2280
2283
  }
@@ -2411,6 +2414,10 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
2411
2414
  tableData.integer("child_pid", {null: true})
2412
2415
  added = true
2413
2416
  }
2417
+ if (!(await table.getColumnByName("runner_hostname"))) {
2418
+ tableData.string("runner_hostname", {null: true})
2419
+ added = true
2420
+ }
2414
2421
 
2415
2422
  if (added) {
2416
2423
  for (const sql of await db.alterTableSQLs(tableData)) await db.query(sql)
@@ -3219,7 +3226,8 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
3219
3226
  childReceivedAtMs: this._normalizeNumber(row.child_received_at_ms),
3220
3227
  childStartedAtMs: this._normalizeNumber(row.child_started_at_ms),
3221
3228
  childInstanceId: row.child_instance_id ? String(row.child_instance_id) : null,
3222
- childPid: this._normalizeNumber(row.child_pid)
3229
+ childPid: this._normalizeNumber(row.child_pid),
3230
+ runnerHostname: row.runner_hostname ? String(row.runner_hostname) : null
3223
3231
  }
3224
3232
  }
3225
3233