@travetto/model-memory 8.0.0-alpha.3 → 8.0.0-alpha.30

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/README.md CHANGED
@@ -14,7 +14,7 @@ yarn add @travetto/model-memory
14
14
  ```
15
15
 
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
- * [CRUD](https://github.com/travetto/travetto/tree/main/module/model/src/types/crud.ts#L11)
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
17
  * [Blob](https://github.com/travetto/travetto/tree/main/module/model/src/types/blob.ts#L8)
18
+ * [CRUD](https://github.com/travetto/travetto/tree/main/module/model/src/types/crud.ts#L10)
19
+ * [Expiry](https://github.com/travetto/travetto/tree/main/module/model/src/types/expiry.ts#L10)
20
+ * [Indexed](https://github.com/travetto/travetto/tree/main/module/model-indexed/src/types/service.ts#L21)
package/__index__.ts CHANGED
@@ -1 +1 @@
1
- export * from './src/service.ts';
1
+ export * from './src/service.ts';
package/package.json CHANGED
@@ -1,39 +1,43 @@
1
1
  {
2
2
  "name": "@travetto/model-memory",
3
- "version": "8.0.0-alpha.3",
4
- "type": "module",
3
+ "version": "8.0.0-alpha.30",
5
4
  "description": "Memory backing for the travetto model module.",
6
5
  "keywords": [
7
6
  "datastore",
8
7
  "memory",
9
8
  "schema",
10
- "typescript",
11
- "travetto"
9
+ "travetto",
10
+ "typescript"
12
11
  ],
13
12
  "homepage": "https://travetto.io",
14
13
  "license": "MIT",
15
14
  "author": {
16
- "email": "travetto.framework@gmail.com",
17
- "name": "Travetto Framework"
15
+ "name": "Travetto Framework",
16
+ "email": "travetto.framework@gmail.com"
17
+ },
18
+ "repository": {
19
+ "url": "git+https://github.com/travetto/travetto.git",
20
+ "directory": "module/model-memory"
18
21
  },
19
22
  "files": [
20
23
  "__index__.ts",
21
24
  "src"
22
25
  ],
26
+ "type": "module",
23
27
  "main": "__index__.ts",
24
- "repository": {
25
- "url": "git+https://github.com/travetto/travetto.git",
26
- "directory": "module/model-memory"
28
+ "publishConfig": {
29
+ "access": "public"
27
30
  },
28
31
  "dependencies": {
29
- "@travetto/config": "^8.0.0-alpha.3",
30
- "@travetto/di": "^8.0.0-alpha.3",
31
- "@travetto/model": "^8.0.0-alpha.3",
32
- "@travetto/schema": "^8.0.0-alpha.3"
32
+ "@travetto/config": "^8.0.0-alpha.27",
33
+ "@travetto/di": "^8.0.0-alpha.24",
34
+ "@travetto/model": "^8.0.0-alpha.28",
35
+ "@travetto/model-indexed": "^8.0.0-alpha.30",
36
+ "@travetto/schema": "^8.0.0-alpha.27"
33
37
  },
34
38
  "peerDependencies": {
35
- "@travetto/cli": "^8.0.0-alpha.4",
36
- "@travetto/test": "^8.0.0-alpha.3"
39
+ "@travetto/cli": "^8.0.0-alpha.33",
40
+ "@travetto/test": "^8.0.0-alpha.26"
37
41
  },
38
42
  "peerDependenciesMeta": {
39
43
  "@travetto/cli": {
@@ -45,8 +49,5 @@
45
49
  },
46
50
  "travetto": {
47
51
  "displayName": "Memory Model Support"
48
- },
49
- "publishConfig": {
50
- "access": "public"
51
52
  }
52
53
  }
package/src/service.ts CHANGED
@@ -1,20 +1,61 @@
1
- import {
2
- type Class, type TimeSpan, type DeepPartial, castTo, type BinaryMetadata,
3
- type ByteRange, type BinaryType, BinaryUtil, type BinaryArray, JSONUtil, BinaryMetadataUtil
4
- } from '@travetto/runtime';
5
- import { Injectable, PostConstruct } from '@travetto/di';
6
1
  import { Config } from '@travetto/config';
2
+ import { Injectable, PostConstruct } from '@travetto/di';
7
3
  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
4
+ ExistsError,
5
+ IndexNotSupported,
6
+ type ModelBlobSupport,
7
+ type ModelCrudSupport,
8
+ ModelCrudUtil,
9
+ type ModelExpirySupport,
10
+ ModelExpiryUtil,
11
+ type ModelListOptions,
12
+ ModelRegistryIndex,
13
+ type ModelStorageSupport,
14
+ ModelStorageUtil,
15
+ type ModelType,
16
+ NotFoundError,
17
+ type OptionalId,
18
+ UniqueError
11
19
  } from '@travetto/model';
20
+ import {
21
+ type AllIndexes,
22
+ type FullKeyedIndexBody,
23
+ type FullKeyedIndexWithPartialBody,
24
+ isModelIndexedIndex,
25
+ type KeyedIndexBody,
26
+ type KeyedIndexSelection,
27
+ ModelIndexedComputedIndex,
28
+ type ModelIndexedSearchOptions,
29
+ type ModelIndexedSupport,
30
+ ModelIndexedUtil,
31
+ type ModelPageOptions,
32
+ type ModelPageResult,
33
+ type SingleItemIndex,
34
+ type SortedIndex,
35
+ type SortedIndexSelection,
36
+ type SortedIndexSelectionType
37
+ } from '@travetto/model-indexed';
38
+ import {
39
+ type BinaryArray,
40
+ type BinaryMetadata,
41
+ BinaryMetadataUtil,
42
+ type BinaryType,
43
+ BinaryUtil,
44
+ type ByteRange,
45
+ type Class,
46
+ castTo,
47
+ JSONUtil,
48
+ type TimeSpan
49
+ } from '@travetto/runtime';
12
50
 
13
51
  const ModelBlobNamespace = '__blobs';
14
52
  const ModelBlobMetaNamespace = `${ModelBlobNamespace}_meta`;
15
53
 
16
54
  type StoreType = Map<string, BinaryArray>;
17
55
 
56
+ const sortValue = (a: string | number, b: string | number): number =>
57
+ typeof a === 'number' && typeof b === 'number' ? a - b : typeof a === 'string' && typeof b === 'string' ? a.localeCompare(b) : 0;
58
+
18
59
  @Config('model.memory')
19
60
  export class MemoryModelConfig {
20
61
  modifyStorage?: boolean = true;
@@ -22,8 +63,8 @@ export class MemoryModelConfig {
22
63
  cullRate?: number | TimeSpan;
23
64
  }
24
65
 
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(':');
66
+ function indexName<T extends ModelType>(cls: Class<T>, idx: AllIndexes<T>, suffix?: string): string {
67
+ return [cls.Ⲑid, idx.name, suffix].filter(part => !!part).join(':');
27
68
  }
28
69
 
29
70
  function getFirstId(data: Map<string, unknown> | Set<string>, value?: string | number): string | undefined {
@@ -40,20 +81,25 @@ function getFirstId(data: Map<string, unknown> | Set<string>, value?: string | n
40
81
  * Standard in-memory support
41
82
  */
42
83
  @Injectable()
43
- export class MemoryModelService implements ModelCrudSupport, ModelBlobSupport, ModelExpirySupport, ModelStorageSupport, ModelIndexedSupport {
44
-
84
+ export class MemoryModelService
85
+ implements ModelCrudSupport, ModelBlobSupport, ModelExpirySupport, ModelStorageSupport, ModelIndexedSupport
86
+ {
45
87
  #store = new Map<string, StoreType>();
46
88
  #indices = {
47
- sorted: new Map<string, Map<string, Map<string, number>>>(),
48
- unsorted: new Map<string, Map<string, Set<string>>>()
49
- };
89
+ 'indexed:sorted': new Map<string, Map<string, Map<string, number | string>>>(),
90
+ 'indexed:keyed': new Map<string, Map<string, Set<string>>>()
91
+ } as const;
50
92
 
51
93
  idSource = ModelCrudUtil.uuidSource();
52
94
  config: MemoryModelConfig;
53
95
 
54
- constructor(config: MemoryModelConfig) { this.config = config; }
96
+ constructor(config: MemoryModelConfig) {
97
+ this.config = config;
98
+ }
55
99
 
56
- get client(): Map<string, StoreType> { return this.#store; }
100
+ get client(): Map<string, StoreType> {
101
+ return this.#store;
102
+ }
57
103
 
58
104
  #getStore<T extends ModelType>(cls: Class<T> | string): StoreType {
59
105
  const key = typeof cls === 'string' ? cls : ModelRegistryIndex.getStoreName(cls);
@@ -73,10 +119,18 @@ export class MemoryModelService implements ModelCrudSupport, ModelBlobSupport, M
73
119
  async #removeIndices<T extends ModelType>(cls: Class<T>, id: string): Promise<void> {
74
120
  try {
75
121
  const item = await this.get(cls, id);
76
- for (const idx of ModelRegistryIndex.getIndices(cls, ['sorted', 'unsorted'])) {
122
+ for (const idx of ModelRegistryIndex.getIndices(cls)) {
123
+ if (!isModelIndexedIndex(idx)) {
124
+ continue; // Only support ModelIndexed indices
125
+ }
77
126
  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);
127
+ const computed = ModelIndexedComputedIndex.get(idx, item).validate({ sort: true });
128
+ switch (idx.type) {
129
+ case 'indexed:sorted':
130
+ case 'indexed:keyed':
131
+ this.#indices[idx.type].get(idxName)?.get(computed.getKey())?.delete(id);
132
+ break;
133
+ }
80
134
  }
81
135
  } catch (error) {
82
136
  if (!(error instanceof NotFoundError)) {
@@ -86,14 +140,28 @@ export class MemoryModelService implements ModelCrudSupport, ModelBlobSupport, M
86
140
  }
87
141
 
88
142
  async #writeIndices<T extends ModelType>(cls: Class<T>, item: T): Promise<void> {
89
- for (const idx of ModelRegistryIndex.getIndices(cls, ['sorted', 'unsorted'])) {
143
+ for (const idx of ModelRegistryIndex.getIndices(cls)) {
144
+ if (!isModelIndexedIndex(idx)) {
145
+ continue; // Only support ModelIndexed indices
146
+ }
90
147
  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);
148
+ const computed = ModelIndexedComputedIndex.get(idx, item).validate({ sort: true });
149
+ const key = computed.getKey();
150
+ switch (idx.type) {
151
+ case 'indexed:keyed': {
152
+ if (idx.unique) {
153
+ const existing = this.#indices[idx.type].get(idxName)?.get(key);
154
+ if (existing && existing.size > 0 && !existing.has(item.id)) {
155
+ throw new UniqueError(cls, key);
156
+ }
157
+ }
158
+ this.#indices[idx.type].getOrInsert(idxName, new Map()).getOrInsert(key, new Set()).add(item.id);
159
+ break;
160
+ }
161
+ case 'indexed:sorted': {
162
+ this.#indices[idx.type].getOrInsert(idxName, new Map()).getOrInsert(key, new Map()).set(item.id, computed.getSort());
163
+ break;
164
+ }
97
165
  }
98
166
  }
99
167
  }
@@ -112,22 +180,82 @@ export class MemoryModelService implements ModelCrudSupport, ModelBlobSupport, M
112
180
  }
113
181
  }
114
182
 
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);
183
+ async #getIdByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
184
+ cls: Class<T>,
185
+ idx: SingleItemIndex<T, K, S>,
186
+ body: FullKeyedIndexBody<T, K, S>
187
+ ): Promise<string> {
188
+ const computed = ModelIndexedComputedIndex.get(idx, body).validate({ sort: true });
189
+
190
+ const index = this.#indices[idx.type].get(indexName(cls, idx))?.get(computed.getKey());
119
191
  let id: string | undefined;
120
192
  if (index) {
121
- if (index instanceof Map) {
122
- id = getFirstId(index, +sort!); // Grab first id
193
+ if (computed.idPart) {
194
+ if (index.has(computed.idPart.value)) {
195
+ id = computed.idPart.value;
196
+ } else {
197
+ throw new NotFoundError(cls, computed.getKey({ sort: true }));
198
+ }
123
199
  } else {
124
- id = getFirstId(index); // Grab first id
200
+ if (index instanceof Map) {
201
+ id = getFirstId(index, computed.getSort()); // Grab first id
202
+ } else if (index instanceof Set) {
203
+ id = getFirstId(index); // Grab first id
204
+ }
125
205
  }
126
206
  }
127
207
  if (id) {
128
208
  return id;
129
209
  }
130
- throw new NotFoundError(cls, key);
210
+ throw new NotFoundError(cls, computed.getKey({ sort: true }));
211
+ }
212
+
213
+ async *#iterateIds(ids: string[], options?: ModelListOptions & ModelPageOptions<number>): AsyncIterable<string[]> {
214
+ let offset = options && 'offset' in options ? (options.offset ?? 0) : 0;
215
+ const batchSize = options?.batchSizeHint ?? 100;
216
+ let produced = 0;
217
+ const maxCount = options?.limit ?? Number.MAX_SAFE_INTEGER;
218
+ while (offset < ids.length && produced < maxCount) {
219
+ if (options?.abort?.aborted) {
220
+ break;
221
+ }
222
+ const end = Math.min(offset + batchSize, ids.length, maxCount - produced + offset);
223
+ const batch = ids.slice(offset, end);
224
+ if (batch.length) {
225
+ yield batch;
226
+ }
227
+ offset += batchSize;
228
+ produced += batch.length;
229
+ }
230
+ }
231
+
232
+ async *#getIndexIds<T extends ModelType>(
233
+ cls: Class<T>,
234
+ idx: AllIndexes<T>,
235
+ body: KeyedIndexBody<T>,
236
+ options?: ModelListOptions & ModelPageOptions<number>,
237
+ filterIndex?: (indexValue: string | number) => boolean
238
+ ): AsyncIterable<string[]> {
239
+ const computed = ModelIndexedComputedIndex.get(idx, body).validate();
240
+ if (!isModelIndexedIndex(idx)) {
241
+ throw new IndexNotSupported(cls, idx, 'Only ModelIndexed indices can be used with MemoryModelService');
242
+ }
243
+
244
+ const base = this.#indices[idx.type].get(indexName(cls, idx));
245
+ const index = base?.get(computed.getKey());
246
+ let ids: string[];
247
+ if (!index) {
248
+ ids = [];
249
+ } else if (index instanceof Map) {
250
+ ids = [...index.entries()]
251
+ .filter(([, sort]) => (filterIndex ? filterIndex(sort) : true))
252
+ .sort((a, b) => sortValue(a[1], b[1]))
253
+ .map(([id]) => id);
254
+ } else {
255
+ ids = [...index];
256
+ }
257
+
258
+ yield* this.#iterateIds(ids, options);
131
259
  }
132
260
 
133
261
  @PostConstruct()
@@ -136,12 +264,9 @@ export class MemoryModelService implements ModelCrudSupport, ModelBlobSupport, M
136
264
  ModelExpiryUtil.registerCull(this);
137
265
 
138
266
  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
- }
267
+ for (const idx of Object.values(ModelRegistryIndex.getConfig(cls).indices ?? {})) {
268
+ if (!isModelIndexedIndex(idx)) {
269
+ console.error(`Indices of type ${idx.type} are not supported for`, { cls: cls.Ⲑid, name: idx.name, type: idx.type });
145
270
  }
146
271
  }
147
272
  }
@@ -203,21 +328,21 @@ export class MemoryModelService implements ModelCrudSupport, ModelBlobSupport, M
203
328
  await this.#persist(cls, where, 'remove');
204
329
  }
205
330
 
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
- }
331
+ async *list<T extends ModelType>(cls: Class<T>, options?: ModelListOptions): AsyncIterable<T[]> {
332
+ for await (const batch of this.#iterateIds([...this.#getStore(cls).keys()], options)) {
333
+ yield ModelCrudUtil.filterOutNotFound(batch.map(id => this.get(cls, id)));
215
334
  }
216
335
  }
217
336
 
218
337
  // Blob Support
219
338
  async upsertBlob(location: string, input: BinaryType, metadata?: BinaryMetadata, overwrite = true): Promise<void> {
220
- if (!overwrite && await this.getBlobMetadata(location).then(() => true, () => false)) {
339
+ if (
340
+ !overwrite &&
341
+ (await this.getBlobMetadata(location).then(
342
+ () => true,
343
+ () => false
344
+ ))
345
+ ) {
221
346
  return;
222
347
  }
223
348
  const resolved = await BinaryMetadataUtil.compute(input, metadata);
@@ -228,7 +353,6 @@ export class MemoryModelService implements ModelCrudSupport, ModelBlobSupport, M
228
353
  }
229
354
 
230
355
  async getBlob(location: string, range?: ByteRange): Promise<Blob> {
231
-
232
356
  const blobs = this.#find(ModelBlobNamespace, location, 'notfound');
233
357
 
234
358
  let data = blobs.get(location)!;
@@ -274,24 +398,24 @@ export class MemoryModelService implements ModelCrudSupport, ModelBlobSupport, M
274
398
  store.delete(key);
275
399
  deleted += 1;
276
400
  }
277
- } catch { } // Do not let a single error stop the process
401
+ } catch {} // Do not let a single error stop the process
278
402
  }
279
403
  return deleted;
280
404
  }
281
405
 
282
406
  // Storage Support
283
- async createStorage(): Promise<void> {
284
- }
407
+ async createStorage(): Promise<void> {}
285
408
 
286
409
  async deleteStorage(): Promise<void> {
287
410
  this.#store.clear();
288
- this.#indices.sorted.clear();
289
- this.#indices.unsorted.clear();
411
+ for (const value of Object.values(this.#indices)) {
412
+ value.clear();
413
+ }
290
414
  }
291
415
 
292
416
  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') {
417
+ for (const idx of ModelRegistryIndex.getIndices(cls)) {
418
+ if (isModelIndexedIndex(idx)) {
295
419
  this.#indices[idx.type].set(indexName(cls, idx), new Map());
296
420
  }
297
421
  }
@@ -307,33 +431,85 @@ export class MemoryModelService implements ModelCrudSupport, ModelBlobSupport, M
307
431
  }
308
432
 
309
433
  // Indexed
310
- async getByIndex<T extends ModelType>(cls: Class<T>, idx: string, body: DeepPartial<T>): Promise<T> {
434
+ async getByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
435
+ cls: Class<T>,
436
+ idx: SingleItemIndex<T, K, S>,
437
+ body: FullKeyedIndexBody<T, K, S>
438
+ ): Promise<T> {
311
439
  return this.get(cls, await this.#getIdByIndex(cls, idx, body));
312
440
  }
313
441
 
314
- async deleteByIndex<T extends ModelType>(cls: Class<T>, idx: string, body: DeepPartial<T>): Promise<void> {
442
+ async deleteByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
443
+ cls: Class<T>,
444
+ idx: SingleItemIndex<T, K, S>,
445
+ body: FullKeyedIndexBody<T, K, S>
446
+ ): Promise<void> {
315
447
  await this.delete(cls, await this.#getIdByIndex(cls, idx, body));
316
448
  }
317
449
 
318
- upsertByIndex<T extends ModelType>(cls: Class<T>, idx: string, body: OptionalId<T>): Promise<T> {
450
+ upsertByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
451
+ cls: Class<T>,
452
+ idx: SingleItemIndex<T, K, S>,
453
+ body: OptionalId<T>
454
+ ): Promise<T> {
319
455
  return ModelIndexedUtil.naiveUpsert(this, cls, idx, body);
320
456
  }
321
457
 
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);
458
+ updateByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
459
+ cls: Class<T>,
460
+ idx: SingleItemIndex<T, K, S>,
461
+ body: T
462
+ ): Promise<T> {
463
+ return ModelIndexedUtil.naiveUpdate(this, cls, idx, body);
464
+ }
326
465
 
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
- }
466
+ async updatePartialByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
467
+ cls: Class<T>,
468
+ idx: SingleItemIndex<T, K, S>,
469
+ body: FullKeyedIndexWithPartialBody<T, K, S>
470
+ ): Promise<T> {
471
+ const item = await ModelCrudUtil.naivePartialUpdate(cls, () => this.getByIndex(cls, idx, castTo(body)), castTo(body));
472
+ return this.update(cls, item);
473
+ }
474
+
475
+ async pageByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
476
+ cls: Class<T>,
477
+ idx: SortedIndex<T, K, S>,
478
+ body: KeyedIndexBody<T, K>,
479
+ options?: ModelPageOptions
480
+ ): Promise<ModelPageResult<T>> {
481
+ const offset = options?.offset ? JSONUtil.fromBase64<number>(options.offset) : 0;
482
+ let produced = 0;
483
+
484
+ const items: T[] = [];
485
+ for await (const batch of this.#getIndexIds(cls, idx, body, { limit: 100, ...options, offset })) {
486
+ produced += batch.length;
487
+ items.push(...(await ModelCrudUtil.filterOutNotFound(batch.map(id => this.get(cls, id)))));
337
488
  }
489
+ return { items, nextOffset: items.length ? JSONUtil.toBase64(offset + produced) : undefined };
338
490
  }
339
- }
491
+
492
+ async *listByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
493
+ cls: Class<T>,
494
+ idx: SortedIndex<T, K, S>,
495
+ body: KeyedIndexBody<T, K>,
496
+ options?: ModelListOptions
497
+ ): AsyncIterable<T[]> {
498
+ for await (const batch of this.#getIndexIds(cls, idx, body, options)) {
499
+ yield ModelCrudUtil.filterOutNotFound(batch.map(id => this.get(cls, id)));
500
+ }
501
+ }
502
+
503
+ async suggestByIndex<
504
+ T extends ModelType,
505
+ S extends SortedIndexSelection<T>,
506
+ K extends KeyedIndexSelection<T>,
507
+ B extends SortedIndexSelectionType<T, S> & string
508
+ >(cls: Class<T>, idx: SortedIndex<T, K, S>, body: KeyedIndexBody<T, K>, prefix: B, options?: ModelIndexedSearchOptions): Promise<T[]> {
509
+ const items: T[] = [];
510
+ for await (const batch of this.#getIndexIds(cls, idx, body, options, id => typeof id === 'string' && id.startsWith(prefix))) {
511
+ items.push(...(await ModelCrudUtil.filterOutNotFound(batch.map(id => this.get(cls, id)))));
512
+ }
513
+ return items;
514
+ }
515
+ }