@certik/skynet 0.22.1 → 0.22.3

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 (101) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/build.ts +23 -0
  3. package/dist/abi.d.ts +1 -2
  4. package/dist/abi.js +569 -563
  5. package/dist/address.d.ts +0 -1
  6. package/dist/address.js +22 -21
  7. package/dist/api.d.ts +0 -1
  8. package/dist/api.js +235 -120
  9. package/dist/app.d.ts +1 -2
  10. package/dist/app.js +2030 -276
  11. package/dist/availability.d.ts +0 -1
  12. package/dist/availability.js +126 -56
  13. package/dist/cli.d.ts +0 -1
  14. package/dist/cli.js +28 -24
  15. package/dist/const.d.ts +0 -1
  16. package/dist/const.js +153 -132
  17. package/dist/databricks.d.ts +0 -1
  18. package/dist/databricks.js +198 -58
  19. package/dist/date.d.ts +0 -1
  20. package/dist/date.js +48 -21
  21. package/dist/deploy.d.ts +0 -1
  22. package/dist/deploy.js +427 -292
  23. package/dist/dynamodb.d.ts +3 -4
  24. package/dist/dynamodb.js +432 -281
  25. package/dist/env.d.ts +2 -3
  26. package/dist/env.js +16 -9
  27. package/dist/graphql.d.ts +0 -1
  28. package/dist/graphql.js +26 -23
  29. package/dist/indexer.d.ts +0 -1
  30. package/dist/indexer.js +1050 -441
  31. package/dist/log.d.ts +0 -1
  32. package/dist/log.js +53 -52
  33. package/dist/object-hash.d.ts +0 -1
  34. package/dist/object-hash.js +49 -59
  35. package/dist/opsgenie.d.ts +97 -19
  36. package/dist/opsgenie.js +35 -30
  37. package/dist/por.d.ts +0 -1
  38. package/dist/por.js +113 -123
  39. package/dist/s3.d.ts +7 -8
  40. package/dist/s3.js +103 -91
  41. package/dist/search.d.ts +0 -1
  42. package/dist/search.js +100 -25
  43. package/dist/selector.d.ts +0 -1
  44. package/dist/selector.js +34 -38
  45. package/dist/slack.d.ts +0 -1
  46. package/dist/slack.js +27 -21
  47. package/dist/util.d.ts +0 -1
  48. package/dist/util.js +21 -20
  49. package/examples/api.ts +1 -1
  50. package/examples/indexer.ts +1 -1
  51. package/examples/mode-indexer.ts +1 -1
  52. package/package.json +4 -3
  53. package/{graphql.ts → src/graphql.ts} +1 -1
  54. package/src/opsgenie.ts +176 -0
  55. package/tsconfig.build.json +2 -5
  56. package/tsconfig.json +11 -20
  57. package/dist/abi.d.ts.map +0 -1
  58. package/dist/address.d.ts.map +0 -1
  59. package/dist/api.d.ts.map +0 -1
  60. package/dist/app.d.ts.map +0 -1
  61. package/dist/availability.d.ts.map +0 -1
  62. package/dist/cli.d.ts.map +0 -1
  63. package/dist/const.d.ts.map +0 -1
  64. package/dist/databricks.d.ts.map +0 -1
  65. package/dist/date.d.ts.map +0 -1
  66. package/dist/deploy.d.ts.map +0 -1
  67. package/dist/dynamodb.d.ts.map +0 -1
  68. package/dist/env.d.ts.map +0 -1
  69. package/dist/graphql.d.ts.map +0 -1
  70. package/dist/indexer.d.ts.map +0 -1
  71. package/dist/log.d.ts.map +0 -1
  72. package/dist/object-hash.d.ts.map +0 -1
  73. package/dist/opsgenie.d.ts.map +0 -1
  74. package/dist/por.d.ts.map +0 -1
  75. package/dist/s3.d.ts.map +0 -1
  76. package/dist/search.d.ts.map +0 -1
  77. package/dist/selector.d.ts.map +0 -1
  78. package/dist/slack.d.ts.map +0 -1
  79. package/dist/util.d.ts.map +0 -1
  80. package/opsgenie.ts +0 -69
  81. /package/{abi.ts → src/abi.ts} +0 -0
  82. /package/{address.ts → src/address.ts} +0 -0
  83. /package/{api.ts → src/api.ts} +0 -0
  84. /package/{app.ts → src/app.ts} +0 -0
  85. /package/{availability.ts → src/availability.ts} +0 -0
  86. /package/{cli.ts → src/cli.ts} +0 -0
  87. /package/{const.ts → src/const.ts} +0 -0
  88. /package/{databricks.ts → src/databricks.ts} +0 -0
  89. /package/{date.ts → src/date.ts} +0 -0
  90. /package/{deploy.ts → src/deploy.ts} +0 -0
  91. /package/{dynamodb.ts → src/dynamodb.ts} +0 -0
  92. /package/{env.ts → src/env.ts} +0 -0
  93. /package/{indexer.ts → src/indexer.ts} +0 -0
  94. /package/{log.ts → src/log.ts} +0 -0
  95. /package/{object-hash.ts → src/object-hash.ts} +0 -0
  96. /package/{por.ts → src/por.ts} +0 -0
  97. /package/{s3.ts → src/s3.ts} +0 -0
  98. /package/{search.ts → src/search.ts} +0 -0
  99. /package/{selector.ts → src/selector.ts} +0 -0
  100. /package/{slack.ts → src/slack.ts} +0 -0
  101. /package/{util.ts → src/util.ts} +0 -0
package/dist/indexer.js CHANGED
@@ -1,381 +1,987 @@
1
- import meow from "meow";
2
- import { createRecord, getRecordByKey } from "./dynamodb";
3
- import { getEnvironment } from "./env";
4
- import { exponentialRetry } from "./availability";
5
- import { range as numberRange, fillRange as fillNumberRange } from "./util";
6
- import { getSelectorDesc, getSelectorFlags, toSelectorString } from "./selector";
7
- import { getBinaryName } from "./cli";
8
- import { inline } from "./log";
9
- import { findDateAfter, dateRange, daysInRange as fillDateRange } from "./date";
10
- const STATE_TABLE_NAME = "skynet-" + getEnvironment() + "-indexer-state";
11
- async function getIndexerLatestId(name, selectorFlags) {
12
- const record = await getRecordByKey(STATE_TABLE_NAME, {
13
- name: `${name}Since(${toSelectorString(selectorFlags)})`,
1
+ // src/env.ts
2
+ function ensureAndGet(envName, defaultValue) {
3
+ return process.env[envName] || defaultValue;
4
+ }
5
+ function getEnvironment() {
6
+ return ensureAndGet("SKYNET_ENVIRONMENT", "dev");
7
+ }
8
+ function getEnvOrThrow(envName) {
9
+ if (!process.env[envName]) {
10
+ throw new Error(`Must set environment variable ${envName}`);
11
+ }
12
+ return process.env[envName];
13
+ }
14
+ function isProduction() {
15
+ return getEnvironment() === "prd";
16
+ }
17
+ function isDev() {
18
+ return getEnvironment() === "dev";
19
+ }
20
+ // src/util.ts
21
+ function range(startAt, endAt, step) {
22
+ const arr = [];
23
+ for (let i = startAt;i <= endAt; i += step) {
24
+ arr.push([i, Math.min(endAt, i + step - 1)]);
25
+ }
26
+ return arr;
27
+ }
28
+ function arrayGroup(array, groupSize) {
29
+ const groups = [];
30
+ for (let i = 0;i < array.length; i += groupSize) {
31
+ groups.push(array.slice(i, i + groupSize));
32
+ }
33
+ return groups;
34
+ }
35
+ function fillRange(start, end) {
36
+ const result = [];
37
+ for (let i = start;i <= end; i++) {
38
+ result.push(i);
39
+ }
40
+ return result;
41
+ }
42
+ // src/date.ts
43
+ var MS_IN_A_DAY = 3600 * 24 * 1000;
44
+ function getDateOnly(date) {
45
+ return new Date(date).toISOString().split("T")[0];
46
+ }
47
+ function findDateAfter(date, n) {
48
+ const d = new Date(date);
49
+ const after = new Date(d.getTime() + MS_IN_A_DAY * n);
50
+ return getDateOnly(after);
51
+ }
52
+ function daysInRange(from, to) {
53
+ const fromTime = new Date(from).getTime();
54
+ const toTime = new Date(to).getTime();
55
+ if (fromTime > toTime) {
56
+ throw new Error(`range to date couldn't be earlier than range from date`);
57
+ }
58
+ const daysBetween = Math.floor((toTime - fromTime) / MS_IN_A_DAY);
59
+ const dates = [getDateOnly(new Date(fromTime))];
60
+ for (let i = 1;i <= daysBetween; i += 1) {
61
+ dates.push(getDateOnly(new Date(fromTime + i * MS_IN_A_DAY)));
62
+ }
63
+ return dates;
64
+ }
65
+ function dateRange(from, to, step) {
66
+ const days = daysInRange(from, to);
67
+ const windows = arrayGroup(days, step);
68
+ return windows.map((w) => [w[0], w[w.length - 1]]);
69
+ }
70
+ // src/object-hash.ts
71
+ import xh from "@node-rs/xxhash";
72
+ function getHash(obj) {
73
+ const xxh3 = xh.xxh3.Xxh3.withSeed();
74
+ hash(obj, xxh3);
75
+ return xxh3.digest().toString(16);
76
+ }
77
+ function hash(obj, xxh3) {
78
+ if (obj === null) {
79
+ xxh3.update("null");
80
+ } else if (obj === undefined) {
81
+ xxh3.update("undefined");
82
+ } else if (typeof obj === "string") {
83
+ xxh3.update(obj);
84
+ } else if (typeof obj === "number") {
85
+ xxh3.update(obj.toString());
86
+ } else if (typeof obj === "boolean") {
87
+ xxh3.update(obj.toString());
88
+ } else if (typeof obj === "bigint") {
89
+ xxh3.update(obj.toString());
90
+ } else if (obj instanceof Date) {
91
+ xxh3.update(obj.toISOString());
92
+ } else if (Array.isArray(obj)) {
93
+ arrayHash(obj, xxh3);
94
+ } else if (obj instanceof Set) {
95
+ setHash(obj, xxh3);
96
+ } else if (obj instanceof Map) {
97
+ mapHash(obj, xxh3);
98
+ } else if (typeof obj === "object") {
99
+ objectHash(obj, xxh3);
100
+ } else {
101
+ throw new Error(`Unsupported type: ${obj}`);
102
+ }
103
+ }
104
+ function arrayHash(array, xxh3) {
105
+ xxh3.update("[");
106
+ for (const obj of array) {
107
+ hash(obj, xxh3);
108
+ }
109
+ xxh3.update("]");
110
+ }
111
+ function setHash(_set, _xxh3) {
112
+ throw new Error("Set hashing not implemented");
113
+ }
114
+ function mapHash(map, xxh3) {
115
+ const array = Array.from(map.entries()).sort(([aKey], [bKey]) => aKey.localeCompare(bKey));
116
+ for (const [key, value] of array) {
117
+ hash(key, xxh3);
118
+ hash(value, xxh3);
119
+ }
120
+ }
121
+ function objectHash(obj, xxh3) {
122
+ const array = Object.entries(obj).sort(([aKey], [bKey]) => aKey.localeCompare(bKey));
123
+ for (const [key, value] of array) {
124
+ hash(key, xxh3);
125
+ hash(value, xxh3);
126
+ }
127
+ }
128
+
129
+ // src/availability.ts
130
+ import pThrottle from "p-throttle";
131
+ import pMemoize from "p-memoize";
132
+ import QuickLRU from "quick-lru";
133
+ async function wait(time) {
134
+ return new Promise((resolve) => {
135
+ setTimeout(resolve, time);
136
+ });
137
+ }
138
+ async function exponentialRetry(func, {
139
+ maxRetry,
140
+ initialDuration,
141
+ growFactor,
142
+ test,
143
+ verbose
144
+ }) {
145
+ let retries = maxRetry;
146
+ let duration = initialDuration || 5000;
147
+ const growFactorFinal = growFactor || 2;
148
+ let result = await func();
149
+ while (!test(result) && retries > 0) {
150
+ if (verbose) {
151
+ console.log("failed attempt result", result);
152
+ console.log(`sleep for ${duration}ms after failed attempt, remaining ${retries} attempts`);
153
+ }
154
+ retries = retries - 1;
155
+ await wait(duration);
156
+ result = await func();
157
+ duration = duration * growFactorFinal;
158
+ }
159
+ if (verbose) {
160
+ console.log(`function to retry ends with status ${test(result)}, number of retries done: ${maxRetry - retries}}`);
161
+ }
162
+ return result;
163
+ }
164
+ function withRetry(func, options) {
165
+ let retries = options?.maxRetry || 3;
166
+ let duration = options?.initialDuration || 500;
167
+ const growFactorFinal = options?.growFactor || 2;
168
+ return async (...args) => {
169
+ do {
170
+ try {
171
+ return await func(...args);
172
+ } catch (error) {
173
+ retries = retries - 1;
174
+ if (retries <= 0) {
175
+ throw error;
176
+ }
177
+ await wait(duration);
178
+ duration = duration * growFactorFinal;
179
+ }
180
+ } while (retries > 0);
181
+ throw new Error("unreachable");
182
+ };
183
+ }
184
+ function memoize(func, options) {
185
+ if (!options) {
186
+ options = {};
187
+ }
188
+ if (!options.cache) {
189
+ options.cache = new QuickLRU({ maxSize: options.lruMaxSize || 1e4 });
190
+ }
191
+ if (!options.cacheKey) {
192
+ options.cacheKey = (args) => getHash(args);
193
+ }
194
+ return pMemoize(func, options);
195
+ }
196
+ // src/log.ts
197
+ function isObject(a) {
198
+ return !!a && a.constructor === Object;
199
+ }
200
+ function print(o) {
201
+ if (Array.isArray(o)) {
202
+ return `[${o.map(print).join(", ")}]`;
203
+ }
204
+ if (isObject(o)) {
205
+ return `{${Object.keys(o).map((k) => `${k}: ${o[k]}`).join(", ")}}`;
206
+ }
207
+ return `${o}`;
208
+ }
209
+ function getLine(params) {
210
+ let line = "";
211
+ for (let i = 0, l = params.length;i < l; i++) {
212
+ line += `${print(params[i])} `.replace(/\n/gm, "\t");
213
+ }
214
+ return line.trim();
215
+ }
216
+ function timestamp() {
217
+ return new Date().toISOString();
218
+ }
219
+ var inline = {
220
+ debug: function(...args) {
221
+ if (true) {
222
+ console.log(`${timestamp()} ${getLine(args)}`);
223
+ }
224
+ },
225
+ log: function(...args) {
226
+ if (true) {
227
+ console.log(`${timestamp()} ${getLine(args)}`);
228
+ }
229
+ },
230
+ error: function(...args) {
231
+ if (true) {
232
+ console.error(`${timestamp()} ${getLine(args)}`);
233
+ }
234
+ }
235
+ };
236
+ var logger = {
237
+ debug: function(...args) {
238
+ if (true) {
239
+ console.log(`[${timestamp()}]`, ...args);
240
+ }
241
+ },
242
+ log: function(...args) {
243
+ if (true) {
244
+ console.log(`[${timestamp()}]`, ...args);
245
+ }
246
+ },
247
+ error: function(...args) {
248
+ if (true) {
249
+ console.error(`[${timestamp()}]`, ...args);
250
+ }
251
+ }
252
+ };
253
+ // src/dynamodb.ts
254
+ import {
255
+ DynamoDBDocumentClient,
256
+ ScanCommand,
257
+ BatchWriteCommand,
258
+ GetCommand,
259
+ PutCommand,
260
+ QueryCommand,
261
+ UpdateCommand
262
+ } from "@aws-sdk/lib-dynamodb";
263
+ import { DynamoDBClient, DescribeTableCommand } from "@aws-sdk/client-dynamodb";
264
+ var _dynamoDB;
265
+ var _docClient;
266
+ function getDynamoDB(forceNew = false) {
267
+ if (!_dynamoDB || forceNew) {
268
+ _dynamoDB = new DynamoDBClient;
269
+ }
270
+ return _dynamoDB;
271
+ }
272
+ function getDocClient(forceNew = false) {
273
+ const marshallOptions = {
274
+ convertEmptyValues: true,
275
+ removeUndefinedValues: true,
276
+ convertClassInstanceToMap: true
277
+ };
278
+ const unmarshallOptions = {
279
+ wrapNumbers: false
280
+ };
281
+ if (!_docClient || forceNew) {
282
+ _docClient = DynamoDBDocumentClient.from(getDynamoDB(), {
283
+ marshallOptions,
284
+ unmarshallOptions
14
285
  });
15
- return record?.value;
286
+ }
287
+ return _docClient;
16
288
  }
17
- async function getIndexerValidatedId(name, selectorFlags) {
18
- const record = await getRecordByKey(STATE_TABLE_NAME, {
19
- name: `${name}Validate(${toSelectorString(selectorFlags)})`,
289
+ async function scanWholeTable(options) {
290
+ const dynamodb = getDocClient();
291
+ let items = [];
292
+ let count = 0;
293
+ let scannedCount = 0;
294
+ let data = await dynamodb.send(new ScanCommand(options));
295
+ while (data.LastEvaluatedKey) {
296
+ if (data.Items) {
297
+ items = items.concat(data.Items);
298
+ }
299
+ count += data.Count || 0;
300
+ scannedCount += data.ScannedCount || 0;
301
+ data = await dynamodb.send(new ScanCommand({ ...options, ExclusiveStartKey: data.LastEvaluatedKey }));
302
+ }
303
+ if (data.Items) {
304
+ items = items.concat(data.Items);
305
+ }
306
+ count += data.Count || 0;
307
+ scannedCount += data.ScannedCount || 0;
308
+ return {
309
+ Items: items,
310
+ Count: count,
311
+ ScannedCount: scannedCount
312
+ };
313
+ }
314
+ async function batchCreateRecords(tableName, records, maxWritingCapacity, verbose = false) {
315
+ if (verbose) {
316
+ console.log(`creating ${records.length} items in ${tableName}`);
317
+ }
318
+ const docClient = getDocClient();
319
+ let remainingItems = records;
320
+ let prevRemainingCount = remainingItems.length + 1;
321
+ let factor = 1;
322
+ let rejection = undefined;
323
+ while (remainingItems.length > 0 && factor <= 128 && !rejection) {
324
+ if (prevRemainingCount === remainingItems.length) {
325
+ await wait(5000 * factor);
326
+ factor = factor * 2;
327
+ }
328
+ if (factor >= 32) {
329
+ console.log(`WARNING: no progress for a long time for batchCreateRecords, please check`);
330
+ }
331
+ const slices = arrayGroup(remainingItems.slice(0, maxWritingCapacity), 25);
332
+ const results = await Promise.allSettled(slices.map((rs) => docClient.send(new BatchWriteCommand({
333
+ RequestItems: {
334
+ [tableName]: rs.map((record) => ({ PutRequest: { Item: record } }))
335
+ }
336
+ }))));
337
+ const isFulfilled = (p) => p.status === "fulfilled";
338
+ const isRejected = (p) => p.status === "rejected";
339
+ prevRemainingCount = remainingItems.length;
340
+ remainingItems = remainingItems.slice(maxWritingCapacity);
341
+ results.forEach((rs, idx) => {
342
+ if (isRejected(rs)) {
343
+ remainingItems = remainingItems.concat(slices[idx]);
344
+ rejection = rs;
345
+ } else if (isFulfilled(rs) && rs.value.UnprocessedItems && Object.keys(rs.value.UnprocessedItems).length > 0) {
346
+ const unprocessedItems = rs.value.UnprocessedItems[tableName].map((it) => it.PutRequest?.Item ?? []).flat();
347
+ remainingItems = remainingItems.concat(unprocessedItems);
348
+ }
20
349
  });
21
- if (record) {
22
- return record.value;
350
+ if (verbose) {
351
+ console.log(`processed=${prevRemainingCount - remainingItems.length}, remaining=${remainingItems.length}`);
23
352
  }
24
- return undefined;
353
+ }
354
+ if (rejection) {
355
+ console.log("batchCreateRecords rejected", rejection);
356
+ throw new Error(`batchCreateRecords rejected, failed items=${remainingItems.length}`);
357
+ }
358
+ if (remainingItems.length > 0) {
359
+ console.log(`failed batchCreateRecords, failed items=${remainingItems.length}`);
360
+ throw new Error(`batchCreateRecords retry failed, failed items=${remainingItems.length}`);
361
+ }
25
362
  }
26
- function increaseId(type, currentId, n) {
27
- if (type === "date") {
28
- if (typeof currentId !== "string") {
29
- throw new Error("invalid type for date id");
30
- }
31
- return findDateAfter(currentId, n);
32
- }
33
- if (typeof currentId !== "number") {
34
- throw new Error("Invalid type for numeric id");
35
- }
36
- return (currentId + n);
37
- }
38
- // for those indexers that can have progress tracked by a numeric state.type
39
- // such as block height, or timestamp
40
- // managing state would be helpful to reduce the build time
41
- // and avoid unnecessary computation & storage
42
- function createModeIndexerApp({ binaryName, name, selector = {}, build,
43
- // number of items run in a batch, determines the { from, to } to the build function
44
- buildBatchSize = 1,
45
- // number of build functions calling at a time
46
- buildConcurrency = 1,
47
- // commit updates every rolling window = buildBatchSize * buildConcurrency
48
- validate,
49
- // number of items run in a batch, determines the { from, to } to the validate function
50
- validateBatchSize = 1,
51
- // number of validate functions calling at a time
52
- validateConcurrency = 1,
53
- // commit updates every rolling window = validateBatchSize * validateConcurrency
54
- maxRetry = 2, state, }) {
55
- const defaultState = {
56
- type: "block",
57
- getMinId: async () => 1,
58
- getMaxId: async () => {
59
- throw new Error("must implement getMaxId");
60
- },
363
+ async function createRecord(tableName, fields, verbose = false) {
364
+ if (verbose) {
365
+ console.log("creating", tableName, fields);
366
+ }
367
+ const docClient = getDocClient();
368
+ const params = {
369
+ TableName: tableName,
370
+ Item: fields
371
+ };
372
+ return docClient.send(new PutCommand(params));
373
+ }
374
+ async function readRecord(tableName, key, verbose = false) {
375
+ if (verbose) {
376
+ console.log("reading", tableName, key);
377
+ }
378
+ const docClient = getDocClient();
379
+ const record = await docClient.send(new GetCommand({
380
+ TableName: tableName,
381
+ Key: key
382
+ }));
383
+ return record.Item;
384
+ }
385
+ async function getRecordsByKey(tableName, keys, indexName) {
386
+ const docClient = getDocClient();
387
+ const keyNames = Object.keys(keys);
388
+ const conditionExpression = keyNames.map((key) => `#${key} = :${key}`).join(" and ");
389
+ const params = {
390
+ TableName: tableName,
391
+ KeyConditionExpression: conditionExpression,
392
+ ExpressionAttributeNames: generateExpressionNames(keyNames),
393
+ ExpressionAttributeValues: generateExpressionValues(keyNames, keys)
394
+ };
395
+ if (indexName) {
396
+ params.IndexName = indexName;
397
+ }
398
+ try {
399
+ let data = await docClient.send(new QueryCommand(params));
400
+ let items = data.Items ?? [];
401
+ while (data.LastEvaluatedKey) {
402
+ data = await docClient.send(new QueryCommand({
403
+ ...params,
404
+ ExclusiveStartKey: data.LastEvaluatedKey
405
+ }));
406
+ if (data.Items) {
407
+ items = items.concat(data.Items);
408
+ }
409
+ }
410
+ return items;
411
+ } catch (err) {
412
+ console.log(err);
413
+ if (err instanceof Error && "statusCode" in err && err.statusCode === 400) {
414
+ return null;
415
+ }
416
+ throw err;
417
+ }
418
+ }
419
+ async function getRecordByKey(tableName, keys, indexName) {
420
+ if (indexName) {
421
+ const records = await getRecordsByKey(tableName, keys, indexName);
422
+ if (records) {
423
+ return records[0];
424
+ } else {
425
+ return null;
426
+ }
427
+ } else {
428
+ return readRecord(tableName, keys);
429
+ }
430
+ }
431
+ function generateExpressionNames(keys) {
432
+ return keys.reduce((acc, key) => ({ ...acc, [`#${key}`]: key }), {});
433
+ }
434
+ function generateExpressionValues(keys, fields) {
435
+ return keys.reduce((acc, key) => ({ ...acc, [`:${key}`]: fields[key] }), {});
436
+ }
437
+ async function updateRecordByKey(tableName, idKey, fields, conditionExpressions = null, verbose = false) {
438
+ if (verbose) {
439
+ console.log("update", tableName, idKey, fields);
440
+ }
441
+ const docClient = getDocClient();
442
+ const idKeyNames = Object.keys(idKey);
443
+ const fieldsToDelete = Object.keys(fields).filter((f) => fields[f] === undefined);
444
+ const fieldsToUpdate = Object.keys(fields).filter((k) => !idKeyNames.includes(k) && !fieldsToDelete.includes(k));
445
+ let data;
446
+ if (fieldsToDelete.length > 0) {
447
+ if (verbose) {
448
+ console.log("delete fields", tableName, fieldsToDelete);
449
+ }
450
+ const deleteParams = {
451
+ TableName: tableName,
452
+ Key: idKey,
453
+ ExpressionAttributeNames: generateExpressionNames(fieldsToDelete),
454
+ UpdateExpression: `REMOVE ${fieldsToDelete.map((f) => `#${f}`).join(", ")}`,
455
+ ReturnValues: "ALL_NEW"
61
456
  };
62
- const finalState = {
63
- ...defaultState,
64
- ...state,
457
+ if (conditionExpressions) {
458
+ deleteParams.ConditionExpression = conditionExpressions;
459
+ }
460
+ data = await docClient.send(new UpdateCommand(deleteParams));
461
+ }
462
+ if (fieldsToUpdate.length > 0) {
463
+ if (verbose) {
464
+ console.log("update fields", tableName, fieldsToUpdate);
465
+ }
466
+ const updateExpressions = fieldsToUpdate.map((key) => `#${key} = :${key}`);
467
+ const params = {
468
+ TableName: tableName,
469
+ Key: idKey,
470
+ ExpressionAttributeNames: generateExpressionNames(fieldsToUpdate),
471
+ ExpressionAttributeValues: generateExpressionValues(fieldsToUpdate, fields),
472
+ UpdateExpression: `SET ${updateExpressions.join(", ")}`,
473
+ ReturnValues: "ALL_NEW"
65
474
  };
66
- // type based range functions
67
- function range(from, to, step) {
68
- if (typeof from === "string" && typeof to === "string") {
69
- if (finalState.type === "date") {
70
- return dateRange(from, to, step);
71
- }
72
- throw new Error("Invalid type for numeric range");
73
- }
74
- if (typeof from === "number" && typeof to === "number") {
75
- return numberRange(from, to, step);
76
- }
77
- throw new Error("Invalid type for range");
78
- }
79
- function fillRange(from, to) {
80
- if (typeof from === "string" && typeof to === "string") {
81
- if (finalState.type === "date") {
82
- return fillDateRange(from, to);
83
- }
84
- throw new Error("Invalid type for numeric range");
85
- }
86
- if (typeof from === "number" && typeof to === "number") {
87
- return fillNumberRange(from, to);
88
- }
89
- throw new Error("Invalid type for range");
90
- }
91
- function offsetRange(from, to) {
92
- return fillRange(from, to).length;
93
- }
94
- async function runMode(flags) {
95
- const { mode, from: fromUntyped, to: toUntyped, status, verbose: verboseUntyped, ...untypeSelectorFlags } = flags;
96
- const from = fromUntyped;
97
- const to = toUntyped;
98
- const verbose = verboseUntyped;
99
- const selectorFlags = untypeSelectorFlags;
100
- if (status) {
101
- const stateItem = await getRecordByKey(STATE_TABLE_NAME, {
102
- name: `${name}RebuildState(${toSelectorString(selectorFlags)})`,
103
- });
104
- const fromItem = await getRecordByKey(STATE_TABLE_NAME, {
105
- name: `${name}Since(${toSelectorString(selectorFlags)})`,
106
- });
107
- const validateItem = await getRecordByKey(STATE_TABLE_NAME, {
108
- name: `${name}Validate(${toSelectorString(selectorFlags)})`,
109
- });
110
- inline.log(`RebuildState=${stateItem?.value} Since=${fromItem?.value} Validated=${validateItem?.value}`);
111
- process.exit(0);
112
- }
113
- inline.log(`[MODE INDEXER] mode=${mode}, env=${getEnvironment()}, ${toSelectorString(selectorFlags, ", ")}`);
114
- if (mode === "reset") {
115
- await runReset(selectorFlags);
116
- }
117
- else if (mode === "rebuild") {
118
- const rebuildFrom = from || (await finalState.getMinId(selectorFlags));
119
- const rebuildTo = to || (await finalState.getMaxId(selectorFlags));
120
- await runReset(selectorFlags);
121
- await runRebuild(selectorFlags, rebuildFrom, rebuildTo, verbose);
122
- }
123
- else if (mode === "resume-rebuild") {
124
- const previousRebuildEnds = await getIndexerLatestId(name, selectorFlags);
125
- const rebuildFrom = from ||
126
- (previousRebuildEnds !== undefined && increaseId(finalState.type, previousRebuildEnds, 1)) ||
127
- (await finalState.getMinId(selectorFlags));
128
- const rebuildTo = to || (await finalState.getMaxId(selectorFlags));
129
- await runRebuild(selectorFlags, rebuildFrom, rebuildTo, verbose);
130
- }
131
- else if (mode === "validate" || mode === "validation") {
132
- const previousRebuildEnds = await getIndexerLatestId(name, selectorFlags);
133
- if (!previousRebuildEnds) {
134
- inline.log(`[MODE INDEXER] cannot validate without a successful rebuild`);
135
- process.exit(0);
136
- }
137
- const previousValidatedTo = await getIndexerValidatedId(name, selectorFlags);
138
- const validateFrom = from || previousValidatedTo || (await finalState.getMinId(selectorFlags));
139
- const validateTo = to || previousRebuildEnds;
140
- const shouldSaveState = !to; // should not save state for manual validations, those are for testing
141
- await runValidate(selectorFlags, validateFrom, validateTo, shouldSaveState, verbose);
142
- }
143
- else if (mode === "one") {
144
- if (to) {
145
- inline.log("[MODE INDEXER] one mode ignores --to option. you may want to use range mode instead");
146
- }
147
- if (!from) {
148
- inline.log(`[MODE INDEXER] must provide --from option for one mode`);
149
- process.exit(1);
150
- }
151
- await runRange(selectorFlags, from, from, verbose);
152
- }
153
- else if (mode === "range") {
154
- if (!from || !to) {
155
- inline.log(`[MODE INDEXER] must provide --from and --to option for range mode`);
156
- process.exit(1);
157
- }
158
- await runRange(selectorFlags, from, to, verbose);
159
- }
160
- else {
161
- const stateItem = await getRecordByKey(STATE_TABLE_NAME, {
162
- name: `${name}RebuildState(${toSelectorString(selectorFlags)})`,
163
- });
164
- // only build when rebuild succeed
165
- if (!stateItem || stateItem.value !== "succeed") {
166
- inline.log("[MODE INDEXER] skip because rebuild hasn't done yet");
167
- process.exit(0);
168
- }
169
- const latestId = await getIndexerLatestId(name, selectorFlags);
170
- if (!latestId) {
171
- throw new Error(`[MODE INDEXER] cannot find the latest ${finalState.type}`);
172
- }
173
- const deltaFrom = increaseId(finalState.type, latestId, 1);
174
- const deltaTo = await state.getMaxId(selectorFlags);
175
- await runDelta(selectorFlags, deltaFrom, deltaTo, verbose);
176
- }
475
+ if (conditionExpressions) {
476
+ params.ConditionExpression = conditionExpressions;
177
477
  }
178
- async function runRange(selectorFlags, from, to, verbose) {
179
- const startTime = Date.now();
180
- inline.log(`[MODE INDEXER] building range, from=${from}, to=${to}, ${toSelectorString(selectorFlags, ", ")}, batchSize=${buildBatchSize}, concurrency=${buildConcurrency}`);
181
- const failedIds = await execBuild(selectorFlags, from, to, verbose, false);
182
- if (failedIds.length > 0) {
183
- inline.log(`[MODE INDEXER] built with some failed ${finalState.type}`, failedIds);
184
- process.exit(1);
185
- }
186
- else {
187
- inline.log(`[MODE INDEXER] built successfully in ${Date.now() - startTime}ms`);
188
- process.exit(0);
189
- }
478
+ data = await docClient.send(new UpdateCommand(params));
479
+ }
480
+ return data?.Attributes;
481
+ }
482
+ async function batchDeleteRecords(tableName, keys) {
483
+ const docClient = getDocClient();
484
+ for (let start = 0;start < keys.length; start += 25) {
485
+ const slice = keys.slice(start, start + 25);
486
+ await docClient.send(new BatchWriteCommand({
487
+ RequestItems: {
488
+ [tableName]: slice.map((key) => {
489
+ return { DeleteRequest: { Key: key } };
490
+ })
491
+ }
492
+ }));
493
+ }
494
+ }
495
+ function getKeyName(keySchema, type) {
496
+ const key = keySchema.find((k) => k.KeyType === type);
497
+ return key?.AttributeName;
498
+ }
499
+ function getIndexKeyName(globalSecondaryIndexes, indexName, type) {
500
+ const idx = globalSecondaryIndexes.find((i) => i.IndexName === indexName);
501
+ return idx?.KeySchema && getKeyName(idx.KeySchema, type);
502
+ }
503
+ async function deleteRecordsByHashKey(tableName, indexName, hashKeyValue, verbose = false) {
504
+ const docClient = getDocClient();
505
+ const meta = await getDynamoDB().send(new DescribeTableCommand({ TableName: tableName }));
506
+ if (!meta.Table) {
507
+ throw new Error(`cannot find table ${tableName}`);
508
+ }
509
+ if (indexName && !meta.Table.GlobalSecondaryIndexes) {
510
+ throw new Error(`cannot find global secondary indexes for table ${tableName}`);
511
+ }
512
+ if (!meta.Table.KeySchema) {
513
+ throw new Error(`cannot find key schema for table ${tableName}`);
514
+ }
515
+ const hashKeyName = indexName ? getIndexKeyName(meta.Table.GlobalSecondaryIndexes, indexName, "HASH") : getKeyName(meta.Table.KeySchema, "HASH");
516
+ if (!hashKeyName) {
517
+ throw new Error(`cannot find hash key name for table ${tableName}`);
518
+ }
519
+ const mainHashKeyName = getKeyName(meta.Table.KeySchema, "HASH");
520
+ if (!mainHashKeyName) {
521
+ throw new Error(`cannot find main hash key name for table ${tableName}`);
522
+ }
523
+ const mainRangeKeyName = getKeyName(meta.Table.KeySchema, "RANGE");
524
+ if (!mainRangeKeyName) {
525
+ throw new Error(`cannot find main range key name for table ${tableName}`);
526
+ }
527
+ let totalDeleted = 0;
528
+ const params = {
529
+ TableName: tableName,
530
+ KeyConditionExpression: "#hashKeyName = :hashKeyValue",
531
+ ExpressionAttributeNames: { "#hashKeyName": hashKeyName },
532
+ ExpressionAttributeValues: { ":hashKeyValue": hashKeyValue }
533
+ };
534
+ if (indexName) {
535
+ params.IndexName = indexName;
536
+ }
537
+ let data = await docClient.send(new QueryCommand(params));
538
+ if (data.Items) {
539
+ await batchDeleteRecords(tableName, data.Items.map((item) => mainRangeKeyName ? {
540
+ [mainHashKeyName]: item[mainHashKeyName],
541
+ [mainRangeKeyName]: item[mainRangeKeyName]
542
+ } : {
543
+ [mainHashKeyName]: item[mainHashKeyName]
544
+ }));
545
+ totalDeleted += data.Items.length;
546
+ }
547
+ while (data.LastEvaluatedKey) {
548
+ data = await docClient.send(new QueryCommand({
549
+ ...params,
550
+ ExclusiveStartKey: data.LastEvaluatedKey
551
+ }));
552
+ if (data.Items) {
553
+ await batchDeleteRecords(tableName, data.Items.map((item) => mainRangeKeyName ? {
554
+ [mainHashKeyName]: item[mainHashKeyName],
555
+ [mainRangeKeyName]: item[mainRangeKeyName]
556
+ } : {
557
+ [mainHashKeyName]: item[mainHashKeyName]
558
+ }));
559
+ totalDeleted += data.Items.length;
190
560
  }
191
- async function runValidate(selectorFlags, from, to, shouldSaveState, verbose) {
192
- if (!validate) {
193
- inline.log(`[MODE INDEXER] the indexer doesn't support validate mode, validate function not implemented`);
194
- process.exit(1);
195
- }
196
- const startTime = Date.now();
197
- inline.log(`[MODE INDEXER] validating, from=${from}, to=${to}, ${toSelectorString(selectorFlags, ", ")}, batchSize=${validateBatchSize}, concurrency=${validateConcurrency}`);
198
- const windows = range(from, to, validateBatchSize * validateConcurrency);
199
- inline.log(`[MODE INDEXER] from=${from}, to=${to}, batchSize=${validateBatchSize}, concurrency=${validateConcurrency}`);
200
- for (const [windowStart, windowEnd] of windows) {
201
- inline.log(`[MODE INDEXER] validating window ${windowStart}~${windowEnd}, concurrency=${validateConcurrency}`);
202
- const batches = range(windowStart, windowEnd, validateBatchSize);
203
- // add a retry for errors
204
- await Promise.all(batches.map(async ([batchStart, batchEnd]) => {
205
- const result = await exponentialRetry(async () => {
206
- try {
207
- await validate({
208
- ...selectorFlags,
209
- from: batchStart,
210
- to: batchEnd,
211
- verbose,
212
- });
213
- return true;
214
- }
215
- catch (err) {
216
- inline.error(`got error in validation`, err);
217
- return false;
218
- }
219
- }, {
220
- maxRetry,
221
- test: (r) => r,
222
- verbose,
223
- });
224
- if (!result) {
225
- throw new Error(`Terminate validation due to critical errors, from=${batchStart}, to=${batchEnd}`);
226
- }
227
- }));
228
- if (shouldSaveState) {
229
- await createRecord(STATE_TABLE_NAME, {
230
- name: `${name}Validate(${toSelectorString(selectorFlags)})`,
231
- value: to,
232
- });
233
- if (verbose) {
234
- inline.log(`[MODE INDEXER] updated processed ${finalState.type} to ${windowEnd}`);
235
- }
236
- }
237
- }
238
- inline.log(`[MODE INDEXER] validated ${offsetRange(from, to)} ${finalState.type} successfully in ${Date.now() - startTime}ms`);
239
- }
240
- async function execBuild(selectorFlags, from, to, verbose, shouldSaveState = false) {
241
- let failedIds = [];
242
- const windows = range(from, to, buildBatchSize * buildConcurrency);
243
- for (const [windowStart, windowEnd] of windows) {
244
- inline.log(`[MODE INDEXER] building window ${windowStart}~${windowEnd}, concurrency = ${buildConcurrency}`);
245
- const batches = range(windowStart, windowEnd, buildBatchSize);
246
- // add a retry for errors
247
- const batchResults = await Promise.all(batches.map(async ([batchStart, batchEnd]) => await exponentialRetry(async () => {
248
- try {
249
- const ids = await build({
250
- ...selectorFlags,
251
- from: batchStart,
252
- to: batchEnd,
253
- verbose,
254
- });
255
- if (ids && ids.length > 0) {
256
- return ids;
257
- }
258
- else {
259
- return false;
260
- }
261
- }
262
- catch (err) {
263
- inline.error(`[MODE INDEXER] got error in build`, err);
264
- return fillRange(batchStart, batchEnd);
265
- }
266
- }, {
267
- maxRetry,
268
- test: (r) => !r,
269
- verbose,
270
- })));
271
- if (shouldSaveState) {
272
- await createRecord(STATE_TABLE_NAME, {
273
- name: `${name}Since(${toSelectorString(selectorFlags)})`,
274
- value: windowEnd,
275
- });
276
- if (verbose) {
277
- inline.log(`[MODE INDEXER] updated processed ${finalState.type} to ${windowEnd}`);
278
- }
279
- }
280
- batchResults.forEach((ids) => {
281
- if (ids) {
282
- failedIds = failedIds.concat(ids);
283
- }
284
- });
285
- }
286
- failedIds.sort();
287
- return failedIds;
561
+ }
562
+ if (verbose) {
563
+ console.log(`successfully delete ${totalDeleted} items`);
564
+ }
565
+ return totalDeleted;
566
+ }
567
+ // src/selector.ts
568
+ function getSelectorDesc(selector) {
569
+ return Object.keys(selector).map((name) => {
570
+ return ` --${name.padEnd(14)}${selector[name].desc || selector[name].description || ""}`;
571
+ }).join(`
572
+ `);
573
+ }
574
+ function getSelectorFlags(selector) {
575
+ return Object.keys(selector).reduce((acc, name) => {
576
+ const flag = {
577
+ type: selector[name].type || "string",
578
+ ...selector[name]
579
+ };
580
+ if (!selector[name].optional && selector[name].isRequired !== false) {
581
+ flag.isRequired = true;
288
582
  }
289
- async function runRebuild(selectorFlags, from, to, verbose) {
290
- const startTime = Date.now();
291
- inline.log(`[MODE INDEXER] rebuilding, from=${from}, to=${to}, ${toSelectorString(selectorFlags, ", ")}, batchSize=${buildBatchSize}, concurrency=${buildConcurrency}`);
292
- // add a flag to stop delta from running
293
- await createRecord(STATE_TABLE_NAME, {
294
- name: `${name}RebuildState(${toSelectorString(selectorFlags)})`,
295
- value: "running",
296
- });
297
- const failedIds = await execBuild(selectorFlags, from, to, verbose, true);
298
- // even if some transactions are failed we should continue to allow delta job running
299
- // because rebuild is usally heavy
300
- await createRecord(STATE_TABLE_NAME, {
301
- name: `${name}Since(${toSelectorString(selectorFlags)})`,
302
- value: to,
583
+ return { ...acc, [name]: flag };
584
+ }, {});
585
+ }
586
+ function toSelectorString(selectorFlags, delim = ",") {
587
+ return Object.keys(selectorFlags).sort().map((flag) => {
588
+ return `${flag}=${selectorFlags[flag]}`;
589
+ }).join(delim);
590
+ }
591
+ function normalizeSelectorValue(v) {
592
+ return v.replace(/[^A-Za-z0-9]+/g, "-");
593
+ }
594
+ function getJobName(name, selectorFlags, mode) {
595
+ const selectorNamePart = Object.keys(selectorFlags).sort().map((name2) => selectorFlags[name2]).join("-");
596
+ let jobName = name;
597
+ if (mode) {
598
+ jobName += `-${mode}`;
599
+ }
600
+ if (selectorNamePart.length > 0) {
601
+ jobName += `-${normalizeSelectorValue(selectorNamePart)}`;
602
+ }
603
+ return jobName;
604
+ }
605
+ // src/cli.ts
606
+ import path from "path";
607
+ import fs from "fs";
608
+ function getBinaryName() {
609
+ const binaryNameParts = process.argv[1].split(path.sep);
610
+ const binaryName = binaryNameParts[binaryNameParts.length - 1];
611
+ return binaryName;
612
+ }
613
+ function detectSkynetDirectory() {
614
+ return detectDirectory(process.argv[1], "SkynetAPIDefinitions.yml");
615
+ }
616
+ function detectWorkingDirectory() {
617
+ const wd = detectDirectory(process.argv[1], "package.json");
618
+ const skynetd = detectDirectory(process.argv[1], "SkynetAPIDefinitions.yml");
619
+ return wd.slice(skynetd.length + path.sep.length).replace(path.sep, "/");
620
+ }
621
+ function detectDirectory(fullBinPath, sentinel = "package.json") {
622
+ let parentFolder = path.dirname(fullBinPath);
623
+ while (parentFolder) {
624
+ const sentinelPath = path.join(parentFolder, sentinel);
625
+ if (fs.existsSync(sentinelPath)) {
626
+ return parentFolder;
627
+ }
628
+ const newParentFolder = path.dirname(parentFolder);
629
+ if (newParentFolder === parentFolder) {
630
+ break;
631
+ }
632
+ parentFolder = newParentFolder;
633
+ }
634
+ throw new Error("Cannot detect current working directory");
635
+ }
636
+ function detectBin() {
637
+ const wd = detectDirectory(process.argv[1], "package.json");
638
+ return process.argv[1].slice(wd.length + path.sep.length).replace(path.sep, "/");
639
+ }
640
+ // src/indexer.ts
641
+ import meow from "meow";
642
+ var STATE_TABLE_NAME = "skynet-" + getEnvironment() + "-indexer-state";
643
+ async function getIndexerLatestId(name, selectorFlags) {
644
+ const record = await getRecordByKey(STATE_TABLE_NAME, {
645
+ name: `${name}Since(${toSelectorString(selectorFlags)})`
646
+ });
647
+ return record?.value;
648
+ }
649
+ async function getIndexerValidatedId(name, selectorFlags) {
650
+ const record = await getRecordByKey(STATE_TABLE_NAME, {
651
+ name: `${name}Validate(${toSelectorString(selectorFlags)})`
652
+ });
653
+ if (record) {
654
+ return record.value;
655
+ }
656
+ return;
657
+ }
658
+ function increaseId(type, currentId, n) {
659
+ if (type === "date") {
660
+ if (typeof currentId !== "string") {
661
+ throw new Error("invalid type for date id");
662
+ }
663
+ return findDateAfter(currentId, n);
664
+ }
665
+ if (typeof currentId !== "number") {
666
+ throw new Error("Invalid type for numeric id");
667
+ }
668
+ return currentId + n;
669
+ }
670
+ function createModeIndexerApp({
671
+ binaryName,
672
+ name,
673
+ selector = {},
674
+ build,
675
+ buildBatchSize = 1,
676
+ buildConcurrency = 1,
677
+ validate,
678
+ validateBatchSize = 1,
679
+ validateConcurrency = 1,
680
+ maxRetry = 2,
681
+ state
682
+ }) {
683
+ const defaultState = {
684
+ type: "block",
685
+ getMinId: async () => 1,
686
+ getMaxId: async () => {
687
+ throw new Error("must implement getMaxId");
688
+ }
689
+ };
690
+ const finalState = {
691
+ ...defaultState,
692
+ ...state
693
+ };
694
+ function range2(from, to, step) {
695
+ if (typeof from === "string" && typeof to === "string") {
696
+ if (finalState.type === "date") {
697
+ return dateRange(from, to, step);
698
+ }
699
+ throw new Error("Invalid type for numeric range");
700
+ }
701
+ if (typeof from === "number" && typeof to === "number") {
702
+ return range(from, to, step);
703
+ }
704
+ throw new Error("Invalid type for range");
705
+ }
706
+ function fillRange2(from, to) {
707
+ if (typeof from === "string" && typeof to === "string") {
708
+ if (finalState.type === "date") {
709
+ return daysInRange(from, to);
710
+ }
711
+ throw new Error("Invalid type for numeric range");
712
+ }
713
+ if (typeof from === "number" && typeof to === "number") {
714
+ return fillRange(from, to);
715
+ }
716
+ throw new Error("Invalid type for range");
717
+ }
718
+ function offsetRange(from, to) {
719
+ return fillRange2(from, to).length;
720
+ }
721
+ async function runMode(flags) {
722
+ const { mode, from: fromUntyped, to: toUntyped, status, verbose: verboseUntyped, ...untypeSelectorFlags } = flags;
723
+ const from = fromUntyped;
724
+ const to = toUntyped;
725
+ const verbose = verboseUntyped;
726
+ const selectorFlags = untypeSelectorFlags;
727
+ if (status) {
728
+ const stateItem = await getRecordByKey(STATE_TABLE_NAME, {
729
+ name: `${name}RebuildState(${toSelectorString(selectorFlags)})`
730
+ });
731
+ const fromItem = await getRecordByKey(STATE_TABLE_NAME, {
732
+ name: `${name}Since(${toSelectorString(selectorFlags)})`
733
+ });
734
+ const validateItem = await getRecordByKey(STATE_TABLE_NAME, {
735
+ name: `${name}Validate(${toSelectorString(selectorFlags)})`
736
+ });
737
+ inline.log(`RebuildState=${stateItem?.value} Since=${fromItem?.value} Validated=${validateItem?.value}`);
738
+ process.exit(0);
739
+ }
740
+ inline.log(`[MODE INDEXER] mode=${mode}, env=${getEnvironment()}, ${toSelectorString(selectorFlags, ", ")}`);
741
+ if (mode === "reset") {
742
+ await runReset(selectorFlags);
743
+ } else if (mode === "rebuild") {
744
+ const rebuildFrom = from || await finalState.getMinId(selectorFlags);
745
+ const rebuildTo = to || await finalState.getMaxId(selectorFlags);
746
+ await runReset(selectorFlags);
747
+ await runRebuild(selectorFlags, rebuildFrom, rebuildTo, verbose);
748
+ } else if (mode === "resume-rebuild") {
749
+ const previousRebuildEnds = await getIndexerLatestId(name, selectorFlags);
750
+ const rebuildFrom = from || previousRebuildEnds !== undefined && increaseId(finalState.type, previousRebuildEnds, 1) || await finalState.getMinId(selectorFlags);
751
+ const rebuildTo = to || await finalState.getMaxId(selectorFlags);
752
+ await runRebuild(selectorFlags, rebuildFrom, rebuildTo, verbose);
753
+ } else if (mode === "validate" || mode === "validation") {
754
+ const previousRebuildEnds = await getIndexerLatestId(name, selectorFlags);
755
+ if (!previousRebuildEnds) {
756
+ inline.log(`[MODE INDEXER] cannot validate without a successful rebuild`);
757
+ process.exit(0);
758
+ }
759
+ const previousValidatedTo = await getIndexerValidatedId(name, selectorFlags);
760
+ const validateFrom = from || previousValidatedTo || await finalState.getMinId(selectorFlags);
761
+ const validateTo = to || previousRebuildEnds;
762
+ const shouldSaveState = !to;
763
+ await runValidate(selectorFlags, validateFrom, validateTo, shouldSaveState, verbose);
764
+ } else if (mode === "one") {
765
+ if (to) {
766
+ inline.log("[MODE INDEXER] one mode ignores --to option. you may want to use range mode instead");
767
+ }
768
+ if (!from) {
769
+ inline.log(`[MODE INDEXER] must provide --from option for one mode`);
770
+ process.exit(1);
771
+ }
772
+ await runRange(selectorFlags, from, from, verbose);
773
+ } else if (mode === "range") {
774
+ if (!from || !to) {
775
+ inline.log(`[MODE INDEXER] must provide --from and --to option for range mode`);
776
+ process.exit(1);
777
+ }
778
+ await runRange(selectorFlags, from, to, verbose);
779
+ } else {
780
+ const stateItem = await getRecordByKey(STATE_TABLE_NAME, {
781
+ name: `${name}RebuildState(${toSelectorString(selectorFlags)})`
782
+ });
783
+ if (!stateItem || stateItem.value !== "succeed") {
784
+ inline.log("[MODE INDEXER] skip because rebuild hasn't done yet");
785
+ process.exit(0);
786
+ }
787
+ const latestId = await getIndexerLatestId(name, selectorFlags);
788
+ if (!latestId) {
789
+ throw new Error(`[MODE INDEXER] cannot find the latest ${finalState.type}`);
790
+ }
791
+ const deltaFrom = increaseId(finalState.type, latestId, 1);
792
+ const deltaTo = await state.getMaxId(selectorFlags);
793
+ await runDelta(selectorFlags, deltaFrom, deltaTo, verbose);
794
+ }
795
+ }
796
+ async function runRange(selectorFlags, from, to, verbose) {
797
+ const startTime = Date.now();
798
+ inline.log(`[MODE INDEXER] building range, from=${from}, to=${to}, ${toSelectorString(selectorFlags, ", ")}, batchSize=${buildBatchSize}, concurrency=${buildConcurrency}`);
799
+ const failedIds = await execBuild(selectorFlags, from, to, verbose, false);
800
+ if (failedIds.length > 0) {
801
+ inline.log(`[MODE INDEXER] built with some failed ${finalState.type}`, failedIds);
802
+ process.exit(1);
803
+ } else {
804
+ inline.log(`[MODE INDEXER] built successfully in ${Date.now() - startTime}ms`);
805
+ process.exit(0);
806
+ }
807
+ }
808
+ async function runValidate(selectorFlags, from, to, shouldSaveState, verbose) {
809
+ if (!validate) {
810
+ inline.log(`[MODE INDEXER] the indexer doesn't support validate mode, validate function not implemented`);
811
+ process.exit(1);
812
+ }
813
+ const startTime = Date.now();
814
+ inline.log(`[MODE INDEXER] validating, from=${from}, to=${to}, ${toSelectorString(selectorFlags, ", ")}, batchSize=${validateBatchSize}, concurrency=${validateConcurrency}`);
815
+ const windows = range2(from, to, validateBatchSize * validateConcurrency);
816
+ inline.log(`[MODE INDEXER] from=${from}, to=${to}, batchSize=${validateBatchSize}, concurrency=${validateConcurrency}`);
817
+ for (const [windowStart, windowEnd] of windows) {
818
+ inline.log(`[MODE INDEXER] validating window ${windowStart}~${windowEnd}, concurrency=${validateConcurrency}`);
819
+ const batches = range2(windowStart, windowEnd, validateBatchSize);
820
+ await Promise.all(batches.map(async ([batchStart, batchEnd]) => {
821
+ const result = await exponentialRetry(async () => {
822
+ try {
823
+ await validate({
824
+ ...selectorFlags,
825
+ from: batchStart,
826
+ to: batchEnd,
827
+ verbose
828
+ });
829
+ return true;
830
+ } catch (err) {
831
+ inline.error(`got error in validation`, err);
832
+ return false;
833
+ }
834
+ }, {
835
+ maxRetry,
836
+ test: (r) => r,
837
+ verbose
303
838
  });
839
+ if (!result) {
840
+ throw new Error(`Terminate validation due to critical errors, from=${batchStart}, to=${batchEnd}`);
841
+ }
842
+ }));
843
+ if (shouldSaveState) {
304
844
  await createRecord(STATE_TABLE_NAME, {
305
- name: `${name}RebuildState(${toSelectorString(selectorFlags)})`,
306
- value: "succeed",
845
+ name: `${name}Validate(${toSelectorString(selectorFlags)})`,
846
+ value: to
307
847
  });
308
- if (failedIds.length > 0) {
309
- inline.log(`[MODE INDEXER] built ${offsetRange(from, to)} ${finalState.type}(s) with some failed ${finalState.type}`, failedIds);
310
- process.exit(1);
311
- }
312
- else {
313
- inline.log(`[MODE INDEXER] built ${offsetRange(from, to)} ${finalState.type}(s) successfully in ${Date.now() - startTime}ms`);
314
- process.exit(0);
848
+ if (verbose) {
849
+ inline.log(`[MODE INDEXER] updated processed ${finalState.type} to ${windowEnd}`);
315
850
  }
851
+ }
316
852
  }
317
- async function runDelta(selectorFlags, from, to, verbose) {
318
- const startTime = Date.now();
319
- if (to < from) {
320
- inline.log(`[MODE INDEXER] skip delta, there're no more items need to be processed, from=${from}, to=${to}, ${toSelectorString(selectorFlags, ", ")}`);
321
- return;
322
- }
323
- inline.log(`[MODE INDEXER] starting delta, from=${from}, to=${to}, ${toSelectorString(selectorFlags, ", ")}, batchSize=${buildBatchSize}, concurrency=${buildConcurrency}`);
853
+ inline.log(`[MODE INDEXER] validated ${offsetRange(from, to)} ${finalState.type} successfully in ${Date.now() - startTime}ms`);
854
+ }
855
+ async function execBuild(selectorFlags, from, to, verbose, shouldSaveState = false) {
856
+ let failedIds = [];
857
+ const windows = range2(from, to, buildBatchSize * buildConcurrency);
858
+ for (const [windowStart, windowEnd] of windows) {
859
+ inline.log(`[MODE INDEXER] building window ${windowStart}~${windowEnd}, concurrency = ${buildConcurrency}`);
860
+ const batches = range2(windowStart, windowEnd, buildBatchSize);
861
+ const batchResults = await Promise.all(batches.map(async ([batchStart, batchEnd]) => await exponentialRetry(async () => {
324
862
  try {
325
- const failedIds = await execBuild(selectorFlags, from, to, verbose, true);
326
- if (failedIds.length > 0) {
327
- inline.log("[MODE INDEXER] built with some failed txs", failedIds);
328
- await createRecord(STATE_TABLE_NAME, {
329
- name: `${name}DeltaState(${toSelectorString(selectorFlags)})`,
330
- value: "failed",
331
- });
332
- await createRecord(STATE_TABLE_NAME, {
333
- name: `${name}Since(${toSelectorString(selectorFlags)})`,
334
- value: to < failedIds[0] ? to : failedIds[0],
335
- });
336
- process.exit(1);
337
- }
338
- else {
339
- await createRecord(STATE_TABLE_NAME, {
340
- name: `${name}DeltaState(${toSelectorString(selectorFlags)})`,
341
- value: "succeed",
342
- });
343
- await createRecord(STATE_TABLE_NAME, {
344
- name: `${name}Since(${toSelectorString(selectorFlags)})`,
345
- value: to,
346
- });
347
- inline.log(`[MODE INDEXER] built successfully in ${Date.now() - startTime}ms`);
348
- process.exit(0);
349
- }
863
+ const ids = await build({
864
+ ...selectorFlags,
865
+ from: batchStart,
866
+ to: batchEnd,
867
+ verbose
868
+ });
869
+ if (ids && ids.length > 0) {
870
+ return ids;
871
+ } else {
872
+ return false;
873
+ }
874
+ } catch (err) {
875
+ inline.error(`[MODE INDEXER] got error in build`, err);
876
+ return fillRange2(batchStart, batchEnd);
350
877
  }
351
- catch (err) {
352
- inline.error("[MODE INDEXER] delta build failed", from, to, err);
353
- process.exit(1);
878
+ }, {
879
+ maxRetry,
880
+ test: (r) => !r,
881
+ verbose
882
+ })));
883
+ if (shouldSaveState) {
884
+ await createRecord(STATE_TABLE_NAME, {
885
+ name: `${name}Since(${toSelectorString(selectorFlags)})`,
886
+ value: windowEnd
887
+ });
888
+ if (verbose) {
889
+ inline.log(`[MODE INDEXER] updated processed ${finalState.type} to ${windowEnd}`);
354
890
  }
891
+ }
892
+ batchResults.forEach((ids) => {
893
+ if (ids) {
894
+ failedIds = failedIds.concat(ids);
895
+ }
896
+ });
355
897
  }
356
- async function runReset(selectorFlags) {
357
- const startTime = Date.now();
358
- inline.log(`[MODE INDEXER] starting reset, ${toSelectorString(selectorFlags, ", ")}`);
359
- inline.log("[MODE INDEXER] reset state", STATE_TABLE_NAME);
898
+ failedIds.sort();
899
+ return failedIds;
900
+ }
901
+ async function runRebuild(selectorFlags, from, to, verbose) {
902
+ const startTime = Date.now();
903
+ inline.log(`[MODE INDEXER] rebuilding, from=${from}, to=${to}, ${toSelectorString(selectorFlags, ", ")}, batchSize=${buildBatchSize}, concurrency=${buildConcurrency}`);
904
+ await createRecord(STATE_TABLE_NAME, {
905
+ name: `${name}RebuildState(${toSelectorString(selectorFlags)})`,
906
+ value: "running"
907
+ });
908
+ const failedIds = await execBuild(selectorFlags, from, to, verbose, true);
909
+ await createRecord(STATE_TABLE_NAME, {
910
+ name: `${name}Since(${toSelectorString(selectorFlags)})`,
911
+ value: to
912
+ });
913
+ await createRecord(STATE_TABLE_NAME, {
914
+ name: `${name}RebuildState(${toSelectorString(selectorFlags)})`,
915
+ value: "succeed"
916
+ });
917
+ if (failedIds.length > 0) {
918
+ inline.log(`[MODE INDEXER] built ${offsetRange(from, to)} ${finalState.type}(s) with some failed ${finalState.type}`, failedIds);
919
+ process.exit(1);
920
+ } else {
921
+ inline.log(`[MODE INDEXER] built ${offsetRange(from, to)} ${finalState.type}(s) successfully in ${Date.now() - startTime}ms`);
922
+ process.exit(0);
923
+ }
924
+ }
925
+ async function runDelta(selectorFlags, from, to, verbose) {
926
+ const startTime = Date.now();
927
+ if (to < from) {
928
+ inline.log(`[MODE INDEXER] skip delta, there're no more items need to be processed, from=${from}, to=${to}, ${toSelectorString(selectorFlags, ", ")}`);
929
+ return;
930
+ }
931
+ inline.log(`[MODE INDEXER] starting delta, from=${from}, to=${to}, ${toSelectorString(selectorFlags, ", ")}, batchSize=${buildBatchSize}, concurrency=${buildConcurrency}`);
932
+ try {
933
+ const failedIds = await execBuild(selectorFlags, from, to, verbose, true);
934
+ if (failedIds.length > 0) {
935
+ inline.log("[MODE INDEXER] built with some failed txs", failedIds);
360
936
  await createRecord(STATE_TABLE_NAME, {
361
- name: `${name}Since(${toSelectorString(selectorFlags)})`,
362
- value: 0,
937
+ name: `${name}DeltaState(${toSelectorString(selectorFlags)})`,
938
+ value: "failed"
363
939
  });
364
940
  await createRecord(STATE_TABLE_NAME, {
365
- name: `${name}Validate(${toSelectorString(selectorFlags)})`,
366
- value: 0,
941
+ name: `${name}Since(${toSelectorString(selectorFlags)})`,
942
+ value: to < failedIds[0] ? to : failedIds[0]
367
943
  });
944
+ process.exit(1);
945
+ } else {
368
946
  await createRecord(STATE_TABLE_NAME, {
369
- name: `${name}RebuildState(${toSelectorString(selectorFlags)})`,
370
- value: "init",
947
+ name: `${name}DeltaState(${toSelectorString(selectorFlags)})`,
948
+ value: "succeed"
371
949
  });
372
- inline.log(`[MODE INDEXER] reset successfully in ${Date.now() - startTime}ms`);
950
+ await createRecord(STATE_TABLE_NAME, {
951
+ name: `${name}Since(${toSelectorString(selectorFlags)})`,
952
+ value: to
953
+ });
954
+ inline.log(`[MODE INDEXER] built successfully in ${Date.now() - startTime}ms`);
955
+ process.exit(0);
956
+ }
957
+ } catch (err) {
958
+ inline.error("[MODE INDEXER] delta build failed", from, to, err);
959
+ process.exit(1);
373
960
  }
374
- async function run() {
375
- if (!binaryName) {
376
- binaryName = getBinaryName();
377
- }
378
- const cli = meow(`
961
+ }
962
+ async function runReset(selectorFlags) {
963
+ const startTime = Date.now();
964
+ inline.log(`[MODE INDEXER] starting reset, ${toSelectorString(selectorFlags, ", ")}`);
965
+ inline.log("[MODE INDEXER] reset state", STATE_TABLE_NAME);
966
+ await createRecord(STATE_TABLE_NAME, {
967
+ name: `${name}Since(${toSelectorString(selectorFlags)})`,
968
+ value: 0
969
+ });
970
+ await createRecord(STATE_TABLE_NAME, {
971
+ name: `${name}Validate(${toSelectorString(selectorFlags)})`,
972
+ value: 0
973
+ });
974
+ await createRecord(STATE_TABLE_NAME, {
975
+ name: `${name}RebuildState(${toSelectorString(selectorFlags)})`,
976
+ value: "init"
977
+ });
978
+ inline.log(`[MODE INDEXER] reset successfully in ${Date.now() - startTime}ms`);
979
+ }
980
+ async function run() {
981
+ if (!binaryName) {
982
+ binaryName = getBinaryName();
983
+ }
984
+ const cli = meow(`
379
985
  Usage
380
986
 
381
987
  $ ${binaryName} <options>
@@ -388,52 +994,53 @@ ${selector ? getSelectorDesc(selector) : ""}
388
994
  --status print status of indexer and exit
389
995
  --verbose Output debug messages
390
996
  `, {
391
- importMeta: import.meta,
392
- description: false,
393
- version: false,
394
- flags: {
395
- ...getSelectorFlags(selector),
396
- mode: {
397
- type: "string",
398
- default: "delta",
399
- },
400
- from: {
401
- aliases: ["since"],
402
- type: "string",
403
- },
404
- to: {
405
- aliases: ["until"],
406
- type: "string",
407
- },
408
- status: {
409
- type: "boolean",
410
- default: false,
411
- },
412
- verbose: {
413
- type: "boolean",
414
- default: false,
415
- },
416
- },
417
- });
418
- try {
419
- return runMode(cli.flags);
420
- }
421
- catch (err) {
422
- inline.error(err);
423
- process.exit(1);
997
+ importMeta: import.meta,
998
+ description: false,
999
+ version: false,
1000
+ flags: {
1001
+ ...getSelectorFlags(selector),
1002
+ mode: {
1003
+ type: "string",
1004
+ default: "delta"
1005
+ },
1006
+ from: {
1007
+ aliases: ["since"],
1008
+ type: "string"
1009
+ },
1010
+ to: {
1011
+ aliases: ["until"],
1012
+ type: "string"
1013
+ },
1014
+ status: {
1015
+ type: "boolean",
1016
+ default: false
1017
+ },
1018
+ verbose: {
1019
+ type: "boolean",
1020
+ default: false
424
1021
  }
1022
+ }
1023
+ });
1024
+ try {
1025
+ return runMode(cli.flags);
1026
+ } catch (err) {
1027
+ inline.error(err);
1028
+ process.exit(1);
425
1029
  }
426
- return { run };
1030
+ }
1031
+ return { run };
427
1032
  }
428
- // for indexers that don't rely on a cursor
429
- // e.g. should always rebuild everything from scratch
430
- // or that the state can be easily inferred from existing data
431
- function createIndexerApp({ binaryName, selector = {}, build, maxRetry = 2, }) {
432
- async function run() {
433
- if (!binaryName) {
434
- binaryName = getBinaryName();
435
- }
436
- const cli = meow(`
1033
+ function createIndexerApp({
1034
+ binaryName,
1035
+ selector = {},
1036
+ build,
1037
+ maxRetry = 2
1038
+ }) {
1039
+ async function run() {
1040
+ if (!binaryName) {
1041
+ binaryName = getBinaryName();
1042
+ }
1043
+ const cli = meow(`
437
1044
  Usage
438
1045
  $ ${binaryName} <options>
439
1046
 
@@ -441,52 +1048,54 @@ function createIndexerApp({ binaryName, selector = {}, build, maxRetry = 2, }) {
441
1048
  ${selector ? getSelectorDesc(selector) : ""}
442
1049
  --verbose Output debug messages
443
1050
  `, {
444
- importMeta: import.meta,
445
- description: false,
446
- version: false,
447
- flags: {
448
- ...getSelectorFlags(selector),
449
- verbose: {
450
- type: "boolean",
451
- default: false,
452
- },
453
- },
454
- });
455
- async function runBuild(flags) {
456
- const { verbose: untypedVerbose, ...untypedSelectorFlags } = flags;
457
- const verbose = untypedVerbose;
458
- const selectorFlags = untypedSelectorFlags;
459
- const startTime = Date.now();
460
- if (Object.keys(selectorFlags).length > 0) {
461
- inline.log(`[INDEXER] starting build, ${toSelectorString(selectorFlags, ", ")}`);
462
- }
463
- else {
464
- inline.log(`[INDEXER] starting build`);
465
- }
466
- const result = await exponentialRetry(async () => {
467
- try {
468
- await build(flags);
469
- return true;
470
- }
471
- catch (err) {
472
- inline.log(`[INDEXER] got error in build`, err);
473
- return false;
474
- }
475
- }, {
476
- maxRetry,
477
- test: (r) => r,
478
- verbose: verbose,
479
- });
480
- if (!result) {
481
- throw new Error(`[INDEXER] Build failed due to critical errors`);
482
- }
483
- inline.log(`[INDEXER] build successfully in ${Date.now() - startTime}ms`);
1051
+ importMeta: import.meta,
1052
+ description: false,
1053
+ version: false,
1054
+ flags: {
1055
+ ...getSelectorFlags(selector),
1056
+ verbose: {
1057
+ type: "boolean",
1058
+ default: false
484
1059
  }
485
- return runBuild(cli.flags).catch((err) => {
486
- inline.error(err);
487
- process.exit(1);
488
- });
1060
+ }
1061
+ });
1062
+ async function runBuild(flags) {
1063
+ const { verbose: untypedVerbose, ...untypedSelectorFlags } = flags;
1064
+ const verbose = untypedVerbose;
1065
+ const selectorFlags = untypedSelectorFlags;
1066
+ const startTime = Date.now();
1067
+ if (Object.keys(selectorFlags).length > 0) {
1068
+ inline.log(`[INDEXER] starting build, ${toSelectorString(selectorFlags, ", ")}`);
1069
+ } else {
1070
+ inline.log(`[INDEXER] starting build`);
1071
+ }
1072
+ const result = await exponentialRetry(async () => {
1073
+ try {
1074
+ await build(flags);
1075
+ return true;
1076
+ } catch (err) {
1077
+ inline.log(`[INDEXER] got error in build`, err);
1078
+ return false;
1079
+ }
1080
+ }, {
1081
+ maxRetry,
1082
+ test: (r) => r,
1083
+ verbose
1084
+ });
1085
+ if (!result) {
1086
+ throw new Error(`[INDEXER] Build failed due to critical errors`);
1087
+ }
1088
+ inline.log(`[INDEXER] build successfully in ${Date.now() - startTime}ms`);
489
1089
  }
490
- return { run };
1090
+ return runBuild(cli.flags).catch((err) => {
1091
+ inline.error(err);
1092
+ process.exit(1);
1093
+ });
1094
+ }
1095
+ return { run };
491
1096
  }
492
- export { increaseId, createModeIndexerApp, createIndexerApp };
1097
+ export {
1098
+ increaseId,
1099
+ createModeIndexerApp,
1100
+ createIndexerApp
1101
+ };