@naturalcycles/datastore-lib 3.18.3 → 3.19.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -20,7 +20,7 @@ export declare class DatastoreDB extends BaseCommonDB implements CommonDB {
20
20
  protected KEY: symbol;
21
21
  ds(): Datastore;
22
22
  ping(): Promise<void>;
23
- getByIds<ROW extends ObjectWithId>(table: string, ids: string[], _opt?: DatastoreDBOptions): Promise<ROW[]>;
23
+ getByIds<ROW extends ObjectWithId>(table: string, ids: ROW['id'][], _opt?: DatastoreDBOptions): Promise<ROW[]>;
24
24
  getQueryKind(q: Query): string;
25
25
  runQuery<ROW extends ObjectWithId>(dbQuery: DBQuery<ROW>, _opt?: DatastoreDBOptions): Promise<RunQueryResult<ROW>>;
26
26
  runQueryCount<ROW extends ObjectWithId>(dbQuery: DBQuery<ROW>, _opt?: DatastoreDBOptions): Promise<number>;
@@ -36,7 +36,7 @@ export declare class DatastoreDB extends BaseCommonDB implements CommonDB {
36
36
  * Limitation: Datastore's delete returns void, so we always return all ids here as "deleted"
37
37
  * regardless if they were actually deleted or not.
38
38
  */
39
- deleteByIds(table: string, ids: string[], opt?: DatastoreDBOptions): Promise<number>;
39
+ deleteByIds<ROW extends ObjectWithId>(table: string, ids: ROW['id'][], opt?: DatastoreDBOptions): Promise<number>;
40
40
  /**
41
41
  * https://cloud.google.com/datastore/docs/concepts/transactions#datastore-datastore-transactional-update-nodejs
42
42
  */
@@ -50,9 +50,9 @@ export declare class DatastoreDB extends BaseCommonDB implements CommonDB {
50
50
  getTableProperties(table: string): Promise<DatastorePropertyStats[]>;
51
51
  mapId<T = any>(o: any, preserveKey?: boolean): T;
52
52
  toDatastoreEntity<T = any>(kind: string, o: T & {
53
- id?: string;
53
+ id?: string | number;
54
54
  }, excludeFromIndexes?: string[]): DatastorePayload<T>;
55
- key(kind: string, id: string): Key;
55
+ key(kind: string, id: string | number): Key;
56
56
  getDsKey(o: any): Key | undefined;
57
57
  getKey(key: Key): string | undefined;
58
58
  getTables(): Promise<string[]>;
@@ -60,25 +60,35 @@ class DatastoreDB extends db_lib_1.BaseCommonDB {
60
60
  return [];
61
61
  const keys = ids.map(id => this.key(table, id));
62
62
  let rows;
63
- try {
64
- if (this.cfg.timeout) {
63
+ if (this.cfg.timeout) {
64
+ // First try
65
+ try {
65
66
  const r = await (0, js_lib_1.pTimeout)(this.ds().get(keys), {
66
67
  timeout: this.cfg.timeout,
67
68
  name: `datastore.getByIds(${table})`,
68
69
  });
69
70
  rows = r[0];
70
71
  }
71
- else {
72
- rows = (await this.ds().get(keys))[0];
72
+ catch {
73
+ this.cfg.logger.log('datastore recreated on error');
74
+ // This is to debug "GCP Datastore Timeout issue"
75
+ const datastoreLib = require('@google-cloud/datastore');
76
+ const DS = datastoreLib.Datastore;
77
+ this.cachedDatastore = new DS(this.cfg);
78
+ // Second try (will throw)
79
+ const r = await (0, js_lib_1.pTimeout)(this.ds().get(keys), {
80
+ timeout: this.cfg.timeout,
81
+ name: `datastore.getByIds(${table}) second try`,
82
+ errorData: {
83
+ // This error will be grouped ACROSS all endpoints and usages
84
+ fingerprint: ['DATASTORE_TIMEOUT'],
85
+ },
86
+ });
87
+ rows = r[0];
73
88
  }
74
89
  }
75
- catch (err) {
76
- this.cfg.logger.log('datastore recreated on error');
77
- // This is to debug "GCP Datastore Timeout issue"
78
- const datastoreLib = require('@google-cloud/datastore');
79
- const DS = datastoreLib.Datastore;
80
- this.cachedDatastore = new DS(this.cfg);
81
- throw err;
90
+ else {
91
+ rows = (await this.ds().get(keys))[0];
82
92
  }
83
93
  return (rows
84
94
  .map(r => this.mapId(r))
@@ -137,12 +147,12 @@ class DatastoreDB extends db_lib_1.BaseCommonDB {
137
147
  */
138
148
  async saveBatch(table, rows, opt = {}) {
139
149
  const entities = rows.map(obj => this.toDatastoreEntity(table, obj, opt.excludeFromIndexes));
140
- const save = (0, js_lib_1.pRetry)(async (batch) => {
150
+ const save = (0, js_lib_1.pRetryFn)(async (batch) => {
141
151
  await (opt.tx || this.ds()).save(batch);
142
152
  }, {
143
153
  // Here we retry the GOAWAY errors that are somewhat common for Datastore
144
154
  // Currently only retrying them here in .saveBatch(), cause probably they're only thrown when saving
145
- predicate: err => RETRY_ON.some(s => err?.message.includes(s)),
155
+ predicate: err => RETRY_ON.some(s => err?.message?.includes(s)),
146
156
  name: `DatastoreLib.saveBatch(${table})`,
147
157
  maxAttempts: 5,
148
158
  delay: 5000,
@@ -153,13 +163,19 @@ class DatastoreDB extends db_lib_1.BaseCommonDB {
153
163
  logger: this.cfg.logger,
154
164
  });
155
165
  try {
156
- await (0, js_lib_1.pMap)((0, js_lib_1._chunk)(entities, MAX_ITEMS), async (batch) => await save(batch));
166
+ const chunks = (0, js_lib_1._chunk)(entities, MAX_ITEMS);
167
+ if (chunks.length === 1) {
168
+ // Not using pMap in hope to preserve stack trace
169
+ await save(chunks[0]);
170
+ }
171
+ else {
172
+ await (0, js_lib_1.pMap)(chunks, async (batch) => await save(batch));
173
+ }
157
174
  }
158
175
  catch (err) {
159
176
  // console.log(`datastore.save ${kind}`, { obj, entity })
160
177
  this.cfg.logger.error(`error in DatastoreLib.saveBatch for ${table} (${rows.length} rows)`, err);
161
- // don't throw, because datastore SDK makes it in separate thread, so exception will be unhandled otherwise
162
- return await Promise.reject(err);
178
+ throw err;
163
179
  }
164
180
  }
165
181
  async deleteByQuery(q, opt) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@naturalcycles/datastore-lib",
3
- "version": "3.18.3",
3
+ "version": "3.19.2",
4
4
  "description": "Opinionated library to work with Google Datastore",
5
5
  "scripts": {
6
6
  "prepare": "husky install"
@@ -17,7 +17,6 @@ import {
17
17
  JsonSchemaObject,
18
18
  JsonSchemaString,
19
19
  pMap,
20
- pRetry,
21
20
  _assert,
22
21
  _chunk,
23
22
  _omit,
@@ -25,6 +24,7 @@ import {
25
24
  CommonLogger,
26
25
  commonLoggerMinLevel,
27
26
  pTimeout,
27
+ pRetryFn,
28
28
  } from '@naturalcycles/js-lib'
29
29
  import { ReadableTyped } from '@naturalcycles/nodejs-lib'
30
30
  import { boldWhite } from '@naturalcycles/nodejs-lib/dist/colors'
@@ -111,32 +111,42 @@ export class DatastoreDB extends BaseCommonDB implements CommonDB {
111
111
 
112
112
  override async getByIds<ROW extends ObjectWithId>(
113
113
  table: string,
114
- ids: string[],
114
+ ids: ROW['id'][],
115
115
  _opt?: DatastoreDBOptions,
116
116
  ): Promise<ROW[]> {
117
117
  if (!ids.length) return []
118
118
  const keys = ids.map(id => this.key(table, id))
119
119
  let rows: any[]
120
120
 
121
- try {
122
- if (this.cfg.timeout) {
121
+ if (this.cfg.timeout) {
122
+ // First try
123
+ try {
123
124
  const r = await pTimeout(this.ds().get(keys), {
124
125
  timeout: this.cfg.timeout,
125
126
  name: `datastore.getByIds(${table})`,
126
127
  })
127
128
  rows = r[0]
128
- } else {
129
- rows = (await this.ds().get(keys))[0]
130
- }
131
- } catch (err) {
132
- this.cfg.logger.log('datastore recreated on error')
129
+ } catch {
130
+ this.cfg.logger.log('datastore recreated on error')
133
131
 
134
- // This is to debug "GCP Datastore Timeout issue"
135
- const datastoreLib = require('@google-cloud/datastore')
136
- const DS = datastoreLib.Datastore as typeof Datastore
137
- this.cachedDatastore = new DS(this.cfg)
132
+ // This is to debug "GCP Datastore Timeout issue"
133
+ const datastoreLib = require('@google-cloud/datastore')
134
+ const DS = datastoreLib.Datastore as typeof Datastore
135
+ this.cachedDatastore = new DS(this.cfg)
138
136
 
139
- throw err
137
+ // Second try (will throw)
138
+ const r = await pTimeout(this.ds().get(keys), {
139
+ timeout: this.cfg.timeout,
140
+ name: `datastore.getByIds(${table}) second try`,
141
+ errorData: {
142
+ // This error will be grouped ACROSS all endpoints and usages
143
+ fingerprint: ['DATASTORE_TIMEOUT'],
144
+ },
145
+ })
146
+ rows = r[0]
147
+ }
148
+ } else {
149
+ rows = (await this.ds().get(keys))[0]
140
150
  }
141
151
 
142
152
  return (
@@ -237,14 +247,14 @@ export class DatastoreDB extends BaseCommonDB implements CommonDB {
237
247
  this.toDatastoreEntity(table, obj, opt.excludeFromIndexes as string[]),
238
248
  )
239
249
 
240
- const save = pRetry(
250
+ const save = pRetryFn(
241
251
  async (batch: DatastorePayload<ROW>[]) => {
242
252
  await (opt.tx || this.ds()).save(batch)
243
253
  },
244
254
  {
245
255
  // Here we retry the GOAWAY errors that are somewhat common for Datastore
246
256
  // Currently only retrying them here in .saveBatch(), cause probably they're only thrown when saving
247
- predicate: err => RETRY_ON.some(s => (err as Error)?.message.includes(s)),
257
+ predicate: err => RETRY_ON.some(s => err?.message?.includes(s)),
248
258
  name: `DatastoreLib.saveBatch(${table})`,
249
259
  maxAttempts: 5,
250
260
  delay: 5000,
@@ -257,15 +267,21 @@ export class DatastoreDB extends BaseCommonDB implements CommonDB {
257
267
  )
258
268
 
259
269
  try {
260
- await pMap(_chunk(entities, MAX_ITEMS), async batch => await save(batch))
270
+ const chunks = _chunk(entities, MAX_ITEMS)
271
+ if (chunks.length === 1) {
272
+ // Not using pMap in hope to preserve stack trace
273
+ await save(chunks[0]!)
274
+ } else {
275
+ await pMap(chunks, async batch => await save(batch))
276
+ }
261
277
  } catch (err) {
262
278
  // console.log(`datastore.save ${kind}`, { obj, entity })
263
279
  this.cfg.logger.error(
264
280
  `error in DatastoreLib.saveBatch for ${table} (${rows.length} rows)`,
265
281
  err,
266
282
  )
267
- // don't throw, because datastore SDK makes it in separate thread, so exception will be unhandled otherwise
268
- return await Promise.reject(err)
283
+
284
+ throw err
269
285
  }
270
286
  }
271
287
 
@@ -286,9 +302,9 @@ export class DatastoreDB extends BaseCommonDB implements CommonDB {
286
302
  * Limitation: Datastore's delete returns void, so we always return all ids here as "deleted"
287
303
  * regardless if they were actually deleted or not.
288
304
  */
289
- override async deleteByIds(
305
+ override async deleteByIds<ROW extends ObjectWithId>(
290
306
  table: string,
291
- ids: string[],
307
+ ids: ROW['id'][],
292
308
  opt: DatastoreDBOptions = {},
293
309
  ): Promise<number> {
294
310
  const keys = ids.map(id => this.key(table, id))
@@ -370,7 +386,7 @@ export class DatastoreDB extends BaseCommonDB implements CommonDB {
370
386
  // if key field exists on entity, it will be used as key (prevent to duplication of numeric keyed entities)
371
387
  toDatastoreEntity<T = any>(
372
388
  kind: string,
373
- o: T & { id?: string },
389
+ o: T & { id?: string | number },
374
390
  excludeFromIndexes: string[] = [],
375
391
  ): DatastorePayload<T> {
376
392
  const key = this.getDsKey(o) || this.key(kind, o.id!)
@@ -385,7 +401,7 @@ export class DatastoreDB extends BaseCommonDB implements CommonDB {
385
401
  }
386
402
  }
387
403
 
388
- key(kind: string, id: string): Key {
404
+ key(kind: string, id: string | number): Key {
389
405
  _assert(id, `Cannot save "${kind}" entity without "id"`)
390
406
  return this.ds().key([kind, String(id)])
391
407
  }
@@ -50,7 +50,7 @@ export class DatastoreKeyValueDB implements CommonKeyValueDB {
50
50
  .limit(limit || 0)
51
51
 
52
52
  return this.db.streamQuery<KVObject>(q).pipe(
53
- transformMapSimple<ObjectWithId, string>(objectWithId => objectWithId.id, {
53
+ transformMapSimple<ObjectWithId<string>, string>(objectWithId => objectWithId.id, {
54
54
  errorMode: ErrorMode.SUPPRESS, // cause .pipe() cannot propagate errors
55
55
  }),
56
56
  )