@toa.io/extensions.realtime 1.0.0-alpha.309 → 1.0.0-alpha.310

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.
@@ -1,4 +1,11 @@
1
1
  import { Stream, Stash } from './lib/index.js';
2
+ /**
3
+ * A stream is opened per call, and a key has as many of them as it has consumers: one stream read
4
+ * by several would hand each of its values to one of them only, and a consumer leaving would
5
+ * destroy it under the others. What arrives under a key is pushed to every stream of it; what
6
+ * belongs to a connection — the welcome, the token, the events missed since a token — to that
7
+ * connection's stream alone.
8
+ */
2
9
  export class Effect {
3
10
  streams = new Map();
4
11
  stash;
@@ -10,28 +17,25 @@ export class Effect {
10
17
  this.stash = context.state.stash;
11
18
  }
12
19
  unmount() {
13
- this.logs.info('Destroying streams', { count: this.streams.size });
20
+ const streams = [...this.streams.values()].flatMap((set) => [...set]);
21
+ this.logs.info('Destroying streams', { count: streams.length });
14
22
  // closed, not destroyed: destroying a stream that is still piped to a response
15
23
  // makes end-of-stream report a premature close, which reaches no one and takes
16
24
  // the process down.
17
- for (const stream of this.streams.values())
25
+ for (const stream of streams)
18
26
  stream.close();
19
27
  }
20
28
  async execute(input) {
21
29
  const key = input.key;
22
- if (!this.streams.has(key)) {
23
- const stream = this.createStream(key);
24
- this.streams.set(key, stream);
25
- this.logs.debug('Stream created', { key });
26
- }
30
+ const stream = this.createStream(key);
27
31
  // welcome
28
- setTimeout(() => this.streams.get(key)?.heartbeat(), 1000);
32
+ setTimeout(() => stream.heartbeat(), 1000);
29
33
  if (input.token === undefined)
30
34
  void this.stash.connect(key).then((token) => {
31
35
  if (token instanceof Error)
32
36
  this.logs.error('Failed to connect to stash', { key, error: token });
33
37
  else
34
- this.streams.get(key)?.push({ event: 'token', data: token });
38
+ stream.push({ event: 'token', data: token });
35
39
  });
36
40
  else
37
41
  void this.stash.pop(key, input.token).then((result) => {
@@ -43,23 +47,31 @@ export class Effect {
43
47
  this.logs.error('Failed to pop from stash', { key, error: result });
44
48
  return;
45
49
  }
46
- const stream = this.streams.get(key);
47
- if (stream === undefined)
50
+ if (stream.destroyed)
48
51
  return;
49
52
  const [token, events] = result;
50
53
  for (const event of events)
51
54
  stream.push({ event: event.event, data: event.data });
52
55
  stream.push({ event: 'token', data: token });
53
56
  });
54
- return this.streams.get(key);
57
+ return stream;
55
58
  }
56
59
  createStream(key) {
57
60
  const stream = new Stream();
61
+ let set = this.streams.get(key);
62
+ if (set === undefined) {
63
+ set = new Set();
64
+ this.streams.set(key, set);
65
+ }
66
+ set.add(stream);
67
+ this.logs.debug('Stream created', { key, count: set.size });
58
68
  stream.events.once('destroy', () => {
59
- this.logs.debug('Stream destroyed', { key });
60
- this.streams.delete(key);
69
+ set.delete(stream);
70
+ if (set.size === 0)
71
+ this.streams.delete(key);
72
+ this.logs.debug('Stream destroyed', { key, count: set.size });
61
73
  });
62
74
  return stream;
63
75
  }
64
76
  }
65
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY3JlYXRlLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vc291cmNlL2NyZWF0ZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFHQSxPQUFPLEVBQUUsTUFBTSxFQUFFLEtBQUssRUFBRSxNQUFNLGdCQUFnQixDQUFBO0FBRTlDLE1BQU0sT0FBTyxNQUFNO0lBQ0EsT0FBTyxHQUFHLElBQUksR0FBRyxFQUFrQixDQUFBO0lBQzVDLEtBQUssQ0FBUTtJQUNiLElBQUksQ0FBSztJQUVWLEtBQUssQ0FBQyxPQUFnQjtRQUMzQixPQUFPLENBQUMsS0FBSyxDQUFDLE9BQU8sR0FBRyxJQUFJLENBQUMsT0FBTyxDQUFBO1FBQ3BDLE9BQU8sQ0FBQyxLQUFLLENBQUMsS0FBSyxHQUFHLElBQUksS0FBSyxDQUFDLE9BQU8sQ0FBQyxLQUFLLEVBQUUsT0FBTyxDQUFDLGFBQWEsRUFBRSxPQUFPLENBQUMsSUFBSSxDQUFDLENBQUE7UUFFbkYsSUFBSSxDQUFDLElBQUksR0FBRyxPQUFPLENBQUMsSUFBSSxDQUFBO1FBQ3hCLElBQUksQ0FBQyxLQUFLLEdBQUcsT0FBTyxDQUFDLEtBQUssQ0FBQyxLQUFLLENBQUE7SUFDbEMsQ0FBQztJQUVNLE9BQU87UUFDWixJQUFJLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxvQkFBb0IsRUFBRSxFQUFFLEtBQUssRUFBRSxJQUFJLENBQUMsT0FBTyxDQUFDLElBQUksRUFBRSxDQUFDLENBQUE7UUFFbEUsK0VBQStFO1FBQy9FLCtFQUErRTtRQUMvRSxvQkFBb0I7UUFDcEIsS0FBSyxNQUFNLE1BQU0sSUFBSSxJQUFJLENBQUMsT0FBTyxDQUFDLE1BQU0sRUFBRTtZQUFFLE1BQU0sQ0FBQyxLQUFLLEVBQUUsQ0FBQTtJQUM1RCxDQUFDO0lBRU0sS0FBSyxDQUFDLE9BQU8sQ0FBQyxLQUFZO1FBQy9CLE1BQU0sR0FBRyxHQUFHLEtBQUssQ0FBQyxHQUFHLENBQUE7UUFFckIsSUFBSSxDQUFDLElBQUksQ0FBQyxPQUFP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
77
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY3JlYXRlLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vc291cmNlL2NyZWF0ZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFHQSxPQUFPLEVBQUUsTUFBTSxFQUFFLEtBQUssRUFBRSxNQUFNLGdCQUFnQixDQUFBO0FBRTlDOzs7Ozs7R0FNRztBQUNILE1BQU0sT0FBTyxNQUFNO0lBQ0EsT0FBTyxHQUFHLElBQUksR0FBRyxFQUF1QixDQUFBO0lBQ2pELEtBQUssQ0FBUTtJQUNiLElBQUksQ0FBSztJQUVWLEtBQUssQ0FBQyxPQUFnQjtRQUMzQixPQUFPLENBQUMsS0FBSyxDQUFDLE9BQU8sR0FBRyxJQUFJLENBQUMsT0FBTyxDQUFBO1FBQ3BDLE9BQU8sQ0FBQyxLQUFLLENBQUMsS0FBSyxHQUFHLElBQUksS0FBSyxDQUFDLE9BQU8sQ0FBQyxLQUFLLEVBQUUsT0FBTyxDQUFDLGFBQWEsRUFBRSxPQUFPLENBQUMsSUFBSSxDQUFDLENBQUE7UUFFbkYsSUFBSSxDQUFDLElBQUksR0FBRyxPQUFPLENBQUMsSUFBSSxDQUFBO1FBQ3hCLElBQUksQ0FBQyxLQUFLLEdBQUcsT0FBTyxDQUFDLEtBQUssQ0FBQyxLQUFLLENBQUE7SUFDbEMsQ0FBQztJQUVNLE9BQU87UUFDWixNQUFNLE9BQU8sR0FBRyxDQUFDLEdBQUcsSUFBSSxDQUFDLE9BQU8sQ0FBQyxNQUFNLEVBQUUsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxDQUFDLEdBQUcsRUFBRSxFQUFFLENBQUMsQ0FBQyxHQUFHLEdBQUcsQ0FBQyxDQUFDLENBQUE7UUFFckUsSUFBSSxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsb0JBQW9CLEVBQUUsRUFBRSxLQUFLLEVBQUUsT0FBTyxDQUFDLE1BQU0sRUFBRSxDQUFDLENBQUE7UUFFL0QsK0VBQStFO1FBQy9FLCtFQUErRTtRQUMvRSxvQkFBb0I7UUFDcEIsS0FBSyxNQUFNLE1BQU0sSUFBSSxPQUFPO1lBQUUsTUFBTSxDQUFDLEtBQUssRUFBRSxD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
@@ -1,10 +1,18 @@
1
1
  export async function effect({ key, event, data }, context) {
2
- context.state.streams.get(key)?.push({ event, data });
2
+ broadcast(context, key, { event, data });
3
3
  void context.state.stash.push(key, event, data).then((token) => {
4
4
  if (token instanceof Error)
5
5
  context.logs.error('Failed to push to stash', { key, error: token });
6
6
  else
7
- context.state.streams.get(key)?.push({ event: 'token', data: token });
7
+ broadcast(context, key, { event: 'token', data: token });
8
8
  });
9
9
  }
10
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicHVzaC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3NvdXJjZS9wdXNoLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUVBLE1BQU0sQ0FBQyxLQUFLLFVBQVUsTUFBTSxDQUMxQixFQUFFLEdBQUcsRUFBRSxLQUFLLEVBQUUsSUFBSSxFQUFhLEVBQy9CLE9BQWdCO0lBRWhCLE9BQU8sQ0FBQyxLQUFLLENBQUMsT0FBTyxDQUFDLEdBQUcsQ0FBQyxHQUFHLENBQUMsRUFBRSxJQUFJLENBQUMsRUFBRSxLQUFLLEVBQUUsSUFBSSxFQUFFLENBQUMsQ0FBQTtJQUVyRCxLQUFLLE9BQU8sQ0FBQyxLQUFLLENBQUMsS0FBSyxDQUFDLElBQUksQ0FBQyxHQUFHLEVBQUUsS0FBSyxFQUFFLElBQUksQ0FBQyxDQUFDLElBQUksQ0FBQyxDQUFDLEtBQUssRUFBRSxFQUFFO1FBQzdELElBQUksS0FBSyxZQUFZLEtBQUs7WUFDeEIsT0FBTyxDQUFDLElBQUksQ0FBQyxLQUFLLENBQUMseUJBQXlCLEVBQUUsRUFBRSxHQUFHLEVBQUUsS0FBSyxFQUFFLEtBQUssRUFBRSxDQUFDLENBQUE7O1lBQ2pFLE9BQU8sQ0FBQyxLQUFLLENBQUMsT0FBTyxDQUFDLEdBQUcsQ0FBQyxHQUFHLENBQUMsRUFBRSxJQUFJLENBQUMsRUFBRSxLQUFLLEVBQUUsT0FBTyxFQUFFLElBQUksRUFBRSxLQUFLLEVBQUUsQ0FBQyxDQUFBO0lBQzVFLENBQUMsQ0FBQyxDQUFBO0FBQ0osQ0FBQyJ9
10
+ /** To every stream the key has: each is a consumer of its own. */
11
+ function broadcast(context, key, message) {
12
+ const streams = context.state.streams.get(key);
13
+ if (streams === undefined)
14
+ return;
15
+ for (const stream of streams)
16
+ stream.push(message);
17
+ }
18
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicHVzaC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3NvdXJjZS9wdXNoLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUVBLE1BQU0sQ0FBQyxLQUFLLFVBQVUsTUFBTSxDQUMxQixFQUFFLEdBQUcsRUFBRSxLQUFLLEVBQUUsSUFBSSxFQUFhLEVBQy9CLE9BQWdCO0lBRWhCLFNBQVMsQ0FBQyxPQUFPLEVBQUUsR0FBRyxFQUFFLEVBQUUsS0FBSyxFQUFFLElBQUksRUFBRSxDQUFDLENBQUE7SUFFeEMsS0FBSyxPQUFPLENBQUMsS0FBSyxDQUFDLEtBQUssQ0FBQyxJQUFJLENBQUMsR0FBRyxFQUFFLEtBQUssRUFBRSxJQUFJLENBQUMsQ0FBQyxJQUFJLENBQUMsQ0FBQyxLQUFLLEVBQUUsRUFBRTtRQUM3RCxJQUFJLEtBQUssWUFBWSxLQUFLO1lBQ3hCLE9BQU8sQ0FBQyxJQUFJLENBQUMsS0FBSyxDQUFDLHlCQUF5QixFQUFFLEVBQUUsR0FBRyxFQUFFLEtBQUssRUFBRSxLQUFLLEVBQUUsQ0FBQyxDQUFBOztZQUNqRSxTQUFTLENBQUMsT0FBTyxFQUFFLEdBQUcsRUFBRSxFQUFFLEtBQUssRUFBRSxPQUFPLEVBQUUsSUFBSSxFQUFFLEtBQUssRUFBRSxDQUFDLENBQUE7SUFDL0QsQ0FBQyxDQUFDLENBQUE7QUFDSixDQUFDO0FBRUQsa0VBQWtFO0FBQ2xFLFNBQVMsU0FBUyxDQUFDLE9BQWdCLEVBQUUsR0FBVyxFQUFFLE9BQWU7SUFDL0QsTUFBTSxPQUFPLEdBQUcsT0FBTyxDQUFDLEtBQUssQ0FBQyxPQUFPLENBQUMsR0FBRyxDQUFDLEdBQUcsQ0FBQyxDQUFBO0lBRTlDLElBQUksT0FBTyxLQUFLLFNBQVM7UUFBRSxPQUFNO0lBRWpDLEtBQUssTUFBTSxNQUFNLElBQUksT0FBTztRQUFFLE1BQU0sQ0FBQyxJQUFJLENBQUMsT0FBTyxDQUFDLENBQUE7QUFDcEQsQ0FBQyJ9
@@ -3,8 +3,15 @@ import { type Operation } from '@toa.io/bridges.node'
3
3
  import type { Context } from '../types/index.d.ts'
4
4
  import { Stream, Stash } from './lib/index.ts'
5
5
 
6
+ /**
7
+ * A stream is opened per call, and a key has as many of them as it has consumers: one stream read
8
+ * by several would hand each of its values to one of them only, and a consumer leaving would
9
+ * destroy it under the others. What arrives under a key is pushed to every stream of it; what
10
+ * belongs to a connection — the welcome, the token, the events missed since a token — to that
11
+ * connection's stream alone.
12
+ */
6
13
  export class Effect implements Operation {
7
- private readonly streams = new Map<string, Stream>()
14
+ private readonly streams = new Map<string, Set<Stream>>()
8
15
  private stash!: Stash
9
16
  private logs: any
10
17
 
@@ -17,32 +24,28 @@ export class Effect implements Operation {
17
24
  }
18
25
 
19
26
  public unmount(): void {
20
- this.logs.info('Destroying streams', { count: this.streams.size })
27
+ const streams = [...this.streams.values()].flatMap((set) => [...set])
28
+
29
+ this.logs.info('Destroying streams', { count: streams.length })
21
30
 
22
31
  // closed, not destroyed: destroying a stream that is still piped to a response
23
32
  // makes end-of-stream report a premature close, which reaches no one and takes
24
33
  // the process down.
25
- for (const stream of this.streams.values()) stream.close()
34
+ for (const stream of streams) stream.close()
26
35
  }
27
36
 
28
37
  public async execute(input: Input): Promise<Readable> {
29
38
  const key = input.key
30
-
31
- if (!this.streams.has(key)) {
32
- const stream = this.createStream(key)
33
-
34
- this.streams.set(key, stream)
35
- this.logs.debug('Stream created', { key })
36
- }
39
+ const stream = this.createStream(key)
37
40
 
38
41
  // welcome
39
- setTimeout(() => this.streams.get(key)?.heartbeat(), 1000)
42
+ setTimeout(() => stream.heartbeat(), 1000)
40
43
 
41
44
  if (input.token === undefined)
42
45
  void this.stash.connect(key).then((token) => {
43
46
  if (token instanceof Error)
44
47
  this.logs.error('Failed to connect to stash', { key, error: token })
45
- else this.streams.get(key)?.push({ event: 'token', data: token })
48
+ else stream.push({ event: 'token', data: token })
46
49
  })
47
50
  else
48
51
  void this.stash.pop(key, input.token).then((result) => {
@@ -56,9 +59,7 @@ export class Effect implements Operation {
56
59
  return
57
60
  }
58
61
 
59
- const stream = this.streams.get(key)
60
-
61
- if (stream === undefined) return
62
+ if (stream.destroyed) return
62
63
 
63
64
  const [token, events] = result
64
65
 
@@ -68,15 +69,27 @@ export class Effect implements Operation {
68
69
  stream.push({ event: 'token', data: token })
69
70
  })
70
71
 
71
- return this.streams.get(key)!
72
+ return stream
72
73
  }
73
74
 
74
75
  private createStream(key: string): Stream {
75
76
  const stream = new Stream()
77
+ let set = this.streams.get(key)
78
+
79
+ if (set === undefined) {
80
+ set = new Set()
81
+ this.streams.set(key, set)
82
+ }
83
+
84
+ set.add(stream)
85
+ this.logs.debug('Stream created', { key, count: set.size })
76
86
 
77
87
  stream.events.once('destroy', () => {
78
- this.logs.debug('Stream destroyed', { key })
79
- this.streams.delete(key)
88
+ set.delete(stream)
89
+
90
+ if (set.size === 0) this.streams.delete(key)
91
+
92
+ this.logs.debug('Stream destroyed', { key, count: set.size })
80
93
  })
81
94
 
82
95
  return stream
@@ -4,11 +4,20 @@ export async function effect(
4
4
  { key, event, data }: PushInput,
5
5
  context: Context
6
6
  ): Promise<void> {
7
- context.state.streams.get(key)?.push({ event, data })
7
+ broadcast(context, key, { event, data })
8
8
 
9
9
  void context.state.stash.push(key, event, data).then((token) => {
10
10
  if (token instanceof Error)
11
11
  context.logs.error('Failed to push to stash', { key, error: token })
12
- else context.state.streams.get(key)?.push({ event: 'token', data: token })
12
+ else broadcast(context, key, { event: 'token', data: token })
13
13
  })
14
14
  }
15
+
16
+ /** To every stream the key has: each is a consumer of its own. */
17
+ function broadcast(context: Context, key: string, message: object): void {
18
+ const streams = context.state.streams.get(key)
19
+
20
+ if (streams === undefined) return
21
+
22
+ for (const stream of streams) stream.push(message)
23
+ }
@@ -12,7 +12,7 @@ import type { Stash } from '../source/lib/Stash.ts'
12
12
  export interface Context {
13
13
  stash: Redis
14
14
  state: {
15
- streams: Map<string, Stream>
15
+ streams: Map<string, Set<Stream>>
16
16
  stash: Stash
17
17
  }
18
18
  logs: Logs
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toa.io/extensions.realtime",
3
- "version": "1.0.0-alpha.309",
3
+ "version": "1.0.0-alpha.310",
4
4
  "type": "module",
5
5
  "description": "Toa Realtime",
6
6
  "author": "temich <tema.gurtovoy@gmail.com>",
@@ -26,14 +26,14 @@
26
26
  "!**/.env*"
27
27
  ],
28
28
  "dependencies": {
29
- "@toa.io/core": "1.0.0-alpha.308",
30
- "@toa.io/definitions": "1.0.0-alpha.309",
31
- "@toa.io/generic": "1.0.0-alpha.304",
29
+ "@toa.io/core": "1.0.0-alpha.310",
30
+ "@toa.io/definitions": "1.0.0-alpha.310",
31
+ "@toa.io/generic": "1.0.0-alpha.310",
32
32
  "openspan": "1.0.0-alpha.305"
33
33
  },
34
34
  "scripts": {
35
35
  "transpile": "npx tsc && npx tsc -p ./components/realtime.streams",
36
36
  "features": "TSX_TSCONFIG_PATH=features/steps/tsconfig.json NODE_OPTIONS=--import=tsx cucumber-js"
37
37
  },
38
- "gitHead": "6e0cd9ef0c9b9f4f9db666f6f19fc44e61bf2606"
38
+ "gitHead": "d952c57f77279729e26d52d50c6df47138ad0a4f"
39
39
  }
package/readme.md CHANGED
@@ -109,6 +109,10 @@ the [`auth:id: key`](/extensions/exposition/documentation/access.md#id) authoriz
109
109
  Opening a stream is not a read, and `GET` is the method a client opens one with, so the route
110
110
  declares [`io:readonly: false`](/extensions/exposition/documentation/io.md#readonly).
111
111
 
112
+ A key may be consumed by several clients at once — the same user on two devices, or in two
113
+ tabs. Each of them receives every event routed to the key, and one of them disconnecting leaves
114
+ the others connected.
115
+
112
116
  Refer to the [Exposition extension](/extensions/exposition) for more details:
113
117
 
114
118
  - [Multipart responses](/extensions/exposition/documentation/protocol.md#multipart-types)