@travetto/model 5.0.0-rc.10 → 5.0.0-rc.12
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 +85 -146
- package/__index__.ts +2 -4
- package/package.json +7 -7
- package/src/internal/service/blob.ts +5 -0
- package/src/internal/service/common.ts +8 -8
- package/src/internal/service/indexed.ts +0 -1
- package/src/service/blob.ts +41 -0
- package/src/util/blob.ts +60 -0
- package/support/doc.support.tsx +2 -2
- package/support/fixtures/alpha.txt +1 -0
- package/support/fixtures/empty +0 -0
- package/support/fixtures/empty.m4a +0 -0
- package/support/fixtures/logo.gif +0 -0
- package/support/fixtures/logo.png +0 -0
- package/support/fixtures/small-audio +0 -0
- package/support/fixtures/small-audio.mp3 +0 -0
- package/support/test/blob.ts +117 -0
- package/support/test/suite.ts +5 -5
- package/src/internal/service/stream.ts +0 -22
- package/src/provider/file.ts +0 -231
- package/src/provider/memory.ts +0 -339
- package/src/service/stream.ts +0 -72
- package/support/test/stream.ts +0 -113
package/src/provider/memory.ts
DELETED
|
@@ -1,339 +0,0 @@
|
|
|
1
|
-
import { Readable } from 'node:stream';
|
|
2
|
-
import { buffer as toBuffer } from 'node:stream/consumers';
|
|
3
|
-
|
|
4
|
-
import { Class, TimeSpan, DeepPartial, castTo } from '@travetto/runtime';
|
|
5
|
-
import { Injectable } from '@travetto/di';
|
|
6
|
-
import { Config } from '@travetto/config';
|
|
7
|
-
|
|
8
|
-
import { ModelCrudSupport } from '../service/crud';
|
|
9
|
-
import { ModelStreamSupport, StreamMeta, StreamRange } from '../service/stream';
|
|
10
|
-
import { ModelType, OptionalId } from '../types/model';
|
|
11
|
-
import { ModelExpirySupport } from '../service/expiry';
|
|
12
|
-
import { ModelRegistry } from '../registry/model';
|
|
13
|
-
import { ModelStorageSupport } from '../service/storage';
|
|
14
|
-
import { ModelCrudUtil } from '../internal/service/crud';
|
|
15
|
-
import { ModelExpiryUtil } from '../internal/service/expiry';
|
|
16
|
-
import { NotFoundError } from '../error/not-found';
|
|
17
|
-
import { ExistsError } from '../error/exists';
|
|
18
|
-
import { ModelIndexedSupport } from '../service/indexed';
|
|
19
|
-
import { ModelIndexedUtil } from '../internal/service/indexed';
|
|
20
|
-
import { ModelStorageUtil } from '../internal/service/storage';
|
|
21
|
-
import { enforceRange, StreamModel, STREAMS } from '../internal/service/stream';
|
|
22
|
-
import { IndexConfig } from '../registry/types';
|
|
23
|
-
|
|
24
|
-
const STREAM_META = `${STREAMS}_meta`;
|
|
25
|
-
|
|
26
|
-
type StoreType = Map<string, Buffer>;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
@Config('model.memory')
|
|
30
|
-
export class MemoryModelConfig {
|
|
31
|
-
autoCreate?: boolean = true;
|
|
32
|
-
namespace?: string;
|
|
33
|
-
cullRate?: number | TimeSpan;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function indexName<T extends ModelType>(cls: Class<T>, idx: IndexConfig<T> | string, suffix?: string): string {
|
|
37
|
-
return [cls.Ⲑid, typeof idx === 'string' ? idx : idx.name, suffix].filter(x => !!x).join(':');
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function getFirstId(data: Map<string, unknown> | Set<string>, value?: string | number): string | undefined {
|
|
41
|
-
let id: string | undefined;
|
|
42
|
-
if (data instanceof Set) {
|
|
43
|
-
id = data.values().next().value;
|
|
44
|
-
} else {
|
|
45
|
-
id = [...data.entries()].find(([k, v]) => value === undefined || v === value)?.[0];
|
|
46
|
-
}
|
|
47
|
-
return id;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Standard in-memory support
|
|
52
|
-
*/
|
|
53
|
-
@Injectable()
|
|
54
|
-
export class MemoryModelService implements ModelCrudSupport, ModelStreamSupport, ModelExpirySupport, ModelStorageSupport, ModelIndexedSupport {
|
|
55
|
-
|
|
56
|
-
#store = new Map<string, StoreType>();
|
|
57
|
-
#indices = {
|
|
58
|
-
sorted: new Map<string, Map<string, Map<string, number>>>(),
|
|
59
|
-
unsorted: new Map<string, Map<string, Set<string>>>()
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
idSource = ModelCrudUtil.uuidSource();
|
|
63
|
-
get client(): Map<string, StoreType> { return this.#store; }
|
|
64
|
-
|
|
65
|
-
constructor(public readonly config: MemoryModelConfig) { }
|
|
66
|
-
|
|
67
|
-
#getStore<T extends ModelType>(cls: Class<T> | string): StoreType {
|
|
68
|
-
const key = typeof cls === 'string' ? cls : ModelRegistry.getStore(cls);
|
|
69
|
-
if (!this.#store.has(key)) {
|
|
70
|
-
this.#store.set(key, new Map());
|
|
71
|
-
}
|
|
72
|
-
return this.#store.get(key)!;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
#find<T extends ModelType>(cls: Class<T> | string, id?: string, errorState?: 'data' | 'notfound'): StoreType {
|
|
76
|
-
const store = this.#getStore(cls);
|
|
77
|
-
|
|
78
|
-
if (id && errorState && (errorState === 'notfound' ? !store.has(id) : store.has(id))) {
|
|
79
|
-
throw errorState === 'notfound' ? new NotFoundError(cls, id) : new ExistsError(cls, id);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return store;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
async #removeIndices<T extends ModelType>(cls: Class<T>, id: string): Promise<void> {
|
|
86
|
-
try {
|
|
87
|
-
const item = await this.get(cls, id);
|
|
88
|
-
for (const idx of ModelRegistry.getIndices(cls, ['sorted', 'unsorted'])) {
|
|
89
|
-
const idxName = indexName(cls, idx);
|
|
90
|
-
const { key } = ModelIndexedUtil.computeIndexKey(cls, idx, castTo(item));
|
|
91
|
-
this.#indices[idx.type].get(idxName)?.get(key)?.delete(id);
|
|
92
|
-
}
|
|
93
|
-
} catch (err) {
|
|
94
|
-
if (!(err instanceof NotFoundError)) {
|
|
95
|
-
throw err;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
async #writeIndices<T extends ModelType>(cls: Class<T>, item: T): Promise<void> {
|
|
101
|
-
for (const idx of ModelRegistry.getIndices(cls, ['sorted', 'unsorted'])) {
|
|
102
|
-
const idxName = indexName(cls, idx);
|
|
103
|
-
const { key, sort } = ModelIndexedUtil.computeIndexKey(cls, idx, castTo(item));
|
|
104
|
-
let index = this.#indices[idx.type].get(idxName)?.get(key);
|
|
105
|
-
|
|
106
|
-
if (!index) {
|
|
107
|
-
if (!this.#indices[idx.type].has(idxName)) {
|
|
108
|
-
this.#indices[idx.type].set(idxName, new Map());
|
|
109
|
-
}
|
|
110
|
-
if (idx.type === 'sorted') {
|
|
111
|
-
this.#indices[idx.type].get(idxName)!.set(key, index = new Map());
|
|
112
|
-
} else {
|
|
113
|
-
this.#indices[idx.type].get(idxName)!.set(key, index = new Set());
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
if (index instanceof Map) {
|
|
118
|
-
index?.set(item.id, +sort!);
|
|
119
|
-
} else {
|
|
120
|
-
index?.add(item.id);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
async #persist<T extends ModelType>(cls: Class<T>, item: T, action: 'remove'): Promise<void>;
|
|
126
|
-
async #persist<T extends ModelType>(cls: Class<T>, item: T, action: 'write'): Promise<T>;
|
|
127
|
-
async #persist<T extends ModelType>(cls: Class<T>, item: T, action: 'write' | 'remove'): Promise<T | void> {
|
|
128
|
-
const store = this.#getStore(cls);
|
|
129
|
-
await this.#removeIndices(cls, item.id);
|
|
130
|
-
if (action === 'write') {
|
|
131
|
-
store.set(item.id, Buffer.from(JSON.stringify(item)));
|
|
132
|
-
await this.#writeIndices(cls, item);
|
|
133
|
-
return item;
|
|
134
|
-
} else {
|
|
135
|
-
store.delete(item.id);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
async #getIdByIndex<T extends ModelType>(cls: Class<T>, idx: string, body: DeepPartial<T>): Promise<string> {
|
|
140
|
-
const config = ModelRegistry.getIndex(cls, idx, ['sorted', 'unsorted']);
|
|
141
|
-
const { key, sort } = ModelIndexedUtil.computeIndexKey(cls, config, body);
|
|
142
|
-
const index = this.#indices[config.type].get(indexName(cls, idx))?.get(key);
|
|
143
|
-
let id: string | undefined;
|
|
144
|
-
if (index) {
|
|
145
|
-
if (index instanceof Map) {
|
|
146
|
-
id = getFirstId(index, +sort!); // Grab first id
|
|
147
|
-
} else {
|
|
148
|
-
id = getFirstId(index); // Grab first id
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
if (id) {
|
|
152
|
-
return id;
|
|
153
|
-
}
|
|
154
|
-
throw new NotFoundError(cls, key);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
async postConstruct(): Promise<void> {
|
|
158
|
-
await ModelStorageUtil.registerModelChangeListener(this);
|
|
159
|
-
ModelExpiryUtil.registerCull(this);
|
|
160
|
-
|
|
161
|
-
for (const el of ModelRegistry.getClasses()) {
|
|
162
|
-
for (const idx of ModelRegistry.get(el).indices ?? []) {
|
|
163
|
-
switch (idx.type) {
|
|
164
|
-
case 'unique': {
|
|
165
|
-
console.error('Unique indices are not supported for', { cls: el.Ⲑid, idx: idx.name });
|
|
166
|
-
break;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// CRUD Support
|
|
174
|
-
async get<T extends ModelType>(cls: Class<T>, id: string): Promise<T> {
|
|
175
|
-
const store = this.#getStore(cls);
|
|
176
|
-
if (store.has(id)) {
|
|
177
|
-
const res = await ModelCrudUtil.load(cls, store.get(id)!);
|
|
178
|
-
if (res) {
|
|
179
|
-
if (ModelRegistry.get(cls).expiresAt) {
|
|
180
|
-
if (!ModelExpiryUtil.getExpiryState(cls, res).expired) {
|
|
181
|
-
return res;
|
|
182
|
-
}
|
|
183
|
-
} else {
|
|
184
|
-
return res;
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
throw new NotFoundError(cls, id);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
async create<T extends ModelType>(cls: Class<T>, item: OptionalId<T>): Promise<T> {
|
|
192
|
-
if (!item.id) {
|
|
193
|
-
item.id = this.idSource.create();
|
|
194
|
-
}
|
|
195
|
-
this.#find(cls, item.id, 'data');
|
|
196
|
-
return await this.upsert(cls, item);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
async update<T extends ModelType>(cls: Class<T>, item: T): Promise<T> {
|
|
200
|
-
await this.get(cls, item.id);
|
|
201
|
-
return await this.upsert(cls, item);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
async upsert<T extends ModelType>(cls: Class<T>, item: OptionalId<T>): Promise<T> {
|
|
205
|
-
const store = this.#getStore(cls);
|
|
206
|
-
if (item.id && store.has(item.id)) {
|
|
207
|
-
await ModelCrudUtil.load(cls, store.get(item.id)!, 'exists');
|
|
208
|
-
}
|
|
209
|
-
const prepped = await ModelCrudUtil.preStore(cls, item, this);
|
|
210
|
-
return await this.#persist(cls, prepped, 'write');
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
async updatePartial<T extends ModelType>(cls: Class<T>, item: Partial<T> & { id: string }, view?: string): Promise<T> {
|
|
214
|
-
const id = item.id;
|
|
215
|
-
const clean = await ModelCrudUtil.naivePartialUpdate(cls, item, view, () => this.get(cls, id));
|
|
216
|
-
return await this.#persist(cls, clean, 'write');
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
async delete<T extends ModelType>(cls: Class<T>, id: string): Promise<void> {
|
|
220
|
-
const store = this.#getStore(cls);
|
|
221
|
-
if (!store.has(id)) {
|
|
222
|
-
throw new NotFoundError(cls, id);
|
|
223
|
-
}
|
|
224
|
-
await ModelCrudUtil.load(cls, store.get(id)!);
|
|
225
|
-
const where: ModelType = { id };
|
|
226
|
-
await this.#persist(cls, where, 'remove');
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
async * list<T extends ModelType>(cls: Class<T>): AsyncIterable<T> {
|
|
230
|
-
for (const id of this.#getStore(cls).keys()) {
|
|
231
|
-
try {
|
|
232
|
-
yield await this.get(cls, id);
|
|
233
|
-
} catch (err) {
|
|
234
|
-
if (!(err instanceof NotFoundError)) {
|
|
235
|
-
throw err;
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// Stream Support
|
|
242
|
-
async upsertStream(location: string, input: Readable, meta: StreamMeta): Promise<void> {
|
|
243
|
-
const streams = this.#getStore(STREAMS);
|
|
244
|
-
const metaContent = this.#getStore(STREAM_META);
|
|
245
|
-
metaContent.set(location, Buffer.from(JSON.stringify(meta)));
|
|
246
|
-
streams.set(location, await toBuffer(input));
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
async getStream(location: string, range?: StreamRange): Promise<Readable> {
|
|
250
|
-
const streams = this.#find(STREAMS, location, 'notfound');
|
|
251
|
-
let buffer = streams.get(location)!;
|
|
252
|
-
if (range) {
|
|
253
|
-
range = enforceRange(range, buffer.length);
|
|
254
|
-
buffer = buffer.subarray(range.start, range.end! + 1);
|
|
255
|
-
}
|
|
256
|
-
return Readable.from(buffer);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
async describeStream(location: string): Promise<StreamMeta> {
|
|
260
|
-
const metaContent = this.#find(STREAM_META, location, 'notfound');
|
|
261
|
-
const meta: StreamMeta = JSON.parse(metaContent.get(location)!.toString('utf8'));
|
|
262
|
-
return meta;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
async deleteStream(location: string): Promise<void> {
|
|
266
|
-
const streams = this.#getStore(STREAMS);
|
|
267
|
-
const metaContent = this.#getStore(STREAM_META);
|
|
268
|
-
if (streams.has(location)) {
|
|
269
|
-
streams.delete(location);
|
|
270
|
-
metaContent.delete(location);
|
|
271
|
-
} else {
|
|
272
|
-
throw new NotFoundError('Stream', location);
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// Expiry
|
|
277
|
-
async deleteExpired<T extends ModelType>(cls: Class<T>): Promise<number> {
|
|
278
|
-
return ModelExpiryUtil.naiveDeleteExpired(this, cls);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
// Storage Support
|
|
282
|
-
async createStorage(): Promise<void> {
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
async deleteStorage(): Promise<void> {
|
|
286
|
-
this.#store.clear();
|
|
287
|
-
this.#indices.sorted.clear();
|
|
288
|
-
this.#indices.unsorted.clear();
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
async createModel<T extends ModelType>(cls: Class<T>): Promise<void> {
|
|
293
|
-
for (const idx of ModelRegistry.get(cls).indices ?? []) {
|
|
294
|
-
if (idx.type === 'sorted' || idx.type === 'unsorted') {
|
|
295
|
-
this.#indices[idx.type].set(indexName(cls, idx), new Map());
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
async truncateModel<T extends ModelType>(cls: Class<T>): Promise<void> {
|
|
301
|
-
if (cls === StreamModel) {
|
|
302
|
-
this.#getStore(STREAMS).clear();
|
|
303
|
-
this.#getStore(STREAM_META).clear();
|
|
304
|
-
} else {
|
|
305
|
-
this.#getStore(cls).clear();
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
// Indexed
|
|
310
|
-
async getByIndex<T extends ModelType>(cls: Class<T>, idx: string, body: DeepPartial<T>): Promise<T> {
|
|
311
|
-
return this.get(cls, await this.#getIdByIndex(cls, idx, body));
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
async deleteByIndex<T extends ModelType>(cls: Class<T>, idx: string, body: DeepPartial<T>): Promise<void> {
|
|
315
|
-
await this.delete(cls, await this.#getIdByIndex(cls, idx, body));
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
upsertByIndex<T extends ModelType>(cls: Class<T>, idx: string, body: OptionalId<T>): Promise<T> {
|
|
319
|
-
return ModelIndexedUtil.naiveUpsert(this, cls, idx, body);
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
async * listByIndex<T extends ModelType>(cls: Class<T>, idx: string, body?: DeepPartial<T>): AsyncIterable<T> {
|
|
323
|
-
const config = ModelRegistry.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);
|
|
326
|
-
|
|
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()].sort((a, b) => +a[1] - +b[1]).map(([a, b]) => a)) {
|
|
334
|
-
yield this.get(cls, id);
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
}
|
package/src/service/stream.ts
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import { Readable } from 'node:stream';
|
|
2
|
-
|
|
3
|
-
export interface StreamMeta {
|
|
4
|
-
/**
|
|
5
|
-
* File size
|
|
6
|
-
*/
|
|
7
|
-
size: number;
|
|
8
|
-
/**
|
|
9
|
-
* Mime type of the content
|
|
10
|
-
*/
|
|
11
|
-
contentType: string;
|
|
12
|
-
/**
|
|
13
|
-
* Hash of the file contents. Different files with the same name, will have the same hash
|
|
14
|
-
*/
|
|
15
|
-
hash?: string;
|
|
16
|
-
/**
|
|
17
|
-
* The original base filename of the file
|
|
18
|
-
*/
|
|
19
|
-
filename?: string;
|
|
20
|
-
/**
|
|
21
|
-
* Filenames title, optional for elements like images, audio, videos
|
|
22
|
-
*/
|
|
23
|
-
title?: string;
|
|
24
|
-
/**
|
|
25
|
-
* Content encoding
|
|
26
|
-
*/
|
|
27
|
-
contentEncoding?: string;
|
|
28
|
-
/**
|
|
29
|
-
* Content language
|
|
30
|
-
*/
|
|
31
|
-
contentLanguage?: string;
|
|
32
|
-
/**
|
|
33
|
-
* Cache control
|
|
34
|
-
*/
|
|
35
|
-
cacheControl?: string;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export type StreamRange = { start: number, end?: number };
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Support for Streams CRD. Stream update is not supported.
|
|
42
|
-
*
|
|
43
|
-
* @concrete ../internal/service/common#ModelStreamSupportTarget
|
|
44
|
-
*/
|
|
45
|
-
export interface ModelStreamSupport {
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Upsert stream to storage
|
|
49
|
-
* @param location The location of the stream
|
|
50
|
-
* @param input The actual stream to write
|
|
51
|
-
* @param meta The stream metadata
|
|
52
|
-
*/
|
|
53
|
-
upsertStream(location: string, input: Readable, meta: StreamMeta): Promise<void>;
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Get stream from asset store
|
|
57
|
-
* @param location The location of the stream
|
|
58
|
-
*/
|
|
59
|
-
getStream(location: string, range?: StreamRange): Promise<Readable>;
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Get metadata for stream
|
|
63
|
-
* @param location The location of the stream
|
|
64
|
-
*/
|
|
65
|
-
describeStream(location: string): Promise<StreamMeta>;
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Delete stream by location
|
|
69
|
-
* @param location The location of the stream
|
|
70
|
-
*/
|
|
71
|
-
deleteStream(location: string): Promise<void>;
|
|
72
|
-
}
|
package/support/test/stream.ts
DELETED
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs/promises';
|
|
2
|
-
import assert from 'node:assert';
|
|
3
|
-
import crypto from 'node:crypto';
|
|
4
|
-
import { Readable } from 'node:stream';
|
|
5
|
-
import { pipeline } from 'node:stream/promises';
|
|
6
|
-
import { text as toText } from 'node:stream/consumers';
|
|
7
|
-
|
|
8
|
-
import { Suite, Test, TestFixtures } from '@travetto/test';
|
|
9
|
-
|
|
10
|
-
import { BaseModelSuite } from './base';
|
|
11
|
-
import { ModelStreamSupport } from '../../src/service/stream';
|
|
12
|
-
import { enforceRange } from '../../src/internal/service/stream';
|
|
13
|
-
|
|
14
|
-
@Suite()
|
|
15
|
-
export abstract class ModelStreamSuite extends BaseModelSuite<ModelStreamSupport> {
|
|
16
|
-
|
|
17
|
-
fixture = new TestFixtures(['@travetto/model']);
|
|
18
|
-
|
|
19
|
-
async getHash(stream: Readable): Promise<string> {
|
|
20
|
-
const hash = crypto.createHash('sha1').setEncoding('hex');
|
|
21
|
-
await pipeline(stream, hash);
|
|
22
|
-
return hash.read().toString();
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
async getStream(resource: string): Promise<readonly [{ size: number, contentType: string, hash: string, filename: string }, Readable]> {
|
|
26
|
-
const file = await this.fixture.resolve(resource);
|
|
27
|
-
const { size } = await fs.stat(file);
|
|
28
|
-
const hash = await this.getHash(await this.fixture.readStream(resource));
|
|
29
|
-
|
|
30
|
-
return [
|
|
31
|
-
{ size, contentType: '', hash, filename: resource },
|
|
32
|
-
await this.fixture.readStream(resource)
|
|
33
|
-
] as const;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
@Test()
|
|
37
|
-
async writeBasic(): Promise<void> {
|
|
38
|
-
const service = await this.service;
|
|
39
|
-
const [meta, stream] = await this.getStream('/asset.yml');
|
|
40
|
-
|
|
41
|
-
await service.upsertStream(meta.hash, stream, meta);
|
|
42
|
-
|
|
43
|
-
const retrieved = await service.describeStream(meta.hash);
|
|
44
|
-
assert(meta === retrieved);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
@Test()
|
|
48
|
-
async writeStream(): Promise<void> {
|
|
49
|
-
const service = await this.service;
|
|
50
|
-
const [meta, stream] = await this.getStream('/asset.yml');
|
|
51
|
-
|
|
52
|
-
await service.upsertStream(meta.hash, stream, meta);
|
|
53
|
-
|
|
54
|
-
const retrieved = await service.getStream(meta.hash);
|
|
55
|
-
assert(await this.getHash(retrieved) === meta.hash);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
@Test()
|
|
59
|
-
async writeAndDelete(): Promise<void> {
|
|
60
|
-
const service = await this.service;
|
|
61
|
-
const [meta, stream] = await this.getStream('/asset.yml');
|
|
62
|
-
|
|
63
|
-
await service.upsertStream(meta.hash, stream, meta);
|
|
64
|
-
|
|
65
|
-
await service.deleteStream(meta.hash);
|
|
66
|
-
|
|
67
|
-
await assert.rejects(async () => {
|
|
68
|
-
await service.getStream(meta.hash);
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
@Test()
|
|
73
|
-
async partialStream(): Promise<void> {
|
|
74
|
-
const service = await this.service;
|
|
75
|
-
const [meta, stream] = await this.getStream('/text.txt');
|
|
76
|
-
|
|
77
|
-
await service.upsertStream(meta.hash, stream, meta);
|
|
78
|
-
|
|
79
|
-
const retrieved = await service.getStream(meta.hash);
|
|
80
|
-
const content = await toText(retrieved);
|
|
81
|
-
assert(content.startsWith('abc'));
|
|
82
|
-
assert(content.endsWith('xyz'));
|
|
83
|
-
|
|
84
|
-
const partial = await service.getStream(meta.hash, { start: 10, end: 20 });
|
|
85
|
-
const subContent = await toText(partial);
|
|
86
|
-
const range = await enforceRange({ start: 10, end: 20 }, meta.size);
|
|
87
|
-
assert(subContent.length === (range.end - range.start) + 1);
|
|
88
|
-
|
|
89
|
-
const og = await this.fixture.read('/text.txt');
|
|
90
|
-
|
|
91
|
-
assert(subContent === og.substring(10, 21));
|
|
92
|
-
|
|
93
|
-
const partialUnbounded = await service.getStream(meta.hash, { start: 10 });
|
|
94
|
-
const subContent2 = await toText(partialUnbounded);
|
|
95
|
-
const range2 = await enforceRange({ start: 10 }, meta.size);
|
|
96
|
-
assert(subContent2.length === (range2.end - range2.start) + 1);
|
|
97
|
-
assert(subContent2.startsWith('klm'));
|
|
98
|
-
assert(subContent2.endsWith('xyz'));
|
|
99
|
-
|
|
100
|
-
const partialSingle = await service.getStream(meta.hash, { start: 10, end: 10 });
|
|
101
|
-
const subContent3 = await toText(partialSingle);
|
|
102
|
-
assert(subContent3.length === 1);
|
|
103
|
-
assert(subContent3 === 'k');
|
|
104
|
-
|
|
105
|
-
const partialOverBounded = await service.getStream(meta.hash, { start: 20, end: 40 });
|
|
106
|
-
const subContent4 = await toText(partialOverBounded);
|
|
107
|
-
assert(subContent4.length === 6);
|
|
108
|
-
assert(subContent4.endsWith('xyz'));
|
|
109
|
-
|
|
110
|
-
await assert.rejects(() => service.getStream(meta.hash, { start: -10, end: 10 }));
|
|
111
|
-
await assert.rejects(() => service.getStream(meta.hash, { start: 30, end: 37 }));
|
|
112
|
-
}
|
|
113
|
-
}
|