@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 CHANGED
@@ -49,11 +49,12 @@ import { Field } from '@travetto/schema';
49
49
  export class RedisModelConfig {
50
50
 
51
51
  @Field(Object)
52
- client: redis.ClientOpts = {};
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.0.0",
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.0.0",
30
- "@travetto/model": "^2.0.0",
31
- "@types/redis": "^2.8.29",
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.ClientOpts = {};
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, SubTypeNotSupportedError,
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: redis.RedisClient;
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' | 'sscan' | 'zscan', search: RedisScan, count = 100): AsyncIterable<string[]> {
49
- let prevCursor: string | undefined;
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 ? ['MATCH', search.match] : [];
53
- const key = 'key' in search ? [search.key] : [];
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 this.#wrap(util.promisify(this.client[op]) as ((...rest: string[]) => Promise<[string, string[]]>))(
57
- ...key, prevCursor ?? '0', ...flags, 'COUNT', `${count}`
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
- if (results.length) {
61
- if (op === 'zscan') {
62
- yield results.filter((x, i) => i % 2 === 0); // Drop scores
63
- } else {
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: redis.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.srem(fullKey, item.id); break;
83
- case 'sorted': multi.zrem(fullKey, item.id); break;
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: redis.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.sadd(fullKey, item.id); break;
95
- case 'sorted': multi.zadd(fullKey, +sort!, item.id); break;
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 new Promise<void>((resolve, reject) => multi.exec(err => err ? reject(err) : resolve()));
122
+ await multi.exec();
124
123
  } else {
125
124
  switch (action) {
126
125
  case 'write': {
127
- await this.#wrap(util.promisify(this.client.set))(key, JSON.stringify(item));
126
+ await this.client.set(key, JSON.stringify(item));
128
127
  break;
129
128
  }
130
129
  case 'delete': {
131
- const count = await this.#wrap(util.promisify(this.client.del as (key2: string, cb: redis.Callback<number>) => void))(this.#resolveKey(cls, item.id));
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.#wrap(util.promisify(this.client.pexpireat))(
143
+ await this.client.pExpireAt(
145
144
  this.#resolveKey(cls, item.id), expiry.expiresAt.getTime()
146
145
  );
147
146
  } else {
148
- await this.#wrap(util.promisify(this.client.persist))(this.#resolveKey(cls, item.id));
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
- if (ModelRegistry.get(cls).subType) {
156
- throw new SubTypeNotSupportedError(cls);
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.#wrap(util.promisify(this.client.srandmember) as (k: string) => Promise<string>)(fullKey);
161
+ id = (await this.client.sRandMember(fullKey))!;
164
162
  } else {
165
- const res = (await this.#wrap(util.promisify(this.client.zrangebyscore) as (k: string, start: string | number, end: string | number, type?: string) => Promise<string[]>)(
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 = new redis.RedisClient(this.config.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.quit());
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 inidices are not supported in redis for', { cls: el.ᚕid, idx: idx.name });
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.#wrap(util.promisify(this.client.exists as (key: string, cb: redis.Callback<number>) => void))(this.#resolveKey(cls, id));
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.#wrap(util.promisify(this.client.get))(this.#resolveKey(cls, id));
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
- if (ModelRegistry.get(cls).subType) {
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
- if (ModelRegistry.get(cls).subType) {
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
- if (ModelRegistry.get(cls).subType) {
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
- if (ModelRegistry.get(cls).subType) {
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
- const bodies = (await this.#wrap(util.promisify(this.client.mget as (keys: string[], cb: redis.Callback<(string | null)[]>) => void))(ids))
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.#wrap(util.promisify(this.client.flushdb))();
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.#wrap(util.promisify(this.client.del) as (...keys: string[]) => Promise<number>)(...ids);
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.#wrap(util.promisify(this.client.del) as (...keys: string[]) => Promise<number>)(...ids);
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
- if (ModelRegistry.get(cls).subType) {
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('sscan', { key: fullKey });
327
+ stream = this.#streamValues('sScan', { key: fullKey });
336
328
  } else {
337
- stream = this.#streamValues('zscan', { key: fullKey });
329
+ stream = this.#streamValues('zScan', { key: fullKey });
338
330
  }
339
331
 
340
332
  for await (const ids of stream) {
341
- const bodies = (await this.#wrap(util.promisify(this.client.mget as (keys: string[], cb: redis.Callback<(string | null)[]>) => void))(
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[];