@toa.io/extensions.exposition 1.0.0-alpha.312 → 1.0.0-alpha.314

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.
Files changed (58) hide show
  1. package/components/exposition.octets/package.json +2 -2
  2. package/components/identity.basic/package.json +1 -1
  3. package/components/identity.federation/package.json +1 -1
  4. package/components/identity.passkeys/package.json +1 -1
  5. package/components/identity.tokens/package.json +2 -2
  6. package/package.json +6 -4
  7. package/readme.md +1 -0
  8. package/transpiled/Factory.d.ts +5 -0
  9. package/transpiled/Factory.js +3 -0
  10. package/transpiled/Factory.js.map +1 -1
  11. package/transpiled/directives/index.js +2 -0
  12. package/transpiled/directives/index.js.map +1 -1
  13. package/transpiled/directives/realtime/Hub.d.ts +45 -0
  14. package/transpiled/directives/realtime/Hub.js +221 -0
  15. package/transpiled/directives/realtime/Hub.js.map +1 -0
  16. package/transpiled/directives/realtime/Realtime.d.ts +18 -0
  17. package/transpiled/directives/realtime/Realtime.js +37 -0
  18. package/transpiled/directives/realtime/Realtime.js.map +1 -0
  19. package/transpiled/directives/realtime/Stream.d.ts +38 -0
  20. package/transpiled/directives/realtime/Stream.js +89 -0
  21. package/transpiled/directives/realtime/Stream.js.map +1 -0
  22. package/transpiled/directives/realtime/index.d.ts +2 -0
  23. package/transpiled/directives/realtime/index.js +3 -0
  24. package/transpiled/directives/realtime/index.js.map +1 -0
  25. package/transpiled/directives/realtime/measurements.d.ts +1 -0
  26. package/transpiled/directives/realtime/measurements.js +11 -0
  27. package/transpiled/directives/realtime/measurements.js.map +1 -0
  28. package/transpiled/realtime/Destination.d.ts +24 -0
  29. package/transpiled/realtime/Destination.js +83 -0
  30. package/transpiled/realtime/Destination.js.map +1 -0
  31. package/transpiled/realtime/Factory.d.ts +11 -0
  32. package/transpiled/realtime/Factory.js +15 -0
  33. package/transpiled/realtime/Factory.js.map +1 -0
  34. package/transpiled/realtime/measurements.d.ts +1 -0
  35. package/transpiled/realtime/measurements.js +11 -0
  36. package/transpiled/realtime/measurements.js.map +1 -0
  37. package/transpiled/realtime/redis.d.ts +36 -0
  38. package/transpiled/realtime/redis.js +83 -0
  39. package/transpiled/realtime/redis.js.map +1 -0
  40. package/transpiled/root.js +15 -0
  41. package/transpiled/root.js.map +1 -1
  42. package/ui/dist/_app/immutable/chunks/{CrvExPUH.js → 3z-MngL5.js} +1 -1
  43. package/ui/dist/_app/immutable/chunks/{DRHH1IZ6.js → CubNYXGc.js} +1 -1
  44. package/ui/dist/_app/immutable/chunks/DnwsDdVR.js +6 -0
  45. package/ui/dist/_app/immutable/chunks/{qtkBHvJX.js → gT7UvzUs.js} +1 -1
  46. package/ui/dist/_app/immutable/entry/{app.prc8D_7j.js → app.BuIzcMse.js} +2 -2
  47. package/ui/dist/_app/immutable/entry/start.Djj5Mm5B.js +1 -0
  48. package/ui/dist/_app/immutable/nodes/{0.viBY06uQ.js → 0.CwHL4JQh.js} +1 -1
  49. package/ui/dist/_app/immutable/nodes/{1.CJVJ5gi6.js → 1.DgOr0IJQ.js} +1 -1
  50. package/ui/dist/_app/immutable/nodes/{2.DhzIPVlj.js → 2.1ODUSz5b.js} +1 -1
  51. package/ui/dist/_app/immutable/nodes/{3.jGOeu924.js → 3._pT85mpt.js} +1 -1
  52. package/ui/dist/_app/immutable/nodes/{4.DXHvFLcC.js → 4.DiXgB9MQ.js} +1 -1
  53. package/ui/dist/_app/immutable/nodes/{5.tGdgzTwp.js → 5.D7U8VR0l.js} +1 -1
  54. package/ui/dist/_app/immutable/nodes/{6.DfOhdTZ4.js → 6.CiZdC04N.js} +1 -1
  55. package/ui/dist/_app/version.json +1 -1
  56. package/ui/dist/index.html +10 -10
  57. package/ui/dist/_app/immutable/chunks/DJsGiBo1.js +0 -6
  58. package/ui/dist/_app/immutable/entry/start.B8R-hX_D.js +0 -1
@@ -3,8 +3,8 @@
3
3
  "private": true,
4
4
  "type": "module",
5
5
  "dependencies": {
6
- "@aws-sdk/client-s3": "3.1131.0",
7
- "@aws-sdk/lib-storage": "3.1131.0",
6
+ "@aws-sdk/client-s3": "3.1133.0",
7
+ "@aws-sdk/lib-storage": "3.1133.0",
8
8
  "cloudinary": "2.11.0"
9
9
  }
10
10
  }
@@ -3,6 +3,6 @@
3
3
  "private": true,
4
4
  "type": "module",
5
5
  "dependencies": {
6
- "@node-rs/bcrypt": "1.10.8"
6
+ "@node-rs/bcrypt": "1.10.9"
7
7
  }
8
8
  }
@@ -3,6 +3,6 @@
3
3
  "private": true,
4
4
  "type": "module",
5
5
  "dependencies": {
6
- "jose": "6.2.10"
6
+ "jose": "6.2.12"
7
7
  }
8
8
  }
@@ -3,6 +3,6 @@
3
3
  "private": true,
4
4
  "type": "module",
5
5
  "dependencies": {
6
- "@simplewebauthn/server": "14.0.0"
6
+ "@simplewebauthn/server": "14.0.2"
7
7
  }
8
8
  }
@@ -3,8 +3,8 @@
3
3
  "private": true,
4
4
  "type": "module",
5
5
  "dependencies": {
6
- "jose": "6.2.10",
6
+ "jose": "6.2.12",
7
7
  "lru-cache": "11.5.2",
8
- "paseto": "4.0.0"
8
+ "paseto": "4.0.1"
9
9
  }
10
10
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toa.io/extensions.exposition",
3
- "version": "1.0.0-alpha.312",
3
+ "version": "1.0.0-alpha.314",
4
4
  "type": "module",
5
5
  "description": "Toa Exposition",
6
6
  "author": "temich <tema.gurtovoy@gmail.com>",
@@ -28,11 +28,13 @@
28
28
  "!**/.env*"
29
29
  ],
30
30
  "dependencies": {
31
- "@toa.io/core": "1.0.0-alpha.312",
32
- "@toa.io/definitions": "1.0.0-alpha.312",
31
+ "@toa.io/core": "1.0.0-alpha.314",
32
+ "@toa.io/definitions": "1.0.0-alpha.314",
33
33
  "@toa.io/generic": "1.0.0-alpha.311",
34
+ "@toa.io/pointer": "1.0.0-alpha.311",
34
35
  "@toa.io/schemas": "1.0.0-alpha.311",
35
36
  "content-type": "3.1.0",
37
+ "ioredis": "6.0.0",
36
38
  "matchacho": "0.6.1",
37
39
  "minimatch": "10.2.6",
38
40
  "msgpackr": "2.1.0",
@@ -72,5 +74,5 @@
72
74
  "lru-cache": "11.5.2",
73
75
  "paseto": "4.0.1"
74
76
  },
75
- "gitHead": "3cdff9b9779eeb6ae83913fefc0b502a10a0236a"
77
+ "gitHead": "4b7c0f4468e6fa89f95a69f43fed85c648fb033d"
76
78
  }
package/readme.md CHANGED
@@ -219,6 +219,7 @@ See also [telemetry ready probe](../telemetry/readme.md#ready-probe) and
219
219
  - [OAuth authorization server](documentation/oauth.md)
220
220
  - [Consent](documentation/consent.md)
221
221
  - [BLOBs](documentation/octets.md)
222
+ - [Realtime streams](documentation/realtime.md)
222
223
  - [Components and resources](documentation/components.md)
223
224
  - [Caching](documentation/cache.md)
224
225
  - [Features](features)
@@ -1,7 +1,12 @@
1
+ import { Realtime } from './realtime/Factory.ts';
1
2
  import type { syntax } from './RTD/index.ts';
2
3
  import type { Connector, Contract, Locator } from '@toa.io/core';
3
4
  import type { extensions } from '@toa.io/core/types';
4
5
  export declare class Factory implements extensions.Factory {
6
+ /** `realtime`, declared beside `exposition`: the routes of a component's events to streams */
7
+ readonly keys: {
8
+ realtime: Realtime;
9
+ };
5
10
  private readonly host;
6
11
  private announcing;
7
12
  private announcements;
@@ -2,8 +2,11 @@ import { createHash } from 'node:crypto';
2
2
  import { contract } from '@toa.io/core';
3
3
  import { Announcements } from './Announcements.js';
4
4
  import { Tenant } from './Tenant.js';
5
+ import { Realtime } from './realtime/Factory.js';
5
6
  import { CHANNEL } from '@toa.io/definitions/extensions.exposition';
6
7
  export class Factory {
8
+ /** `realtime`, declared beside `exposition`: the routes of a component's events to streams */
9
+ keys = { realtime: new Realtime() };
7
10
  host;
8
11
  announcing;
9
12
  announcements;
@@ -1 +1 @@
1
- {"version":3,"file":"Factory.js","sourceRoot":"","sources":["../source/Factory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AACxC,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAA;AACvC,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAClD,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACpC,OAAO,EAAE,OAAO,EAAE,MAAM,2CAA2C,CAAA;AAMnE,MAAM,OAAO,OAAO;IACD,IAAI,CAAM;IACnB,UAAU,CAAoC;IAC9C,aAAa,CAA2B;IAEhD,YAAmB,IAAU;QAC3B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;IAClB,CAAC;IAEM,KAAK,CAAC,MAAM,CACjB,OAAgB,EAChB,IAAiB,EACjB,QAAkB;QAElB,MAAM,aAAa,GAAG,MAAM,IAAI,CAAC,MAAM,EAAE,CAAA;QACzC,MAAM,MAAM,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;QAE9E,4EAA4E;QAC5E,MAAM,MAAM,GAA8B;YACxC,oFAAoF;YACpF,SAAS,EAAE,OAAO,CAAC,SAAU;YAC7B,SAAS,EAAE,OAAO,CAAC,IAAI;YACvB,QAAQ,EAAE,OAAO,CAAC,SAAS,KAAK,UAAU;YAC1C,IAAI;YACJ,OAAO,EAAE,QAAQ,CAAC,OAAO;YACzB,QAAQ,EAAE,QAAQ,CAAC,SAAS,CAAC,QAAQ,CAAC;YACtC,MAAM;SACP,CAAA;QAED,OAAO,IAAI,MAAM,CAAC,aAAa,EAAE,MAAM,CAAC,CAAA;IAC1C,CAAC;IAED;;;;OAIG;IACK,KAAK,CAAC,MAAM;QAClB,IAAI,IAAI,CAAC,aAAa,EAAE,QAAQ,KAAK,IAAI,EAAE,CAAC;YAC1C,IAAI,CAAC,UAAU,GAAG,SAAS,CAAA;YAC3B,IAAI,CAAC,aAAa,GAAG,SAAS,CAAA;QAChC,CAAC;QAED,IAAI,CAAC,UAAU,KAAK,IAAI,CAAC,QAAQ,EAAE,CAAA;QAEnC,OAAO,MAAM,IAAI,CAAC,UAAU,CAAA;IAC9B,CAAC;IAEO,KAAK,CAAC,QAAQ;QACpB,MAAM,aAAa,GAAG,IAAI,aAAa,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAA;QAE3E,IAAI,CAAC,aAAa,GAAG,aAAa,CAAA;QAElC,OAAO,aAAa,CAAA;IACtB,CAAC;IAEM,KAAK,CAAC,OAAO;QAClB,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,MAAM,CAAC,cAAc,CAAC,CAAA;QAEhD,OAAO,MAAM,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IACjC,CAAC;CACF"}
1
+ {"version":3,"file":"Factory.js","sourceRoot":"","sources":["../source/Factory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AACxC,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAA;AACvC,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAClD,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACpC,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAA;AAChD,OAAO,EAAE,OAAO,EAAE,MAAM,2CAA2C,CAAA;AAMnE,MAAM,OAAO,OAAO;IAClB,8FAA8F;IAC9E,IAAI,GAAG,EAAE,QAAQ,EAAE,IAAI,QAAQ,EAAE,EAAE,CAAA;IAElC,IAAI,CAAM;IACnB,UAAU,CAAoC;IAC9C,aAAa,CAA2B;IAEhD,YAAmB,IAAU;QAC3B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;IAClB,CAAC;IAEM,KAAK,CAAC,MAAM,CACjB,OAAgB,EAChB,IAAiB,EACjB,QAAkB;QAElB,MAAM,aAAa,GAAG,MAAM,IAAI,CAAC,MAAM,EAAE,CAAA;QACzC,MAAM,MAAM,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;QAE9E,4EAA4E;QAC5E,MAAM,MAAM,GAA8B;YACxC,oFAAoF;YACpF,SAAS,EAAE,OAAO,CAAC,SAAU;YAC7B,SAAS,EAAE,OAAO,CAAC,IAAI;YACvB,QAAQ,EAAE,OAAO,CAAC,SAAS,KAAK,UAAU;YAC1C,IAAI;YACJ,OAAO,EAAE,QAAQ,CAAC,OAAO;YACzB,QAAQ,EAAE,QAAQ,CAAC,SAAS,CAAC,QAAQ,CAAC;YACtC,MAAM;SACP,CAAA;QAED,OAAO,IAAI,MAAM,CAAC,aAAa,EAAE,MAAM,CAAC,CAAA;IAC1C,CAAC;IAED;;;;OAIG;IACK,KAAK,CAAC,MAAM;QAClB,IAAI,IAAI,CAAC,aAAa,EAAE,QAAQ,KAAK,IAAI,EAAE,CAAC;YAC1C,IAAI,CAAC,UAAU,GAAG,SAAS,CAAA;YAC3B,IAAI,CAAC,aAAa,GAAG,SAAS,CAAA;QAChC,CAAC;QAED,IAAI,CAAC,UAAU,KAAK,IAAI,CAAC,QAAQ,EAAE,CAAA;QAEnC,OAAO,MAAM,IAAI,CAAC,UAAU,CAAA;IAC9B,CAAC;IAEO,KAAK,CAAC,QAAQ;QACpB,MAAM,aAAa,GAAG,IAAI,aAAa,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAA;QAE3E,IAAI,CAAC,aAAa,GAAG,aAAa,CAAA;QAElC,OAAO,aAAa,CAAA;IACtB,CAAC;IAEM,KAAK,CAAC,OAAO;QAClB,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,MAAM,CAAC,cAAc,CAAC,CAAA;QAEhD,OAAO,MAAM,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IACjC,CAAC;CACF"}
@@ -9,6 +9,7 @@ import { map } from './map/index.js';
9
9
  import { mcp } from './mcp/index.js';
10
10
  import { req } from './require/index.js';
11
11
  import { flow } from './flow/index.js';
12
+ import { realtime } from './realtime/index.js';
12
13
  import { help } from './help/index.js';
13
14
  import { discovery } from './oauth/index.js';
14
15
  import { Site } from '../Discovery/index.js';
@@ -22,6 +23,7 @@ export const families = [
22
23
  req,
23
24
  flow,
24
25
  octets,
26
+ realtime,
25
27
  dev
26
28
  ];
27
29
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../source/directives/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAC/C,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAA;AACxC,OAAO,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAA;AACtC,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAA;AAC1C,OAAO,EAAE,GAAG,EAAE,MAAM,gBAAgB,CAAA;AACpC,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAA;AAC1C,OAAO,EAAE,EAAE,EAAE,MAAM,eAAe,CAAA;AAClC,OAAO,EAAE,GAAG,EAAE,MAAM,gBAAgB,CAAA;AACpC,OAAO,EAAE,GAAG,EAAE,MAAM,gBAAgB,CAAA;AACpC,OAAO,EAAE,GAAG,EAAE,MAAM,oBAAoB,CAAA;AACxC,OAAO,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAA;AACtC,OAAO,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAA;AACtC,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAC5C,OAAO,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAA;AAI5C,MAAM,CAAC,MAAM,QAAQ,GAAsB;IACzC,aAAa;IACb,EAAE;IACF,KAAK;IACL,GAAG;IACH,GAAG;IACH,IAAI;IACJ,GAAG;IACH,IAAI;IACJ,MAAM;IACN,GAAG;CACJ,CAAA;AACD;;;;GAIG;AACH,MAAM,CAAC,MAAM,YAAY,GAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../source/directives/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAC/C,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAA;AACxC,OAAO,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAA;AACtC,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAA;AAC1C,OAAO,EAAE,GAAG,EAAE,MAAM,gBAAgB,CAAA;AACpC,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAA;AAC1C,OAAO,EAAE,EAAE,EAAE,MAAM,eAAe,CAAA;AAClC,OAAO,EAAE,GAAG,EAAE,MAAM,gBAAgB,CAAA;AACpC,OAAO,EAAE,GAAG,EAAE,MAAM,gBAAgB,CAAA;AACpC,OAAO,EAAE,GAAG,EAAE,MAAM,oBAAoB,CAAA;AACxC,OAAO,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAA;AACtC,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAA;AAC9C,OAAO,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAA;AACtC,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAC5C,OAAO,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAA;AAI5C,MAAM,CAAC,MAAM,QAAQ,GAAsB;IACzC,aAAa;IACb,EAAE;IACF,KAAK;IACL,GAAG;IACH,GAAG;IACH,IAAI;IACJ,GAAG;IACH,IAAI;IACJ,MAAM;IACN,QAAQ;IACR,GAAG;CACJ,CAAA;AACD;;;;GAIG;AACH,MAAM,CAAC,MAAM,YAAY,GAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,CAAA"}
@@ -0,0 +1,45 @@
1
+ import { Stream } from './Stream.ts';
2
+ /**
3
+ * The streams this gateway serves, by key.
4
+ *
5
+ * A key that has a reader here is subscribed to on its channel, and its stream in Redis is kept
6
+ * alive: a component writes a routed event only to a stream that exists, so a stream that expired
7
+ * under a reader would stop receiving. After its last reader leaves, the stream lives `expire`
8
+ * more seconds — the window that reader reconnects in with its token.
9
+ *
10
+ * A channel keeps nothing for a subscriber that was away. Whenever the subscriber is back, what
11
+ * each key was written meanwhile is read from its stream, from the last event this gateway
12
+ * delivered of it; each reader skips what it was given already.
13
+ */
14
+ export declare class Hub {
15
+ private readonly keys;
16
+ private shards;
17
+ private subscribers;
18
+ private connecting;
19
+ private prefix;
20
+ /** Connects on the first stream it serves, not before: a gateway may serve none. */
21
+ connect(): Promise<void>;
22
+ close(): void;
23
+ private start;
24
+ open(key: string, token?: string): Promise<Stream>;
25
+ private enter;
26
+ private leave;
27
+ /** Creates the stream where it is gone, and answers where it ends now. */
28
+ private marker;
29
+ private read;
30
+ private receive;
31
+ private broadcast;
32
+ /**
33
+ * What every key was written while the subscriber was away. Its channels are subscribed to
34
+ * again first, where the client has not done it on its own, so nothing written after the read
35
+ * is missed.
36
+ */
37
+ private recover;
38
+ /** The connection a key's stream is kept on. */
39
+ private redis;
40
+ private subscriber;
41
+ private expire;
42
+ /** Well within the expiry, and no more often than the heartbeat where that is. */
43
+ private renewal;
44
+ private renew;
45
+ }
@@ -0,0 +1,221 @@
1
+ import { console } from 'openspan';
2
+ import { Stream, HEARTBEAT, decode, encode } from './Stream.js';
3
+ import { MAXLEN, Shards, expire, prefix, shard } from '../../realtime/redis.js';
4
+ import * as measure from './measurements.js';
5
+ /**
6
+ * The streams this gateway serves, by key.
7
+ *
8
+ * A key that has a reader here is subscribed to on its channel, and its stream in Redis is kept
9
+ * alive: a component writes a routed event only to a stream that exists, so a stream that expired
10
+ * under a reader would stop receiving. After its last reader leaves, the stream lives `expire`
11
+ * more seconds — the window that reader reconnects in with its token.
12
+ *
13
+ * A channel keeps nothing for a subscriber that was away. Whenever the subscriber is back, what
14
+ * each key was written meanwhile is read from its stream, from the last event this gateway
15
+ * delivered of it; each reader skips what it was given already.
16
+ */
17
+ export class Hub {
18
+ keys = new Map();
19
+ shards = null;
20
+ // a subscribing connection can do nothing else, so each Redis has one of its own
21
+ subscribers = null;
22
+ connecting = null;
23
+ prefix = '';
24
+ /** Connects on the first stream it serves, not before: a gateway may serve none. */
25
+ async connect() {
26
+ this.connecting ??= this.start();
27
+ await this.connecting;
28
+ }
29
+ close() {
30
+ for (const { streams, timer } of this.keys.values()) {
31
+ clearInterval(timer);
32
+ for (const stream of streams)
33
+ stream.close();
34
+ }
35
+ this.keys.clear();
36
+ this.subscribers?.disconnect();
37
+ this.shards?.disconnect();
38
+ this.subscribers = null;
39
+ this.shards = null;
40
+ this.connecting = null;
41
+ }
42
+ async start() {
43
+ this.prefix = prefix();
44
+ const shards = new Shards();
45
+ const subscribers = new Shards();
46
+ for (const connection of [...shards.connections, ...subscribers.connections])
47
+ connection.on('error', (error) => console.debug('Realtime streams unreachable', {
48
+ error: error.code ?? error.message
49
+ }));
50
+ subscribers.connections.forEach((subscriber, i) => {
51
+ // a key's channel is subscribed to by the key, the prefix applying to it; what arrives is
52
+ // named in full
53
+ subscriber.on('smessage', (channel, message) => this.receive(channel.slice(this.prefix.length), message));
54
+ // `ready` follows every reconnect too, and what was written meanwhile never arrives
55
+ let connected = false;
56
+ subscriber.on('ready', () => {
57
+ if (connected)
58
+ void this.recover(i);
59
+ connected = true;
60
+ });
61
+ });
62
+ this.shards = shards;
63
+ this.subscribers = subscribers;
64
+ await Promise.all([shards.connect(), subscribers.connect()]);
65
+ }
66
+ async open(key, token) {
67
+ await this.connect();
68
+ const stream = new Stream();
69
+ const entry = await this.enter(key, stream);
70
+ stream.once('close', () => this.leave(key, stream));
71
+ // welcome: a reader knows it is connected before anything is written
72
+ setTimeout(() => stream.heartbeat(), 1000).unref();
73
+ // written after the subscription, so nothing written after it misses this reader, and on
74
+ // every connection: a stream that expired while its reader was away is created again, or
75
+ // nothing would be written to it
76
+ const marker = await this.marker(key);
77
+ entry.last ??= marker;
78
+ const cursor = token === undefined ? marker : decode(token);
79
+ const entries = token === undefined ? [] : await this.read(key, cursor);
80
+ stream.cursor = cursor;
81
+ stream.ready(entries);
82
+ // nothing was written since: the reader goes on from here
83
+ if (entries.length === 0)
84
+ stream.token(encode(marker));
85
+ return stream;
86
+ }
87
+ async enter(key, stream) {
88
+ let entry = this.keys.get(key);
89
+ if (entry === undefined) {
90
+ entry = {
91
+ streams: new Set(),
92
+ last: null,
93
+ timer: setInterval(() => this.renew(key), this.renewal()),
94
+ subscribed: this.subscriber(key)
95
+ .ssubscribe(key)
96
+ .then(() => undefined)
97
+ };
98
+ entry.timer.unref();
99
+ this.keys.set(key, entry);
100
+ }
101
+ entry.streams.add(stream);
102
+ // the first reader subscribes, and every reader waits for it: nothing is read or written
103
+ // for this one before the channel carries the key
104
+ await entry.subscribed;
105
+ return entry;
106
+ }
107
+ leave(key, stream) {
108
+ const entry = this.keys.get(key);
109
+ if (entry === undefined)
110
+ return;
111
+ entry.streams.delete(stream);
112
+ if (entry.streams.size > 0)
113
+ return;
114
+ clearInterval(entry.timer);
115
+ this.keys.delete(key);
116
+ this.renew(key);
117
+ this.subscriber(key)
118
+ .sunsubscribe(key)
119
+ .catch((error) => console.debug('Realtime key not unsubscribed', { key, error }));
120
+ }
121
+ /** Creates the stream where it is gone, and answers where it ends now. */
122
+ async marker(key) {
123
+ const [[error, id]] = (await this.redis(key)
124
+ .multi()
125
+ .xadd(key, 'MAXLEN', '~', MAXLEN, '*', 'type', 'connect')
126
+ .expire(key, this.expire())
127
+ .exec());
128
+ if (error !== null)
129
+ throw error;
130
+ return id;
131
+ }
132
+ async read(key, after) {
133
+ const results = await this.redis(key).xread('STREAMS', key, after);
134
+ const entries = [];
135
+ for (const [id, fields] of records(results)) {
136
+ const [, event, , data] = fields;
137
+ // the marker a reader wrote is a position, not an event
138
+ if (event === 'connect')
139
+ continue;
140
+ entries.push({ id, event, data: data === undefined ? undefined : JSON.parse(data) });
141
+ }
142
+ return entries;
143
+ }
144
+ receive(key, message) {
145
+ const entry = this.keys.get(key);
146
+ if (entry === undefined)
147
+ return;
148
+ const [id, event, data] = JSON.parse(message);
149
+ this.broadcast(entry, { id, event, data: JSON.parse(data) });
150
+ }
151
+ broadcast(entry, event) {
152
+ entry.last = event.id;
153
+ for (const stream of entry.streams)
154
+ stream.deliver(event);
155
+ measure.deliver(event.event);
156
+ }
157
+ /**
158
+ * What every key was written while the subscriber was away. Its channels are subscribed to
159
+ * again first, where the client has not done it on its own, so nothing written after the read
160
+ * is missed.
161
+ */
162
+ async recover(i) {
163
+ const n = this.shards?.connections.length ?? 1;
164
+ const keys = [...this.keys.keys()].filter((key) => shard(key, n) === i);
165
+ if (keys.length > 0)
166
+ await this.subscribers?.connections[i].ssubscribe(...keys).catch((error) => {
167
+ console.warn('Realtime keys not subscribed again', { error });
168
+ });
169
+ for (const key of keys) {
170
+ const entry = this.keys.get(key);
171
+ // its last reader left meanwhile
172
+ if (entry === undefined)
173
+ continue;
174
+ try {
175
+ const after = entry.last ?? '0';
176
+ for (const event of await this.read(key, after))
177
+ this.broadcast(entry, event);
178
+ }
179
+ catch (error) {
180
+ console.warn('Realtime key not recovered', { key, error });
181
+ }
182
+ }
183
+ }
184
+ /** The connection a key's stream is kept on. */
185
+ redis(key) {
186
+ return this.shards.of(key);
187
+ }
188
+ subscriber(key) {
189
+ return this.subscribers.of(key);
190
+ }
191
+ expire() {
192
+ return expire();
193
+ }
194
+ /** Well within the expiry, and no more often than the heartbeat where that is. */
195
+ renewal() {
196
+ return Math.min(HEARTBEAT, (this.expire() * 1000) / 3);
197
+ }
198
+ renew(key) {
199
+ this.shards
200
+ ?.of(key)
201
+ .expire(key, this.expire())
202
+ .catch((error) => {
203
+ // the next renewal tries again, well within the expiry
204
+ console.debug('Realtime key not renewed', { key, error });
205
+ });
206
+ }
207
+ }
208
+ /**
209
+ * The entries `XREAD` answers for one stream: an array of `[stream, entries]` pairs over RESP2,
210
+ * an object keyed by stream over RESP3, and nothing where nothing was written since.
211
+ */
212
+ function records(results) {
213
+ if (Array.isArray(results))
214
+ return results.length === 0
215
+ ? []
216
+ : results[0][1];
217
+ if (typeof results === 'object' && results !== null)
218
+ return Object.values(results)[0] ?? [];
219
+ return [];
220
+ }
221
+ //# sourceMappingURL=Hub.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Hub.js","sourceRoot":"","sources":["../../../source/directives/realtime/Hub.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,CAAA;AAClC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAc,MAAM,aAAa,CAAA;AAC3E,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,yBAAyB,CAAA;AAC/E,OAAO,KAAK,OAAO,MAAM,mBAAmB,CAAA;AAG5C;;;;;;;;;;;GAWG;AACH,MAAM,OAAO,GAAG;IACG,IAAI,GAAG,IAAI,GAAG,EAAe,CAAA;IACtC,MAAM,GAAkB,IAAI,CAAA;IAEpC,iFAAiF;IACzE,WAAW,GAAkB,IAAI,CAAA;IACjC,UAAU,GAAyB,IAAI,CAAA;IACvC,MAAM,GAAG,EAAE,CAAA;IAEnB,oFAAoF;IAC7E,KAAK,CAAC,OAAO;QAClB,IAAI,CAAC,UAAU,KAAK,IAAI,CAAC,KAAK,EAAE,CAAA;QAEhC,MAAM,IAAI,CAAC,UAAU,CAAA;IACvB,CAAC;IAEM,KAAK;QACV,KAAK,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC;YACpD,aAAa,CAAC,KAAK,CAAC,CAAA;YAEpB,KAAK,MAAM,MAAM,IAAI,OAAO;gBAAE,MAAM,CAAC,KAAK,EAAE,CAAA;QAC9C,CAAC;QAED,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAA;QACjB,IAAI,CAAC,WAAW,EAAE,UAAU,EAAE,CAAA;QAC9B,IAAI,CAAC,MAAM,EAAE,UAAU,EAAE,CAAA;QACzB,IAAI,CAAC,WAAW,GAAG,IAAI,CAAA;QACvB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAA;QAClB,IAAI,CAAC,UAAU,GAAG,IAAI,CAAA;IACxB,CAAC;IAEO,KAAK,CAAC,KAAK;QACjB,IAAI,CAAC,MAAM,GAAG,MAAM,EAAE,CAAA;QAEtB,MAAM,MAAM,GAAG,IAAI,MAAM,EAAE,CAAA;QAC3B,MAAM,WAAW,GAAG,IAAI,MAAM,EAAE,CAAA;QAEhC,KAAK,MAAM,UAAU,IAAI,CAAC,GAAG,MAAM,CAAC,WAAW,EAAE,GAAG,WAAW,CAAC,WAAW,CAAC;YAC1E,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,KAA4B,EAAE,EAAE,CACtD,OAAO,CAAC,KAAK,CAAC,8BAA8B,EAAE;gBAC5C,KAAK,EAAE,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC,OAAO;aACnC,CAAC,CACH,CAAA;QAEH,WAAW,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,UAAU,EAAE,CAAC,EAAE,EAAE;YAChD,0FAA0F;YAC1F,gBAAgB;YAChB,UAAU,CAAC,EAAE,CAAC,UAAU,EAAE,CAAC,OAAe,EAAE,OAAe,EAAE,EAAE,CAC7D,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,CACzD,CAAA;YAED,oFAAoF;YACpF,IAAI,SAAS,GAAG,KAAK,CAAA;YAErB,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE;gBAC1B,IAAI,SAAS;oBAAE,KAAK,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAA;gBAEnC,SAAS,GAAG,IAAI,CAAA;YAClB,CAAC,CAAC,CAAA;QACJ,CAAC,CAAC,CAAA;QAEF,IAAI,CAAC,MAAM,GAAG,MAAM,CAAA;QACpB,IAAI,CAAC,WAAW,GAAG,WAAW,CAAA;QAE9B,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,WAAW,CAAC,OAAO,EAAE,CAAC,CAAC,CAAA;IAC9D,CAAC;IAEM,KAAK,CAAC,IAAI,CAAC,GAAW,EAAE,KAAc;QAC3C,MAAM,IAAI,CAAC,OAAO,EAAE,CAAA;QAEpB,MAAM,MAAM,GAAG,IAAI,MAAM,EAAE,CAAA;QAC3B,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,MAAM,CAAC,CAAA;QAE3C,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC,CAAA;QAEnD,qEAAqE;QACrE,UAAU,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,SAAS,EAAE,EAAE,IAAI,CAAC,CAAC,KAAK,EAAE,CAAA;QAElD,yFAAyF;QACzF,yFAAyF;QACzF,iCAAiC;QACjC,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;QAErC,KAAK,CAAC,IAAI,KAAK,MAAM,CAAA;QAErB,MAAM,MAAM,GAAG,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;QAC3D,MAAM,OAAO,GAAG,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC,CAAA;QAEvE,MAAM,CAAC,MAAM,GAAG,MAAM,CAAA;QACtB,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;QAErB,0DAA0D;QAC1D,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;YAAE,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAA;QAEtD,OAAO,MAAM,CAAA;IACf,CAAC;IAEO,KAAK,CAAC,KAAK,CAAC,GAAW,EAAE,MAAc;QAC7C,IAAI,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QAE9B,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YACxB,KAAK,GAAG;gBACN,OAAO,EAAE,IAAI,GAAG,EAAE;gBAClB,IAAI,EAAE,IAAI;gBACV,KAAK,EAAE,WAAW,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC;gBACzD,UAAU,EAAE,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;qBAC7B,UAAU,CAAC,GAAG,CAAC;qBACf,IAAI,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC;aACzB,CAAA;YAED,KAAK,CAAC,KAAK,CAAC,KAAK,EAAE,CAAA;YACnB,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAA;QAC3B,CAAC;QAED,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;QAEzB,yFAAyF;QACzF,kDAAkD;QAClD,MAAM,KAAK,CAAC,UAAU,CAAA;QAEtB,OAAO,KAAK,CAAA;IACd,CAAC;IAEO,KAAK,CAAC,GAAW,EAAE,MAAc;QACvC,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QAEhC,IAAI,KAAK,KAAK,SAAS;YAAE,OAAM;QAE/B,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;QAE5B,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,GAAG,CAAC;YAAE,OAAM;QAElC,aAAa,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;QAC1B,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;QACrB,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;QAEf,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;aACjB,YAAY,CAAC,GAAG,CAAC;aACjB,KAAK,CAAC,CAAC,KAAY,EAAE,EAAE,CACtB,OAAO,CAAC,KAAK,CAAC,+BAA+B,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,CAC/D,CAAA;IACL,CAAC;IAED,0EAA0E;IAClE,KAAK,CAAC,MAAM,CAAC,GAAW;QAC9B,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC;aACzC,KAAK,EAAE;aACP,IAAI,CAAC,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,SAAS,CAAC;aACxD,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC;aAC1B,IAAI,EAAE,CAAE,CAAA;QAEX,IAAI,KAAK,KAAK,IAAI;YAAE,MAAM,KAAK,CAAA;QAE/B,OAAO,EAAY,CAAA;IACrB,CAAC;IAEO,KAAK,CAAC,IAAI,CAAC,GAAW,EAAE,KAAa;QAC3C,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,SAAS,EAAE,GAAG,EAAE,KAAK,CAAC,CAAA;QAClE,MAAM,OAAO,GAAY,EAAE,CAAA;QAE3B,KAAK,MAAM,CAAC,EAAE,EAAE,MAAM,CAAC,IAAI,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;YAC5C,MAAM,CAAC,EAAE,KAAK,EAAE,AAAD,EAAG,IAAI,CAAC,GAAG,MAAM,CAAA;YAEhC,wDAAwD;YACxD,IAAI,KAAK,KAAK,SAAS;gBAAE,SAAQ;YAEjC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;QACtF,CAAC;QAED,OAAO,OAAO,CAAA;IAChB,CAAC;IAEO,OAAO,CAAC,GAAW,EAAE,OAAe;QAC1C,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QAEhC,IAAI,KAAK,KAAK,SAAS;YAAE,OAAM;QAE/B,MAAM,CAAC,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAA6B,CAAA;QAEzE,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;IAC9D,CAAC;IAEO,SAAS,CAAC,KAAU,EAAE,KAAY;QACxC,KAAK,CAAC,IAAI,GAAG,KAAK,CAAC,EAAE,CAAA;QAErB,KAAK,MAAM,MAAM,IAAI,KAAK,CAAC,OAAO;YAAE,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAA;QAEzD,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;IAC9B,CAAC;IAED;;;;OAIG;IACK,KAAK,CAAC,OAAO,CAAC,CAAS;QAC7B,MAAM,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,IAAI,CAAC,CAAA;QAC9C,MAAM,IAAI,GAAG,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAA;QAEvE,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC;YACjB,MAAM,IAAI,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,KAAY,EAAE,EAAE;gBAChF,OAAO,CAAC,IAAI,CAAC,oCAAoC,EAAE,EAAE,KAAK,EAAE,CAAC,CAAA;YAC/D,CAAC,CAAC,CAAA;QAEJ,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;YACvB,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;YAEhC,iCAAiC;YACjC,IAAI,KAAK,KAAK,SAAS;gBAAE,SAAQ;YAEjC,IAAI,CAAC;gBACH,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,IAAI,GAAG,CAAA;gBAE/B,KAAK,MAAM,KAAK,IAAI,MAAM,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC;oBAAE,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,KAAK,CAAC,CAAA;YAC/E,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,OAAO,CAAC,IAAI,CAAC,4BAA4B,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,CAAA;YAC5D,CAAC;QACH,CAAC;IACH,CAAC;IAED,gDAAgD;IACxC,KAAK,CAAC,GAAW;QACvB,OAAO,IAAI,CAAC,MAAO,CAAC,EAAE,CAAC,GAAG,CAAC,CAAA;IAC7B,CAAC;IAEO,UAAU,CAAC,GAAW;QAC5B,OAAO,IAAI,CAAC,WAAY,CAAC,EAAE,CAAC,GAAG,CAAC,CAAA;IAClC,CAAC;IAEO,MAAM;QACZ,OAAO,MAAM,EAAE,CAAA;IACjB,CAAC;IAED,kFAAkF;IAC1E,OAAO;QACb,OAAO,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAA;IACxD,CAAC;IAEO,KAAK,CAAC,GAAW;QACvB,IAAI,CAAC,MAAM;YACT,EAAE,EAAE,CAAC,GAAG,CAAC;aACR,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC;aAC1B,KAAK,CAAC,CAAC,KAAY,EAAE,EAAE;YACtB,uDAAuD;YACvD,OAAO,CAAC,KAAK,CAAC,0BAA0B,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,CAAA;QAC3D,CAAC,CAAC,CAAA;IACN,CAAC;CACF;AAED;;;GAGG;AACH,SAAS,OAAO,CAAC,OAAgB;IAC/B,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC;QACxB,OAAO,OAAO,CAAC,MAAM,KAAK,CAAC;YACzB,CAAC,CAAC,EAAE;YACJ,CAAC,CAAE,OAAO,CAAC,CAAC,CAAyC,CAAC,CAAC,CAAC,CAAA;IAE5D,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,KAAK,IAAI;QACjD,OAAO,MAAM,CAAC,MAAM,CAAC,OAAoD,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;IAErF,OAAO,EAAE,CAAA;AACX,CAAC"}
@@ -0,0 +1,18 @@
1
+ import type { Context } from '../../HTTP/index.ts';
2
+ import type { DirectiveFamily, Parameter } from '../../RTD/index.ts';
3
+ import type { Output } from '../../io.ts';
4
+ /**
5
+ * `realtime:stream` answers with the stream of the key a route variable names. Who may read it is
6
+ * what the route's own directives say, as for anything else it serves.
7
+ */
8
+ export declare class Realtime implements DirectiveFamily<Directive> {
9
+ readonly name = "realtime";
10
+ readonly mandatory = false;
11
+ private readonly hub;
12
+ create(name: string, value: unknown, _: unknown, route: string): Directive;
13
+ precall(directives: Directive[], context: Context, parameters: Parameter[]): Promise<Output>;
14
+ dispose(): void;
15
+ }
16
+ export interface Directive {
17
+ variable: string;
18
+ }
@@ -0,0 +1,37 @@
1
+ import { NotFound } from '../../HTTP/index.js';
2
+ import { configured } from '../../realtime/redis.js';
3
+ import { Hub } from './Hub.js';
4
+ /**
5
+ * `realtime:stream` answers with the stream of the key a route variable names. Who may read it is
6
+ * what the route's own directives say, as for anything else it serves.
7
+ */
8
+ export class Realtime {
9
+ name = 'realtime';
10
+ mandatory = false;
11
+ hub = new Hub();
12
+ // the arguments every family is given a directive with
13
+ // eslint-disable-next-line max-params
14
+ create(name, value, _, route) {
15
+ if (name !== 'stream')
16
+ throw new Error(`Directive 'realtime:${name}' is not implemented`);
17
+ if (typeof value !== 'string' || !route.includes(`:${value}`))
18
+ throw new Error(`'realtime:stream' names the route variable that is the key, and '${String(value)}' ` +
19
+ `is not one of '${route}'`);
20
+ return { variable: value };
21
+ }
22
+ async precall(directives, context, parameters) {
23
+ const [directive] = directives;
24
+ const key = parameters.find(({ name }) => name === directive.variable)?.value;
25
+ if (key === undefined)
26
+ throw new Error(`Route variable '${directive.variable}' is not found`);
27
+ // nothing in the context declares realtime, so nothing is kept to be read
28
+ if (!configured())
29
+ throw new NotFound('Realtime streams are not configured');
30
+ const token = context.url.searchParams.get('token') ?? undefined;
31
+ return { body: await this.hub.open(key, token) };
32
+ }
33
+ dispose() {
34
+ this.hub.close();
35
+ }
36
+ }
37
+ //# sourceMappingURL=Realtime.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Realtime.js","sourceRoot":"","sources":["../../../source/directives/realtime/Realtime.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAA;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAA;AACpD,OAAO,EAAE,GAAG,EAAE,MAAM,UAAU,CAAA;AAK9B;;;GAGG;AACH,MAAM,OAAO,QAAQ;IACH,IAAI,GAAG,UAAU,CAAA;IACjB,SAAS,GAAG,KAAK,CAAA;IAEhB,GAAG,GAAG,IAAI,GAAG,EAAE,CAAA;IAEhC,uDAAuD;IACvD,sCAAsC;IAC/B,MAAM,CAAC,IAAY,EAAE,KAAc,EAAE,CAAU,EAAE,KAAa;QACnE,IAAI,IAAI,KAAK,QAAQ;YACnB,MAAM,IAAI,KAAK,CAAC,uBAAuB,IAAI,sBAAsB,CAAC,CAAA;QAEpE,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,KAAK,EAAE,CAAC;YAC3D,MAAM,IAAI,KAAK,CACb,oEAAoE,MAAM,CAAC,KAAK,CAAC,IAAI;gBACnF,kBAAkB,KAAK,GAAG,CAC7B,CAAA;QAEH,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAA;IAC5B,CAAC;IAEM,KAAK,CAAC,OAAO,CAClB,UAAuB,EACvB,OAAgB,EAChB,UAAuB;QAEvB,MAAM,CAAC,SAAS,CAAC,GAAG,UAAU,CAAA;QAC9B,MAAM,GAAG,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,IAAI,KAAK,SAAS,CAAC,QAAQ,CAAC,EAAE,KAAK,CAAA;QAE7E,IAAI,GAAG,KAAK,SAAS;YACnB,MAAM,IAAI,KAAK,CAAC,mBAAmB,SAAS,CAAC,QAAQ,gBAAgB,CAAC,CAAA;QAExE,0EAA0E;QAC1E,IAAI,CAAC,UAAU,EAAE;YAAE,MAAM,IAAI,QAAQ,CAAC,qCAAqC,CAAC,CAAA;QAE5E,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,SAAS,CAAA;QAEhE,OAAO,EAAE,IAAI,EAAE,MAAM,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,CAAA;IAClD,CAAC;IAEM,OAAO;QACZ,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,CAAA;IAClB,CAAC;CACF"}
@@ -0,0 +1,38 @@
1
+ import { Readable } from 'node:stream';
2
+ /**
3
+ * One reader of a key: what it is sent, in order, and a heartbeat while nothing is.
4
+ *
5
+ * `cursor` is the id of the last event it was given. What arrives while it is being replayed to
6
+ * is held until the replay is done, and whatever it was given already is not given again: a replay
7
+ * and the channel may both carry the same event.
8
+ */
9
+ export declare class Stream extends Readable {
10
+ cursor: string | null;
11
+ private held;
12
+ private interval;
13
+ private ended;
14
+ constructor();
15
+ _read(): void;
16
+ _destroy(error: Error | null, callback: (error?: Error | null) => void): void;
17
+ /** An event of the key, or one held back while the replay is in flight. */
18
+ deliver(entry: Entry): void;
19
+ /** What the replay found is sent, then what was held back meanwhile, and from now on as it comes. */
20
+ ready(entries: Entry[]): void;
21
+ token(token: string): void;
22
+ heartbeat(): void;
23
+ /** Ends it: whoever reads it gets the end, not a broken pipe. */
24
+ close(): void;
25
+ private write;
26
+ private send;
27
+ private stop;
28
+ }
29
+ export interface Entry {
30
+ id: string;
31
+ event: string;
32
+ data: unknown;
33
+ }
34
+ export declare function encode(id: string): string;
35
+ export declare function decode(token: string): string;
36
+ /** Stream ids are `<ms>-<seq>`, ordered as two numbers. */
37
+ export declare function compare(a: string, b: string): number;
38
+ export declare const HEARTBEAT = 16000;
@@ -0,0 +1,89 @@
1
+ import { Readable } from 'node:stream';
2
+ /**
3
+ * One reader of a key: what it is sent, in order, and a heartbeat while nothing is.
4
+ *
5
+ * `cursor` is the id of the last event it was given. What arrives while it is being replayed to
6
+ * is held until the replay is done, and whatever it was given already is not given again: a replay
7
+ * and the channel may both carry the same event.
8
+ */
9
+ export class Stream extends Readable {
10
+ cursor = null;
11
+ held = [];
12
+ interval = null;
13
+ ended = false;
14
+ constructor() {
15
+ super({ objectMode: true });
16
+ }
17
+ // a stream is read from when the reply is written, and the heartbeat starts with the reading
18
+ _read() {
19
+ this.interval ??= setInterval(() => this.heartbeat(), HEARTBEAT);
20
+ }
21
+ _destroy(error, callback) {
22
+ this.stop();
23
+ callback(error);
24
+ }
25
+ /** An event of the key, or one held back while the replay is in flight. */
26
+ deliver(entry) {
27
+ if (this.held !== null) {
28
+ this.held.push(entry);
29
+ return;
30
+ }
31
+ this.write(entry);
32
+ }
33
+ /** What the replay found is sent, then what was held back meanwhile, and from now on as it comes. */
34
+ ready(entries) {
35
+ const held = this.held ?? [];
36
+ this.held = null;
37
+ for (const entry of entries)
38
+ this.write(entry);
39
+ for (const entry of held)
40
+ this.write(entry);
41
+ }
42
+ token(token) {
43
+ this.send({ event: 'token', data: token });
44
+ }
45
+ heartbeat() {
46
+ this.send('heartbeat ' + Date.now());
47
+ }
48
+ /** Ends it: whoever reads it gets the end, not a broken pipe. */
49
+ close() {
50
+ this.stop();
51
+ this.ended = true;
52
+ this.push(null);
53
+ }
54
+ write(entry) {
55
+ if (this.cursor !== null && compare(entry.id, this.cursor) <= 0)
56
+ return;
57
+ this.cursor = entry.id;
58
+ this.send({ event: entry.event, data: entry.data });
59
+ this.token(encode(entry.id));
60
+ }
61
+ send(message) {
62
+ if (this.ended || this.destroyed)
63
+ return;
64
+ this.push(message);
65
+ }
66
+ stop() {
67
+ if (this.interval !== null)
68
+ clearInterval(this.interval);
69
+ this.interval = null;
70
+ }
71
+ }
72
+ export function encode(id) {
73
+ return Buffer.from(id).toString('base64url');
74
+ }
75
+ export function decode(token) {
76
+ return Buffer.from(token, 'base64url').toString();
77
+ }
78
+ /** Stream ids are `<ms>-<seq>`, ordered as two numbers. */
79
+ export function compare(a, b) {
80
+ const [ams, aseq] = a.split('-').map(BigInt);
81
+ const [bms, bseq] = b.split('-').map(BigInt);
82
+ if (ams !== bms)
83
+ return ams < bms ? -1 : 1;
84
+ if (aseq !== bseq)
85
+ return aseq < bseq ? -1 : 1;
86
+ return 0;
87
+ }
88
+ export const HEARTBEAT = 16_000;
89
+ //# sourceMappingURL=Stream.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Stream.js","sourceRoot":"","sources":["../../../source/directives/realtime/Stream.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import { Realtime } from './Realtime.ts';
2
+ export declare const realtime: Realtime;
@@ -0,0 +1,3 @@
1
+ import { Realtime } from './Realtime.js';
2
+ export const realtime = new Realtime();
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../source/directives/realtime/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AAExC,MAAM,CAAC,MAAM,QAAQ,GAAG,IAAI,QAAQ,EAAE,CAAA"}
@@ -0,0 +1 @@
1
+ export declare function deliver(event: string): void;