@carno.js/live 1.8.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.
Files changed (199) hide show
  1. package/LICENSE +21 -0
  2. package/dist/LiveEngine.d.ts +89 -0
  3. package/dist/LiveEngine.js +520 -0
  4. package/dist/LivePlugin.d.ts +55 -0
  5. package/dist/LivePlugin.js +165 -0
  6. package/dist/LiveService.d.ts +32 -0
  7. package/dist/LiveService.js +51 -0
  8. package/dist/auth/authorizer.d.ts +33 -0
  9. package/dist/auth/authorizer.js +32 -0
  10. package/dist/bus/InProcessBus.d.ts +8 -0
  11. package/dist/bus/InProcessBus.js +29 -0
  12. package/dist/bus/InvalidationBus.d.ts +15 -0
  13. package/dist/bus/InvalidationBus.js +2 -0
  14. package/dist/bus/PgNotifyBus.d.ts +55 -0
  15. package/dist/bus/PgNotifyBus.js +138 -0
  16. package/dist/client/angular.d.ts +19 -0
  17. package/dist/client/angular.js +71 -0
  18. package/dist/client/core.d.ts +111 -0
  19. package/dist/client/core.js +337 -0
  20. package/dist/client/hydrate.d.ts +19 -0
  21. package/dist/client/hydrate.js +65 -0
  22. package/dist/client/optimistic.d.ts +17 -0
  23. package/dist/client/optimistic.js +2 -0
  24. package/dist/client/react.d.ts +22 -0
  25. package/dist/client/react.js +69 -0
  26. package/dist/client/transport.d.ts +133 -0
  27. package/dist/client/transport.js +431 -0
  28. package/dist/client/vanilla.d.ts +54 -0
  29. package/dist/client/vanilla.js +111 -0
  30. package/dist/client/vue.d.ts +12 -0
  31. package/dist/client/vue.js +35 -0
  32. package/dist/config.d.ts +32 -0
  33. package/dist/config.js +21 -0
  34. package/dist/decorators/Live.d.ts +4 -0
  35. package/dist/decorators/Live.js +17 -0
  36. package/dist/emitters/AppEmitter.d.ts +20 -0
  37. package/dist/emitters/AppEmitter.js +59 -0
  38. package/dist/emitters/pg-listener.d.ts +50 -0
  39. package/dist/emitters/pg-listener.js +120 -0
  40. package/dist/emitters/pg-notify-emitter.d.ts +59 -0
  41. package/dist/emitters/pg-notify-emitter.js +105 -0
  42. package/dist/emitters/pg-trigger-sql.d.ts +27 -0
  43. package/dist/emitters/pg-trigger-sql.js +107 -0
  44. package/dist/emitters/statement-keys.d.ts +16 -0
  45. package/dist/emitters/statement-keys.js +182 -0
  46. package/dist/graph/DependencyGraph.d.ts +27 -0
  47. package/dist/graph/DependencyGraph.js +118 -0
  48. package/dist/graph/SubscriptionRegistry.d.ts +22 -0
  49. package/dist/graph/SubscriptionRegistry.js +92 -0
  50. package/dist/graph/dep-key.d.ts +14 -0
  51. package/dist/graph/dep-key.js +37 -0
  52. package/dist/graph/types.d.ts +13 -0
  53. package/dist/graph/types.js +2 -0
  54. package/dist/http/etag.d.ts +41 -0
  55. package/dist/http/etag.js +109 -0
  56. package/dist/index.d.ts +56 -0
  57. package/dist/index.js +111 -0
  58. package/dist/metadata.d.ts +16 -0
  59. package/dist/metadata.js +4 -0
  60. package/dist/observability.d.ts +25 -0
  61. package/dist/observability.js +44 -0
  62. package/dist/patch/PatchEngine.d.ts +20 -0
  63. package/dist/patch/PatchEngine.js +180 -0
  64. package/dist/patch/types.d.ts +33 -0
  65. package/dist/patch/types.js +2 -0
  66. package/dist/resource/ResourceRegistry.d.ts +32 -0
  67. package/dist/resource/ResourceRegistry.js +126 -0
  68. package/dist/resource/dependency-context.d.ts +25 -0
  69. package/dist/resource/dependency-context.js +45 -0
  70. package/dist/resource/instance-id.d.ts +17 -0
  71. package/dist/resource/instance-id.js +61 -0
  72. package/dist/resource/prefetch.d.ts +20 -0
  73. package/dist/resource/prefetch.js +31 -0
  74. package/dist/resource/route-executor.d.ts +13 -0
  75. package/dist/resource/route-executor.js +86 -0
  76. package/dist/resource/types.d.ts +26 -0
  77. package/dist/resource/types.js +2 -0
  78. package/dist/runtime.d.ts +41 -0
  79. package/dist/runtime.js +47 -0
  80. package/dist/shared/canonical.d.ts +14 -0
  81. package/dist/shared/canonical.js +60 -0
  82. package/dist/shared/descriptor.d.ts +44 -0
  83. package/dist/shared/descriptor.js +19 -0
  84. package/dist/shared/hash.d.ts +12 -0
  85. package/dist/shared/hash.js +29 -0
  86. package/dist/shared/inputs.d.ts +19 -0
  87. package/dist/shared/inputs.js +2 -0
  88. package/dist/shared/protocol.d.ts +83 -0
  89. package/dist/shared/protocol.js +9 -0
  90. package/dist/transport/FanTransport.d.ts +18 -0
  91. package/dist/transport/FanTransport.js +29 -0
  92. package/dist/transport/LiveGateway.d.ts +16 -0
  93. package/dist/transport/LiveGateway.js +126 -0
  94. package/dist/transport/SocketTransport.d.ts +18 -0
  95. package/dist/transport/SocketTransport.js +40 -0
  96. package/dist/transport/SseTransport.d.ts +30 -0
  97. package/dist/transport/SseTransport.js +92 -0
  98. package/dist/transport/scope-resolver.d.ts +20 -0
  99. package/dist/transport/scope-resolver.js +17 -0
  100. package/dist/transport/sse-routes.d.ts +24 -0
  101. package/dist/transport/sse-routes.js +78 -0
  102. package/package.json +92 -0
  103. package/src/LiveEngine.ts +730 -0
  104. package/src/LivePlugin.ts +253 -0
  105. package/src/LiveService.ts +47 -0
  106. package/src/auth/authorizer.ts +56 -0
  107. package/src/bus/InProcessBus.ts +28 -0
  108. package/src/bus/InvalidationBus.ts +17 -0
  109. package/src/bus/PgNotifyBus.ts +188 -0
  110. package/src/client/angular.ts +115 -0
  111. package/src/client/core.ts +492 -0
  112. package/src/client/hydrate.ts +80 -0
  113. package/src/client/optimistic.ts +19 -0
  114. package/src/client/react.ts +113 -0
  115. package/src/client/transport.ts +568 -0
  116. package/src/client/vanilla.ts +165 -0
  117. package/src/client/vue.ts +67 -0
  118. package/src/config.ts +49 -0
  119. package/src/decorators/Live.ts +16 -0
  120. package/src/emitters/AppEmitter.ts +69 -0
  121. package/src/emitters/pg-listener.ts +171 -0
  122. package/src/emitters/pg-notify-emitter.ts +143 -0
  123. package/src/emitters/pg-trigger-sql.ts +111 -0
  124. package/src/emitters/statement-keys.ts +229 -0
  125. package/src/graph/DependencyGraph.ts +147 -0
  126. package/src/graph/SubscriptionRegistry.ts +109 -0
  127. package/src/graph/dep-key.ts +41 -0
  128. package/src/graph/types.ts +15 -0
  129. package/src/http/etag.ts +144 -0
  130. package/src/index.ts +81 -0
  131. package/src/metadata.ts +21 -0
  132. package/src/observability.ts +54 -0
  133. package/src/patch/PatchEngine.ts +235 -0
  134. package/src/patch/types.ts +39 -0
  135. package/src/resource/ResourceRegistry.ts +178 -0
  136. package/src/resource/dependency-context.ts +48 -0
  137. package/src/resource/instance-id.ts +65 -0
  138. package/src/resource/prefetch.ts +49 -0
  139. package/src/resource/route-executor.ts +134 -0
  140. package/src/resource/types.ts +35 -0
  141. package/src/runtime.ts +69 -0
  142. package/src/shared/canonical.ts +63 -0
  143. package/src/shared/descriptor.ts +44 -0
  144. package/src/shared/hash.ts +31 -0
  145. package/src/shared/inputs.ts +20 -0
  146. package/src/shared/protocol.ts +96 -0
  147. package/src/transport/FanTransport.ts +34 -0
  148. package/src/transport/LiveGateway.ts +124 -0
  149. package/src/transport/SocketTransport.ts +43 -0
  150. package/src/transport/SseTransport.ts +112 -0
  151. package/src/transport/scope-resolver.ts +25 -0
  152. package/src/transport/sse-routes.ts +94 -0
  153. package/test/acceptance-fase-2.test.ts +248 -0
  154. package/test/acceptance-fase-3.test.ts +216 -0
  155. package/test/acceptance.test.ts +306 -0
  156. package/test/angular-adapter.test.ts +151 -0
  157. package/test/app-emitter.test.ts +112 -0
  158. package/test/authorization.test.ts +175 -0
  159. package/test/client-core.test.ts +287 -0
  160. package/test/client-transport.test.ts +154 -0
  161. package/test/dependency-graph.test.ts +83 -0
  162. package/test/etag.test.ts +193 -0
  163. package/test/fan-transport.test.ts +62 -0
  164. package/test/happydom.ts +24 -0
  165. package/test/inputs-body.test.ts +81 -0
  166. package/test/instance-id.test.ts +104 -0
  167. package/test/live-engine.test.ts +593 -0
  168. package/test/live-post.test.ts +81 -0
  169. package/test/metrics.test.ts +131 -0
  170. package/test/optimistic.test.ts +166 -0
  171. package/test/orm-integration.test.ts +75 -0
  172. package/test/patch-engine.test.ts +121 -0
  173. package/test/pg-bus.test.ts +162 -0
  174. package/test/pg-listen-probe.test.ts +56 -0
  175. package/test/pg-listener.test.ts +134 -0
  176. package/test/pg-notify-emitter.test.ts +113 -0
  177. package/test/pg-notify-integration.test.ts +88 -0
  178. package/test/polling-auth.test.ts +169 -0
  179. package/test/prefetch.test.ts +137 -0
  180. package/test/react-adapter.test.tsx +83 -0
  181. package/test/react-rerender.test.tsx +131 -0
  182. package/test/resource-registry-helper.ts +35 -0
  183. package/test/resource-registry.test.ts +144 -0
  184. package/test/route-pipeline.test.ts +139 -0
  185. package/test/shared.test.ts +53 -0
  186. package/test/sse-routes.test.ts +204 -0
  187. package/test/sse-transport.test.ts +91 -0
  188. package/test/statement-keys.test.ts +126 -0
  189. package/test/subscription-registry.test.ts +61 -0
  190. package/test/transport-ladder.test.ts +293 -0
  191. package/test/transport.test.ts +274 -0
  192. package/test/types/optimistic-types.ts +34 -0
  193. package/test/use-live-action.test.ts +127 -0
  194. package/test/use-live.test.ts +109 -0
  195. package/test/vanilla-adapter.test.ts +200 -0
  196. package/test/vue-adapter.test.ts +122 -0
  197. package/tsconfig.json +15 -0
  198. package/tsconfig.tsbuildinfo +1 -0
  199. package/tsconfig.types.json +10 -0
@@ -0,0 +1,44 @@
1
+ import type { LiveInputs } from './inputs';
2
+ /**
3
+ * Structural twin of the descriptor @carno.js/client emits.
4
+ *
5
+ * The generated file imports nothing, on purpose, so the two definitions meet
6
+ * through TypeScript's structural typing. Keep the field names identical.
7
+ */
8
+ export interface LiveDescriptor<R = unknown> {
9
+ readonly method: string;
10
+ readonly path: string;
11
+ readonly resourceId?: string;
12
+ readonly live?: {
13
+ readonly shared: 'private' | 'tenant' | 'public';
14
+ readonly key?: string;
15
+ };
16
+ readonly __route?: R;
17
+ }
18
+ /** What the route answers, as the client sees it. */
19
+ export type LiveDataOf<R> = R extends {
20
+ response: infer S;
21
+ } ? Exclude<S, undefined | void> : unknown;
22
+ /** What the route takes, as a subscription sends it. */
23
+ export type LiveInputsOf<R> = (R extends {
24
+ params: infer P;
25
+ } ? {
26
+ params: P;
27
+ } : {
28
+ params?: Record<string, string>;
29
+ }) & (R extends {
30
+ query: infer Q;
31
+ } ? {
32
+ query: Q;
33
+ } : {
34
+ query?: Record<string, string | string[]>;
35
+ }) & (R extends {
36
+ body: infer B;
37
+ } ? {
38
+ body: B;
39
+ } : {
40
+ body?: undefined;
41
+ });
42
+ export declare function resourceIdOf(descriptor: LiveDescriptor<any>): string;
43
+ /** Fill the three input slots, whichever of them the caller bothered with. */
44
+ export declare function normalizeLiveInputs(inputs?: Partial<LiveInputs>): LiveInputs;
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resourceIdOf = resourceIdOf;
4
+ exports.normalizeLiveInputs = normalizeLiveInputs;
5
+ function resourceIdOf(descriptor) {
6
+ if (!descriptor.resourceId || !descriptor.live) {
7
+ throw new Error(`${descriptor.method.toUpperCase()} ${descriptor.path} is not a live resource. ` +
8
+ `Add @Live() to the handler and re-run the client codegen.`);
9
+ }
10
+ return descriptor.resourceId;
11
+ }
12
+ /** Fill the three input slots, whichever of them the caller bothered with. */
13
+ function normalizeLiveInputs(inputs = {}) {
14
+ return {
15
+ params: inputs.params ?? {},
16
+ query: inputs.query ?? {},
17
+ body: inputs.body
18
+ };
19
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * FNV-1a widened to 64 output bits by running two lanes with different offset
3
+ * bases, the second one position-sensitive.
4
+ *
5
+ * `packages/orm/src/cache/cache-key-generator.ts` uses the 32-bit variant,
6
+ * which is right for a cache — a collision there costs one stale entry. Here a
7
+ * content-hash collision means "data changed but no patch was sent", and 32
8
+ * bits reach 50% collision odds around 65k keys while `maxInstancesPerNode`
9
+ * alone is 50000. This lives in one module so it can be swapped for a stronger
10
+ * hash without touching anything else.
11
+ */
12
+ export declare function fnv1a64(input: string): string;
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.fnv1a64 = fnv1a64;
4
+ const FNV_OFFSET_BASIS_A = 2166136261;
5
+ const FNV_OFFSET_BASIS_B = 0x9e3779b9;
6
+ const FNV_PRIME = 16777619;
7
+ /**
8
+ * FNV-1a widened to 64 output bits by running two lanes with different offset
9
+ * bases, the second one position-sensitive.
10
+ *
11
+ * `packages/orm/src/cache/cache-key-generator.ts` uses the 32-bit variant,
12
+ * which is right for a cache — a collision there costs one stale entry. Here a
13
+ * content-hash collision means "data changed but no patch was sent", and 32
14
+ * bits reach 50% collision odds around 65k keys while `maxInstancesPerNode`
15
+ * alone is 50000. This lives in one module so it can be swapped for a stronger
16
+ * hash without touching anything else.
17
+ */
18
+ function fnv1a64(input) {
19
+ let laneA = FNV_OFFSET_BASIS_A;
20
+ let laneB = FNV_OFFSET_BASIS_B;
21
+ for (let i = 0; i < input.length; i++) {
22
+ const code = input.charCodeAt(i);
23
+ laneA ^= code;
24
+ laneA = Math.imul(laneA, FNV_PRIME);
25
+ laneB ^= code + i;
26
+ laneB = Math.imul(laneB, FNV_PRIME);
27
+ }
28
+ return (laneA >>> 0).toString(16).padStart(8, '0') + (laneB >>> 0).toString(16).padStart(8, '0');
29
+ }
@@ -0,0 +1,19 @@
1
+ /** Everything a resource compute is allowed to read from the caller. */
2
+ export interface LiveInputs {
3
+ params: Record<string, string>;
4
+ query: Record<string, string | string[]>;
5
+ /**
6
+ * Body of a @Post() live resource. Absent on @Get().
7
+ *
8
+ * It is part of the instance identity, not extra baggage: two clients
9
+ * posting different filters must not share one computed instance.
10
+ */
11
+ body?: unknown;
12
+ }
13
+ /** Ambient dimensions resolved on the server; never sent by the client. */
14
+ export interface LiveScope {
15
+ principal?: string | number;
16
+ tenant?: string | number;
17
+ /** Optional request headers for replaying route middleware during compute. */
18
+ headers?: HeadersInit;
19
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,83 @@
1
+ import type { PatchOp } from '../patch/types';
2
+ import type { LiveInputs } from '../resource/types';
3
+ export declare const LIVE_PROTOCOL_VERSION = 1;
4
+ /** Headers used by the HTTP polling rung to carry the live auth contract. */
5
+ export declare const LIVE_POLL_HEADER = "X-Carno-Live-Poll";
6
+ export declare const LIVE_CONNECTION_HEADER = "X-Carno-Live-Connection";
7
+ export declare const LIVE_RESOURCE_HEADER = "X-Carno-Live-Resource";
8
+ export declare const LIVE_TOKEN_HEADER = "X-Carno-Live-Token";
9
+ /** Sent once per connection, before any subscription. */
10
+ export interface ClientHello {
11
+ t: 'hello';
12
+ v: number;
13
+ /** Opaque credential; the server's LiveScopeResolver interprets it. */
14
+ token?: string;
15
+ }
16
+ export interface ClientSub {
17
+ t: 'sub';
18
+ /** Client-chosen subscription id. The instance id never leaves the server. */
19
+ sid: string;
20
+ resource: string;
21
+ inputs: LiveInputs;
22
+ /** Content hash of what the client already has on screen, if anything. */
23
+ hash?: string;
24
+ }
25
+ export interface ClientUnsub {
26
+ t: 'unsub';
27
+ sid: string;
28
+ }
29
+ export interface ClientResync {
30
+ t: 'resync';
31
+ sid: string;
32
+ hash?: string;
33
+ }
34
+ export type ClientMessage = ClientHello | ClientSub | ClientUnsub | ClientResync;
35
+ /** Full state. */
36
+ export interface ServerSnapshot {
37
+ t: 'snapshot';
38
+ sid: string;
39
+ rev: number;
40
+ hash: string;
41
+ data: unknown;
42
+ /**
43
+ * The resource's `@Live({ key })`, if it declared one. The client needs it
44
+ * to apply keyed ops, and this is the only message that establishes state,
45
+ * so it is the only place it has to travel.
46
+ */
47
+ key?: string;
48
+ }
49
+ /** The client's hash matched what the server computed: nothing on the wire. */
50
+ export interface ServerCurrent {
51
+ t: 'current';
52
+ sid: string;
53
+ rev: number;
54
+ hash: string;
55
+ /** Same reason as on `snapshot`: this also establishes state, on hydration. */
56
+ key?: string;
57
+ }
58
+ export interface ServerPatch {
59
+ t: 'patch';
60
+ sid: string;
61
+ from: number;
62
+ to: number;
63
+ hash: string;
64
+ ops: PatchOp[];
65
+ }
66
+ /**
67
+ * The server cannot vouch for this instance being current (a recompute is
68
+ * failing). The client keeps showing the last data and flags it, so the UI can
69
+ * say so.
70
+ */
71
+ export interface ServerStale {
72
+ t: 'stale';
73
+ sid: string;
74
+ reason: string;
75
+ }
76
+ /** The subscription is invalid or not allowed; the client ends the instance. */
77
+ export interface ServerError {
78
+ t: 'error';
79
+ sid: string;
80
+ code: string;
81
+ message: string;
82
+ }
83
+ export type ServerMessage = ServerSnapshot | ServerCurrent | ServerPatch | ServerStale | ServerError;
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LIVE_TOKEN_HEADER = exports.LIVE_RESOURCE_HEADER = exports.LIVE_CONNECTION_HEADER = exports.LIVE_POLL_HEADER = exports.LIVE_PROTOCOL_VERSION = void 0;
4
+ exports.LIVE_PROTOCOL_VERSION = 1;
5
+ /** Headers used by the HTTP polling rung to carry the live auth contract. */
6
+ exports.LIVE_POLL_HEADER = 'X-Carno-Live-Poll';
7
+ exports.LIVE_CONNECTION_HEADER = 'X-Carno-Live-Connection';
8
+ exports.LIVE_RESOURCE_HEADER = 'X-Carno-Live-Resource';
9
+ exports.LIVE_TOKEN_HEADER = 'X-Carno-Live-Token';
@@ -0,0 +1,18 @@
1
+ import type { LiveTransport } from '../LiveEngine';
2
+ import type { ServerMessage } from '../shared/protocol';
3
+ export interface OwnedTransport extends LiveTransport {
4
+ /** Whether this transport is the one holding that connection. */
5
+ owns(connectionId: string): boolean;
6
+ }
7
+ /**
8
+ * One engine, several pipes.
9
+ *
10
+ * The engine addresses connections by an opaque id and never asks how they are
11
+ * reached, which is exactly what makes SSE a transport rather than a second
12
+ * engine. This routes each send to whichever transport claims the id.
13
+ */
14
+ export declare class FanTransport implements LiveTransport {
15
+ private readonly children;
16
+ add(child: OwnedTransport): void;
17
+ send(connectionId: string, message: ServerMessage): number;
18
+ }
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FanTransport = void 0;
4
+ /**
5
+ * One engine, several pipes.
6
+ *
7
+ * The engine addresses connections by an opaque id and never asks how they are
8
+ * reached, which is exactly what makes SSE a transport rather than a second
9
+ * engine. This routes each send to whichever transport claims the id.
10
+ */
11
+ class FanTransport {
12
+ constructor() {
13
+ this.children = [];
14
+ }
15
+ add(child) {
16
+ this.children.push(child);
17
+ }
18
+ send(connectionId, message) {
19
+ for (const child of this.children) {
20
+ if (child.owns(connectionId)) {
21
+ return child.send(connectionId, message);
22
+ }
23
+ }
24
+ // Nobody holds it any more. Zero is "dropped", which the engine already
25
+ // handles; throwing here would take a whole fan-out down with it.
26
+ return 0;
27
+ }
28
+ }
29
+ exports.FanTransport = FanTransport;
@@ -0,0 +1,16 @@
1
+ import { CarnoSocket } from '@carno.js/websocket';
2
+ export declare const LIVE_GATEWAY_PATH = "/live";
3
+ export declare class LiveGateway {
4
+ onOpen(socket: CarnoSocket): void;
5
+ onMessage(socket: CarnoSocket, raw: string | ArrayBuffer | Uint8Array): void;
6
+ onClose(socket: CarnoSocket): void;
7
+ }
8
+ /**
9
+ * Tear down a connection from any pipe.
10
+ *
11
+ * WebSocket and SSE must leave the engine, the inbound queue and the scope
12
+ * map in the same state: an in-flight `hello` cannot subscribe after the
13
+ * client has gone, and a cancelled stream cannot leak a principal.
14
+ */
15
+ export declare function dropLiveConnection(connectionId: string): void;
16
+ export declare function handleMessage(connectionId: string, raw: string): Promise<void>;
@@ -0,0 +1,126 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.LiveGateway = exports.LIVE_GATEWAY_PATH = void 0;
13
+ exports.dropLiveConnection = dropLiveConnection;
14
+ exports.handleMessage = handleMessage;
15
+ const websocket_1 = require("@carno.js/websocket");
16
+ const runtime_1 = require("../runtime");
17
+ exports.LIVE_GATEWAY_PATH = '/live';
18
+ /** Per-connection tail so `hello` finishes before a back-to-back `sub`. */
19
+ const inbound = new Map();
20
+ let LiveGateway = class LiveGateway {
21
+ onOpen(socket) {
22
+ const runtime = (0, runtime_1.getLiveRuntime)();
23
+ runtime.transport.add(socket);
24
+ runtime.handshakes.delete(socket.id);
25
+ // Until a `hello` arrives, the connection is its own principal: safe,
26
+ // shares nothing.
27
+ runtime.scopes.set(socket.id, { principal: socket.id });
28
+ }
29
+ onMessage(socket, raw) {
30
+ if (typeof raw !== 'string') {
31
+ return;
32
+ }
33
+ void handleMessage(socket.id, raw).catch(() => { });
34
+ }
35
+ onClose(socket) {
36
+ dropLiveConnection(socket.id);
37
+ (0, runtime_1.getLiveRuntime)().transport.remove(socket.id);
38
+ }
39
+ };
40
+ exports.LiveGateway = LiveGateway;
41
+ __decorate([
42
+ (0, websocket_1.OnOpen)(),
43
+ __metadata("design:type", Function),
44
+ __metadata("design:paramtypes", [websocket_1.CarnoSocket]),
45
+ __metadata("design:returntype", void 0)
46
+ ], LiveGateway.prototype, "onOpen", null);
47
+ __decorate([
48
+ (0, websocket_1.OnMessage)(),
49
+ __metadata("design:type", Function),
50
+ __metadata("design:paramtypes", [websocket_1.CarnoSocket, Object]),
51
+ __metadata("design:returntype", void 0)
52
+ ], LiveGateway.prototype, "onMessage", null);
53
+ __decorate([
54
+ (0, websocket_1.OnClose)(),
55
+ __metadata("design:type", Function),
56
+ __metadata("design:paramtypes", [websocket_1.CarnoSocket]),
57
+ __metadata("design:returntype", void 0)
58
+ ], LiveGateway.prototype, "onClose", null);
59
+ exports.LiveGateway = LiveGateway = __decorate([
60
+ (0, websocket_1.Gateway)(exports.LIVE_GATEWAY_PATH)
61
+ ], LiveGateway);
62
+ /**
63
+ * Tear down a connection from any pipe.
64
+ *
65
+ * WebSocket and SSE must leave the engine, the inbound queue and the scope
66
+ * map in the same state: an in-flight `hello` cannot subscribe after the
67
+ * client has gone, and a cancelled stream cannot leak a principal.
68
+ */
69
+ function dropLiveConnection(connectionId) {
70
+ inbound.delete(connectionId);
71
+ const runtime = (0, runtime_1.getLiveRuntime)();
72
+ runtime.engine.dropConnection(connectionId);
73
+ runtime.scopes.delete(connectionId);
74
+ runtime.handshakes.delete(connectionId);
75
+ }
76
+ function handleMessage(connectionId, raw) {
77
+ const previous = inbound.get(connectionId) ?? Promise.resolve();
78
+ const next = previous.then(() => dispatch(connectionId, raw), () => dispatch(connectionId, raw));
79
+ inbound.set(connectionId, next);
80
+ return next;
81
+ }
82
+ async function dispatch(connectionId, raw) {
83
+ if (!inbound.has(connectionId)) {
84
+ return;
85
+ }
86
+ const runtime = (0, runtime_1.getLiveRuntime)();
87
+ let message;
88
+ try {
89
+ message = JSON.parse(raw);
90
+ }
91
+ catch {
92
+ return;
93
+ }
94
+ if (!message || typeof message.t !== 'string') {
95
+ return;
96
+ }
97
+ switch (message.t) {
98
+ case 'hello': {
99
+ if (runtime.handshakes.has(connectionId)) {
100
+ return;
101
+ }
102
+ const scope = await runtime.resolver.resolve({ connectionId, token: message.token });
103
+ if (!inbound.has(connectionId)) {
104
+ return;
105
+ }
106
+ runtime.scopes.set(connectionId, scope);
107
+ runtime.handshakes.add(connectionId);
108
+ return;
109
+ }
110
+ case 'sub': {
111
+ const scope = runtime.scopes.get(connectionId) ?? { principal: connectionId };
112
+ await runtime.engine.subscribe(connectionId, message.sid, message.resource, {
113
+ params: message.inputs?.params ?? {},
114
+ query: message.inputs?.query ?? {},
115
+ body: message.inputs?.body
116
+ }, scope, message.hash);
117
+ return;
118
+ }
119
+ case 'unsub':
120
+ runtime.engine.unsubscribe(connectionId, message.sid);
121
+ return;
122
+ case 'resync':
123
+ await runtime.engine.resync(connectionId, message.sid, message.hash);
124
+ return;
125
+ }
126
+ }
@@ -0,0 +1,18 @@
1
+ import type { CarnoSocket } from '@carno.js/websocket';
2
+ import type { ServerMessage } from '../shared/protocol';
3
+ import type { OwnedTransport } from './FanTransport';
4
+ /**
5
+ * Sends protocol messages over the raw socket.
6
+ *
7
+ * We use `socket.send()` rather than `socket.emit()` because emit wraps the
8
+ * payload in `{ event, data }` for the gateway's own event protocol, and this
9
+ * is a different protocol.
10
+ */
11
+ export declare class SocketTransport implements OwnedTransport {
12
+ private readonly sockets;
13
+ add(socket: CarnoSocket): void;
14
+ remove(connectionId: string): void;
15
+ owns(connectionId: string): boolean;
16
+ /** <= 0 means back-pressured or dropped; the engine counts those. */
17
+ send(connectionId: string, message: ServerMessage): number;
18
+ }
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SocketTransport = void 0;
4
+ /**
5
+ * Sends protocol messages over the raw socket.
6
+ *
7
+ * We use `socket.send()` rather than `socket.emit()` because emit wraps the
8
+ * payload in `{ event, data }` for the gateway's own event protocol, and this
9
+ * is a different protocol.
10
+ */
11
+ class SocketTransport {
12
+ constructor() {
13
+ this.sockets = new Map();
14
+ }
15
+ add(socket) {
16
+ this.sockets.set(socket.id, socket);
17
+ }
18
+ remove(connectionId) {
19
+ this.sockets.delete(connectionId);
20
+ }
21
+ owns(connectionId) {
22
+ return this.sockets.has(connectionId);
23
+ }
24
+ /** <= 0 means back-pressured or dropped; the engine counts those. */
25
+ send(connectionId, message) {
26
+ const socket = this.sockets.get(connectionId);
27
+ if (!socket) {
28
+ return 0;
29
+ }
30
+ try {
31
+ return socket.send(JSON.stringify(message));
32
+ }
33
+ catch {
34
+ // The socket closed between fan-out and send. Treat as dropped;
35
+ // the close handler will clean it up.
36
+ return 0;
37
+ }
38
+ }
39
+ }
40
+ exports.SocketTransport = SocketTransport;
@@ -0,0 +1,30 @@
1
+ import type { ServerMessage } from '../shared/protocol';
2
+ import type { OwnedTransport } from './FanTransport';
3
+ export interface SseTransportOptions {
4
+ /** 0 disables the heartbeat. Only tests want that. */
5
+ heartbeatMs: number;
6
+ maxConnections: number;
7
+ /** Called when the client goes away, so the engine can drop the connection. */
8
+ onDisconnect?: (connectionId: string) => void;
9
+ }
10
+ /**
11
+ * The downstream half of the SSE transport.
12
+ *
13
+ * Upstream is `POST /live/control`, which speaks the same protocol into the
14
+ * same handler the WebSocket gateway uses -- see `sse-routes.ts`. This half
15
+ * only writes frames, so the engine cannot tell it apart from a socket.
16
+ */
17
+ export declare class SseTransport implements OwnedTransport {
18
+ private readonly options;
19
+ private readonly streams;
20
+ private heartbeat;
21
+ constructor(options: SseTransportOptions);
22
+ open(connectionId: string): ReadableStream<Uint8Array>;
23
+ owns(connectionId: string): boolean;
24
+ send(connectionId: string, message: ServerMessage): number;
25
+ close(connectionId: string): void;
26
+ count(): number;
27
+ stop(): void;
28
+ private ensureHeartbeat;
29
+ private write;
30
+ }
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SseTransport = void 0;
4
+ const ENCODER = new TextEncoder();
5
+ const NativeReadableStream = ReadableStream;
6
+ /**
7
+ * The downstream half of the SSE transport.
8
+ *
9
+ * Upstream is `POST /live/control`, which speaks the same protocol into the
10
+ * same handler the WebSocket gateway uses -- see `sse-routes.ts`. This half
11
+ * only writes frames, so the engine cannot tell it apart from a socket.
12
+ */
13
+ class SseTransport {
14
+ constructor(options) {
15
+ this.options = options;
16
+ this.streams = new Map();
17
+ this.heartbeat = null;
18
+ }
19
+ open(connectionId) {
20
+ if (this.streams.size >= this.options.maxConnections) {
21
+ throw new Error(`[carno:live] the SSE transport is at capacity (${this.options.maxConnections}).`);
22
+ }
23
+ return new NativeReadableStream({
24
+ start: controller => {
25
+ this.streams.set(connectionId, controller);
26
+ // The client cannot learn its own connection id any other way,
27
+ // and it needs it to address the control endpoint.
28
+ this.write(controller, `data: ${JSON.stringify({ t: 'ready', cid: connectionId })}\n\n`);
29
+ this.ensureHeartbeat();
30
+ },
31
+ cancel: () => {
32
+ this.streams.delete(connectionId);
33
+ this.options.onDisconnect?.(connectionId);
34
+ }
35
+ });
36
+ }
37
+ owns(connectionId) {
38
+ return this.streams.has(connectionId);
39
+ }
40
+ send(connectionId, message) {
41
+ const controller = this.streams.get(connectionId);
42
+ if (!controller) {
43
+ return 0;
44
+ }
45
+ return this.write(controller, `data: ${JSON.stringify(message)}\n\n`);
46
+ }
47
+ close(connectionId) {
48
+ const controller = this.streams.get(connectionId);
49
+ this.streams.delete(connectionId);
50
+ try {
51
+ controller?.close();
52
+ }
53
+ catch {
54
+ // Already closed from the other end.
55
+ }
56
+ }
57
+ count() {
58
+ return this.streams.size;
59
+ }
60
+ stop() {
61
+ if (this.heartbeat) {
62
+ clearInterval(this.heartbeat);
63
+ this.heartbeat = null;
64
+ }
65
+ for (const connectionId of [...this.streams.keys()]) {
66
+ this.close(connectionId);
67
+ }
68
+ }
69
+ ensureHeartbeat() {
70
+ if (this.heartbeat || this.options.heartbeatMs <= 0) {
71
+ return;
72
+ }
73
+ // A comment frame. EventSource ignores it; an idle-timeout proxy does
74
+ // not, which is the whole point.
75
+ this.heartbeat = setInterval(() => {
76
+ for (const controller of this.streams.values()) {
77
+ this.write(controller, ': ping\n\n');
78
+ }
79
+ }, this.options.heartbeatMs);
80
+ this.heartbeat.unref?.();
81
+ }
82
+ write(controller, frame) {
83
+ try {
84
+ controller.enqueue(ENCODER.encode(frame));
85
+ return 1;
86
+ }
87
+ catch {
88
+ return 0;
89
+ }
90
+ }
91
+ }
92
+ exports.SseTransport = SseTransport;
@@ -0,0 +1,20 @@
1
+ import type { LiveScope } from '../shared/inputs';
2
+ export interface LiveHandshake {
3
+ connectionId: string;
4
+ /** Opaque credential from the client's `hello`. */
5
+ token?: string;
6
+ }
7
+ export interface LiveScopeResolver {
8
+ resolve(handshake: LiveHandshake): LiveScope | Promise<LiveScope>;
9
+ }
10
+ /**
11
+ * Default resolver: every connection is its own principal.
12
+ *
13
+ * Safe by construction — nothing is ever shared between connections, so no
14
+ * application can leak one user's data to another by forgetting to configure
15
+ * this. Applications that want `shared: 'tenant'` or a real user identity
16
+ * replace it.
17
+ */
18
+ export declare class ConnectionScopeResolver implements LiveScopeResolver {
19
+ resolve(handshake: LiveHandshake): LiveScope;
20
+ }
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ConnectionScopeResolver = void 0;
4
+ /**
5
+ * Default resolver: every connection is its own principal.
6
+ *
7
+ * Safe by construction — nothing is ever shared between connections, so no
8
+ * application can leak one user's data to another by forgetting to configure
9
+ * this. Applications that want `shared: 'tenant'` or a real user identity
10
+ * replace it.
11
+ */
12
+ class ConnectionScopeResolver {
13
+ resolve(handshake) {
14
+ return { principal: handshake.connectionId };
15
+ }
16
+ }
17
+ exports.ConnectionScopeResolver = ConnectionScopeResolver;
@@ -0,0 +1,24 @@
1
+ import type { SseTransport } from './SseTransport';
2
+ export interface SseRouteOptions {
3
+ transport: SseTransport;
4
+ streamPath: string;
5
+ controlPath: string;
6
+ }
7
+ /**
8
+ * The two halves of the SSE transport, as HTTP.
9
+ *
10
+ * `GET streamPath` opens the downstream and names the connection; every
11
+ * client message goes up through `POST controlPath` and into the same
12
+ * `handleMessage` the WebSocket gateway uses. There is no second protocol
13
+ * here, and there must never be one.
14
+ *
15
+ * Handlers registered through `Carno.route()` receive a Bun `Request`, not a
16
+ * `Context` -- the docstring on that method says otherwise, the runtime does
17
+ * not.
18
+ */
19
+ export declare function createSseRoutes(options: SseRouteOptions): {
20
+ streamPath: string;
21
+ controlPath: string;
22
+ stream: (request: Request) => Response;
23
+ control: (request: Request) => Promise<Response>;
24
+ };