@travetto/model-redis 2.0.0 → 2.1.0
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 +2 -1
- package/package.json +4 -5
- package/src/config.ts +2 -1
- package/src/service.ts +61 -65
package/README.md
CHANGED
|
@@ -49,11 +49,12 @@ import { Field } from '@travetto/schema';
|
|
|
49
49
|
export class RedisModelConfig {
|
|
50
50
|
|
|
51
51
|
@Field(Object)
|
|
52
|
-
client: redis.
|
|
52
|
+
client: redis.RedisClientOptions = {};
|
|
53
53
|
namespace?: string;
|
|
54
54
|
autoCreate?: boolean;
|
|
55
55
|
|
|
56
56
|
postConstruct() {
|
|
57
|
+
|
|
57
58
|
}
|
|
58
59
|
}
|
|
59
60
|
```
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/model-redis",
|
|
3
3
|
"displayName": "Redis Model Support",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.1.0",
|
|
5
5
|
"description": "Redis backing for the travetto model module.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"typescript",
|
|
@@ -26,10 +26,9 @@
|
|
|
26
26
|
"directory": "module/model-redis"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@travetto/config": "^2.
|
|
30
|
-
"@travetto/model": "^2.
|
|
31
|
-
"
|
|
32
|
-
"redis": "^3.1.2"
|
|
29
|
+
"@travetto/config": "^2.1.0",
|
|
30
|
+
"@travetto/model": "^2.1.0",
|
|
31
|
+
"redis": "^4.0.3"
|
|
33
32
|
},
|
|
34
33
|
"publishConfig": {
|
|
35
34
|
"access": "public"
|
package/src/config.ts
CHANGED
|
@@ -7,10 +7,11 @@ import { Field } from '@travetto/schema';
|
|
|
7
7
|
export class RedisModelConfig {
|
|
8
8
|
|
|
9
9
|
@Field(Object)
|
|
10
|
-
client: redis.
|
|
10
|
+
client: redis.RedisClientOptions = {};
|
|
11
11
|
namespace?: string;
|
|
12
12
|
autoCreate?: boolean;
|
|
13
13
|
|
|
14
14
|
postConstruct() {
|
|
15
|
+
|
|
15
16
|
}
|
|
16
17
|
}
|
package/src/service.ts
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import * as redis from 'redis';
|
|
2
|
-
import * as util from 'util';
|
|
3
2
|
|
|
4
3
|
import { Class, ShutdownManager, Util } from '@travetto/base';
|
|
5
4
|
import { DeepPartial } from '@travetto/schema';
|
|
6
5
|
import {
|
|
7
6
|
ModelCrudSupport, ModelExpirySupport, ModelRegistry, ModelType, ModelStorageSupport,
|
|
8
|
-
NotFoundError, ExistsError, ModelIndexedSupport,
|
|
9
|
-
IndexConfig, OptionalId
|
|
7
|
+
NotFoundError, ExistsError, ModelIndexedSupport, IndexConfig, OptionalId
|
|
10
8
|
} from '@travetto/model';
|
|
11
9
|
import { Injectable } from '@travetto/di';
|
|
12
10
|
|
|
@@ -18,6 +16,8 @@ import { ModelStorageUtil } from '@travetto/model/src/internal/service/storage';
|
|
|
18
16
|
import { RedisModelConfig } from './config';
|
|
19
17
|
|
|
20
18
|
type RedisScan = { key: string } | { match: string };
|
|
19
|
+
type RedisClient = ReturnType<typeof redis.createClient>;
|
|
20
|
+
type RedisMulti = ReturnType<RedisClient['multi']>;
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* A model service backed by redis
|
|
@@ -25,12 +25,10 @@ type RedisScan = { key: string } | { match: string };
|
|
|
25
25
|
@Injectable()
|
|
26
26
|
export class RedisModelService implements ModelCrudSupport, ModelExpirySupport, ModelStorageSupport, ModelIndexedSupport {
|
|
27
27
|
|
|
28
|
-
client:
|
|
28
|
+
client: RedisClient;
|
|
29
29
|
|
|
30
30
|
constructor(public readonly config: RedisModelConfig) { }
|
|
31
31
|
|
|
32
|
-
#wrap = <T>(fn: T): T => (fn as unknown as Function).bind(this.client) as T;
|
|
33
|
-
|
|
34
32
|
#resolveKey(cls: Class | string, id?: string, extra?: string) {
|
|
35
33
|
let key = typeof cls === 'string' ? cls : ModelRegistry.getStore(cls);
|
|
36
34
|
if (id) {
|
|
@@ -45,26 +43,27 @@ export class RedisModelService implements ModelCrudSupport, ModelExpirySupport,
|
|
|
45
43
|
return key;
|
|
46
44
|
}
|
|
47
45
|
|
|
48
|
-
async * #streamValues(op: 'scan' | '
|
|
49
|
-
let prevCursor
|
|
46
|
+
async * #streamValues(op: 'scan' | 'sScan' | 'zScan', search: RedisScan, count = 100): AsyncIterable<string[]> {
|
|
47
|
+
let prevCursor = 0;
|
|
50
48
|
let done = false;
|
|
51
49
|
|
|
52
|
-
const flags = 'match' in search ?
|
|
53
|
-
const key = 'key' in search ?
|
|
50
|
+
const flags = { COUNT: count, ...('match' in search ? { MATCH: search.match } : {}) };
|
|
51
|
+
const key = 'key' in search ? search.key : '';
|
|
54
52
|
|
|
55
53
|
while (!done) {
|
|
56
|
-
const [cursor, results] = await
|
|
57
|
-
|
|
54
|
+
const [cursor, results] = await (
|
|
55
|
+
op === 'scan' ?
|
|
56
|
+
this.client.scan(prevCursor, flags).then(x => [x.cursor, x.keys] as const) :
|
|
57
|
+
op === 'sScan' ?
|
|
58
|
+
this.client.sScan(key, prevCursor, flags).then(x => [x.cursor, x.members] as const) :
|
|
59
|
+
this.client.zScan(key, prevCursor, flags).then(x => [x.cursor, x.members.map(y => y.value)] as const)
|
|
58
60
|
);
|
|
61
|
+
|
|
59
62
|
prevCursor = cursor;
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
yield results;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
if (cursor === '0') {
|
|
63
|
+
|
|
64
|
+
yield results;
|
|
65
|
+
|
|
66
|
+
if (cursor === 0) {
|
|
68
67
|
done = true;
|
|
69
68
|
}
|
|
70
69
|
}
|
|
@@ -74,25 +73,25 @@ export class RedisModelService implements ModelCrudSupport, ModelExpirySupport,
|
|
|
74
73
|
return this.#streamValues('scan', { match: `${this.#resolveKey(prefix)}*` });
|
|
75
74
|
}
|
|
76
75
|
|
|
77
|
-
#removeIndices<T extends ModelType>(cls: Class, item: T, multi:
|
|
76
|
+
#removeIndices<T extends ModelType>(cls: Class, item: T, multi: RedisMulti) {
|
|
78
77
|
for (const idx of ModelRegistry.getIndices(cls, ['sorted', 'unsorted'])) {
|
|
79
78
|
const { key } = ModelIndexedUtil.computeIndexKey(cls, idx, item);
|
|
80
79
|
const fullKey = this.#resolveKey(cls, idx.name, key);
|
|
81
80
|
switch (idx.type) {
|
|
82
|
-
case 'unsorted': multi.
|
|
83
|
-
case 'sorted': multi.
|
|
81
|
+
case 'unsorted': multi.sRem(fullKey, item.id); break;
|
|
82
|
+
case 'sorted': multi.zRem(fullKey, item.id); break;
|
|
84
83
|
}
|
|
85
84
|
}
|
|
86
85
|
}
|
|
87
86
|
|
|
88
|
-
#addIndices<T extends ModelType>(cls: Class, item: T, multi:
|
|
87
|
+
#addIndices<T extends ModelType>(cls: Class, item: T, multi: RedisMulti) {
|
|
89
88
|
for (const idx of ModelRegistry.getIndices(cls, ['sorted', 'unsorted'])) {
|
|
90
89
|
const { key, sort } = ModelIndexedUtil.computeIndexKey(cls, idx, item);
|
|
91
90
|
const fullKey = this.#resolveKey(cls, idx.name, key);
|
|
92
91
|
|
|
93
92
|
switch (idx.type) {
|
|
94
|
-
case 'unsorted': multi.
|
|
95
|
-
case 'sorted': multi.
|
|
93
|
+
case 'unsorted': multi.sAdd(fullKey, item.id); break;
|
|
94
|
+
case 'sorted': multi.zAdd(fullKey, { score: +sort!, value: item.id }); break;
|
|
96
95
|
}
|
|
97
96
|
}
|
|
98
97
|
}
|
|
@@ -120,15 +119,15 @@ export class RedisModelService implements ModelCrudSupport, ModelExpirySupport,
|
|
|
120
119
|
break;
|
|
121
120
|
}
|
|
122
121
|
}
|
|
123
|
-
await
|
|
122
|
+
await multi.exec();
|
|
124
123
|
} else {
|
|
125
124
|
switch (action) {
|
|
126
125
|
case 'write': {
|
|
127
|
-
await this
|
|
126
|
+
await this.client.set(key, JSON.stringify(item));
|
|
128
127
|
break;
|
|
129
128
|
}
|
|
130
129
|
case 'delete': {
|
|
131
|
-
const count = await this
|
|
130
|
+
const count = await this.client.del(this.#resolveKey(cls, item.id));
|
|
132
131
|
if (!count) {
|
|
133
132
|
throw new NotFoundError(cls, item.id);
|
|
134
133
|
}
|
|
@@ -141,30 +140,29 @@ export class RedisModelService implements ModelCrudSupport, ModelExpirySupport,
|
|
|
141
140
|
const expiry = ModelExpiryUtil.getExpiryState(cls, item);
|
|
142
141
|
if (expiry.expiresAt !== undefined) {
|
|
143
142
|
if (expiry.expiresAt) {
|
|
144
|
-
await this
|
|
143
|
+
await this.client.pExpireAt(
|
|
145
144
|
this.#resolveKey(cls, item.id), expiry.expiresAt.getTime()
|
|
146
145
|
);
|
|
147
146
|
} else {
|
|
148
|
-
await this
|
|
147
|
+
await this.client.persist(this.#resolveKey(cls, item.id));
|
|
149
148
|
}
|
|
150
149
|
}
|
|
151
150
|
}
|
|
152
151
|
}
|
|
153
152
|
|
|
154
153
|
async #getIdByIndex<T extends ModelType>(cls: Class<T>, idx: string, body: DeepPartial<T>) {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
154
|
+
ModelCrudUtil.ensureNotSubType(cls);
|
|
155
|
+
|
|
158
156
|
const idxCfg = ModelRegistry.getIndex(cls, idx, ['sorted', 'unsorted']);
|
|
159
157
|
const { key, sort } = ModelIndexedUtil.computeIndexKey(cls, idxCfg, body);
|
|
160
158
|
const fullKey = this.#resolveKey(cls, idxCfg.name, key);
|
|
161
159
|
let id: string | undefined;
|
|
162
160
|
if (idxCfg.type === 'unsorted') {
|
|
163
|
-
id = await this
|
|
161
|
+
id = (await this.client.sRandMember(fullKey))!;
|
|
164
162
|
} else {
|
|
165
|
-
const res =
|
|
163
|
+
const res = await this.client.zRangeByScore(
|
|
166
164
|
fullKey, +sort!, +sort!
|
|
167
|
-
)
|
|
165
|
+
);
|
|
168
166
|
id = res[0];
|
|
169
167
|
}
|
|
170
168
|
if (id) {
|
|
@@ -174,14 +172,15 @@ export class RedisModelService implements ModelCrudSupport, ModelExpirySupport,
|
|
|
174
172
|
}
|
|
175
173
|
|
|
176
174
|
async postConstruct() {
|
|
177
|
-
this.client =
|
|
175
|
+
this.client = redis.createClient(this.config.client);
|
|
176
|
+
await this.client.connect();
|
|
178
177
|
await ModelStorageUtil.registerModelChangeListener(this);
|
|
179
|
-
ShutdownManager.onShutdown(this.constructor.ᚕid, () => this.client.
|
|
178
|
+
ShutdownManager.onShutdown(this.constructor.ᚕid, () => this.client.disconnect());
|
|
180
179
|
for (const el of ModelRegistry.getClasses()) {
|
|
181
180
|
for (const idx of ModelRegistry.get(el).indices ?? []) {
|
|
182
181
|
switch (idx.type) {
|
|
183
182
|
case 'unique': {
|
|
184
|
-
console.error('Unique
|
|
183
|
+
console.error('Unique indices are not supported in redis for', { cls: el.ᚕid, idx: idx.name });
|
|
185
184
|
break;
|
|
186
185
|
}
|
|
187
186
|
}
|
|
@@ -194,7 +193,7 @@ export class RedisModelService implements ModelCrudSupport, ModelExpirySupport,
|
|
|
194
193
|
}
|
|
195
194
|
|
|
196
195
|
async has<T extends ModelType>(cls: Class<T>, id: string, error?: 'notfound' | 'data') {
|
|
197
|
-
const res = await this
|
|
196
|
+
const res = await this.client.exists(this.#resolveKey(cls, id));
|
|
198
197
|
if (res === 0 && error === 'notfound') {
|
|
199
198
|
throw new NotFoundError(cls, id);
|
|
200
199
|
} else if (res === 1 && error === 'data') {
|
|
@@ -203,7 +202,7 @@ export class RedisModelService implements ModelCrudSupport, ModelExpirySupport,
|
|
|
203
202
|
}
|
|
204
203
|
|
|
205
204
|
async get<T extends ModelType>(cls: Class<T>, id: string) {
|
|
206
|
-
const payload = await this
|
|
205
|
+
const payload = await this.client.get(this.#resolveKey(cls, id));
|
|
207
206
|
if (payload) {
|
|
208
207
|
const item = await ModelCrudUtil.load(cls, payload);
|
|
209
208
|
if (item) {
|
|
@@ -223,26 +222,20 @@ export class RedisModelService implements ModelCrudSupport, ModelExpirySupport,
|
|
|
223
222
|
}
|
|
224
223
|
|
|
225
224
|
async update<T extends ModelType>(cls: Class<T>, item: T) {
|
|
226
|
-
|
|
227
|
-
throw new SubTypeNotSupportedError(cls);
|
|
228
|
-
}
|
|
225
|
+
ModelCrudUtil.ensureNotSubType(cls);
|
|
229
226
|
await this.has(cls, item.id, 'notfound');
|
|
230
227
|
return this.upsert(cls, item);
|
|
231
228
|
}
|
|
232
229
|
|
|
233
230
|
async upsert<T extends ModelType>(cls: Class<T>, item: OptionalId<T>) {
|
|
234
|
-
|
|
235
|
-
throw new SubTypeNotSupportedError(cls);
|
|
236
|
-
}
|
|
231
|
+
ModelCrudUtil.ensureNotSubType(cls);
|
|
237
232
|
const prepped = await ModelCrudUtil.preStore(cls, item, this);
|
|
238
233
|
await this.#store(cls, prepped, 'write');
|
|
239
234
|
return prepped;
|
|
240
235
|
}
|
|
241
236
|
|
|
242
237
|
async updatePartial<T extends ModelType>(cls: Class<T>, item: Partial<T> & { id: string }, view?: string) {
|
|
243
|
-
|
|
244
|
-
throw new SubTypeNotSupportedError(cls);
|
|
245
|
-
}
|
|
238
|
+
ModelCrudUtil.ensureNotSubType(cls);
|
|
246
239
|
const id = item.id;
|
|
247
240
|
item = await ModelCrudUtil.naivePartialUpdate(cls, item, view, () => this.get(cls, id)) as T;
|
|
248
241
|
await this.#store(cls, item as T, 'write');
|
|
@@ -250,17 +243,18 @@ export class RedisModelService implements ModelCrudSupport, ModelExpirySupport,
|
|
|
250
243
|
}
|
|
251
244
|
|
|
252
245
|
async delete<T extends ModelType>(cls: Class<T>, id: string) {
|
|
253
|
-
|
|
254
|
-
throw new SubTypeNotSupportedError(cls);
|
|
255
|
-
}
|
|
256
|
-
|
|
246
|
+
ModelCrudUtil.ensureNotSubType(cls);
|
|
257
247
|
await this.#store(cls, { id } as T, 'delete');
|
|
258
248
|
}
|
|
259
249
|
|
|
260
250
|
async * list<T extends ModelType>(cls: Class<T>): AsyncIterable<T> {
|
|
261
251
|
for await (const ids of this.#iterate(cls)) {
|
|
262
252
|
|
|
263
|
-
|
|
253
|
+
if (!ids.length) {
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const bodies = (await this.client.mGet(ids))
|
|
264
258
|
.filter(x => !!x) as string[];
|
|
265
259
|
|
|
266
260
|
for (const body of bodies) {
|
|
@@ -288,11 +282,11 @@ export class RedisModelService implements ModelCrudSupport, ModelExpirySupport,
|
|
|
288
282
|
|
|
289
283
|
async deleteStorage() {
|
|
290
284
|
if (!this.config.namespace) {
|
|
291
|
-
await this
|
|
285
|
+
await this.client.flushDb();
|
|
292
286
|
} else {
|
|
293
287
|
for await (const ids of this.#iterate('')) {
|
|
294
288
|
if (ids.length) {
|
|
295
|
-
await this
|
|
289
|
+
await this.client.del(ids);
|
|
296
290
|
}
|
|
297
291
|
}
|
|
298
292
|
}
|
|
@@ -301,7 +295,7 @@ export class RedisModelService implements ModelCrudSupport, ModelExpirySupport,
|
|
|
301
295
|
async truncateModel<T extends ModelType>(model: Class<T>) {
|
|
302
296
|
for await (const ids of this.#iterate(model)) {
|
|
303
297
|
if (ids.length) {
|
|
304
|
-
await this
|
|
298
|
+
await this.client.del(ids);
|
|
305
299
|
}
|
|
306
300
|
}
|
|
307
301
|
}
|
|
@@ -320,9 +314,7 @@ export class RedisModelService implements ModelCrudSupport, ModelExpirySupport,
|
|
|
320
314
|
}
|
|
321
315
|
|
|
322
316
|
async * listByIndex<T extends ModelType>(cls: Class<T>, idx: string, body?: DeepPartial<T>): AsyncIterable<T> {
|
|
323
|
-
|
|
324
|
-
throw new SubTypeNotSupportedError(cls);
|
|
325
|
-
}
|
|
317
|
+
ModelCrudUtil.ensureNotSubType(cls);
|
|
326
318
|
|
|
327
319
|
const idxCfg = ModelRegistry.getIndex(cls, idx, ['sorted', 'unsorted']);
|
|
328
320
|
|
|
@@ -332,13 +324,17 @@ export class RedisModelService implements ModelCrudSupport, ModelExpirySupport,
|
|
|
332
324
|
const fullKey = this.#resolveKey(cls, idx, key);
|
|
333
325
|
|
|
334
326
|
if (idxCfg.type === 'unsorted') {
|
|
335
|
-
stream = this.#streamValues('
|
|
327
|
+
stream = this.#streamValues('sScan', { key: fullKey });
|
|
336
328
|
} else {
|
|
337
|
-
stream = this.#streamValues('
|
|
329
|
+
stream = this.#streamValues('zScan', { key: fullKey });
|
|
338
330
|
}
|
|
339
331
|
|
|
340
332
|
for await (const ids of stream) {
|
|
341
|
-
|
|
333
|
+
if (!ids.length) {
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const bodies = (await this.client.mGet(
|
|
342
338
|
ids.map(x => this.#resolveKey(cls, x))
|
|
343
339
|
))
|
|
344
340
|
.filter(x => !!x) as string[];
|