@travetto/model 3.1.6 → 3.1.8
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 +10 -3
- package/package.json +1 -1
- package/src/internal/service/expiry.ts +19 -0
- package/src/internal/service/stream.ts +17 -2
- package/src/provider/file.ts +13 -13
- package/src/provider/memory.ts +14 -15
- package/src/service/stream.ts +16 -1
- package/support/fixtures/text.txt +1 -0
- package/support/test/stream.ts +38 -0
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,9 +314,10 @@ 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
|
-
// Expiry
|
|
320
|
+
// Expiry
|
|
314
321
|
async deleteExpired<T extends ModelType>(cls: Class<T>): Promise<number>;
|
|
315
322
|
// Storage Support
|
|
316
323
|
async createStorage(): Promise<void>;
|
package/package.json
CHANGED
|
@@ -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,21 @@
|
|
|
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 enforceRange(start: number, end: number | undefined, size: number): [start: number, end: number] {
|
|
10
|
+
|
|
11
|
+
end ??= size - 1;
|
|
12
|
+
|
|
13
|
+
if (Number.isNaN(start) || Number.isNaN(end) || !Number.isFinite(start) || start >= size || start < 0) {
|
|
14
|
+
throw new AppError('Invalid position, out of range', 'data');
|
|
15
|
+
}
|
|
16
|
+
if (end >= size) {
|
|
17
|
+
end = size - 1;
|
|
18
|
+
}
|
|
19
|
+
return [start, end];
|
|
20
|
+
}
|
|
21
|
+
}
|
package/src/provider/file.ts
CHANGED
|
@@ -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,16 @@ 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
|
+
|
|
197
|
+
[start, end] = ModelStreamUtil.enforceRange(start, end, meta.size);
|
|
198
|
+
|
|
199
|
+
const stream = createReadStream(file, { start, end });
|
|
200
|
+
return { stream, range: [start, end] };
|
|
201
|
+
}
|
|
202
|
+
|
|
193
203
|
async describeStream(location: string): Promise<StreamMeta> {
|
|
194
204
|
const file = await this.#find(STREAMS, META, location);
|
|
195
205
|
const content = await StreamUtil.streamToBuffer(createReadStream(file));
|
|
@@ -211,17 +221,7 @@ export class FileModelService implements ModelCrudSupport, ModelStreamSupport, M
|
|
|
211
221
|
|
|
212
222
|
// Expiry
|
|
213
223
|
async deleteExpired<T extends ModelType>(cls: Class<T>): Promise<number> {
|
|
214
|
-
|
|
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;
|
|
224
|
+
return ModelExpiryUtil.naiveDeleteExpired(this, cls);
|
|
225
225
|
}
|
|
226
226
|
|
|
227
227
|
// Storage management
|
package/src/provider/memory.ts
CHANGED
|
@@ -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,16 @@ 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
|
+
|
|
256
|
+
[start, end] = ModelStreamUtil.enforceRange(start, end, buffer.length);
|
|
257
|
+
|
|
258
|
+
const stream = await StreamUtil.bufferToStream(buffer.subarray(start, end + 1));
|
|
259
|
+
return { stream, range: [start, end] };
|
|
260
|
+
}
|
|
261
|
+
|
|
252
262
|
async describeStream(location: string): Promise<StreamMeta> {
|
|
253
263
|
const metaContent = this.#find(STREAM_META, location, 'notfound');
|
|
254
264
|
const meta: StreamMeta = JSON.parse(metaContent.get(location)!.toString('utf8'));
|
|
@@ -266,20 +276,9 @@ export class MemoryModelService implements ModelCrudSupport, ModelStreamSupport,
|
|
|
266
276
|
}
|
|
267
277
|
}
|
|
268
278
|
|
|
269
|
-
// Expiry
|
|
279
|
+
// Expiry
|
|
270
280
|
async deleteExpired<T extends ModelType>(cls: Class<T>): Promise<number> {
|
|
271
|
-
|
|
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;
|
|
281
|
+
return ModelExpiryUtil.naiveDeleteExpired(this, cls);
|
|
283
282
|
}
|
|
284
283
|
|
|
285
284
|
// Storage Support
|
package/src/service/stream.ts
CHANGED
|
@@ -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
|
package/support/test/stream.ts
CHANGED
|
@@ -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,41 @@ 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
|
+
const partialOverbounded = await service.getStreamPartial(meta.hash, 20, 40);
|
|
102
|
+
const subContent4 = (await StreamUtil.toBuffer(partialOverbounded.stream)).toString('utf8');
|
|
103
|
+
assert(subContent4.length === 6);
|
|
104
|
+
assert(subContent4.endsWith('xyz'));
|
|
105
|
+
|
|
106
|
+
await assert.rejects(() => service.getStreamPartial(meta.hash, -10, 10));
|
|
107
|
+
await assert.rejects(() => service.getStreamPartial(meta.hash, 30, 37));
|
|
108
|
+
}
|
|
71
109
|
}
|