@toa.io/extensions.stash 1.0.0-alpha.29 → 1.0.0-alpha.292

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/CHANGELOG.md ADDED
@@ -0,0 +1,34 @@
1
+ # Change Log
2
+
3
+ All notable changes to this project will be documented in this file.
4
+ See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
+
6
+ # [1.0.0-alpha.292](https://github.com/toa-io/toa/compare/v1.0.0-alpha.291...v1.0.0-alpha.292) (2026-09-07)
7
+
8
+ ### Features
9
+
10
+ * **definitions:** what a package declares is read from one package ([7052341](https://github.com/toa-io/toa/commit/70523411b5d9e2b204c999aa02365173f3bb518d))
11
+
12
+
13
+ # [1.0.0-alpha.291](https://github.com/toa-io/toa/compare/v1.0.0-alpha.290...v1.0.0-alpha.291) (2026-09-07)
14
+
15
+ **Note:** Version bump only for package @toa.io/extensions.stash
16
+
17
+
18
+
19
+
20
+
21
+ # [1.0.0-alpha.289](https://github.com/toa-io/toa/compare/v1.0.0-alpha.288...v1.0.0-alpha.289) (2026-09-07)
22
+
23
+ * A deploy moves only what changed, and a component sees none of the runtime's environment (#1073) ([f38e3db](https://github.com/toa-io/toa/commit/f38e3db533f24db866c57bfa1ef294eff1eaf499)), closes [#1073](https://github.com/toa-io/toa/issues/1073) [#1064](https://github.com/toa-io/toa/issues/1064) [#1066](https://github.com/toa-io/toa/issues/1066) [#1067](https://github.com/toa-io/toa/issues/1067) [#1068](https://github.com/toa-io/toa/issues/1068) [#1069](https://github.com/toa-io/toa/issues/1069) [#1071](https://github.com/toa-io/toa/issues/1071) [#1070](https://github.com/toa-io/toa/issues/1070) [#1072](https://github.com/toa-io/toa/issues/1072)
24
+
25
+ ### BREAKING CHANGES
26
+
27
+ * a component that read `process.env.TOA_*` reads `context` instead;
28
+ `echo(input)` no longer substitutes from the environment; a bash operation sees no
29
+ `TOA_*`; images no longer set `USER node` — see migrations/289.md.
30
+
31
+
32
+ # [1.0.0-alpha.287](https://github.com/toa-io/toa/compare/v1.0.0-alpha.286...v1.0.0-alpha.287) (2026-09-06)
33
+
34
+ **Note:** Version bump only for package @toa.io/extensions.stash
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@toa.io/extensions.stash",
3
- "version": "1.0.0-alpha.29",
3
+ "version": "1.0.0-alpha.292",
4
+ "type": "module",
4
5
  "description": "Toa Stash",
5
6
  "author": "temich <tema.gurtovoy@gmail.com>",
6
7
  "homepage": "https://github.com/toa-io/toa#readme",
@@ -17,17 +18,15 @@
17
18
  "access": "public"
18
19
  },
19
20
  "dependencies": {
20
- "@toa.io/pointer": "1.0.0-alpha.29",
21
- "ioredis": "5.3.2",
22
- "msgpackr": "1.10.1",
23
- "redlock-temp-fix": "5.0.0-beta.2"
21
+ "@toa.io/definitions": "1.0.0-alpha.292",
22
+ "@toa.io/generic": "1.0.0-alpha.292",
23
+ "@toa.io/pointer": "1.0.0-alpha.292",
24
+ "ioredis": "6.0.0",
25
+ "msgpackr": "2.1.0",
26
+ "openspan": "1.0.0-alpha.286"
24
27
  },
25
28
  "scripts": {
26
29
  "transpile": "tsc"
27
30
  },
28
- "jest": {
29
- "preset": "ts-jest",
30
- "testEnvironment": "node"
31
- },
32
- "gitHead": "186d0e17c12a9016b8076edf925963cbb32b9219"
31
+ "gitHead": "0f6aa3ad9dbec86bb632a7975ea61d1dde1d8fc5"
33
32
  }
package/readme.md CHANGED
@@ -1,14 +1,13 @@
1
1
  # Toa Stash
2
2
 
3
- Shared cache and distributed lock manager on top of [ioredis](https://github.com/redis/ioredis)
4
- and [redlock](https://github.com/mike-marcacci/node-redlock).
3
+ Shared cache on top of [ioredis](https://github.com/redis/ioredis).
5
4
 
6
5
  ## Shared cache
7
6
 
8
7
  `stash` aspect exposes [`ioredis` methods](https://redis.github.io/ioredis/classes/Cluster.html).
9
8
 
10
9
  ```javascript
11
- async function computation (input, context) {
10
+ async function computation(input, context) {
12
11
  await context.stash.set('key', 'value')
13
12
  await context.stash.get('key')
14
13
  }
@@ -27,18 +26,6 @@ starting from third.
27
26
 
28
27
  Values are encoded using [msgpack](https://msgpack.org).
29
28
 
30
- ## Distributed lock manager
31
-
32
- `async lock<T>(id: string | string[], routine: async? () => T): T`
33
-
34
- Executes `routine` once a lock is successfully acquired. Lock ID is component-scoped.
35
-
36
- ```javascript
37
- async function computation (input, context) {
38
- await context.stash.lock('lock id', () => console.log('Lock acquired'))
39
- }
40
- ```
41
-
42
29
  ## Manifest
43
30
 
44
31
  To enable extension for a component, add `null` definition to its manifest:
package/source/Aspect.ts CHANGED
@@ -1,47 +1,71 @@
1
- import Redlock from 'redlock-temp-fix'
1
+ /* eslint-disable @typescript-eslint/no-floating-promises */
2
2
  import { encode, decode } from 'msgpackr'
3
- import { Connector, type extensions } from '@toa.io/core'
4
- import type { Connection } from './Connection'
5
- import type { Redis } from 'ioredis'
3
+ import { console, type SpanOptions } from 'openspan'
4
+ import { Connector } from '@toa.io/core'
5
+ import type { extensions } from '@toa.io/core/types'
6
+ import type { Connection } from './Connection.js'
7
+ import type { Redis, ChainableCommander } from 'ioredis'
6
8
 
7
9
  export class Aspect extends Connector implements extensions.Aspect {
8
10
  public readonly name = 'stash'
9
11
  private readonly connection: Connection
10
12
  private redis: Redis | null = null
11
- private redlock: Redlock | null = null
12
13
 
13
- public constructor (connection: Connection) {
14
+ public constructor(connection: Connection) {
14
15
  super()
15
16
 
16
17
  this.connection = connection
17
18
  this.depends(connection)
18
19
  }
19
20
 
20
- public async invoke (method: 'store', key: string, value: object): Promise<void>
21
- public async invoke (method: 'fetch', key: string): Promise<object>
22
- public async invoke<T> (method: 'lock', key: Resources, routine: Routine<T>): Promise<T>
23
- public async invoke (method: string, ...args: unknown[]): Promise<any>
24
- public async invoke (method: string, ...args: unknown[]): Promise<any> {
21
+ public invoke(method: 'store', key: string, value: object): any
22
+ public invoke(method: 'fetch', key: string): any
23
+ public invoke(method: string, ...args: unknown[]): any
24
+ // eslint-disable-next-line @typescript-eslint/promise-function-async
25
+ public invoke(method: string, ...args: unknown[]): any {
25
26
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
26
27
  // @ts-expect-error
27
- if (typeof this.redis[method] === 'function') return this.redis[method](...args)
28
+ if (typeof this.redis[method] === 'function') {
29
+ // multi/pipeline return a sync chainable; the span wraps exec() instead
30
+ if (method === 'multi' || method === 'pipeline') {
31
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
32
+ // @ts-expect-error
33
+ const chain: ChainableCommander = this.redis[method](...args)
34
+ const exec = chain.exec.bind(chain)
35
+
36
+ chain.exec = async () => {
37
+ const options = span(method, args[0])
38
+
39
+ Object.assign(options.attributes!, { 'db.operation.batch.size': chain.length })
40
+
41
+ return await console.span(options, exec)
42
+ }
43
+
44
+ return chain
45
+ }
46
+
47
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
48
+ // @ts-expect-error
49
+ return console.span(span(method, args[0]), () => this.redis[method](...args))
50
+ }
28
51
 
29
52
  if (method === 'store')
30
- await this.store(args[0] as string, args[1] as object, ...args.slice(2))
53
+ console.span(span(method, args[0]), async () => {
54
+ await this.store(args[0] as string, args[1] as object, ...args.slice(2))
55
+ })
31
56
 
32
57
  if (method === 'fetch')
33
- return await this.fetch(args[0] as string)
34
-
35
- if (method === 'lock')
36
- return await this.lock(args[0] as Resources, args[1] as () => any)
58
+ return console.span(
59
+ span(method, args[0]),
60
+ async () => await this.fetch(args[0] as string)
61
+ )
37
62
  }
38
63
 
39
- protected override async open (): Promise<void> {
40
- this.redis = this.connection.redises[0]
41
- this.redlock = new Redlock(this.connection.redises, { retryCount: -1 })
64
+ protected override async open(): Promise<void> {
65
+ this.redis = this.connection.redis
42
66
  }
43
67
 
44
- private async store (key: string, value: object, ...args: unknown[]): Promise<void> {
68
+ private async store(key: string, value: object, ...args: unknown[]): Promise<void> {
45
69
  const buffer = encode(value)
46
70
 
47
71
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
@@ -49,22 +73,27 @@ export class Aspect extends Connector implements extensions.Aspect {
49
73
  await this.redis.set(key, buffer, ...args)
50
74
  }
51
75
 
52
- private async fetch (key: string): Promise<object | null> {
76
+ private async fetch(key: string): Promise<object | null> {
53
77
  if (this.redis === null) return null
54
78
 
55
79
  const buffer = await this.redis.getBuffer(key)
56
80
 
57
81
  return buffer === null ? null : decode(buffer)
58
82
  }
83
+ }
59
84
 
60
- private async lock<T> (key: Resources, routine: Routine<T>): Promise<T | null> {
61
- if (this.redlock === null) return null
85
+ function span(method: string, key: unknown): SpanOptions {
86
+ // https://opentelemetry.io/docs/specs/semconv/database/redis/
87
+ // `db.namespace` names the database node on service graphs,
88
+ // which otherwise displays 'unknown'
89
+ const attributes: Record<string, unknown> = {
90
+ 'db.system': 'redis',
91
+ 'db.namespace': 'stash',
92
+ 'db.operation.name': method
93
+ }
62
94
 
63
- if (typeof key === 'string') key = [key]
95
+ if (typeof key === 'string') attributes.key = key
96
+ else if (Array.isArray(key)) attributes.key = key.join(' ')
64
97
 
65
- return await this.redlock.using<T>(key, 5000, routine)
66
- }
98
+ return { name: `${method} stash`, kind: 'client', attributes }
67
99
  }
68
-
69
- type Routine<T> = () => Promise<T>
70
- type Resources = string | string[]
@@ -1,48 +1,57 @@
1
- import { Redis, type ClusterOptions } from 'ioredis'
1
+ import { Redis, type RedisOptions } from 'ioredis'
2
+ import { console } from 'openspan'
2
3
  import { Connector, type Locator } from '@toa.io/core'
3
4
  import { resolve } from '@toa.io/pointer'
4
- import { ID } from './extension'
5
+ import { ID } from '@toa.io/definitions/extensions.stash'
6
+ import { environment } from '@toa.io/generic'
5
7
 
6
8
  export class Connection extends Connector {
7
- public readonly redises: Redis[] = []
8
- private readonly locator: Locator
9
+ public redis: Redis | null = null
10
+ public readonly locator: Locator
9
11
 
10
- public constructor (locator: Locator) {
12
+ public constructor(locator: Locator) {
11
13
  super()
12
14
 
13
15
  this.locator = locator
14
16
  }
15
17
 
16
- protected override async open (): Promise<void> {
18
+ protected override async open(): Promise<void> {
17
19
  const keyPrefix = `${this.locator.namespace}:${this.locator.name}:`
18
- const options: ClusterOptions = { keyPrefix, enableReadyCheck: true, lazyConnect: true }
19
- const urls = await this.resolveURLs()
20
20
 
21
- for (const url of urls)
22
- this.redises.push(new Redis(url, options))
21
+ const options: RedisOptions = {
22
+ keyPrefix,
23
+ enableReadyCheck: true,
24
+ lazyConnect: true,
25
+ protocol: 3,
26
+ replyMapping: 'resp3'
27
+ }
23
28
 
24
- const connecting = this.redises.map(this.connectNode.bind(this))
29
+ this.redis = new Redis(await this.resolveURL(), options)
25
30
 
26
- await Promise.all(connecting)
31
+ await this.redis.connect()
32
+
33
+ console.info('Stash connected to redis', { host: this.redis.options.host })
27
34
  }
28
35
 
29
- protected override async close (): Promise<void> {
30
- for (const redis of this.redises)
31
- redis.disconnect()
36
+ protected override async close(): Promise<void> {
37
+ this.redis?.disconnect()
38
+ this.redis = null
32
39
 
33
- console.log('Stash disconnected')
40
+ console.info('Stash shutdown complete')
34
41
  }
35
42
 
36
- private async connectNode (redis: Redis): Promise<void> {
37
- await redis.connect()
43
+ private async resolveURL(): Promise<string> {
44
+ // Toa's own development stack is not on the conventional ports: the applications built on
45
+ // Toa are, and they share the machine. See CONTRIBUTING.md.
46
+ if (environment.get('TOA_DEV') === '1') return 'redis://localhost:31040'
38
47
 
39
- console.log(`Stash connected to ${redis.options.host}:${String(redis.options.port)}`)
40
- }
48
+ const urls = resolve(ID, this.locator.id)
49
+
50
+ // several addresses used to be independent masters for the lock manager, which the atom
51
+ // aspect holds now. A cache is one Redis, and the rest have never been read from
52
+ if (urls.length > 1)
53
+ console.warn('Stash takes the first of several addresses', { count: urls.length })
41
54
 
42
- private async resolveURLs (): Promise<string[]> {
43
- if (process.env.TOA_DEV === '1')
44
- return ['redis://localhost']
45
- else
46
- return resolve(ID, this.locator.id)
55
+ return urls[0]
47
56
  }
48
57
  }
@@ -0,0 +1,12 @@
1
+ import type { RedisCommander } from 'ioredis'
2
+
3
+ /** What `context.stash` is: every Redis command, and what the aspect adds to them. */
4
+ export interface Stash extends RedisCommander {
5
+ store: <T>(key: string, value: T) => Promise<void>
6
+ fetch: <T>(key: string) => Promise<T>
7
+ lock: <T>(key: Resources, routine: Routine<T>) => Promise<T>
8
+ count: (name: string, interval: number, amount?: number) => number
9
+ }
10
+
11
+ type Routine<T> = () => Promise<T>
12
+ type Resources = string | string[]
@@ -1,30 +1,12 @@
1
- import { createVariables, type URIMap, type Request } from '@toa.io/pointer'
2
- import { Aspect } from './Aspect'
3
- import { Connection } from './Connection'
4
- import type { Locator, extensions } from '@toa.io/core'
5
- import type { context } from '@toa.io/norm'
6
- import type { Dependency } from '@toa.io/operations'
1
+ import { Aspect } from './Aspect.js'
2
+ import { Connection } from './Connection.js'
3
+ import type { Locator } from '@toa.io/core'
4
+ import type { extensions } from '@toa.io/core/types'
7
5
 
8
6
  export class Factory implements extensions.Factory {
9
- public aspect (locator: Locator): extensions.Aspect {
7
+ public aspect(locator: Locator): extensions.Aspect {
10
8
  const connection = new Connection(locator)
11
9
 
12
10
  return new Aspect(connection)
13
11
  }
14
12
  }
15
-
16
- export function deployment (instances: context.Dependency[], annotation: URIMap): Dependency {
17
- const requests: Request[] = instances.map((instance) => createRequest(instance))
18
- const variables = createVariables(ID, annotation, requests)
19
-
20
- return { variables }
21
- }
22
-
23
- function createRequest (instance: context.Dependency): Request {
24
- return {
25
- group: instance.locator.label,
26
- selectors: [instance.locator.id]
27
- }
28
- }
29
-
30
- export const ID = 'stash'
package/source/index.ts CHANGED
@@ -1 +1,2 @@
1
- export * from './extension'
1
+ export * from './extension.js'
2
+ export type { Stash } from './Stash.js'
@@ -1,20 +1,15 @@
1
- import { Connector, type extensions } from '@toa.io/core';
2
- import type { Connection } from './Connection';
1
+ import { Connector } from '@toa.io/core';
2
+ import type { extensions } from '@toa.io/core/types';
3
+ import type { Connection } from './Connection.js';
3
4
  export declare class Aspect extends Connector implements extensions.Aspect {
4
5
  readonly name = "stash";
5
6
  private readonly connection;
6
7
  private redis;
7
- private redlock;
8
8
  constructor(connection: Connection);
9
- invoke(method: 'store', key: string, value: object): Promise<void>;
10
- invoke(method: 'fetch', key: string): Promise<object>;
11
- invoke<T>(method: 'lock', key: Resources, routine: Routine<T>): Promise<T>;
12
- invoke(method: string, ...args: unknown[]): Promise<any>;
9
+ invoke(method: 'store', key: string, value: object): any;
10
+ invoke(method: 'fetch', key: string): any;
11
+ invoke(method: string, ...args: unknown[]): any;
13
12
  protected open(): Promise<void>;
14
13
  private store;
15
14
  private fetch;
16
- private lock;
17
15
  }
18
- type Routine<T> = () => Promise<T>;
19
- type Resources = string | string[];
20
- export {};
@@ -1,40 +1,50 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.Aspect = void 0;
7
- const redlock_temp_fix_1 = __importDefault(require("redlock-temp-fix"));
8
- const msgpackr_1 = require("msgpackr");
9
- const core_1 = require("@toa.io/core");
10
- class Aspect extends core_1.Connector {
1
+ /* eslint-disable @typescript-eslint/no-floating-promises */
2
+ import { encode, decode } from 'msgpackr';
3
+ import { console } from 'openspan';
4
+ import { Connector } from '@toa.io/core';
5
+ export class Aspect extends Connector {
11
6
  name = 'stash';
12
7
  connection;
13
8
  redis = null;
14
- redlock = null;
15
9
  constructor(connection) {
16
10
  super();
17
11
  this.connection = connection;
18
12
  this.depends(connection);
19
13
  }
20
- async invoke(method, ...args) {
14
+ // eslint-disable-next-line @typescript-eslint/promise-function-async
15
+ invoke(method, ...args) {
21
16
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
22
17
  // @ts-expect-error
23
- if (typeof this.redis[method] === 'function')
24
- return this.redis[method](...args);
18
+ if (typeof this.redis[method] === 'function') {
19
+ // multi/pipeline return a sync chainable; the span wraps exec() instead
20
+ if (method === 'multi' || method === 'pipeline') {
21
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
22
+ // @ts-expect-error
23
+ const chain = this.redis[method](...args);
24
+ const exec = chain.exec.bind(chain);
25
+ chain.exec = async () => {
26
+ const options = span(method, args[0]);
27
+ Object.assign(options.attributes, { 'db.operation.batch.size': chain.length });
28
+ return await console.span(options, exec);
29
+ };
30
+ return chain;
31
+ }
32
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
33
+ // @ts-expect-error
34
+ return console.span(span(method, args[0]), () => this.redis[method](...args));
35
+ }
25
36
  if (method === 'store')
26
- await this.store(args[0], args[1], ...args.slice(2));
37
+ console.span(span(method, args[0]), async () => {
38
+ await this.store(args[0], args[1], ...args.slice(2));
39
+ });
27
40
  if (method === 'fetch')
28
- return await this.fetch(args[0]);
29
- if (method === 'lock')
30
- return await this.lock(args[0], args[1]);
41
+ return console.span(span(method, args[0]), async () => await this.fetch(args[0]));
31
42
  }
32
43
  async open() {
33
- this.redis = this.connection.redises[0];
34
- this.redlock = new redlock_temp_fix_1.default(this.connection.redises, { retryCount: -1 });
44
+ this.redis = this.connection.redis;
35
45
  }
36
46
  async store(key, value, ...args) {
37
- const buffer = (0, msgpackr_1.encode)(value);
47
+ const buffer = encode(value);
38
48
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
39
49
  // @ts-expect-error
40
50
  await this.redis.set(key, buffer, ...args);
@@ -43,15 +53,22 @@ class Aspect extends core_1.Connector {
43
53
  if (this.redis === null)
44
54
  return null;
45
55
  const buffer = await this.redis.getBuffer(key);
46
- return buffer === null ? null : (0, msgpackr_1.decode)(buffer);
47
- }
48
- async lock(key, routine) {
49
- if (this.redlock === null)
50
- return null;
51
- if (typeof key === 'string')
52
- key = [key];
53
- return await this.redlock.using(key, 5000, routine);
56
+ return buffer === null ? null : decode(buffer);
54
57
  }
55
58
  }
56
- exports.Aspect = Aspect;
59
+ function span(method, key) {
60
+ // https://opentelemetry.io/docs/specs/semconv/database/redis/
61
+ // `db.namespace` names the database node on service graphs,
62
+ // which otherwise displays 'unknown'
63
+ const attributes = {
64
+ 'db.system': 'redis',
65
+ 'db.namespace': 'stash',
66
+ 'db.operation.name': method
67
+ };
68
+ if (typeof key === 'string')
69
+ attributes.key = key;
70
+ else if (Array.isArray(key))
71
+ attributes.key = key.join(' ');
72
+ return { name: `${method} stash`, kind: 'client', attributes };
73
+ }
57
74
  //# sourceMappingURL=Aspect.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"Aspect.js","sourceRoot":"","sources":["../source/Aspect.ts"],"names":[],"mappings":";;;;;;AAAA,wEAAsC;AACtC,uCAAyC;AACzC,uCAAyD;AAIzD,MAAa,MAAO,SAAQ,gBAAS;IACnB,IAAI,GAAG,OAAO,CAAA;IACb,UAAU,CAAY;IAC/B,KAAK,GAAiB,IAAI,CAAA;IAC1B,OAAO,GAAmB,IAAI,CAAA;IAEtC,YAAoB,UAAsB;QACxC,KAAK,EAAE,CAAA;QAEP,IAAI,CAAC,UAAU,GAAG,UAAU,CAAA;QAC5B,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAA;IAC1B,CAAC;IAMM,KAAK,CAAC,MAAM,CAAE,MAAc,EAAE,GAAG,IAAe;QACrD,6DAA6D;QAC7D,mBAAmB;QACnB,IAAI,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,UAAU;YAAE,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,GAAG,IAAI,CAAC,CAAA;QAEhF,IAAI,MAAM,KAAK,OAAO;YACpB,MAAM,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAW,EAAE,IAAI,CAAC,CAAC,CAAW,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAA;QAE1E,IAAI,MAAM,KAAK,OAAO;YACpB,OAAO,MAAM,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAW,CAAC,CAAA;QAE5C,IAAI,MAAM,KAAK,MAAM;YACnB,OAAO,MAAM,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAc,EAAE,IAAI,CAAC,CAAC,CAAc,CAAC,CAAA;IACtE,CAAC;IAEkB,KAAK,CAAC,IAAI;QAC3B,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,CAAA;QACvC,IAAI,CAAC,OAAO,GAAG,IAAI,0BAAO,CAAC,IAAI,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,UAAU,EAAE,CAAC,CAAC,EAAE,CAAC,CAAA;IACzE,CAAC;IAEO,KAAK,CAAC,KAAK,CAAE,GAAW,EAAE,KAAa,EAAE,GAAG,IAAe;QACjE,MAAM,MAAM,GAAG,IAAA,iBAAM,EAAC,KAAK,CAAC,CAAA;QAE5B,6DAA6D;QAC7D,mBAAmB;QACnB,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC,CAAA;IAC5C,CAAC;IAEO,KAAK,CAAC,KAAK,CAAE,GAAW;QAC9B,IAAI,IAAI,CAAC,KAAK,KAAK,IAAI;YAAE,OAAO,IAAI,CAAA;QAEpC,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,GAAG,CAAC,CAAA;QAE9C,OAAO,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAA,iBAAM,EAAC,MAAM,CAAC,CAAA;IAChD,CAAC;IAEO,KAAK,CAAC,IAAI,CAAK,GAAc,EAAE,OAAmB;QACxD,IAAI,IAAI,CAAC,OAAO,KAAK,IAAI;YAAE,OAAO,IAAI,CAAA;QAEtC,IAAI,OAAO,GAAG,KAAK,QAAQ;YAAE,GAAG,GAAG,CAAC,GAAG,CAAC,CAAA;QAExC,OAAO,MAAM,IAAI,CAAC,OAAO,CAAC,KAAK,CAAI,GAAG,EAAE,IAAI,EAAE,OAAO,CAAC,CAAA;IACxD,CAAC;CACF;AA5DD,wBA4DC"}
1
+ {"version":3,"file":"Aspect.js","sourceRoot":"","sources":["../source/Aspect.ts"],"names":[],"mappings":"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"}
@@ -1,11 +1,10 @@
1
1
  import { Redis } from 'ioredis';
2
2
  import { Connector, type Locator } from '@toa.io/core';
3
3
  export declare class Connection extends Connector {
4
- readonly redises: Redis[];
5
- private readonly locator;
4
+ redis: Redis | null;
5
+ readonly locator: Locator;
6
6
  constructor(locator: Locator);
7
7
  protected open(): Promise<void>;
8
8
  protected close(): Promise<void>;
9
- private connectNode;
10
- private resolveURLs;
9
+ private resolveURL;
11
10
  }
@@ -1,12 +1,11 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Connection = void 0;
4
- const ioredis_1 = require("ioredis");
5
- const core_1 = require("@toa.io/core");
6
- const pointer_1 = require("@toa.io/pointer");
7
- const extension_1 = require("./extension");
8
- class Connection extends core_1.Connector {
9
- redises = [];
1
+ import { Redis } from 'ioredis';
2
+ import { console } from 'openspan';
3
+ import { Connector } from '@toa.io/core';
4
+ import { resolve } from '@toa.io/pointer';
5
+ import { ID } from '@toa.io/definitions/extensions.stash';
6
+ import { environment } from '@toa.io/generic';
7
+ export class Connection extends Connector {
8
+ redis = null;
10
9
  locator;
11
10
  constructor(locator) {
12
11
  super();
@@ -14,28 +13,33 @@ class Connection extends core_1.Connector {
14
13
  }
15
14
  async open() {
16
15
  const keyPrefix = `${this.locator.namespace}:${this.locator.name}:`;
17
- const options = { keyPrefix, enableReadyCheck: true, lazyConnect: true };
18
- const urls = await this.resolveURLs();
19
- for (const url of urls)
20
- this.redises.push(new ioredis_1.Redis(url, options));
21
- const connecting = this.redises.map(this.connectNode.bind(this));
22
- await Promise.all(connecting);
16
+ const options = {
17
+ keyPrefix,
18
+ enableReadyCheck: true,
19
+ lazyConnect: true,
20
+ protocol: 3,
21
+ replyMapping: 'resp3'
22
+ };
23
+ this.redis = new Redis(await this.resolveURL(), options);
24
+ await this.redis.connect();
25
+ console.info('Stash connected to redis', { host: this.redis.options.host });
23
26
  }
24
27
  async close() {
25
- for (const redis of this.redises)
26
- redis.disconnect();
27
- console.log('Stash disconnected');
28
+ this.redis?.disconnect();
29
+ this.redis = null;
30
+ console.info('Stash shutdown complete');
28
31
  }
29
- async connectNode(redis) {
30
- await redis.connect();
31
- console.log(`Stash connected to ${redis.options.host}:${String(redis.options.port)}`);
32
- }
33
- async resolveURLs() {
34
- if (process.env.TOA_DEV === '1')
35
- return ['redis://localhost'];
36
- else
37
- return (0, pointer_1.resolve)(extension_1.ID, this.locator.id);
32
+ async resolveURL() {
33
+ // Toa's own development stack is not on the conventional ports: the applications built on
34
+ // Toa are, and they share the machine. See CONTRIBUTING.md.
35
+ if (environment.get('TOA_DEV') === '1')
36
+ return 'redis://localhost:31040';
37
+ const urls = resolve(ID, this.locator.id);
38
+ // several addresses used to be independent masters for the lock manager, which the atom
39
+ // aspect holds now. A cache is one Redis, and the rest have never been read from
40
+ if (urls.length > 1)
41
+ console.warn('Stash takes the first of several addresses', { count: urls.length });
42
+ return urls[0];
38
43
  }
39
44
  }
40
- exports.Connection = Connection;
41
45
  //# sourceMappingURL=Connection.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"Connection.js","sourceRoot":"","sources":["../source/Connection.ts"],"names":[],"mappings":";;;AAAA,qCAAoD;AACpD,uCAAsD;AACtD,6CAAyC;AACzC,2CAAgC;AAEhC,MAAa,UAAW,SAAQ,gBAAS;IACvB,OAAO,GAAY,EAAE,CAAA;IACpB,OAAO,CAAS;IAEjC,YAAoB,OAAgB;QAClC,KAAK,EAAE,CAAA;QAEP,IAAI,CAAC,OAAO,GAAG,OAAO,CAAA;IACxB,CAAC;IAEkB,KAAK,CAAC,IAAI;QAC3B,MAAM,SAAS,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,GAAG,CAAA;QACnE,MAAM,OAAO,GAAmB,EAAE,SAAS,EAAE,gBAAgB,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,CAAA;QACxF,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,WAAW,EAAE,CAAA;QAErC,KAAK,MAAM,GAAG,IAAI,IAAI;YACpB,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,eAAK,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC,CAAA;QAE5C,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAA;QAEhE,MAAM,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,CAAA;IAC/B,CAAC;IAEkB,KAAK,CAAC,KAAK;QAC5B,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,OAAO;YAC9B,KAAK,CAAC,UAAU,EAAE,CAAA;QAEpB,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAA;IACnC,CAAC;IAEO,KAAK,CAAC,WAAW,CAAE,KAAY;QACrC,MAAM,KAAK,CAAC,OAAO,EAAE,CAAA;QAErB,OAAO,CAAC,GAAG,CAAC,sBAAsB,KAAK,CAAC,OAAO,CAAC,IAAI,IAAI,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;IACvF,CAAC;IAEO,KAAK,CAAC,WAAW;QACvB,IAAI,OAAO,CAAC,GAAG,CAAC,OAAO,KAAK,GAAG;YAC7B,OAAO,CAAC,mBAAmB,CAAC,CAAA;;YAE5B,OAAO,IAAA,iBAAO,EAAC,cAAE,EAAE,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;IACvC,CAAC;CACF;AA1CD,gCA0CC"}
1
+ {"version":3,"file":"Connection.js","sourceRoot":"","sources":["../source/Connection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAqB,MAAM,SAAS,CAAA;AAClD,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,CAAA;AAClC,OAAO,EAAE,SAAS,EAAgB,MAAM,cAAc,CAAA;AACtD,OAAO,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAA;AACzC,OAAO,EAAE,EAAE,EAAE,MAAM,sCAAsC,CAAA;AACzD,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA;AAE7C,MAAM,OAAO,UAAW,SAAQ,SAAS;IAChC,KAAK,GAAiB,IAAI,CAAA;IACjB,OAAO,CAAS;IAEhC,YAAmB,OAAgB;QACjC,KAAK,EAAE,CAAA;QAEP,IAAI,CAAC,OAAO,GAAG,OAAO,CAAA;IACxB,CAAC;IAEkB,KAAK,CAAC,IAAI;QAC3B,MAAM,SAAS,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,GAAG,CAAA;QAEnE,MAAM,OAAO,GAAiB;YAC5B,SAAS;YACT,gBAAgB,EAAE,IAAI;YACtB,WAAW,EAAE,IAAI;YACjB,QAAQ,EAAE,CAAC;YACX,YAAY,EAAE,OAAO;SACtB,CAAA;QAED,IAAI,CAAC,KAAK,GAAG,IAAI,KAAK,CAAC,MAAM,IAAI,CAAC,UAAU,EAAE,EAAE,OAAO,CAAC,CAAA;QAExD,MAAM,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,CAAA;QAE1B,OAAO,CAAC,IAAI,CAAC,0BAA0B,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAA;IAC7E,CAAC;IAEkB,KAAK,CAAC,KAAK;QAC5B,IAAI,CAAC,KAAK,EAAE,UAAU,EAAE,CAAA;QACxB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAA;QAEjB,OAAO,CAAC,IAAI,CAAC,yBAAyB,CAAC,CAAA;IACzC,CAAC;IAEO,KAAK,CAAC,UAAU;QACtB,0FAA0F;QAC1F,4DAA4D;QAC5D,IAAI,WAAW,CAAC,GAAG,CAAC,SAAS,CAAC,KAAK,GAAG;YAAE,OAAO,yBAAyB,CAAA;QAExE,MAAM,IAAI,GAAG,OAAO,CAAC,EAAE,EAAE,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;QAEzC,wFAAwF;QACxF,iFAAiF;QACjF,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC;YACjB,OAAO,CAAC,IAAI,CAAC,4CAA4C,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAA;QAEpF,OAAO,IAAI,CAAC,CAAC,CAAC,CAAA;IAChB,CAAC;CACF"}
@@ -0,0 +1,11 @@
1
+ import type { RedisCommander } from 'ioredis';
2
+ /** What `context.stash` is: every Redis command, and what the aspect adds to them. */
3
+ export interface Stash extends RedisCommander {
4
+ store: <T>(key: string, value: T) => Promise<void>;
5
+ fetch: <T>(key: string) => Promise<T>;
6
+ lock: <T>(key: Resources, routine: Routine<T>) => Promise<T>;
7
+ count: (name: string, interval: number, amount?: number) => number;
8
+ }
9
+ type Routine<T> = () => Promise<T>;
10
+ type Resources = string | string[];
11
+ export {};