@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,165 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LivePlugin = void 0;
4
+ const core_1 = require("@carno.js/core");
5
+ const orm_1 = require("@carno.js/orm");
6
+ const websocket_1 = require("@carno.js/websocket");
7
+ const authorizer_1 = require("./auth/authorizer");
8
+ const InProcessBus_1 = require("./bus/InProcessBus");
9
+ const PgNotifyBus_1 = require("./bus/PgNotifyBus");
10
+ const pg_notify_emitter_1 = require("./emitters/pg-notify-emitter");
11
+ const config_1 = require("./config");
12
+ const AppEmitter_1 = require("./emitters/AppEmitter");
13
+ const DependencyGraph_1 = require("./graph/DependencyGraph");
14
+ const SubscriptionRegistry_1 = require("./graph/SubscriptionRegistry");
15
+ const LiveEngine_1 = require("./LiveEngine");
16
+ const observability_1 = require("./observability");
17
+ const LiveService_1 = require("./LiveService");
18
+ const ResourceRegistry_1 = require("./resource/ResourceRegistry");
19
+ const route_executor_1 = require("./resource/route-executor");
20
+ const runtime_1 = require("./runtime");
21
+ const etag_1 = require("./http/etag");
22
+ const FanTransport_1 = require("./transport/FanTransport");
23
+ const LiveGateway_1 = require("./transport/LiveGateway");
24
+ const scope_resolver_1 = require("./transport/scope-resolver");
25
+ const SocketTransport_1 = require("./transport/SocketTransport");
26
+ const SseTransport_1 = require("./transport/SseTransport");
27
+ const sse_routes_1 = require("./transport/sse-routes");
28
+ const protocol_1 = require("./shared/protocol");
29
+ class LivePlugin {
30
+ static create(options) {
31
+ const config = (0, config_1.resolveLiveConfig)(options.config);
32
+ const resources = new ResourceRegistry_1.ResourceRegistry();
33
+ const graph = new DependencyGraph_1.DependencyGraph();
34
+ const subs = new SubscriptionRegistry_1.SubscriptionRegistry();
35
+ const distributedBus = options.distributed
36
+ ? new PgNotifyBus_1.PgNotifyBus({
37
+ url: options.distributed.url ?? '',
38
+ channel: options.distributed.channel,
39
+ nodeId: options.distributed.nodeId
40
+ })
41
+ : null;
42
+ const bus = distributedBus ?? new InProcessBus_1.InProcessBus();
43
+ const sockets = new SocketTransport_1.SocketTransport();
44
+ const fan = new FanTransport_1.FanTransport();
45
+ fan.add(sockets);
46
+ let sink = null;
47
+ const metrics = new observability_1.LiveMetrics({
48
+ onMetric: (name, value, tags) => sink?.onMetric(name, value, tags)
49
+ });
50
+ const engine = new LiveEngine_1.LiveEngine(resources, graph, subs, bus, fan, config, options.authorizer ?? new authorizer_1.AllowAllAuthorizer(), metrics);
51
+ const emitter = new AppEmitter_1.AppEmitter(bus, config);
52
+ const scopeResolver = options.scopeResolver ?? new scope_resolver_1.ConnectionScopeResolver();
53
+ const dispose = [() => engine.stop()];
54
+ (0, runtime_1.setLiveRuntime)({
55
+ engine,
56
+ transport: sockets,
57
+ resolver: scopeResolver,
58
+ scopes: new Map(),
59
+ handshakes: new Set(),
60
+ resources,
61
+ dispose
62
+ });
63
+ const plugin = new core_1.Carno({ exports: [] });
64
+ plugin.controllers(options.controllers);
65
+ plugin.services([LiveService_1.LiveService]);
66
+ if (options.sse) {
67
+ const sse = new SseTransport_1.SseTransport({
68
+ heartbeatMs: config.sseHeartbeatMs,
69
+ maxConnections: config.sseMaxConnections,
70
+ onDisconnect: LiveGateway_1.dropLiveConnection
71
+ });
72
+ fan.add(sse);
73
+ dispose.push(() => sse.stop());
74
+ const routes = (0, sse_routes_1.createSseRoutes)({
75
+ transport: sse,
76
+ streamPath: config.ssePath,
77
+ controlPath: config.sseControlPath
78
+ });
79
+ plugin.route('GET', routes.streamPath, routes.stream);
80
+ plugin.route('POST', routes.controlPath, routes.control);
81
+ }
82
+ let teachEtag = null;
83
+ // Registered now, taught later: `plugin.middlewares()` runs before
84
+ // bootstrap, and the resources are only known inside the builder.
85
+ // The middleware also gates polling, even when ETags are disabled.
86
+ const etag = new etag_1.LiveETagMiddleware([], { enabled: options.etag !== false });
87
+ etag.setPollingGuard(async ({ resourceId, inputs, request }) => {
88
+ const connectionId = request.headers.get(protocol_1.LIVE_CONNECTION_HEADER);
89
+ if (!connectionId) {
90
+ return null;
91
+ }
92
+ let scope;
93
+ try {
94
+ scope = await scopeResolver.resolve({
95
+ connectionId,
96
+ token: request.headers.get(protocol_1.LIVE_TOKEN_HEADER) ?? undefined
97
+ });
98
+ }
99
+ catch {
100
+ return null;
101
+ }
102
+ return await engine.authorizePolling(connectionId, resourceId, inputs, scope)
103
+ ? scope
104
+ : null;
105
+ });
106
+ plugin.middlewares([etag]);
107
+ teachEtag = paths => etag.setPaths(paths);
108
+ const websocket = websocket_1.WebSocketPlugin.create([LiveGateway_1.LiveGateway, ...(options.gateways ?? [])], options.websocket);
109
+ const innerBuilder = websocket._wsHandlerBuilder;
110
+ const upgradePaths = [...websocket._wsUpgradePaths];
111
+ plugin.use(websocket);
112
+ // The builder runs after bootstrap, when the container holds the
113
+ // controller instances and the ORM holds its connection — which is why
114
+ // everything that needs a database URL is started here and not above.
115
+ plugin.wsHandler((container) => {
116
+ // Resolved here and not above: the container does not exist until
117
+ // bootstrap, and an app with no observability plugin never
118
+ // registers one.
119
+ sink = container.has(core_1.ObservabilityService) ? container.get(core_1.ObservabilityService) : null;
120
+ const routeExecutor = (0, route_executor_1.createLiveRouteExecutor)(container.get(core_1.Carno));
121
+ for (const ControllerClass of options.controllers) {
122
+ resources.register(ControllerClass, container.get(ControllerClass), routeExecutor);
123
+ }
124
+ teachEtag?.(resources.livePaths());
125
+ if (options.pgNotify) {
126
+ const driver = orm_1.Orm.getInstance().driverInstance;
127
+ const deliver = (events) => {
128
+ // A trigger already notified every node. Publishing it on
129
+ // the bus would send it around a second time.
130
+ if (distributedBus) {
131
+ distributedBus.publishLocal(events);
132
+ return;
133
+ }
134
+ bus.publish(events);
135
+ };
136
+ const pgEmitter = new pg_notify_emitter_1.PgNotifyEmitter(deliver, {
137
+ tables: options.pgNotify.tables,
138
+ url: options.pgNotify.url ?? driver.connectionString,
139
+ channel: options.pgNotify.channel,
140
+ execute: sql => driver.executeSql(sql)
141
+ });
142
+ // Two emitters on one table would wake the same instance twice.
143
+ emitter.setCoveredTables(pgEmitter.coveredTables());
144
+ dispose.push(() => pgEmitter.detach());
145
+ void pgEmitter.attach().catch(error => {
146
+ console.error('[carno:live] the Postgres emitter failed to attach', error);
147
+ });
148
+ }
149
+ if (distributedBus) {
150
+ if (!options.distributed?.url) {
151
+ distributedBus.setUrl(orm_1.Orm.getInstance().driverInstance.connectionString);
152
+ }
153
+ void distributedBus.start().catch(error => {
154
+ console.error('[carno:live] the distributed bus failed to start', error);
155
+ });
156
+ dispose.push(() => distributedBus.stop());
157
+ }
158
+ emitter.attach();
159
+ engine.start();
160
+ return innerBuilder(container);
161
+ }, upgradePaths);
162
+ return plugin;
163
+ }
164
+ }
165
+ exports.LivePlugin = LivePlugin;
@@ -0,0 +1,32 @@
1
+ import { type LivePayload } from './resource/prefetch';
2
+ import { type LiveDescriptor } from './shared/descriptor';
3
+ import type { LiveExecutionContext, LiveInputs } from './resource/types';
4
+ /**
5
+ * Manual invalidation — the third emitter of §4.4, for data the ORM cannot
6
+ * see: a rebuilt report, a webhook, an external cache.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * @Service()
11
+ * export class ReportJob {
12
+ * constructor(private readonly live: LiveService) {}
13
+ *
14
+ * @Cron('0 * * * *')
15
+ * async run() {
16
+ * await this.rebuild();
17
+ * this.live.invalidate('app:report:current');
18
+ * }
19
+ * }
20
+ * ```
21
+ */
22
+ export declare class LiveService {
23
+ invalidate(key: string): void;
24
+ /**
25
+ * Compute a live resource for a server-rendered first paint.
26
+ *
27
+ * Hand the payload to the template; the client starts full and its
28
+ * subscription carries only the hash, so the first screen costs one
29
+ * request instead of two and the data is never sent twice.
30
+ */
31
+ prefetch(resource: string | LiveDescriptor<any>, inputs?: Partial<LiveInputs>, context?: LiveExecutionContext): Promise<LivePayload>;
32
+ }
@@ -0,0 +1,51 @@
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
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.LiveService = void 0;
10
+ const core_1 = require("@carno.js/core");
11
+ const runtime_1 = require("./runtime");
12
+ const prefetch_1 = require("./resource/prefetch");
13
+ const descriptor_1 = require("./shared/descriptor");
14
+ /**
15
+ * Manual invalidation — the third emitter of §4.4, for data the ORM cannot
16
+ * see: a rebuilt report, a webhook, an external cache.
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * @Service()
21
+ * export class ReportJob {
22
+ * constructor(private readonly live: LiveService) {}
23
+ *
24
+ * @Cron('0 * * * *')
25
+ * async run() {
26
+ * await this.rebuild();
27
+ * this.live.invalidate('app:report:current');
28
+ * }
29
+ * }
30
+ * ```
31
+ */
32
+ let LiveService = class LiveService {
33
+ invalidate(key) {
34
+ (0, runtime_1.getLiveRuntime)().engine.invalidate(key);
35
+ }
36
+ /**
37
+ * Compute a live resource for a server-rendered first paint.
38
+ *
39
+ * Hand the payload to the template; the client starts full and its
40
+ * subscription carries only the hash, so the first screen costs one
41
+ * request instead of two and the data is never sent twice.
42
+ */
43
+ prefetch(resource, inputs = {}, context = {}) {
44
+ const resourceId = typeof resource === 'string' ? resource : (0, descriptor_1.resourceIdOf)(resource);
45
+ return (0, prefetch_1.prefetchLive)((0, runtime_1.getLiveRuntime)().resources, resourceId, inputs, context);
46
+ }
47
+ };
48
+ exports.LiveService = LiveService;
49
+ exports.LiveService = LiveService = __decorate([
50
+ (0, core_1.Service)()
51
+ ], LiveService);
@@ -0,0 +1,33 @@
1
+ import type { LiveMeta } from '../metadata';
2
+ import type { LiveInputs, LiveScope } from '../shared/inputs';
3
+ export interface LiveAuthorizationRequest {
4
+ resourceId: string;
5
+ controllerName: string;
6
+ handlerName: string;
7
+ meta: LiveMeta;
8
+ inputs: LiveInputs;
9
+ scope: LiveScope;
10
+ connectionId: string;
11
+ }
12
+ /**
13
+ * Decides whether one connection may hold one subscription.
14
+ *
15
+ * Called when the subscription is created and again whenever the connection's
16
+ * `auth:` key is invalidated. Returning false ends that connection's
17
+ * subscription; it never affects the other subscribers of a shared instance.
18
+ */
19
+ export interface LiveAuthorizer {
20
+ authorize(request: LiveAuthorizationRequest): boolean | Promise<boolean>;
21
+ }
22
+ /** Default. The scope already isolates instances, so nothing extra is refused. */
23
+ export declare class AllowAllAuthorizer implements LiveAuthorizer {
24
+ authorize(): boolean;
25
+ }
26
+ export declare function isAuthKey(key: string): boolean;
27
+ /**
28
+ * The authorization keys a connection with this scope answers to.
29
+ *
30
+ * Same two-level shape as the ORM keys: `auth:principal#42` is contained by
31
+ * `auth:principal`, so invalidating the parent re-checks everyone.
32
+ */
33
+ export declare function authKeysOf(scope: LiveScope): string[];
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AllowAllAuthorizer = void 0;
4
+ exports.isAuthKey = isAuthKey;
5
+ exports.authKeysOf = authKeysOf;
6
+ /** Default. The scope already isolates instances, so nothing extra is refused. */
7
+ class AllowAllAuthorizer {
8
+ authorize() {
9
+ return true;
10
+ }
11
+ }
12
+ exports.AllowAllAuthorizer = AllowAllAuthorizer;
13
+ const AUTH_PREFIX = 'auth:';
14
+ function isAuthKey(key) {
15
+ return key.startsWith(AUTH_PREFIX);
16
+ }
17
+ /**
18
+ * The authorization keys a connection with this scope answers to.
19
+ *
20
+ * Same two-level shape as the ORM keys: `auth:principal#42` is contained by
21
+ * `auth:principal`, so invalidating the parent re-checks everyone.
22
+ */
23
+ function authKeysOf(scope) {
24
+ const keys = [];
25
+ if (scope.principal !== undefined && scope.principal !== null && scope.principal !== '') {
26
+ keys.push(`${AUTH_PREFIX}principal#${scope.principal}`);
27
+ }
28
+ if (scope.tenant !== undefined && scope.tenant !== null && scope.tenant !== '') {
29
+ keys.push(`${AUTH_PREFIX}tenant#${scope.tenant}`);
30
+ }
31
+ return keys;
32
+ }
@@ -0,0 +1,8 @@
1
+ import type { InvalidationBus, InvalidationHandler } from './InvalidationBus';
2
+ import type { InvalidationEvent } from '../graph/types';
3
+ /** Single-process bus. Correct for one node; phase 2 adds the distributed ones. */
4
+ export declare class InProcessBus implements InvalidationBus {
5
+ private readonly handlers;
6
+ publish(events: InvalidationEvent[]): void;
7
+ subscribe(handler: InvalidationHandler): () => void;
8
+ }
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.InProcessBus = void 0;
4
+ /** Single-process bus. Correct for one node; phase 2 adds the distributed ones. */
5
+ class InProcessBus {
6
+ constructor() {
7
+ this.handlers = new Set();
8
+ }
9
+ publish(events) {
10
+ if (events.length === 0) {
11
+ return;
12
+ }
13
+ for (const handler of this.handlers) {
14
+ try {
15
+ handler(events);
16
+ }
17
+ catch (error) {
18
+ // One broken subscriber must not swallow invalidations for the
19
+ // others: a dropped invalidation is a screen frozen on stale data.
20
+ console.error('[carno:live] invalidation handler failed', error);
21
+ }
22
+ }
23
+ }
24
+ subscribe(handler) {
25
+ this.handlers.add(handler);
26
+ return () => this.handlers.delete(handler);
27
+ }
28
+ }
29
+ exports.InProcessBus = InProcessBus;
@@ -0,0 +1,15 @@
1
+ import type { InvalidationEvent } from '../graph/types';
2
+ export type InvalidationHandler = (events: InvalidationEvent[]) => void;
3
+ /**
4
+ * Carries invalidations from an emitter to the nodes holding subscriptions.
5
+ *
6
+ * The graph does not care where an invalidation came from, which is why the
7
+ * ORM emitter, the Postgres emitter and manual invalidation are three
8
+ * implementations feeding one interface. Phase 1 ships the in-process one;
9
+ * Redis and pg_notify buses arrive in phase 2 without touching the graph.
10
+ */
11
+ export interface InvalidationBus {
12
+ publish(events: InvalidationEvent[]): void;
13
+ /** Returns an unsubscribe function. */
14
+ subscribe(handler: InvalidationHandler): () => void;
15
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,55 @@
1
+ import type { InvalidationEvent } from '../graph/types';
2
+ import { PgListener } from '../emitters/pg-listener';
3
+ import type { InvalidationBus, InvalidationHandler } from './InvalidationBus';
4
+ export declare const DEFAULT_PG_BUS_CHANNEL = "carno_live_bus";
5
+ export declare const DEFAULT_PG_BUS_MAX_PAYLOAD_BYTES = 7000;
6
+ export interface PgNotifyBusOptions {
7
+ /** May be empty at construction; `setUrl` fills it before `start()`. */
8
+ url: string;
9
+ channel?: string;
10
+ /** Identifies this process so its own echo can be dropped. */
11
+ nodeId?: string;
12
+ maxPayloadBytes?: number;
13
+ heartbeatMs?: number;
14
+ retryMs?: number;
15
+ /** Injected in tests. */
16
+ listener?: PgListener;
17
+ }
18
+ /**
19
+ * Split events into `pg_notify` frames under the payload ceiling.
20
+ *
21
+ * A single event that does not fit on its own degrades to its table key: a
22
+ * coarse invalidation costs CPU, a dropped one costs a screen frozen on stale
23
+ * data.
24
+ */
25
+ export declare function chunkEvents(events: InvalidationEvent[], nodeId: string, maxPayloadBytes: number): string[];
26
+ /**
27
+ * Invalidation bus across nodes, over `LISTEN/NOTIFY`.
28
+ *
29
+ * Two entry points, on purpose. `publish` is for a source local to this node —
30
+ * the ORM emitter, `LiveService.invalidate()` — and has to reach the others.
31
+ * `publishLocal` is for a source that already reached every node, which is what
32
+ * a Postgres trigger is; re-publishing that would multiply one write by the
33
+ * size of the cluster.
34
+ */
35
+ export declare class PgNotifyBus implements InvalidationBus {
36
+ private readonly options;
37
+ readonly nodeId: string;
38
+ private readonly handlers;
39
+ private readonly channel;
40
+ private readonly maxPayloadBytes;
41
+ private listener;
42
+ private url;
43
+ constructor(options: PgNotifyBusOptions);
44
+ /** Called by the plugin once the ORM knows where the database is. */
45
+ setUrl(url: string): void;
46
+ start(): Promise<void>;
47
+ stop(): Promise<void>;
48
+ publish(events: InvalidationEvent[]): void;
49
+ /** Deliver here only: the source already reached every node. */
50
+ publishLocal(events: InvalidationEvent[]): void;
51
+ subscribe(handler: InvalidationHandler): () => void;
52
+ private broadcast;
53
+ private onFrame;
54
+ private deliver;
55
+ }
@@ -0,0 +1,138 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PgNotifyBus = exports.DEFAULT_PG_BUS_MAX_PAYLOAD_BYTES = exports.DEFAULT_PG_BUS_CHANNEL = void 0;
4
+ exports.chunkEvents = chunkEvents;
5
+ const dep_key_1 = require("../graph/dep-key");
6
+ const pg_listener_1 = require("../emitters/pg-listener");
7
+ exports.DEFAULT_PG_BUS_CHANNEL = 'carno_live_bus';
8
+ exports.DEFAULT_PG_BUS_MAX_PAYLOAD_BYTES = 7000;
9
+ /**
10
+ * Split events into `pg_notify` frames under the payload ceiling.
11
+ *
12
+ * A single event that does not fit on its own degrades to its table key: a
13
+ * coarse invalidation costs CPU, a dropped one costs a screen frozen on stale
14
+ * data.
15
+ */
16
+ function chunkEvents(events, nodeId, maxPayloadBytes) {
17
+ const encode = (items) => JSON.stringify({ n: nodeId, e: items });
18
+ const frames = [];
19
+ let batch = [];
20
+ for (const event of events) {
21
+ let item = event;
22
+ if (Buffer.byteLength(encode([item]), 'utf8') > maxPayloadBytes) {
23
+ const table = (0, dep_key_1.tableOfKey)(item.key);
24
+ item = { key: table ? (0, dep_key_1.tableKey)(table) : item.key, columns: null };
25
+ }
26
+ if (batch.length > 0 && Buffer.byteLength(encode([...batch, item]), 'utf8') > maxPayloadBytes) {
27
+ frames.push(encode(batch));
28
+ batch = [item];
29
+ continue;
30
+ }
31
+ batch.push(item);
32
+ }
33
+ if (batch.length > 0) {
34
+ frames.push(encode(batch));
35
+ }
36
+ return frames;
37
+ }
38
+ /**
39
+ * Invalidation bus across nodes, over `LISTEN/NOTIFY`.
40
+ *
41
+ * Two entry points, on purpose. `publish` is for a source local to this node —
42
+ * the ORM emitter, `LiveService.invalidate()` — and has to reach the others.
43
+ * `publishLocal` is for a source that already reached every node, which is what
44
+ * a Postgres trigger is; re-publishing that would multiply one write by the
45
+ * size of the cluster.
46
+ */
47
+ class PgNotifyBus {
48
+ constructor(options) {
49
+ this.options = options;
50
+ this.handlers = new Set();
51
+ this.nodeId = options.nodeId ?? crypto.randomUUID();
52
+ this.channel = options.channel ?? exports.DEFAULT_PG_BUS_CHANNEL;
53
+ this.maxPayloadBytes = options.maxPayloadBytes ?? exports.DEFAULT_PG_BUS_MAX_PAYLOAD_BYTES;
54
+ this.listener = options.listener ?? null;
55
+ this.url = options.url;
56
+ }
57
+ /** Called by the plugin once the ORM knows where the database is. */
58
+ setUrl(url) {
59
+ this.url = url;
60
+ }
61
+ async start() {
62
+ if (!this.listener) {
63
+ this.listener = new pg_listener_1.PgListener({
64
+ url: this.url,
65
+ heartbeatMs: this.options.heartbeatMs,
66
+ retryMs: this.options.retryMs
67
+ });
68
+ }
69
+ await this.listener.listen(this.channel, raw => this.onFrame(raw));
70
+ }
71
+ async stop() {
72
+ await this.listener?.close();
73
+ }
74
+ publish(events) {
75
+ if (events.length === 0) {
76
+ return;
77
+ }
78
+ this.deliver(events);
79
+ void this.broadcast(events);
80
+ }
81
+ /** Deliver here only: the source already reached every node. */
82
+ publishLocal(events) {
83
+ if (events.length === 0) {
84
+ return;
85
+ }
86
+ this.deliver(events);
87
+ }
88
+ subscribe(handler) {
89
+ this.handlers.add(handler);
90
+ return () => this.handlers.delete(handler);
91
+ }
92
+ // ------------------------------------------------------------ internals
93
+ async broadcast(events) {
94
+ const listener = this.listener;
95
+ if (!listener) {
96
+ console.error('[carno:live] the distributed bus is not started; invalidation stayed local');
97
+ return;
98
+ }
99
+ for (const frame of chunkEvents(events, this.nodeId, this.maxPayloadBytes)) {
100
+ try {
101
+ await listener.notify(this.channel, frame);
102
+ }
103
+ catch (error) {
104
+ // The other nodes will miss this one. Loud, because the symptom
105
+ // over there is a screen that simply stops updating.
106
+ console.error('[carno:live] failed to broadcast an invalidation', error);
107
+ }
108
+ }
109
+ }
110
+ onFrame(raw) {
111
+ let frame;
112
+ try {
113
+ frame = JSON.parse(raw);
114
+ }
115
+ catch {
116
+ return;
117
+ }
118
+ if (!frame || typeof frame.n !== 'string' || !Array.isArray(frame.e)) {
119
+ return;
120
+ }
121
+ if (frame.n === this.nodeId) {
122
+ // Our own echo. These were delivered locally before being sent.
123
+ return;
124
+ }
125
+ this.deliver(frame.e);
126
+ }
127
+ deliver(events) {
128
+ for (const handler of this.handlers) {
129
+ try {
130
+ handler(events);
131
+ }
132
+ catch (error) {
133
+ console.error('[carno:live] invalidation handler failed', error);
134
+ }
135
+ }
136
+ }
137
+ }
138
+ exports.PgNotifyBus = PgNotifyBus;
@@ -0,0 +1,19 @@
1
+ import { InjectionToken, Injector, type Provider, type Signal } from '@angular/core';
2
+ import type { LiveDataOf, LiveDescriptor, LiveInputsOf } from '../shared/descriptor';
3
+ import type { LiveClient, LiveState } from './core';
4
+ /** How a component finds the client without every call site passing it. */
5
+ export declare const LIVE_CLIENT: InjectionToken<LiveClient>;
6
+ export declare function provideLive(client: LiveClient): Provider;
7
+ export interface LiveSignalOptions {
8
+ /** Overrides the injected client. Mostly for tests and for multi-backend apps. */
9
+ client?: LiveClient;
10
+ /** Required when calling outside an injection context. */
11
+ injector?: Injector;
12
+ }
13
+ /**
14
+ * The function production `effect()` calls. bun:test has no EffectScheduler,
15
+ * so tests invoke this in place of `TestBed.flushEffects()`.
16
+ */
17
+ export declare function reconcileLiveSignal(state: object): void;
18
+ export declare function liveSignal<R>(descriptor: LiveDescriptor<R>, inputs?: () => LiveInputsOf<R>, options?: LiveSignalOptions): Signal<LiveState<LiveDataOf<R>>>;
19
+ export declare function liveSignal<T>(resourceId: string, inputs?: () => Record<string, any>, options?: LiveSignalOptions): Signal<LiveState<T>>;
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LIVE_CLIENT = void 0;
4
+ exports.provideLive = provideLive;
5
+ exports.reconcileLiveSignal = reconcileLiveSignal;
6
+ exports.liveSignal = liveSignal;
7
+ const core_1 = require("@angular/core");
8
+ const vanilla_1 = require("./vanilla");
9
+ /** How a component finds the client without every call site passing it. */
10
+ exports.LIVE_CLIENT = new core_1.InjectionToken('carno.live.client');
11
+ function provideLive(client) {
12
+ return { provide: exports.LIVE_CLIENT, useValue: client };
13
+ }
14
+ const PENDING = { data: undefined, pending: true, error: null, stale: false };
15
+ /** bun:test Injector.create has no APP_EFFECT_SCHEDULER (an unnamed InjectionToken). */
16
+ function isMissingEffectScheduler(error) {
17
+ const message = error instanceof Error ? error.message : String(error);
18
+ return message.includes('NullInjectorError') && message.includes('No provider for InjectionToken');
19
+ }
20
+ const reconcilers = new WeakMap();
21
+ /**
22
+ * The function production `effect()` calls. bun:test has no EffectScheduler,
23
+ * so tests invoke this in place of `TestBed.flushEffects()`.
24
+ */
25
+ function reconcileLiveSignal(state) {
26
+ const reconcile = reconcilers.get(state);
27
+ if (!reconcile) {
28
+ throw new Error('reconcileLiveSignal() expected a liveSignal() state');
29
+ }
30
+ reconcile();
31
+ }
32
+ /**
33
+ * Subscribe a component to server-owned state, as a signal.
34
+ *
35
+ * `inputs` is read reactively, so changing a signal it touches re-points the
36
+ * subscription and cancels the previous one. Teardown is `DestroyRef`, so
37
+ * there is nothing to unsubscribe by hand, and nothing here touches zone.js.
38
+ */
39
+ function liveSignal(resource, inputs = () => ({}), options = {}) {
40
+ const client = options.client ?? (0, core_1.inject)(exports.LIVE_CLIENT);
41
+ const destroyRef = options.injector
42
+ ? options.injector.get(core_1.DestroyRef)
43
+ : (0, core_1.inject)(core_1.DestroyRef);
44
+ const state = (0, core_1.signal)(PENDING);
45
+ const slot = new vanilla_1.LiveSlot(client, next => state.set(next));
46
+ // A computed, not a raw call: the effect below then re-runs only when the
47
+ // inputs actually recompute to something different, and LiveSlot ignores
48
+ // the ones that recompute to the same thing.
49
+ const target = (0, core_1.computed)(() => inputs());
50
+ function reconcile() {
51
+ slot.point(resource, target());
52
+ }
53
+ try {
54
+ // LiveSlot.point() writes the output signal synchronously. Angular 18
55
+ // forbids that inside an effect unless allowSignalWrites is set.
56
+ (0, core_1.effect)(() => reconcile(), {
57
+ allowSignalWrites: true,
58
+ injector: options.injector
59
+ });
60
+ }
61
+ catch (error) {
62
+ // bun:test has no EffectScheduler. Any other construction failure is real.
63
+ if (!isMissingEffectScheduler(error)) {
64
+ throw error;
65
+ }
66
+ }
67
+ destroyRef.onDestroy(() => slot.close());
68
+ const output = state.asReadonly();
69
+ reconcilers.set(output, reconcile);
70
+ return output;
71
+ }