@travetto/model 3.1.6 → 3.1.7

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
@@ -174,6 +174,12 @@ export interface ModelStreamSupport {
174
174
  */
175
175
  getStream(location: string): Promise<Readable>;
176
176
 
177
+ /**
178
+ * Get partial stream from asset store given a starting byte and an optional ending byte
179
+ * @param location The location of the stream
180
+ */
181
+ getStreamPartial(location: string, start: number, end?: number): Promise<PartialStream>;
182
+
177
183
  /**
178
184
  * Get metadata for stream
179
185
  * @param location The location of the stream
@@ -251,7 +257,7 @@ import { DeepPartial } from '@travetto/schema';
251
257
  import { Injectable } from '@travetto/di';
252
258
  import { Config } from '@travetto/config';
253
259
  import { ModelCrudSupport } from '../service/crud';
254
- import { ModelStreamSupport, StreamMeta } from '../service/stream';
260
+ import { ModelStreamSupport, PartialStream, StreamMeta } from '../service/stream';
255
261
  import { ModelType, OptionalId } from '../types/model';
256
262
  import { ModelExpirySupport } from '../service/expiry';
257
263
  import { ModelRegistry } from '../registry/model';
@@ -263,7 +269,7 @@ import { ExistsError } from '../error/exists';
263
269
  import { ModelIndexedSupport } from '../service/indexed';
264
270
  import { ModelIndexedUtil } from '../internal/service/indexed';
265
271
  import { ModelStorageUtil } from '../internal/service/storage';
266
- import { StreamModel, STREAMS } from '../internal/service/stream';
272
+ import { ModelStreamUtil, StreamModel, STREAMS } from '../internal/service/stream';
267
273
  import { IndexConfig } from '../registry/types';
268
274
  const STREAM_META = `${STREAMS}_meta`;
269
275
  type StoreType = Map<string, Buffer>;
@@ -308,6 +314,7 @@ export class MemoryModelService implements ModelCrudSupport, ModelStreamSupport,
308
314
  // Stream Support
309
315
  async upsertStream(location: string, input: Readable, meta: StreamMeta): Promise<void>;
310
316
  async getStream(location: string): Promise<Readable>;
317
+ async getStreamPartial(location: string, start: number, end?: number): Promise<PartialStream>;
311
318
  async describeStream(location: string): Promise<StreamMeta>;
312
319
  async deleteStream(location: string): Promise<void>;
313
320
  // Expiry Support
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/model",
3
- "version": "3.1.6",
3
+ "version": "3.1.7",
4
4
  "description": "Datastore abstraction for core operations.",
5
5
  "keywords": [
6
6
  "datastore",
@@ -3,6 +3,7 @@ import { ShutdownManager, Class, TimeSpan, TimeUtil } from '@travetto/base';
3
3
  import { ModelRegistry } from '../../registry/model';
4
4
  import { ModelExpirySupport } from '../../service/expiry';
5
5
  import { ModelType } from '../../types/model';
6
+ import { NotFoundError } from '../../error/not-found';
6
7
 
7
8
  /**
8
9
  * Utils for model expiry
@@ -50,4 +51,22 @@ export class ModelExpiryUtil {
50
51
  })();
51
52
  }
52
53
  }
54
+
55
+ /**
56
+ * Simple cull operation for a given model type
57
+ * @param svc
58
+ */
59
+ static async naiveDeleteExpired<T extends ModelType>(svc: ModelExpirySupport, cls: Class<T>, suppressErrors = false): Promise<number> {
60
+ const deleting = [];
61
+ for await (const el of svc.list(cls)) {
62
+ if (this.getExpiryState(cls, el).expired) {
63
+ deleting.push(svc.delete(cls, el.id).catch(err => {
64
+ if (!suppressErrors && !(err instanceof NotFoundError)) {
65
+ throw err;
66
+ }
67
+ }));
68
+ }
69
+ }
70
+ return (await Promise.all(deleting)).length;
71
+ }
53
72
  }
@@ -1,6 +1,14 @@
1
- import { Class } from '@travetto/base';
1
+ import { AppError, Class } from '@travetto/base';
2
2
  import { ModelType } from '../../types/model';
3
3
 
4
4
  class Cls { id: string; }
5
5
  export const StreamModel: Class<ModelType> = Cls;
6
- export const STREAMS = '_streams';
6
+ export const STREAMS = '_streams';
7
+
8
+ export class ModelStreamUtil {
9
+ static checkRange(start: number, end: number, size: number): void {
10
+ if (Number.isNaN(start) || Number.isNaN(end) || !Number.isFinite(start) || start < 0 || end >= size) {
11
+ throw new AppError('Invalid position, out of range', 'data');
12
+ }
13
+ }
14
+ }
@@ -12,7 +12,7 @@ import { Config } from '@travetto/config';
12
12
  import { Required } from '@travetto/schema';
13
13
 
14
14
  import { ModelCrudSupport } from '../service/crud';
15
- import { ModelStreamSupport, StreamMeta } from '../service/stream';
15
+ import { ModelStreamSupport, PartialStream, StreamMeta } from '../service/stream';
16
16
  import { ModelType, OptionalId } from '../types/model';
17
17
  import { ModelExpirySupport } from '../service/expiry';
18
18
  import { ModelRegistry } from '../registry/model';
@@ -21,7 +21,7 @@ import { ModelCrudUtil } from '../internal/service/crud';
21
21
  import { ModelExpiryUtil } from '../internal/service/expiry';
22
22
  import { NotFoundError } from '../error/not-found';
23
23
  import { ExistsError } from '../error/exists';
24
- import { StreamModel, STREAMS } from '../internal/service/stream';
24
+ import { ModelStreamUtil, StreamModel, STREAMS } from '../internal/service/stream';
25
25
 
26
26
  type Suffix = '.bin' | '.meta' | '.json' | '.expires';
27
27
 
@@ -190,6 +190,17 @@ export class FileModelService implements ModelCrudSupport, ModelStreamSupport, M
190
190
  return createReadStream(file);
191
191
  }
192
192
 
193
+ async getStreamPartial(location: string, start: number, end?: number): Promise<PartialStream> {
194
+ const file = await this.#find(STREAMS, BIN, location);
195
+ const meta = await this.describeStream(location);
196
+ end ??= meta.size - 1;
197
+
198
+ ModelStreamUtil.checkRange(start, end, meta.size);
199
+
200
+ const stream = createReadStream(file, { start, end });
201
+ return { stream, range: [start, end] };
202
+ }
203
+
193
204
  async describeStream(location: string): Promise<StreamMeta> {
194
205
  const file = await this.#find(STREAMS, META, location);
195
206
  const content = await StreamUtil.streamToBuffer(createReadStream(file));
@@ -211,17 +222,7 @@ export class FileModelService implements ModelCrudSupport, ModelStreamSupport, M
211
222
 
212
223
  // Expiry
213
224
  async deleteExpired<T extends ModelType>(cls: Class<T>): Promise<number> {
214
- const deleting = [];
215
- for await (const el of this.list(cls)) {
216
- if (ModelExpiryUtil.getExpiryState(cls, el).expired) {
217
- deleting.push(this.delete(cls, el.id).catch(err => {
218
- if (!(err instanceof NotFoundError)) {
219
- throw err;
220
- }
221
- }));
222
- }
223
- }
224
- return (await Promise.all(deleting)).length;
225
+ return ModelExpiryUtil.naiveDeleteExpired(this, cls);
225
226
  }
226
227
 
227
228
  // Storage management
@@ -6,7 +6,7 @@ import { Injectable } from '@travetto/di';
6
6
  import { Config } from '@travetto/config';
7
7
 
8
8
  import { ModelCrudSupport } from '../service/crud';
9
- import { ModelStreamSupport, StreamMeta } from '../service/stream';
9
+ import { ModelStreamSupport, PartialStream, StreamMeta } from '../service/stream';
10
10
  import { ModelType, OptionalId } from '../types/model';
11
11
  import { ModelExpirySupport } from '../service/expiry';
12
12
  import { ModelRegistry } from '../registry/model';
@@ -18,7 +18,7 @@ import { ExistsError } from '../error/exists';
18
18
  import { ModelIndexedSupport } from '../service/indexed';
19
19
  import { ModelIndexedUtil } from '../internal/service/indexed';
20
20
  import { ModelStorageUtil } from '../internal/service/storage';
21
- import { StreamModel, STREAMS } from '../internal/service/stream';
21
+ import { ModelStreamUtil, StreamModel, STREAMS } from '../internal/service/stream';
22
22
  import { IndexConfig } from '../registry/types';
23
23
 
24
24
  const STREAM_META = `${STREAMS}_meta`;
@@ -249,6 +249,15 @@ export class MemoryModelService implements ModelCrudSupport, ModelStreamSupport,
249
249
  return StreamUtil.bufferToStream(streams.get(location)!);
250
250
  }
251
251
 
252
+ async getStreamPartial(location: string, start: number, end?: number): Promise<PartialStream> {
253
+ const streams = this.#find(STREAMS, location, 'notfound');
254
+ const buffer = streams.get(location)!;
255
+ end ??= (buffer.length - 1);
256
+ ModelStreamUtil.checkRange(start, end, buffer.length);
257
+ const stream = await StreamUtil.bufferToStream(buffer.subarray(start, end + 1));
258
+ return { stream, range: [start, end] };
259
+ }
260
+
252
261
  async describeStream(location: string): Promise<StreamMeta> {
253
262
  const metaContent = this.#find(STREAM_META, location, 'notfound');
254
263
  const meta: StreamMeta = JSON.parse(metaContent.get(location)!.toString('utf8'));
@@ -266,20 +275,9 @@ export class MemoryModelService implements ModelCrudSupport, ModelStreamSupport,
266
275
  }
267
276
  }
268
277
 
269
- // Expiry Support
278
+ // Expiry
270
279
  async deleteExpired<T extends ModelType>(cls: Class<T>): Promise<number> {
271
- const deleting = [];
272
- const store = this.#getStore(cls);
273
- for (const id of [...store.keys()]) {
274
- if ((ModelExpiryUtil.getExpiryState(cls, await this.get(cls, id))).expired) {
275
- deleting.push(this.delete(cls, id).catch(err => {
276
- if (!(err instanceof NotFoundError)) {
277
- throw err;
278
- }
279
- }));
280
- }
281
- }
282
- return (await Promise.all(deleting)).length;
280
+ return ModelExpiryUtil.naiveDeleteExpired(this, cls);
283
281
  }
284
282
 
285
283
  // Storage Support
@@ -14,9 +14,18 @@ export interface StreamMeta {
14
14
  */
15
15
  hash: string;
16
16
  /**
17
- * The original filename of the file
17
+ * The original base filename of the file
18
18
  */
19
19
  filename: string;
20
+ /**
21
+ * Filenames title, optional for elements like images, audio, videos
22
+ */
23
+ title?: string;
24
+ }
25
+
26
+ export interface PartialStream {
27
+ stream: Readable;
28
+ range: [number, number];
20
29
  }
21
30
 
22
31
  /**
@@ -40,6 +49,12 @@ export interface ModelStreamSupport {
40
49
  */
41
50
  getStream(location: string): Promise<Readable>;
42
51
 
52
+ /**
53
+ * Get partial stream from asset store given a starting byte and an optional ending byte
54
+ * @param location The location of the stream
55
+ */
56
+ getStreamPartial(location: string, start: number, end?: number): Promise<PartialStream>;
57
+
43
58
  /**
44
59
  * Get metadata for stream
45
60
  * @param location The location of the stream
@@ -0,0 +1 @@
1
+ abcdefghijklmnopqrstuvwxyz
@@ -6,6 +6,7 @@ import { Suite, Test, TestFixtures } from '@travetto/test';
6
6
 
7
7
  import { BaseModelSuite } from './base';
8
8
  import { ModelStreamSupport } from '../../src/service/stream';
9
+ import { StreamUtil } from '@travetto/base';
9
10
 
10
11
  @Suite()
11
12
  export abstract class ModelStreamSuite extends BaseModelSuite<ModelStreamSupport> {
@@ -68,4 +69,36 @@ export abstract class ModelStreamSuite extends BaseModelSuite<ModelStreamSupport
68
69
  await service.getStream(meta.hash);
69
70
  });
70
71
  }
72
+
73
+ @Test()
74
+ async partialStream(): Promise<void> {
75
+ const service = await this.service;
76
+ const [meta, stream] = await this.getStream('/text.txt');
77
+
78
+ await service.upsertStream(meta.hash, stream, meta);
79
+
80
+ const retrieved = await service.getStream(meta.hash);
81
+ const content = (await StreamUtil.toBuffer(retrieved)).toString('utf8');
82
+ assert(content.startsWith('abc'));
83
+ assert(content.endsWith('xyz'));
84
+
85
+ const partial = await service.getStreamPartial(meta.hash, 10, 20);
86
+ const subContent = (await StreamUtil.toBuffer(partial.stream)).toString('utf8');
87
+ assert(subContent.length === (partial.range[1] - partial.range[0]) + 1);
88
+ assert(subContent === 'klmnopqrstu');
89
+
90
+ const partialUnbounded = await service.getStreamPartial(meta.hash, 10);
91
+ const subContent2 = (await StreamUtil.toBuffer(partialUnbounded.stream)).toString('utf8');
92
+ assert(subContent2.length === (partialUnbounded.range[1] - partialUnbounded.range[0]) + 1);
93
+ assert(subContent2.startsWith('klm'));
94
+ assert(subContent2.endsWith('xyz'));
95
+
96
+ const partialSingle = await service.getStreamPartial(meta.hash, 10, 10);
97
+ const subContent3 = (await StreamUtil.toBuffer(partialSingle.stream)).toString('utf8');
98
+ assert(subContent3.length === 1);
99
+ assert(subContent3 === 'k');
100
+
101
+ await assert.rejects(() => service.getStreamPartial(meta.hash, -10, 10));
102
+ await assert.rejects(() => service.getStreamPartial(meta.hash, 0, 27));
103
+ }
71
104
  }