@travetto/model-memory 8.0.0-alpha.2 → 8.0.0-alpha.21

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 (3) hide show
  1. package/README.md +1 -1
  2. package/package.json +8 -7
  3. package/src/service.ts +220 -65
package/README.md CHANGED
@@ -16,5 +16,5 @@ yarn add @travetto/model-memory
16
16
  This module provides a memory-based implementation for the [Data Modeling Support](https://github.com/travetto/travetto/tree/main/module/model#readme "Datastore abstraction for core operations."). Supported features:
17
17
  * [CRUD](https://github.com/travetto/travetto/tree/main/module/model/src/types/crud.ts#L11)
18
18
  * [Expiry](https://github.com/travetto/travetto/tree/main/module/model/src/types/expiry.ts#L10)
19
- * [Indexed](https://github.com/travetto/travetto/tree/main/module/model/src/types/indexed.ts#L11)
20
19
  * [Blob](https://github.com/travetto/travetto/tree/main/module/model/src/types/blob.ts#L8)
20
+ * [Indexed](https://github.com/travetto/travetto/tree/main/module/model-indexed/src/types/service.ts#L16)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/model-memory",
3
- "version": "8.0.0-alpha.2",
3
+ "version": "8.0.0-alpha.21",
4
4
  "type": "module",
5
5
  "description": "Memory backing for the travetto model module.",
6
6
  "keywords": [
@@ -26,14 +26,15 @@
26
26
  "directory": "module/model-memory"
27
27
  },
28
28
  "dependencies": {
29
- "@travetto/config": "^8.0.0-alpha.2",
30
- "@travetto/di": "^8.0.0-alpha.2",
31
- "@travetto/model": "^8.0.0-alpha.2",
32
- "@travetto/schema": "^8.0.0-alpha.2"
29
+ "@travetto/config": "^8.0.0-alpha.18",
30
+ "@travetto/di": "^8.0.0-alpha.17",
31
+ "@travetto/model": "^8.0.0-alpha.19",
32
+ "@travetto/model-indexed": "^8.0.0-alpha.21",
33
+ "@travetto/schema": "^8.0.0-alpha.18"
33
34
  },
34
35
  "peerDependencies": {
35
- "@travetto/cli": "^8.0.0-alpha.3",
36
- "@travetto/test": "^8.0.0-alpha.2"
36
+ "@travetto/cli": "^8.0.0-alpha.24",
37
+ "@travetto/test": "^8.0.0-alpha.17"
37
38
  },
38
39
  "peerDependenciesMeta": {
39
40
  "@travetto/cli": {
package/src/service.ts CHANGED
@@ -1,20 +1,31 @@
1
1
  import {
2
- type Class, type TimeSpan, type DeepPartial, castTo, type BinaryMetadata,
3
- type ByteRange, type BinaryType, BinaryUtil, type BinaryArray, JSONUtil, BinaryMetadataUtil
2
+ type Class, type TimeSpan, castTo, type BinaryMetadata,
3
+ type ByteRange, type BinaryType, BinaryUtil, type BinaryArray, JSONUtil, BinaryMetadataUtil,
4
4
  } from '@travetto/runtime';
5
5
  import { Injectable, PostConstruct } from '@travetto/di';
6
6
  import { Config } from '@travetto/config';
7
7
  import {
8
- type ModelType, type IndexConfig, type ModelCrudSupport, type ModelExpirySupport, type ModelStorageSupport, type ModelIndexedSupport,
9
- ModelRegistryIndex, NotFoundError, ExistsError, type OptionalId, type ModelBlobSupport,
10
- ModelCrudUtil, ModelExpiryUtil, ModelIndexedUtil, ModelStorageUtil
8
+ type ModelType, type ModelCrudSupport, type ModelExpirySupport, type ModelStorageSupport, ModelRegistryIndex,
9
+ NotFoundError, ExistsError, type OptionalId, type ModelBlobSupport, ModelCrudUtil, ModelExpiryUtil, ModelStorageUtil,
10
+ IndexNotSupported,
11
+ type ModelListOptions,
11
12
  } from '@travetto/model';
13
+ import {
14
+ type ModelIndexedSupport, type KeyedIndexSelection, type KeyedIndexBody, type ModelPageOptions, ModelIndexedUtil,
15
+ type SingleItemIndex, type SortedIndexSelection, type ModelPageResult, type SortedIndex,
16
+ type AllIndexes, isModelIndexedIndex, type FullKeyedIndexBody, type FullKeyedIndexWithPartialBody, ModelIndexedComputedIndex,
17
+ type ModelIndexedSearchOptions, type SortedIndexSelectionType,
18
+ } from '@travetto/model-indexed';
12
19
 
13
20
  const ModelBlobNamespace = '__blobs';
14
21
  const ModelBlobMetaNamespace = `${ModelBlobNamespace}_meta`;
15
22
 
16
23
  type StoreType = Map<string, BinaryArray>;
17
24
 
25
+ const sortValue = (a: string | number, b: string | number): number =>
26
+ (typeof a === 'number' && typeof b === 'number') ?
27
+ a - b : (typeof a === 'string' && typeof b === 'string') ? a.localeCompare(b) : 0;
28
+
18
29
  @Config('model.memory')
19
30
  export class MemoryModelConfig {
20
31
  modifyStorage?: boolean = true;
@@ -22,8 +33,8 @@ export class MemoryModelConfig {
22
33
  cullRate?: number | TimeSpan;
23
34
  }
24
35
 
25
- function indexName<T extends ModelType>(cls: Class<T>, idx: IndexConfig<T> | string, suffix?: string): string {
26
- return [cls.Ⲑid, typeof idx === 'string' ? idx : idx.name, suffix].filter(part => !!part).join(':');
36
+ function indexName<T extends ModelType>(cls: Class<T>, idx: AllIndexes<T>, suffix?: string): string {
37
+ return [cls.Ⲑid, idx.name, suffix].filter(part => !!part).join(':');
27
38
  }
28
39
 
29
40
  function getFirstId(data: Map<string, unknown> | Set<string>, value?: string | number): string | undefined {
@@ -40,13 +51,16 @@ function getFirstId(data: Map<string, unknown> | Set<string>, value?: string | n
40
51
  * Standard in-memory support
41
52
  */
42
53
  @Injectable()
43
- export class MemoryModelService implements ModelCrudSupport, ModelBlobSupport, ModelExpirySupport, ModelStorageSupport, ModelIndexedSupport {
54
+ export class MemoryModelService implements
55
+ ModelCrudSupport, ModelBlobSupport,
56
+ ModelExpirySupport, ModelStorageSupport,
57
+ ModelIndexedSupport {
44
58
 
45
59
  #store = new Map<string, StoreType>();
46
60
  #indices = {
47
- sorted: new Map<string, Map<string, Map<string, number>>>(),
48
- unsorted: new Map<string, Map<string, Set<string>>>()
49
- };
61
+ 'indexed:sorted': new Map<string, Map<string, Map<string, number | string>>>(),
62
+ 'indexed:keyed': new Map<string, Map<string, Set<string>>>(),
63
+ } as const;
50
64
 
51
65
  idSource = ModelCrudUtil.uuidSource();
52
66
  config: MemoryModelConfig;
@@ -73,10 +87,16 @@ export class MemoryModelService implements ModelCrudSupport, ModelBlobSupport, M
73
87
  async #removeIndices<T extends ModelType>(cls: Class<T>, id: string): Promise<void> {
74
88
  try {
75
89
  const item = await this.get(cls, id);
76
- for (const idx of ModelRegistryIndex.getIndices(cls, ['sorted', 'unsorted'])) {
90
+ for (const idx of ModelRegistryIndex.getIndices(cls)) {
91
+ if (!isModelIndexedIndex(idx)) {
92
+ continue; // Only support ModelIndexed indices
93
+ }
77
94
  const idxName = indexName(cls, idx);
78
- const { key } = ModelIndexedUtil.computeIndexKey(cls, idx, castTo(item));
79
- this.#indices[idx.type].get(idxName)?.get(key)?.delete(id);
95
+ const computed = ModelIndexedComputedIndex.get(idx, item).validate({ sort: true });
96
+ switch (idx.type) {
97
+ case 'indexed:sorted':
98
+ case 'indexed:keyed': this.#indices[idx.type].get(idxName)?.get(computed.getKey())?.delete(id); break;
99
+ }
80
100
  }
81
101
  } catch (error) {
82
102
  if (!(error instanceof NotFoundError)) {
@@ -86,14 +106,28 @@ export class MemoryModelService implements ModelCrudSupport, ModelBlobSupport, M
86
106
  }
87
107
 
88
108
  async #writeIndices<T extends ModelType>(cls: Class<T>, item: T): Promise<void> {
89
- for (const idx of ModelRegistryIndex.getIndices(cls, ['sorted', 'unsorted'])) {
109
+ for (const idx of ModelRegistryIndex.getIndices(cls)) {
110
+ if (!isModelIndexedIndex(idx)) {
111
+ continue; // Only support ModelIndexed indices
112
+ }
90
113
  const idxName = indexName(cls, idx);
91
- const { key, sort } = ModelIndexedUtil.computeIndexKey(cls, idx, castTo(item));
92
-
93
- if (idx.type === 'sorted') {
94
- this.#indices[idx.type].getOrInsert(idxName, new Map()).getOrInsert(key, new Map()).set(item.id, +sort!);
95
- } else {
96
- this.#indices[idx.type].getOrInsert(idxName, new Map()).getOrInsert(key, new Set()).add(item.id);
114
+ const computed = ModelIndexedComputedIndex.get(idx, item).validate({ sort: true });
115
+ const key = computed.getKey();
116
+ switch (idx.type) {
117
+ case 'indexed:keyed': {
118
+ if (idx.unique) {
119
+ const existing = this.#indices[idx.type].get(idxName)?.get(key);
120
+ if (existing && existing.size > 0 && !existing.has(item.id)) {
121
+ throw new ExistsError(cls, key);
122
+ }
123
+ }
124
+ this.#indices[idx.type].getOrInsert(idxName, new Map()).getOrInsert(key, new Set()).add(item.id);
125
+ break;
126
+ }
127
+ case 'indexed:sorted': {
128
+ this.#indices[idx.type].getOrInsert(idxName, new Map()).getOrInsert(key, new Map()).set(item.id, computed.getSort());
129
+ break;
130
+ }
97
131
  }
98
132
  }
99
133
  }
@@ -112,22 +146,87 @@ export class MemoryModelService implements ModelCrudSupport, ModelBlobSupport, M
112
146
  }
113
147
  }
114
148
 
115
- async #getIdByIndex<T extends ModelType>(cls: Class<T>, idx: string, body: DeepPartial<T>): Promise<string> {
116
- const config = ModelRegistryIndex.getIndex(cls, idx, ['sorted', 'unsorted']);
117
- const { key, sort } = ModelIndexedUtil.computeIndexKey(cls, config, body);
118
- const index = this.#indices[config.type].get(indexName(cls, idx))?.get(key);
149
+ async #getIdByIndex<
150
+ T extends ModelType,
151
+ K extends KeyedIndexSelection<T>,
152
+ S extends SortedIndexSelection<T>
153
+ >(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: FullKeyedIndexBody<T, K, S>): Promise<string> {
154
+ const computed = ModelIndexedComputedIndex.get(idx, body).validate({ sort: true });
155
+
156
+ const index = this.#indices[idx.type].get(indexName(cls, idx))?.get(computed.getKey());
119
157
  let id: string | undefined;
120
158
  if (index) {
121
- if (index instanceof Map) {
122
- id = getFirstId(index, +sort!); // Grab first id
159
+ if (computed.idPart) {
160
+ if (index.has(computed.idPart.value)) {
161
+ id = computed.idPart.value;
162
+ } else {
163
+ throw new NotFoundError(cls, computed.getKey({ sort: true }));
164
+ }
123
165
  } else {
124
- id = getFirstId(index); // Grab first id
166
+ if (index instanceof Map) {
167
+ id = getFirstId(index, computed.getSort()); // Grab first id
168
+ } else if (index instanceof Set) {
169
+ id = getFirstId(index); // Grab first id
170
+ }
125
171
  }
126
172
  }
127
173
  if (id) {
128
174
  return id;
129
175
  }
130
- throw new NotFoundError(cls, key);
176
+ throw new NotFoundError(cls, computed.getKey({ sort: true }));
177
+ }
178
+
179
+ async * #iterateIds(
180
+ ids: string[],
181
+ options?: ModelListOptions & ModelPageOptions<number>,
182
+ ): AsyncIterable<string[]> {
183
+ let offset = options && 'offset' in options ? options.offset ?? 0 : 0;
184
+ const batchSize = options?.batchSizeHint ?? 100;
185
+ let produced = 0;
186
+ const maxCount = options?.limit ?? Number.MAX_SAFE_INTEGER;
187
+ while (offset < ids.length && produced < maxCount) {
188
+ if (options?.abort?.aborted) {
189
+ break;
190
+ }
191
+ const end = Math.min(offset + batchSize, ids.length, maxCount - produced + offset);
192
+ const batch = ids.slice(offset, end);
193
+ if (batch.length) {
194
+ yield batch;
195
+ }
196
+ offset += batchSize;
197
+ produced += batch.length;
198
+ }
199
+ }
200
+
201
+ async * #getIndexIds<
202
+ T extends ModelType,
203
+ >(
204
+ cls: Class<T>,
205
+ idx: AllIndexes<T>,
206
+ body: KeyedIndexBody<T>,
207
+ options?: ModelListOptions & ModelPageOptions<number>,
208
+ filterIndex?: (indexValue: string | number) => boolean
209
+ ): AsyncIterable<string[]> {
210
+ const computed = ModelIndexedComputedIndex.get(idx, body).validate();
211
+ if (!isModelIndexedIndex(idx)) {
212
+ throw new IndexNotSupported(cls, idx, 'Only ModelIndexed indices can be used with MemoryModelService');
213
+ }
214
+
215
+ const base = this.#indices[idx.type].get(indexName(cls, idx));
216
+ const index = base?.get(computed.getKey());
217
+ let ids: string[];
218
+ if (!index) {
219
+ ids = [];
220
+ } else if (index instanceof Map) {
221
+ ids = [...index.entries()]
222
+ .filter(([, sort]) => filterIndex ? filterIndex(sort) : true)
223
+ .sort((a, b) => sortValue(a[1], b[1]))
224
+ .map(([id,]) => id);
225
+ } else {
226
+ ids = [...index];
227
+ }
228
+
229
+ yield* this.#iterateIds(ids, options);
131
230
  }
132
231
 
133
232
  @PostConstruct()
@@ -136,12 +235,9 @@ export class MemoryModelService implements ModelCrudSupport, ModelBlobSupport, M
136
235
  ModelExpiryUtil.registerCull(this);
137
236
 
138
237
  for (const cls of ModelRegistryIndex.getClasses()) {
139
- for (const idx of ModelRegistryIndex.getConfig(cls).indices ?? []) {
140
- switch (idx.type) {
141
- case 'unique': {
142
- console.error('Unique indices are not supported for', { cls: cls.Ⲑid, idx: idx.name });
143
- break;
144
- }
238
+ for (const idx of Object.values(ModelRegistryIndex.getConfig(cls).indices ?? {})) {
239
+ if (!isModelIndexedIndex(idx)) {
240
+ console.error(`Indices of type ${idx.type} are not supported for`, { cls: cls.Ⲑid, name: idx.name, type: idx.type });
145
241
  }
146
242
  }
147
243
  }
@@ -203,15 +299,9 @@ export class MemoryModelService implements ModelCrudSupport, ModelBlobSupport, M
203
299
  await this.#persist(cls, where, 'remove');
204
300
  }
205
301
 
206
- async * list<T extends ModelType>(cls: Class<T>): AsyncIterable<T> {
207
- for (const id of this.#getStore(cls).keys()) {
208
- try {
209
- yield await this.get(cls, id);
210
- } catch (error) {
211
- if (!(error instanceof NotFoundError)) {
212
- throw error;
213
- }
214
- }
302
+ async * list<T extends ModelType>(cls: Class<T>, options?: ModelListOptions): AsyncIterable<T[]> {
303
+ for await (const batch of this.#iterateIds([...this.#getStore(cls).keys()], options)) {
304
+ yield ModelCrudUtil.filterOutNotFound(batch.map(id => this.get(cls, id)));
215
305
  }
216
306
  }
217
307
 
@@ -285,13 +375,14 @@ export class MemoryModelService implements ModelCrudSupport, ModelBlobSupport, M
285
375
 
286
376
  async deleteStorage(): Promise<void> {
287
377
  this.#store.clear();
288
- this.#indices.sorted.clear();
289
- this.#indices.unsorted.clear();
378
+ for (const value of Object.values(this.#indices)) {
379
+ value.clear();
380
+ }
290
381
  }
291
382
 
292
383
  async upsertModel<T extends ModelType>(cls: Class<T>): Promise<void> {
293
- for (const idx of ModelRegistryIndex.getConfig(cls).indices ?? []) {
294
- if (idx.type === 'sorted' || idx.type === 'unsorted') {
384
+ for (const idx of ModelRegistryIndex.getIndices(cls)) {
385
+ if (isModelIndexedIndex(idx)) {
295
386
  this.#indices[idx.type].set(indexName(cls, idx), new Map());
296
387
  }
297
388
  }
@@ -307,33 +398,97 @@ export class MemoryModelService implements ModelCrudSupport, ModelBlobSupport, M
307
398
  }
308
399
 
309
400
  // Indexed
310
- async getByIndex<T extends ModelType>(cls: Class<T>, idx: string, body: DeepPartial<T>): Promise<T> {
401
+ async getByIndex<
402
+ T extends ModelType,
403
+ K extends KeyedIndexSelection<T>,
404
+ S extends SortedIndexSelection<T>
405
+ >(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: FullKeyedIndexBody<T, K, S>): Promise<T> {
311
406
  return this.get(cls, await this.#getIdByIndex(cls, idx, body));
407
+
312
408
  }
313
409
 
314
- async deleteByIndex<T extends ModelType>(cls: Class<T>, idx: string, body: DeepPartial<T>): Promise<void> {
410
+ async deleteByIndex<
411
+ T extends ModelType,
412
+ K extends KeyedIndexSelection<T>,
413
+ S extends SortedIndexSelection<T>
414
+ >(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: FullKeyedIndexBody<T, K, S>): Promise<void> {
315
415
  await this.delete(cls, await this.#getIdByIndex(cls, idx, body));
316
416
  }
317
417
 
318
- upsertByIndex<T extends ModelType>(cls: Class<T>, idx: string, body: OptionalId<T>): Promise<T> {
418
+ upsertByIndex<
419
+ T extends ModelType,
420
+ K extends KeyedIndexSelection<T>,
421
+ S extends SortedIndexSelection<T>
422
+ >(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: OptionalId<T>): Promise<T> {
319
423
  return ModelIndexedUtil.naiveUpsert(this, cls, idx, body);
320
424
  }
321
425
 
322
- async * listByIndex<T extends ModelType>(cls: Class<T>, idx: string, body?: DeepPartial<T>): AsyncIterable<T> {
323
- const config = ModelRegistryIndex.getIndex(cls, idx, ['sorted', 'unsorted']);
324
- const { key } = ModelIndexedUtil.computeIndexKey(cls, idx, body, { emptySortValue: null });
325
- const index = this.#indices[config.type].get(indexName(cls, idx))?.get(key);
426
+ updateByIndex<
427
+ T extends ModelType,
428
+ K extends KeyedIndexSelection<T>,
429
+ S extends SortedIndexSelection<T>
430
+ >(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: T): Promise<T> {
431
+ return ModelIndexedUtil.naiveUpdate(this, cls, idx, body);
432
+ }
326
433
 
327
- if (index) {
328
- if (index instanceof Set) {
329
- for (const id of index) {
330
- yield this.get(cls, id);
331
- }
332
- } else {
333
- for (const id of [...index.entries()].toSorted((a, b) => +a[1] - +b[1]).map(([a,]) => a)) {
334
- yield this.get(cls, id);
335
- }
336
- }
434
+ async updatePartialByIndex<
435
+ T extends ModelType,
436
+ K extends KeyedIndexSelection<T>,
437
+ S extends SortedIndexSelection<T>
438
+ >(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: FullKeyedIndexWithPartialBody<T, K, S>): Promise<T> {
439
+ const item = await ModelCrudUtil.naivePartialUpdate(cls, () => this.getByIndex(cls, idx, castTo(body)), castTo(body));
440
+ return this.update(cls, item);
441
+ }
442
+
443
+ async pageByIndex<
444
+ T extends ModelType,
445
+ K extends KeyedIndexSelection<T>,
446
+ S extends SortedIndexSelection<T>
447
+ >(
448
+ cls: Class<T>,
449
+ idx: SortedIndex<T, K, S>,
450
+ body: KeyedIndexBody<T, K>,
451
+ options?: ModelPageOptions
452
+ ): Promise<ModelPageResult<T>> {
453
+ const offset = options?.offset ? JSONUtil.fromBase64<number>(options.offset) : 0;
454
+ let produced = 0;
455
+
456
+ const items: T[] = [];
457
+ for await (const batch of this.#getIndexIds(cls, idx, body, { limit: 100, ...options, offset })) {
458
+ produced += batch.length;
459
+ items.push(...await ModelCrudUtil.filterOutNotFound(batch.map(id => this.get(cls, id))));
460
+ }
461
+ return { items, nextOffset: items.length ? JSONUtil.toBase64(offset + produced) : undefined };
462
+ }
463
+
464
+ async * listByIndex<
465
+ T extends ModelType,
466
+ K extends KeyedIndexSelection<T>,
467
+ S extends SortedIndexSelection<T>
468
+ >(
469
+ cls: Class<T>,
470
+ idx: SortedIndex<T, K, S>,
471
+ body: KeyedIndexBody<T, K>,
472
+ options?: ModelListOptions
473
+ ): AsyncIterable<T[]> {
474
+ for await (const batch of this.#getIndexIds(cls, idx, body, options)) {
475
+ yield ModelCrudUtil.filterOutNotFound(batch.map(id => this.get(cls, id)));
476
+ }
477
+ }
478
+
479
+ async suggestByIndex<
480
+ T extends ModelType,
481
+ S extends SortedIndexSelection<T>,
482
+ K extends KeyedIndexSelection<T>,
483
+ B extends SortedIndexSelectionType<T, S> & string
484
+ >(cls: Class<T>, idx: SortedIndex<T, K, S>, body: KeyedIndexBody<T, K>, prefix: B, options?: ModelIndexedSearchOptions): Promise<T[]> {
485
+ const items: T[] = [];
486
+ for await (const batch of this.#getIndexIds(
487
+ cls, idx, body, options,
488
+ id => (typeof id === 'string' && id.startsWith(prefix))
489
+ )) {
490
+ items.push(...await ModelCrudUtil.filterOutNotFound(batch.map(id => this.get(cls, id))));
337
491
  }
492
+ return items;
338
493
  }
339
494
  }