@incoqnito.io/bajadab 1.2.1 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/basics.js CHANGED
@@ -1,17 +1,15 @@
1
- import logger from "winston";
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
2
  import { uuidv7, } from "uuidv7";
3
- import { runMultiReentrantProtected, runReentrantProtected } from "@incoqnito.io/ts-iq-core";
3
+ import { runReentrantProtected } from "@incoqnito.io/ts-iq-core";
4
4
  import { EBArrayDeletionStrategy, EBAutoFlushStrategy, EBAutoIDStrategy, EBDirtyUnloadStrategy, EBDuplicateIDStrategy, EBHandlerMode, EBMemStorageStrategy, EBModificationType, } from "./base.js";
5
5
  import { canonicalName } from "./utils.js";
6
6
  /** Default {@link TGetIDFunction}: reads `v.id`, `undefined` if absent. */
7
- export const DEFAULT_GET_ID = (v) => ("id" in v) ? v.id : undefined;
7
+ export const DEFAULT_GET_ID = (v) => (v !== null && typeof v === 'object' && "id" in v) ? v.id : undefined;
8
8
  /** Default {@link TSetIDFunction}: assigns to `v.id`. */
9
9
  export const DEFAULT_SET_ID = async (v, id) => { v.id = id; };
10
- var MutexKeys;
11
- (function (MutexKeys) {
12
- // because of the spaces this will never be a collection name and as such is safe to use as a mutex key
13
- MutexKeys["DATABASE"] = " database ";
14
- })(MutexKeys || (MutexKeys = {}));
10
+ function normalizeID(id) {
11
+ return id === undefined || id === null || id === "" ? undefined : String(id);
12
+ }
15
13
  /** {@link IBCollectionConfig} used when a collection is created without an explicit one. */
16
14
  export const DEFAULT_COLLECTION_CONFIG = {
17
15
  arrayDeletionStrategy: EBArrayDeletionStrategy.IN_PLACE,
@@ -30,16 +28,24 @@ export class BDatabase {
30
28
  collections = new Map();
31
29
  io;
32
30
  functionRegistry;
31
+ iid = uuidv7();
32
+ transactionScope = new AsyncLocalStorage();
33
33
  constructor(io, config, functionRegistry) {
34
- this.config = config ?? DEFAULT_DATABASE_CONFIG;
34
+ this.config = { ...(config ?? DEFAULT_DATABASE_CONFIG) };
35
35
  this.io = io;
36
36
  this.functionRegistry = functionRegistry;
37
37
  }
38
+ async runExclusive(fn) {
39
+ return runReentrantProtected(this.iid, fn);
40
+ }
41
+ isInTransaction(collectionName) {
42
+ return this.transactionScope.getStore()?.has(collectionName) ?? false;
43
+ }
38
44
  addCollection(collection) {
39
45
  this.collections.set(collection.collectionName, collection);
40
46
  }
41
47
  async getCollection(collectionName, collectionConfig) {
42
- return runReentrantProtected(MutexKeys.DATABASE, async () => {
48
+ return this.runExclusive(async () => {
43
49
  const cname = canonicalName(collectionName);
44
50
  let ret = this.collections.get(cname);
45
51
  if (!ret) {
@@ -49,25 +55,26 @@ export class BDatabase {
49
55
  });
50
56
  }
51
57
  async createCollection(collectionName, collectionConfig) {
52
- return runReentrantProtected(MutexKeys.DATABASE, async () => {
58
+ return this.runExclusive(async () => {
53
59
  const cname = canonicalName(collectionName);
54
60
  if (this.collections.has(cname)) {
55
61
  throw new Error(`cannot create already existing collection ${collectionName}`);
56
62
  }
57
63
  const id = uuidv7();
58
- const ret = new BCollection(this, id, collectionName, collectionConfig ?? this.config.defaultCollectionConfig);
64
+ const ret = new BCollection(this, id, collectionName, { ...(collectionConfig ?? this.config.defaultCollectionConfig) });
65
+ await this.io.createCollection(ret);
59
66
  this.addCollection(ret);
60
- return this.io.createCollection(ret);
67
+ return ret;
61
68
  });
62
69
  }
63
70
  async hasCollection(collectionName) {
64
- return runReentrantProtected(MutexKeys.DATABASE, async () => {
71
+ return this.runExclusive(async () => {
65
72
  return this.collections.has(canonicalName(collectionName));
66
73
  });
67
74
  }
68
75
  async dropCollection(collectionName) {
69
76
  const cname = canonicalName(collectionName);
70
- return runMultiReentrantProtected([MutexKeys.DATABASE, cname], async () => {
77
+ return this.runExclusive(async () => {
71
78
  const coll = this.collections.get(cname);
72
79
  if (coll) {
73
80
  coll.markDropped();
@@ -80,14 +87,73 @@ export class BDatabase {
80
87
  });
81
88
  }
82
89
  async snapshot(targetDir, options) {
83
- const collectionNames = Array.from(this.collections.keys());
84
- return runMultiReentrantProtected([MutexKeys.DATABASE, ...collectionNames], async () => {
90
+ return this.runExclusive(async () => {
85
91
  for (const collection of this.collections.values()) {
86
92
  await collection.flush();
87
93
  }
88
94
  return this.io.snapshot(targetDir, options);
89
95
  });
90
96
  }
97
+ async transaction(collectionNames, fn) {
98
+ if (this.transactionScope.getStore() !== undefined) {
99
+ throw new Error("nested transactions are not supported");
100
+ }
101
+ const canonicalNames = Array.from(new Set(collectionNames.map(canonicalName)));
102
+ return this.runExclusive(async () => {
103
+ const originals = new Map();
104
+ const clones = new Map();
105
+ for (const name of canonicalNames) {
106
+ const original = this.collections.get(name);
107
+ if (!original) {
108
+ throw new Error(`no such collection ${name}`);
109
+ }
110
+ originals.set(name, original);
111
+ }
112
+ for (const [name, original] of originals) {
113
+ clones.set(name, await original.cloneForTransaction());
114
+ }
115
+ const tx = {
116
+ getCollection: async (collectionName) => {
117
+ const clone = clones.get(canonicalName(collectionName));
118
+ if (!clone) {
119
+ throw new Error(`collection "${collectionName}" was not declared to this transaction`);
120
+ }
121
+ return clone;
122
+ },
123
+ };
124
+ try {
125
+ await this.transactionScope.run(new Set(canonicalNames), () => fn(tx));
126
+ const prepared = [];
127
+ try {
128
+ for (const name of canonicalNames) {
129
+ prepared.push(await originals.get(name).prepareTransactionCommit(clones.get(name)));
130
+ }
131
+ }
132
+ catch (err) {
133
+ await Promise.all(prepared.map(p => p.write.discard()));
134
+ throw err;
135
+ }
136
+ let renamed = 0;
137
+ try {
138
+ for (const p of prepared) {
139
+ await p.write.commit();
140
+ renamed++;
141
+ }
142
+ }
143
+ finally {
144
+ await Promise.all(prepared.slice(renamed).map(p => p.write.discard()));
145
+ const applied = prepared.slice(0, renamed);
146
+ applied.forEach(p => p.apply());
147
+ for (const p of applied) {
148
+ await p.fire();
149
+ }
150
+ }
151
+ }
152
+ finally {
153
+ clones.forEach(clone => clone.markDropped());
154
+ }
155
+ });
156
+ }
91
157
  }
92
158
  export class BCollection {
93
159
  database;
@@ -99,26 +165,19 @@ export class BCollection {
99
165
  loaded = false;
100
166
  dirty = false;
101
167
  dropped = false;
168
+ transactionClone = false;
102
169
  addHandlers = [];
103
170
  updateHandlers = [];
104
171
  deleteHandlers = [];
105
172
  indexDefinitions = new Map();
106
173
  indexRuntime = new Map();
174
+ indexedKeys = new Map();
175
+ txChanges;
107
176
  constructor(database, collectionID, collectionName, collectionConfig) {
108
177
  this.database = database;
109
178
  this.collectionID = collectionID;
110
179
  this.collectionName = canonicalName(collectionName);
111
180
  this.collectionConfig = collectionConfig;
112
- switch (collectionConfig.memStorageStrategy) {
113
- case EBMemStorageStrategy.MAP:
114
- // if (!collectionConfig.getID || !collectionConfig.setID) {
115
- // throw new Error("need id accessors configured when using map mem storage");
116
- // }
117
- break;
118
- default:
119
- case EBMemStorageStrategy.ARRAY:
120
- break;
121
- }
122
181
  }
123
182
  async checkLoaded() {
124
183
  if (!this.loaded) {
@@ -126,7 +185,7 @@ export class BCollection {
126
185
  }
127
186
  }
128
187
  async runExclusive(fn) {
129
- return runReentrantProtected(this.collectionName, fn);
188
+ return this.database.runExclusive(fn);
130
189
  }
131
190
  async runProtected(fn) {
132
191
  return this.runExclusive(async () => {
@@ -136,6 +195,14 @@ export class BCollection {
136
195
  return fn();
137
196
  });
138
197
  }
198
+ async runMutation(fn) {
199
+ return this.runProtected(async () => {
200
+ if (!this.transactionClone && this.database.isInTransaction(this.collectionName)) {
201
+ throw new Error(`collection "${this.collectionName}" is part of a running transaction; write through tx.getCollection() instead`);
202
+ }
203
+ return fn();
204
+ });
205
+ }
139
206
  /**
140
207
  * call this only within the protecting mutex
141
208
  */
@@ -181,7 +248,7 @@ export class BCollection {
181
248
  if (!byKey) {
182
249
  throw new Error(`index "${indexKey}" is not configured`);
183
250
  }
184
- return byKey.get(key)?.slice() ?? [];
251
+ return Array.from(byKey.get(key) ?? []);
185
252
  });
186
253
  }
187
254
  async invokeHandler(fn, modificationType) {
@@ -189,7 +256,8 @@ export class BCollection {
189
256
  await fn();
190
257
  }
191
258
  catch (error) {
192
- logger.error(`bajadab: collection "${this.collectionName}" ${modificationType} handler failed`, error);
259
+ // TODO switch logging to ILogging where necessary
260
+ console.error(`bajadab: collection "${this.collectionName}" ${modificationType} handler failed`, error);
193
261
  }
194
262
  }
195
263
  async fireAdd(added) {
@@ -246,10 +314,11 @@ export class BCollection {
246
314
  });
247
315
  }
248
316
  async delete(predicate) {
249
- return this.runProtected(async () => {
317
+ return this.runMutation(async () => {
250
318
  await this.checkLoaded();
251
319
  let ret = 0;
252
320
  const trackDeletes = this.deleteHandlers.length > 0;
321
+ const collectDeleted = trackDeletes || this.txChanges !== undefined;
253
322
  const indexDeletes = this.indexDefinitions.size > 0;
254
323
  const deleted = [];
255
324
  switch (this.collectionConfig.memStorageStrategy) {
@@ -258,10 +327,10 @@ export class BCollection {
258
327
  if (predicate(value)) {
259
328
  ret++;
260
329
  if (indexDeletes) {
261
- await this.removeFromIndexes(value);
330
+ this.removeFromIndexes(value);
262
331
  }
263
332
  this.mapData.delete(key);
264
- if (trackDeletes) {
333
+ if (collectDeleted) {
265
334
  deleted.push(value);
266
335
  }
267
336
  }
@@ -281,9 +350,9 @@ export class BCollection {
281
350
  else {
282
351
  ret++;
283
352
  if (indexDeletes) {
284
- await this.removeFromIndexes(item);
353
+ this.removeFromIndexes(item);
285
354
  }
286
- if (trackDeletes) {
355
+ if (collectDeleted) {
287
356
  deleted.push(item);
288
357
  }
289
358
  }
@@ -292,20 +361,29 @@ export class BCollection {
292
361
  break;
293
362
  default:
294
363
  case EBArrayDeletionStrategy.NEW_ARRAY:
295
- const ol = this.arrayData.length;
296
- if (trackDeletes || indexDeletes) {
297
- const removed = this.arrayData.filter(predicate);
298
- if (trackDeletes) {
299
- deleted.push(...removed);
300
- }
301
- if (indexDeletes) {
302
- for (const v of removed) {
303
- await this.removeFromIndexes(v);
364
+ if (collectDeleted || indexDeletes) {
365
+ const kept = [];
366
+ for (const v of this.arrayData) {
367
+ if (predicate(v)) {
368
+ ret++;
369
+ if (indexDeletes) {
370
+ this.removeFromIndexes(v);
371
+ }
372
+ if (collectDeleted) {
373
+ deleted.push(v);
374
+ }
375
+ }
376
+ else {
377
+ kept.push(v);
304
378
  }
305
379
  }
380
+ this.arrayData = kept;
381
+ }
382
+ else {
383
+ const ol = this.arrayData.length;
384
+ this.arrayData = this.arrayData.filter(v => !predicate(v));
385
+ ret += (ol - this.arrayData.length);
306
386
  }
307
- this.arrayData = this.arrayData.filter(v => !predicate(v));
308
- ret += (ol - this.arrayData.length);
309
387
  break;
310
388
  }
311
389
  break;
@@ -313,7 +391,8 @@ export class BCollection {
313
391
  if (ret > 0) {
314
392
  await this.doOnDirty();
315
393
  }
316
- if (deleted.length > 0) {
394
+ await this.recordTxChanges(EBModificationType.DELETE, deleted);
395
+ if (trackDeletes && deleted.length > 0) {
317
396
  await this.fireDelete(deleted);
318
397
  }
319
398
  return ret;
@@ -324,10 +403,10 @@ export class BCollection {
324
403
  await this.checkLoaded();
325
404
  switch (this.collectionConfig.memStorageStrategy) {
326
405
  case EBMemStorageStrategy.MAP:
327
- return this.mapData.values().find(predicate) !== undefined;
406
+ return this.mapData.values().some(predicate);
328
407
  default:
329
408
  case EBMemStorageStrategy.ARRAY:
330
- return this.arrayData.find(predicate) !== undefined;
409
+ return this.arrayData.some(predicate);
331
410
  }
332
411
  });
333
412
  }
@@ -356,31 +435,46 @@ export class BCollection {
356
435
  return result;
357
436
  }
358
437
  async addToIndexes(v) {
359
- for (const [indexKey, keys] of await this.indexKeysFor(v)) {
438
+ this.addKeysToIndexes(v, await this.indexKeysFor(v));
439
+ }
440
+ async indexKeysForAll(vs) {
441
+ const ret = [];
442
+ for (const v of vs) {
443
+ ret.push(await this.indexKeysFor(v));
444
+ }
445
+ return ret;
446
+ }
447
+ addKeysToIndexes(v, indexKeys) {
448
+ for (const [indexKey, keys] of indexKeys) {
360
449
  const byKey = this.indexRuntime.get(indexKey);
361
450
  for (const key of keys) {
362
451
  let bucket = byKey.get(key);
363
452
  if (!bucket) {
364
- bucket = [];
453
+ bucket = new Set();
365
454
  byKey.set(key, bucket);
366
455
  }
367
- bucket.push(v);
456
+ bucket.add(v);
368
457
  }
369
458
  }
459
+ if (indexKeys.length > 0) {
460
+ this.indexedKeys.set(v, indexKeys);
461
+ }
370
462
  }
371
- async removeFromIndexes(v) {
372
- for (const [indexKey, keys] of await this.indexKeysFor(v)) {
463
+ removeFromIndexes(v) {
464
+ const indexKeys = this.indexedKeys.get(v);
465
+ if (!indexKeys) {
466
+ return;
467
+ }
468
+ this.indexedKeys.delete(v);
469
+ for (const [indexKey, keys] of indexKeys) {
373
470
  const byKey = this.indexRuntime.get(indexKey);
374
471
  for (const key of keys) {
375
472
  const bucket = byKey.get(key);
376
473
  if (!bucket) {
377
474
  continue;
378
475
  }
379
- const at = bucket.indexOf(v);
380
- if (at !== -1) {
381
- bucket.splice(at, 1);
382
- }
383
- if (bucket.length === 0) {
476
+ bucket.delete(v);
477
+ if (bucket.size === 0) {
384
478
  byKey.delete(key);
385
479
  }
386
480
  }
@@ -394,6 +488,7 @@ export class BCollection {
394
488
  // every configured index gets an entry up front, even if empty, so getByIndex()/
395
489
  // addToIndexes()/removeFromIndexes() can assume it's there instead of checking twice
396
490
  this.indexRuntime = new Map(Array.from(this.indexDefinitions.keys(), (indexKey) => [indexKey, new Map()]));
491
+ this.indexedKeys = new Map();
397
492
  if (this.indexDefinitions.size === 0) {
398
493
  return;
399
494
  }
@@ -403,54 +498,122 @@ export class BCollection {
403
498
  await this.addToIndexes(v);
404
499
  }
405
500
  }
406
- async getID(v) {
407
- let getter;
501
+ resolveIDGetter() {
408
502
  if (this.collectionConfig.getID) {
409
503
  const found = this.database.functionRegistry?.fetch(this.collectionConfig.getID);
410
504
  if (!found) {
411
505
  throw new Error(`getID function "${this.collectionConfig.getID}" is not registered`);
412
506
  }
413
- getter = found;
507
+ return found;
414
508
  }
415
- else {
416
- getter = DEFAULT_GET_ID;
417
- }
418
- let id = await getter(v);
419
- if (!id) {
420
- switch (this.collectionConfig.autoIDStrategy) {
421
- case EBAutoIDStrategy.NO_AUTO_ID:
422
- throw new Error("cannot omit auto-ids and than trying to add a value without an id");
423
- default:
424
- case EBAutoIDStrategy.AUTO_ID_UUID:
425
- id = uuidv7();
426
- let setter;
427
- if (this.collectionConfig.setID) {
428
- const found = this.database.functionRegistry?.fetch(this.collectionConfig.setID);
429
- if (!found) {
430
- throw new Error(`setID function "${this.collectionConfig.setID}" is not registered`);
431
- }
432
- setter = found;
433
- }
434
- else {
435
- setter = DEFAULT_SET_ID;
509
+ return DEFAULT_GET_ID;
510
+ }
511
+ /**
512
+ * id -> value for every stored item, resolved via the plain id getter (no auto-id assignment - every
513
+ * stored item already has one). Used to diff a transaction's working copy against the pre-transaction
514
+ * state in {@link prepareTransactionCommit}.
515
+ */
516
+ async idKeyedSnapshot() {
517
+ switch (this.collectionConfig.memStorageStrategy) {
518
+ case EBMemStorageStrategy.MAP:
519
+ return this.mapData;
520
+ default:
521
+ case EBMemStorageStrategy.ARRAY:
522
+ const getter = this.resolveIDGetter();
523
+ const ret = new Map();
524
+ for (const v of this.arrayData) {
525
+ const id = normalizeID(await getter(v));
526
+ if (id) {
527
+ ret.set(id, v);
436
528
  }
437
- await setter(v, id);
438
- break;
529
+ }
530
+ return ret;
531
+ }
532
+ }
533
+ resolveIDSetter() {
534
+ if (this.collectionConfig.setID) {
535
+ const found = this.database.functionRegistry?.fetch(this.collectionConfig.setID);
536
+ if (!found) {
537
+ throw new Error(`setID function "${this.collectionConfig.setID}" is not registered`);
538
+ }
539
+ return found;
540
+ }
541
+ return DEFAULT_SET_ID;
542
+ }
543
+ async resolveIncomingIDs(vs) {
544
+ const getter = this.resolveIDGetter();
545
+ const ids = [];
546
+ for (const v of vs) {
547
+ const id = normalizeID(await getter(v));
548
+ if (!id && this.collectionConfig.autoIDStrategy === EBAutoIDStrategy.NO_AUTO_ID) {
549
+ throw new Error("cannot omit auto-ids and than trying to add a value without an id");
550
+ }
551
+ ids.push(id);
552
+ }
553
+ return ids;
554
+ }
555
+ async assignMissingIDs(vs, ids) {
556
+ let setter;
557
+ const ret = [];
558
+ for (let i = 0; i < vs.length; i++) {
559
+ let id = ids[i];
560
+ if (!id) {
561
+ setter ??= this.resolveIDSetter();
562
+ id = uuidv7();
563
+ await setter(vs[i], id);
564
+ }
565
+ ret.push(id);
566
+ }
567
+ return ret;
568
+ }
569
+ async storedArrayIDs() {
570
+ const getter = this.resolveIDGetter();
571
+ const ids = [];
572
+ for (const v of this.arrayData) {
573
+ ids.push(normalizeID(await getter(v)));
574
+ }
575
+ return ids;
576
+ }
577
+ async recordTxChanges(modificationType, values) {
578
+ if (!this.txChanges || values.length === 0) {
579
+ return;
580
+ }
581
+ const getter = this.resolveIDGetter();
582
+ for (const v of values) {
583
+ const id = normalizeID(await getter(v));
584
+ if (id) {
585
+ this.txChanges.set(id, modificationType);
439
586
  }
440
587
  }
441
- return id;
442
588
  }
443
589
  async add(...vs) {
444
- return this.runProtected(async () => {
590
+ return this.runMutation(async () => {
445
591
  await this.checkLoaded();
592
+ const trackAdds = this.addHandlers.length > 0;
593
+ const collectAdded = trackAdds || this.txChanges !== undefined;
594
+ const indexAdds = this.indexDefinitions.size > 0;
595
+ const incomingIDs = await this.resolveIncomingIDs(vs);
596
+ const added = [];
597
+ let ret = 0;
446
598
  switch (this.collectionConfig.memStorageStrategy) {
447
- case EBMemStorageStrategy.MAP:
448
- let ret = 0;
449
- const trackAddsMap = this.addHandlers.length > 0;
450
- const indexAddsMap = this.indexDefinitions.size > 0;
451
- const addedMap = [];
452
- for (const v of vs) {
453
- const id = await this.getID(v);
599
+ case EBMemStorageStrategy.MAP: {
600
+ if (this.collectionConfig.duplicateIDStrategy === EBDuplicateIDStrategy.ERROR) {
601
+ const batchIDs = new Set();
602
+ for (const id of incomingIDs) {
603
+ if (id === undefined) {
604
+ continue;
605
+ }
606
+ if (this.mapData.has(id) || batchIDs.has(id)) {
607
+ throw new Error(`duplicate id ${id}`);
608
+ }
609
+ batchIDs.add(id);
610
+ }
611
+ }
612
+ const ids = await this.assignMissingIDs(vs, incomingIDs);
613
+ const indexKeys = indexAdds ? await this.indexKeysForAll(vs) : [];
614
+ for (let i = 0; i < vs.length; i++) {
615
+ const id = ids[i];
616
+ const v = vs[i];
454
617
  if (this.mapData.has(id)) {
455
618
  switch (this.collectionConfig.duplicateIDStrategy) {
456
619
  case EBDuplicateIDStrategy.ERROR:
@@ -462,15 +625,15 @@ export class BCollection {
462
625
  case EBDuplicateIDStrategy.OVERWRITE:
463
626
  // replace the new value
464
627
  ret++;
465
- if (indexAddsMap) {
466
- await this.removeFromIndexes(this.mapData.get(id));
628
+ if (indexAdds) {
629
+ this.removeFromIndexes(this.mapData.get(id));
467
630
  }
468
631
  this.mapData.set(id, v);
469
- if (indexAddsMap) {
470
- await this.addToIndexes(v);
632
+ if (indexAdds) {
633
+ this.addKeysToIndexes(v, indexKeys[i]);
471
634
  }
472
- if (trackAddsMap) {
473
- addedMap.push(v);
635
+ if (collectAdded) {
636
+ added.push(v);
474
637
  }
475
638
  break;
476
639
  }
@@ -478,169 +641,164 @@ export class BCollection {
478
641
  else {
479
642
  ret++;
480
643
  this.mapData.set(id, v);
481
- if (indexAddsMap) {
482
- await this.addToIndexes(v);
644
+ if (indexAdds) {
645
+ this.addKeysToIndexes(v, indexKeys[i]);
483
646
  }
484
- if (trackAddsMap) {
485
- addedMap.push(v);
647
+ if (collectAdded) {
648
+ added.push(v);
486
649
  }
487
650
  }
488
651
  }
489
- if (ret > 0) {
490
- await this.doOnDirty();
491
- }
492
- if (addedMap.length > 0) {
493
- await this.fireAdd(addedMap);
494
- }
495
- return ret;
652
+ break;
653
+ }
496
654
  default:
497
- case EBMemStorageStrategy.ARRAY:
498
- const trackAddsArray = this.addHandlers.length > 0;
499
- const indexAddsArray = this.indexDefinitions.size > 0;
500
- const addedArray = [];
655
+ case EBMemStorageStrategy.ARRAY: {
656
+ const storedIDs = await this.storedArrayIDs();
501
657
  switch (this.collectionConfig.duplicateIDStrategy) {
502
- case EBDuplicateIDStrategy.ERROR:
503
- const haveIDs = new Set();
504
- for (const v of this.arrayData) {
505
- haveIDs.add(await this.getID(v));
506
- }
507
- for (const v of vs) {
508
- const id = await this.getID(v);
658
+ case EBDuplicateIDStrategy.ERROR: {
659
+ const haveIDs = new Set(storedIDs.filter((id) => id !== undefined));
660
+ for (const id of incomingIDs) {
661
+ if (id === undefined) {
662
+ continue;
663
+ }
509
664
  if (haveIDs.has(id)) {
510
665
  throw new Error(`duplicate id ${id}`);
511
666
  }
512
667
  haveIDs.add(id);
668
+ }
669
+ await this.assignMissingIDs(vs, incomingIDs);
670
+ const indexKeys = indexAdds ? await this.indexKeysForAll(vs) : [];
671
+ vs.forEach((v, i) => {
672
+ ret++;
513
673
  this.arrayData.push(v);
514
- if (indexAddsArray) {
515
- await this.addToIndexes(v);
674
+ if (indexAdds) {
675
+ this.addKeysToIndexes(v, indexKeys[i]);
516
676
  }
517
- if (trackAddsArray) {
518
- addedArray.push(v);
677
+ if (collectAdded) {
678
+ added.push(v);
519
679
  }
520
- }
680
+ });
521
681
  break;
522
- case EBDuplicateIDStrategy.IGNORE:
523
- const seenIDs = new Set();
524
- for (const v of this.arrayData) {
525
- seenIDs.add(await this.getID(v));
526
- }
527
- for (const v of vs) {
528
- const id = await this.getID(v);
682
+ }
683
+ case EBDuplicateIDStrategy.IGNORE: {
684
+ const ids = await this.assignMissingIDs(vs, incomingIDs);
685
+ const seenIDs = new Set(storedIDs.filter((id) => id !== undefined));
686
+ const toAdd = [];
687
+ ids.forEach((id, i) => {
529
688
  if (!seenIDs.has(id)) {
530
689
  seenIDs.add(id);
531
- this.arrayData.push(v);
532
- if (indexAddsArray) {
533
- await this.addToIndexes(v);
534
- }
535
- if (trackAddsArray) {
536
- addedArray.push(v);
537
- }
690
+ toAdd.push(vs[i]);
538
691
  }
539
- }
692
+ });
693
+ const indexKeys = indexAdds ? await this.indexKeysForAll(toAdd) : [];
694
+ toAdd.forEach((v, i) => {
695
+ ret++;
696
+ this.arrayData.push(v);
697
+ if (indexAdds) {
698
+ this.addKeysToIndexes(v, indexKeys[i]);
699
+ }
700
+ if (collectAdded) {
701
+ added.push(v);
702
+ }
703
+ });
540
704
  break;
705
+ }
541
706
  default:
542
- case EBDuplicateIDStrategy.OVERWRITE:
543
- const indexMap = new Map();
544
- for (let i = 0; i < this.arrayData.length; i++) {
545
- indexMap.set(await this.getID(this.arrayData[i]), i);
546
- }
547
- for (const v of vs) {
548
- const id = await this.getID(v);
549
- const existingIndex = indexMap.get(id);
707
+ case EBDuplicateIDStrategy.OVERWRITE: {
708
+ const ids = await this.assignMissingIDs(vs, incomingIDs);
709
+ const indexKeys = indexAdds ? await this.indexKeysForAll(vs) : [];
710
+ const positions = new Map();
711
+ storedIDs.forEach((id, i) => {
712
+ if (id !== undefined) {
713
+ positions.set(id, i);
714
+ }
715
+ });
716
+ vs.forEach((v, i) => {
717
+ const id = ids[i];
718
+ const existingIndex = positions.get(id);
719
+ ret++;
550
720
  if (existingIndex !== undefined) {
551
- if (indexAddsArray) {
552
- await this.removeFromIndexes(this.arrayData[existingIndex]);
721
+ if (indexAdds) {
722
+ this.removeFromIndexes(this.arrayData[existingIndex]);
553
723
  }
554
724
  this.arrayData[existingIndex] = v;
555
725
  }
556
726
  else {
557
- // store new index in indexCache in the case this id appears again
558
- indexMap.set(id, this.arrayData.length);
727
+ positions.set(id, this.arrayData.length);
559
728
  this.arrayData.push(v);
560
729
  }
561
- if (indexAddsArray) {
562
- await this.addToIndexes(v);
730
+ if (indexAdds) {
731
+ this.addKeysToIndexes(v, indexKeys[i]);
563
732
  }
564
- if (trackAddsArray) {
565
- addedArray.push(v);
733
+ if (collectAdded) {
734
+ added.push(v);
566
735
  }
567
- }
736
+ });
568
737
  break;
738
+ }
569
739
  }
570
- if (vs.length > 0) {
571
- await this.doOnDirty();
572
- }
573
- if (addedArray.length > 0) {
574
- await this.fireAdd(addedArray);
575
- }
576
- return vs.length;
740
+ break;
741
+ }
742
+ }
743
+ if (ret > 0) {
744
+ await this.doOnDirty();
745
+ }
746
+ await this.recordTxChanges(EBModificationType.ADD, added);
747
+ if (trackAdds && added.length > 0) {
748
+ await this.fireAdd(added);
577
749
  }
750
+ return ret;
578
751
  });
579
752
  }
580
753
  async update(predicate, updater) {
581
- return this.runProtected(async () => {
754
+ return this.runMutation(async () => {
582
755
  await this.checkLoaded();
583
- let ret = 0;
584
756
  const trackUpdates = this.updateHandlers.length > 0;
585
757
  const indexUpdates = this.indexDefinitions.size > 0;
586
- const updated = [];
758
+ const planned = [];
759
+ const plan = async (previous, store) => {
760
+ const replacement = await updater(previous);
761
+ if (replacement === undefined) {
762
+ throw new Error("updater gave undefined");
763
+ }
764
+ const replacementKeys = indexUpdates ? await this.indexKeysFor(replacement) : [];
765
+ planned.push({ previous, replacement, replacementKeys, store });
766
+ };
587
767
  switch (this.collectionConfig.memStorageStrategy) {
588
768
  case EBMemStorageStrategy.MAP:
589
769
  for (const [key, value] of this.mapData) {
590
770
  if (predicate(value)) {
591
- const replacement = await updater(value);
592
- if (replacement !== undefined) {
593
- ret++;
594
- if (indexUpdates) {
595
- await this.removeFromIndexes(value);
596
- }
597
- this.mapData.set(key, replacement);
598
- if (indexUpdates) {
599
- await this.addToIndexes(replacement);
600
- }
601
- if (trackUpdates) {
602
- updated.push([value, replacement]);
603
- }
604
- }
605
- else {
606
- throw new Error("updater gave undefined");
607
- }
771
+ await plan(value, (replacement) => { this.mapData.set(key, replacement); });
608
772
  }
609
773
  }
610
774
  break;
611
775
  default:
612
776
  case EBMemStorageStrategy.ARRAY:
613
777
  for (let i = 0; i < this.arrayData.length; i++) {
614
- if (predicate(this.arrayData[i])) {
615
- const previous = this.arrayData[i];
616
- const replacement = await updater(previous);
617
- if (replacement !== undefined) {
618
- ret++;
619
- if (indexUpdates) {
620
- await this.removeFromIndexes(previous);
621
- }
622
- this.arrayData[i] = replacement;
623
- if (indexUpdates) {
624
- await this.addToIndexes(replacement);
625
- }
626
- if (trackUpdates) {
627
- updated.push([previous, replacement]);
628
- }
629
- }
630
- else {
631
- throw new Error("updater gave undefined");
632
- }
778
+ const previous = this.arrayData[i];
779
+ if (predicate(previous)) {
780
+ await plan(previous, (replacement) => { this.arrayData[i] = replacement; });
633
781
  }
634
782
  }
635
783
  break;
636
784
  }
637
- if (ret > 0) {
785
+ for (const { previous, replacement, replacementKeys, store } of planned) {
786
+ if (indexUpdates) {
787
+ this.removeFromIndexes(previous);
788
+ }
789
+ store(replacement);
790
+ if (indexUpdates) {
791
+ this.addKeysToIndexes(replacement, replacementKeys);
792
+ }
793
+ }
794
+ if (planned.length > 0) {
638
795
  await this.doOnDirty();
639
796
  }
640
- if (updated.length > 0) {
641
- await this.fireUpdate(updated);
797
+ await this.recordTxChanges(EBModificationType.UPDATE, planned.map(({ replacement }) => replacement));
798
+ if (trackUpdates && planned.length > 0) {
799
+ await this.fireUpdate(planned.map(({ previous, replacement }) => [previous, replacement]));
642
800
  }
643
- return ret;
801
+ return planned.length;
644
802
  });
645
803
  }
646
804
  async doOnDirty() {
@@ -676,10 +834,10 @@ export class BCollection {
676
834
  this.arrayData = await this.database.io.loadArrayData(this.collectionID);
677
835
  break;
678
836
  }
679
- this.loaded = true;
680
- this.dirty = false;
681
837
  this.resolveIndexDefinitions();
682
838
  await this.rebuildIndexRuntime();
839
+ this.loaded = true;
840
+ this.dirty = false;
683
841
  });
684
842
  }
685
843
  async unload() {
@@ -706,9 +864,96 @@ export class BCollection {
706
864
  this.arrayData = [];
707
865
  break;
708
866
  }
867
+ this.indexRuntime.clear();
868
+ this.indexedKeys.clear();
709
869
  this.loaded = false;
710
870
  this.dirty = false;
711
871
  });
712
872
  }
873
+ /**
874
+ * Creates a transaction-scoped working copy: same identity/config as this collection, but with
875
+ * independently mutable in-memory state (auto-flush forced off, regardless of this collection's own
876
+ * `autoFlushStrategy`) and no change-event handlers of its own. Call only while already holding this
877
+ * collection's mutex for the whole transaction (see {@link BDatabase.transaction}).
878
+ */
879
+ async cloneForTransaction() {
880
+ return this.runProtected(async () => {
881
+ await this.checkLoaded();
882
+ const txConfig = { ...this.collectionConfig, autoFlushStrategy: EBAutoFlushStrategy.NO_AUTO_FLUSH };
883
+ const clone = new BCollection(this.database, this.collectionID, this.collectionName, txConfig);
884
+ clone.mapData = new Map(this.mapData);
885
+ clone.arrayData = this.arrayData.slice();
886
+ clone.loaded = true;
887
+ clone.transactionClone = true;
888
+ if (this.addHandlers.length > 0 || this.updateHandlers.length > 0 || this.deleteHandlers.length > 0) {
889
+ clone.txChanges = new Map();
890
+ }
891
+ clone.resolveIndexDefinitions();
892
+ await clone.rebuildIndexRuntime();
893
+ return clone;
894
+ });
895
+ }
896
+ /**
897
+ * Writes `clone`'s state to a temp file (see {@link IPreparedWrite}) and computes the handler events for the
898
+ * net changes against this collection's current state: one per id the transaction touched, typed by the last
899
+ * operation on it (`DELETE` if gone, `ADD` if new or last stored via `add()`, `UPDATE` otherwise). Nothing is
900
+ * applied until `apply()`; call only while already holding this collection's mutex for the whole transaction.
901
+ */
902
+ async prepareTransactionCommit(clone) {
903
+ return this.runProtected(async () => {
904
+ await this.checkLoaded();
905
+ const added = [];
906
+ const updated = [];
907
+ const deleted = [];
908
+ if (clone.txChanges && clone.txChanges.size > 0) {
909
+ const before = await this.idKeyedSnapshot();
910
+ const after = await clone.idKeyedSnapshot();
911
+ for (const [id, modificationType] of clone.txChanges) {
912
+ if (!after.has(id)) {
913
+ if (before.has(id)) {
914
+ deleted.push(before.get(id));
915
+ }
916
+ }
917
+ else if (!before.has(id) || modificationType === EBModificationType.ADD) {
918
+ added.push(after.get(id));
919
+ }
920
+ else {
921
+ updated.push([before.get(id), after.get(id)]);
922
+ }
923
+ }
924
+ }
925
+ let write;
926
+ switch (this.collectionConfig.memStorageStrategy) {
927
+ case EBMemStorageStrategy.MAP:
928
+ write = await this.database.io.prepareMapData(this.collectionID, clone.mapData);
929
+ break;
930
+ default:
931
+ case EBMemStorageStrategy.ARRAY:
932
+ write = await this.database.io.prepareArrayData(this.collectionID, clone.arrayData);
933
+ break;
934
+ }
935
+ return {
936
+ write,
937
+ apply: () => {
938
+ this.mapData = clone.mapData;
939
+ this.arrayData = clone.arrayData;
940
+ this.indexRuntime = clone.indexRuntime;
941
+ this.indexedKeys = clone.indexedKeys;
942
+ this.dirty = false;
943
+ },
944
+ fire: () => this.runProtected(async () => {
945
+ if (added.length > 0) {
946
+ await this.fireAdd(added);
947
+ }
948
+ if (updated.length > 0) {
949
+ await this.fireUpdate(updated);
950
+ }
951
+ if (deleted.length > 0) {
952
+ await this.fireDelete(deleted);
953
+ }
954
+ }),
955
+ };
956
+ });
957
+ }
713
958
  }
714
959
  //# sourceMappingURL=basics.js.map