@carno.js/live 1.8.0 → 1.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/config.ts CHANGED
@@ -1,49 +1,61 @@
1
- /**
2
- * Tunables from §10.1 of the design. These are starting points to calibrate
3
- * against the recompute-without-patch metric, not measured values.
4
- */
5
- export interface LiveConfig {
6
- /** Window in ms over which invalidations for one instance are grouped. */
7
- coalesceMs: number;
8
- /** Above this many row keys, one read collapses to its table key. */
9
- maxKeysPerRead: number;
10
- /** Ceiling on the canonicalized inputs of a single subscription. */
11
- maxInputBytes: number;
12
- /** Grace period before dropping an instance whose refcount hit zero. */
13
- unsubGraceMs: number;
14
- /** Consecutive back-pressured sends before collapsing to a snapshot. */
15
- maxPendingPatches: number;
16
- /** Above this fan-out, recompute is queued instead of run inline. */
17
- fanoutQueueThreshold: number;
18
- /** Ceiling on live instances held by a single connection. */
19
- maxInstancesPerConnection: number;
20
- /** Ceiling on live instances held by this process. */
21
- maxInstancesPerNode: number;
22
- /** Path of the SSE downstream, when the SSE transport is on. */
23
- ssePath: string;
24
- /** Path client messages are posted to, when the SSE transport is on. */
25
- sseControlPath: string;
26
- /** Comment frame interval that keeps idle-timeout proxies from reaping. */
27
- sseHeartbeatMs: number;
28
- /** Ceiling on concurrent SSE streams held by this process. */
29
- sseMaxConnections: number;
30
- }
31
-
32
- export const DEFAULT_LIVE_CONFIG: LiveConfig = {
33
- coalesceMs: 16,
34
- maxKeysPerRead: 64,
35
- maxInputBytes: 8192,
36
- unsubGraceMs: 5000,
37
- maxPendingPatches: 32,
38
- fanoutQueueThreshold: 500,
39
- maxInstancesPerConnection: 64,
40
- maxInstancesPerNode: 50000,
41
- ssePath: '/live/sse',
42
- sseControlPath: '/live/control',
43
- sseHeartbeatMs: 15000,
44
- sseMaxConnections: 10000
45
- };
46
-
47
- export function resolveLiveConfig(overrides: Partial<LiveConfig> = {}): LiveConfig {
48
- return { ...DEFAULT_LIVE_CONFIG, ...overrides };
49
- }
1
+ /**
2
+ * Tunables from §10.1 of the design. These are starting points to calibrate
3
+ * against the recompute-without-patch metric, not measured values.
4
+ */
5
+ export interface LiveConfig {
6
+ /** Window in ms over which invalidations for one instance are grouped. */
7
+ coalesceMs: number;
8
+ /** Above this many row keys, one read collapses to its table key. */
9
+ maxKeysPerRead: number;
10
+ /** Ceiling on the canonicalized inputs of a single subscription. */
11
+ maxInputBytes: number;
12
+ /** Grace period before dropping an instance whose refcount hit zero. */
13
+ unsubGraceMs: number;
14
+ /** Consecutive back-pressured sends before collapsing to a snapshot. */
15
+ maxPendingPatches: number;
16
+ /** Recomputes finished in one run before the loop is yielded back. */
17
+ fanoutQueueThreshold: number;
18
+ /**
19
+ * Recomputes allowed to run at once, across every path.
20
+ *
21
+ * Each one executes the resource's route, and so its queries. Raising it
22
+ * past the database pool buys no parallelism -- the driver queues the
23
+ * excess either way -- it only puts live queries in front of the ordinary
24
+ * HTTP requests competing for the same connections. The default sits well
25
+ * under Bun's pool default of 10; raise it with the pool, not with the
26
+ * fan-out.
27
+ */
28
+ maxConcurrentRecomputes: number;
29
+ /** Ceiling on live instances held by a single connection. */
30
+ maxInstancesPerConnection: number;
31
+ /** Ceiling on live instances held by this process. */
32
+ maxInstancesPerNode: number;
33
+ /** Path of the SSE downstream, when the SSE transport is on. */
34
+ ssePath: string;
35
+ /** Path client messages are posted to, when the SSE transport is on. */
36
+ sseControlPath: string;
37
+ /** Comment frame interval that keeps idle-timeout proxies from reaping. */
38
+ sseHeartbeatMs: number;
39
+ /** Ceiling on concurrent SSE streams held by this process. */
40
+ sseMaxConnections: number;
41
+ }
42
+
43
+ export const DEFAULT_LIVE_CONFIG: LiveConfig = {
44
+ coalesceMs: 16,
45
+ maxKeysPerRead: 64,
46
+ maxInputBytes: 8192,
47
+ unsubGraceMs: 5000,
48
+ maxPendingPatches: 32,
49
+ fanoutQueueThreshold: 500,
50
+ maxConcurrentRecomputes: 4,
51
+ maxInstancesPerConnection: 64,
52
+ maxInstancesPerNode: 50000,
53
+ ssePath: '/live/sse',
54
+ sseControlPath: '/live/control',
55
+ sseHeartbeatMs: 15000,
56
+ sseMaxConnections: 10000
57
+ };
58
+
59
+ export function resolveLiveConfig(overrides: Partial<LiveConfig> = {}): LiveConfig {
60
+ return { ...DEFAULT_LIVE_CONFIG, ...overrides };
61
+ }
@@ -1,147 +1,223 @@
1
- import { ancestorsOf, type DepKey } from './dep-key';
2
- import type { Dependency, InvalidationEvent } from './types';
3
-
4
- /** Column sets registered per instance under one key. null means wildcard. */
5
- type ColumnSet = Set<string> | null;
6
-
7
- /**
8
- * Key ↔ instance index with ancestor resolution and column filtering.
9
- *
10
- * Knows nothing about WebSocket, the ORM, or resources — it is a pure data
11
- * structure, which is why the hard part of invalidation is testable without
12
- * a server, a database or a socket.
13
- */
14
- export class DependencyGraph {
15
- private readonly byKey = new Map<DepKey, Map<string, ColumnSet>>();
16
- private readonly byInstance = new Map<string, Set<DepKey>>();
17
-
18
- /** Replace every dependency held by this instance. */
19
- setDependencies(instanceId: string, deps: Dependency[]): void {
20
- this.remove(instanceId);
21
-
22
- if (deps.length === 0) {
23
- return;
24
- }
25
-
26
- const keys = new Set<DepKey>();
27
-
28
- for (const dep of deps) {
29
- keys.add(dep.key);
30
-
31
- let holders = this.byKey.get(dep.key);
32
- if (!holders) {
33
- holders = new Map<string, ColumnSet>();
34
- this.byKey.set(dep.key, holders);
35
- }
36
-
37
- if (!holders.has(instanceId)) {
38
- holders.set(instanceId, dep.columns === null ? null : new Set(dep.columns));
39
- continue;
40
- }
41
-
42
- const existing = holders.get(instanceId)!;
43
-
44
- if (existing === null) {
45
- continue;
46
- }
47
-
48
- if (dep.columns === null) {
49
- holders.set(instanceId, null);
50
- continue;
51
- }
52
-
53
- for (const column of dep.columns) {
54
- existing.add(column);
55
- }
56
- }
57
-
58
- this.byInstance.set(instanceId, keys);
59
- }
60
-
61
- /** Forget the instance entirely. */
62
- remove(instanceId: string): void {
63
- const keys = this.byInstance.get(instanceId);
64
-
65
- if (!keys) {
66
- return;
67
- }
68
-
69
- for (const key of keys) {
70
- const holders = this.byKey.get(key);
71
-
72
- if (!holders) {
73
- continue;
74
- }
75
-
76
- holders.delete(instanceId);
77
-
78
- if (holders.size === 0) {
79
- this.byKey.delete(key);
80
- }
81
- }
82
-
83
- this.byInstance.delete(instanceId);
84
- }
85
-
86
- /**
87
- * Instances concerned by this write.
88
- *
89
- * Both directions of the hierarchy matter. A row write wakes table
90
- * subscribers, while a table write wakes row subscribers because a
91
- * predicate write may have touched that row.
92
- */
93
- resolve(event: InvalidationEvent): string[] {
94
- const matched = new Set<string>();
95
-
96
- for (const key of ancestorsOf(event.key)) {
97
- this.collect(key, event.columns, matched);
98
- }
99
-
100
- const descendantPrefix = `${event.key}#`;
101
- if (!event.key.includes('#')) {
102
- for (const key of this.byKey.keys()) {
103
- if (key.startsWith(descendantPrefix)) {
104
- this.collect(key, event.columns, matched);
105
- }
106
- }
107
- }
108
-
109
- return [...matched];
110
- }
111
-
112
- keyCount(): number {
113
- return this.byKey.size;
114
- }
115
-
116
- instanceCount(): number {
117
- return this.byInstance.size;
118
- }
119
-
120
- private collect(key: DepKey, writtenColumns: string[] | null, into: Set<string>): void {
121
- const holders = this.byKey.get(key);
122
-
123
- if (!holders) {
124
- return;
125
- }
126
-
127
- for (const [instanceId, readColumns] of holders) {
128
- if (intersects(readColumns, writtenColumns)) {
129
- into.add(instanceId);
130
- }
131
- }
132
- }
133
- }
134
-
135
- function intersects(readColumns: Set<string> | null, writtenColumns: string[] | null): boolean {
136
- if (readColumns === null || writtenColumns === null) {
137
- return true;
138
- }
139
-
140
- for (const column of writtenColumns) {
141
- if (readColumns.has(column)) {
142
- return true;
143
- }
144
- }
145
-
146
- return false;
147
- }
1
+ import { ancestorsOf, type DepKey } from './dep-key';
2
+ import type { Dependency, InvalidationEvent } from './types';
3
+
4
+ /** Column sets registered per instance under one key. null means wildcard. */
5
+ type ColumnSet = Set<string> | null;
6
+
7
+ /**
8
+ * Key ↔ instance index with ancestor resolution and column filtering.
9
+ *
10
+ * Knows nothing about WebSocket, the ORM, or resources — it is a pure data
11
+ * structure, which is why the hard part of invalidation is testable without
12
+ * a server, a database or a socket.
13
+ */
14
+ export class DependencyGraph {
15
+ private readonly byKey = new Map<DepKey, Map<string, ColumnSet>>();
16
+ private readonly byInstance = new Map<string, Set<DepKey>>();
17
+ /**
18
+ * Registered row keys grouped by the key that contains them, so a write
19
+ * that names a whole table finds its rows instead of being searched for.
20
+ *
21
+ * Without it, resolving a table event means walking every key in the
22
+ * graph: at the configured `maxInstancesPerNode` that is a five-millisecond
23
+ * scan, run synchronously once per event of the batch, with every HTTP
24
+ * request in the process waiting behind it. Table events are not the rare
25
+ * case either -- a write degrades to its table key unless its WHERE clause
26
+ * is a literal primary-key match.
27
+ */
28
+ private readonly byParent = new Map<DepKey, Set<Map<string, ColumnSet>>>();
29
+
30
+ /** Replace every dependency held by this instance. */
31
+ setDependencies(instanceId: string, deps: Dependency[]): void {
32
+ this.remove(instanceId);
33
+
34
+ if (deps.length === 0) {
35
+ return;
36
+ }
37
+
38
+ const keys = new Set<DepKey>();
39
+
40
+ for (const dep of deps) {
41
+ keys.add(dep.key);
42
+
43
+ let holders = this.byKey.get(dep.key);
44
+ if (!holders) {
45
+ holders = new Map<string, ColumnSet>();
46
+ this.byKey.set(dep.key, holders);
47
+ this.index(dep.key, holders);
48
+ }
49
+
50
+ if (!holders.has(instanceId)) {
51
+ holders.set(instanceId, dep.columns === null ? null : new Set(dep.columns));
52
+ continue;
53
+ }
54
+
55
+ const existing = holders.get(instanceId)!;
56
+
57
+ if (existing === null) {
58
+ continue;
59
+ }
60
+
61
+ if (dep.columns === null) {
62
+ holders.set(instanceId, null);
63
+ continue;
64
+ }
65
+
66
+ for (const column of dep.columns) {
67
+ existing.add(column);
68
+ }
69
+ }
70
+
71
+ this.byInstance.set(instanceId, keys);
72
+ }
73
+
74
+ /** Forget the instance entirely. */
75
+ remove(instanceId: string): void {
76
+ const keys = this.byInstance.get(instanceId);
77
+
78
+ if (!keys) {
79
+ return;
80
+ }
81
+
82
+ for (const key of keys) {
83
+ const holders = this.byKey.get(key);
84
+
85
+ if (!holders) {
86
+ continue;
87
+ }
88
+
89
+ holders.delete(instanceId);
90
+
91
+ if (holders.size === 0) {
92
+ this.byKey.delete(key);
93
+ this.unindex(key, holders);
94
+ }
95
+ }
96
+
97
+ this.byInstance.delete(instanceId);
98
+ }
99
+
100
+ /**
101
+ * Instances concerned by this write.
102
+ *
103
+ * Both directions of the hierarchy matter. A row write wakes table
104
+ * subscribers, while a table write wakes row subscribers because a
105
+ * predicate write may have touched that row.
106
+ */
107
+ resolve(event: InvalidationEvent): string[] {
108
+ const matched = new Set<string>();
109
+
110
+ for (const key of ancestorsOf(event.key)) {
111
+ this.collect(key, event.columns, matched);
112
+ }
113
+
114
+ // A key with a `#` is already a row: it has no descendants, and the
115
+ // ancestor pass above has covered its table.
116
+ if (!event.key.includes('#')) {
117
+ const rows = this.byParent.get(event.key);
118
+
119
+ if (rows) {
120
+ // Holder maps, not keys: the key would only be looked up again.
121
+ for (const holders of rows) {
122
+ this.collectFrom(holders, event.columns, matched);
123
+ }
124
+ }
125
+ }
126
+
127
+ return [...matched];
128
+ }
129
+
130
+ keyCount(): number {
131
+ return this.byKey.size;
132
+ }
133
+
134
+ instanceCount(): number {
135
+ return this.byInstance.size;
136
+ }
137
+
138
+ /** Keys that currently hold indexed rows. Zero when the graph is empty. */
139
+ parentCount(): number {
140
+ return this.byParent.size;
141
+ }
142
+
143
+ /**
144
+ * Record a row key's holders under the key that contains it.
145
+ *
146
+ * A table key has no separator and so no parent; it is reached directly.
147
+ * The holder map is stored rather than the key because it is what
148
+ * `resolve` actually needs, and because its identity is stable: it is
149
+ * created once in `setDependencies` and dropped only in `remove`.
150
+ */
151
+ private index(key: DepKey, holders: Map<string, ColumnSet>): void {
152
+ const separator = key.indexOf('#');
153
+
154
+ if (separator === -1) {
155
+ return;
156
+ }
157
+
158
+ const parent = key.slice(0, separator);
159
+ let rows = this.byParent.get(parent);
160
+
161
+ if (!rows) {
162
+ rows = new Set<Map<string, ColumnSet>>();
163
+ this.byParent.set(parent, rows);
164
+ }
165
+
166
+ rows.add(holders);
167
+ }
168
+
169
+ private unindex(key: DepKey, holders: Map<string, ColumnSet>): void {
170
+ const separator = key.indexOf('#');
171
+
172
+ if (separator === -1) {
173
+ return;
174
+ }
175
+
176
+ const parent = key.slice(0, separator);
177
+ const rows = this.byParent.get(parent);
178
+
179
+ if (!rows) {
180
+ return;
181
+ }
182
+
183
+ rows.delete(holders);
184
+
185
+ if (rows.size === 0) {
186
+ this.byParent.delete(parent);
187
+ }
188
+ }
189
+
190
+ private collect(key: DepKey, writtenColumns: string[] | null, into: Set<string>): void {
191
+ const holders = this.byKey.get(key);
192
+
193
+ if (holders) {
194
+ this.collectFrom(holders, writtenColumns, into);
195
+ }
196
+ }
197
+
198
+ private collectFrom(
199
+ holders: Map<string, ColumnSet>,
200
+ writtenColumns: string[] | null,
201
+ into: Set<string>
202
+ ): void {
203
+ for (const [instanceId, readColumns] of holders) {
204
+ if (intersects(readColumns, writtenColumns)) {
205
+ into.add(instanceId);
206
+ }
207
+ }
208
+ }
209
+ }
210
+
211
+ function intersects(readColumns: Set<string> | null, writtenColumns: string[] | null): boolean {
212
+ if (readColumns === null || writtenColumns === null) {
213
+ return true;
214
+ }
215
+
216
+ for (const column of writtenColumns) {
217
+ if (readColumns.has(column)) {
218
+ return true;
219
+ }
220
+ }
221
+
222
+ return false;
223
+ }
package/src/index.ts CHANGED
@@ -1,81 +1,83 @@
1
- import 'reflect-metadata';
2
-
3
- // Decorator and metadata
4
- export { Live } from './decorators/Live';
5
- export { LIVE_META } from './metadata';
6
- export type { LiveMeta, LiveOptions, LiveShared } from './metadata';
7
-
8
- // Plugin and services
9
- export { LivePlugin } from './LivePlugin';
10
- export type { LivePluginOptions } from './LivePlugin';
11
- export { closeLiveRuntime } from './runtime';
12
- export { LiveService } from './LiveService';
13
- export { LiveEngine } from './LiveEngine';
14
- export type { LiveTransport, LiveStats } from './LiveEngine';
15
-
16
- // Metrics
17
- export { LiveMetrics } from './observability';
18
- export type { MetricSink } from './observability';
19
-
20
- // Configuration
21
- export { DEFAULT_LIVE_CONFIG, resolveLiveConfig } from './config';
22
- export type { LiveConfig } from './config';
23
-
24
- // Scope
25
- export { ConnectionScopeResolver } from './transport/scope-resolver';
26
- export type { LiveHandshake, LiveScopeResolver } from './transport/scope-resolver';
27
- export type { LiveExecutionContext, LiveInputs, LiveResourceExecutor, LiveScope } from './resource/types';
28
-
29
- // Authorization
30
- export { AllowAllAuthorizer, authKeysOf, isAuthKey } from './auth/authorizer';
31
- export type { LiveAuthorizationRequest, LiveAuthorizer } from './auth/authorizer';
32
-
33
- // Invalidation
34
- export { InProcessBus } from './bus/InProcessBus';
35
- export type { InvalidationBus, InvalidationHandler } from './bus/InvalidationBus';
36
- export type { Dependency, InvalidationEvent } from './graph/types';
37
- export { ancestorsOf, rowKey, tableKey } from './graph/dep-key';
38
- export type { DepKey } from './graph/dep-key';
39
- export { WriteDuringComputeError } from './emitters/AppEmitter';
40
- export { PgNotifyBus, chunkEvents } from './bus/PgNotifyBus';
41
- export type { PgNotifyBusOptions } from './bus/PgNotifyBus';
42
- export { PgNotifyEmitter, eventsFromPayload } from './emitters/pg-notify-emitter';
43
- export type { PgNotifyEmitterOptions, PgNotifyTable } from './emitters/pg-notify-emitter';
44
- export { PgListener } from './emitters/pg-listener';
45
- export type { ListenableSql, PgListenerOptions } from './emitters/pg-listener';
46
- export { tableOfKey } from './graph/dep-key';
47
-
48
- // Protocol and patches, shared with the client
49
- export * from './shared/protocol';
50
- export type { PatchOp, PathSegment } from './patch/types';
51
- export { PatchEngine } from './patch/PatchEngine';
52
- export { canonical, NonSerializableInputError } from './shared/canonical';
53
- export { fnv1a64 } from './shared/hash';
54
- export { normalizeLiveInputs, resourceIdOf } from './shared/descriptor';
55
- export type { LiveDataOf, LiveDescriptor, LiveInputsOf } from './shared/descriptor';
56
- export type { OptimisticEntry, OptimisticList } from './client/optimistic';
57
-
58
- // Framework-free client adapter
59
- export { liveStore, liveStoreOf, liveIdentity, LiveSlot } from './client/vanilla';
60
- export type { LiveHandle } from './client/vanilla';
61
-
62
- export { WebSocketTransport, LadderTransport, PollingTransport, SseClientTransport, routeIndex } from './client/transport';
63
- export type { ClientTransport, TransportHandlers, EventSourceLike, RoutePath } from './client/transport';
64
-
65
- // Transports
66
- export { FanTransport } from './transport/FanTransport';
67
- export type { OwnedTransport } from './transport/FanTransport';
68
- export { SseTransport } from './transport/SseTransport';
69
- export type { SseTransportOptions } from './transport/SseTransport';
70
- export { createSseRoutes } from './transport/sse-routes';
71
- export type { SseRouteOptions } from './transport/sse-routes';
72
-
73
- // Conditional GET
74
- export { LiveETagMiddleware, pathMatcher } from './http/etag';
75
- export type { LivePollingGuard, LivePollingRequest, LiveRoutePath } from './http/etag';
76
-
77
- // First paint
78
- export { prefetchLive } from './resource/prefetch';
79
- export type { LivePayload } from './resource/prefetch';
80
- export { LiveRouteExecutionError } from './resource/route-executor';
81
- export { hydrationKey, toHydrateMap, readHydrationPayload, HYDRATION_ATTRIBUTE } from './client/hydrate';
1
+ import 'reflect-metadata';
2
+
3
+ // Decorator and metadata
4
+ export { Live } from './decorators/Live';
5
+ export { LIVE_META } from './metadata';
6
+ export type { LiveMeta, LiveOptions, LiveShared } from './metadata';
7
+
8
+ // Plugin and services
9
+ export { LivePlugin } from './LivePlugin';
10
+ export type { LivePluginOptions } from './LivePlugin';
11
+ export { closeLiveRuntime } from './runtime';
12
+ export { LiveService } from './LiveService';
13
+ export { LiveEngine } from './LiveEngine';
14
+ export type { LiveTransport, LiveStats } from './LiveEngine';
15
+
16
+ // Metrics
17
+ export { LiveMetrics } from './observability';
18
+ export type { MetricSink } from './observability';
19
+
20
+ // Configuration
21
+ export { DEFAULT_LIVE_CONFIG, resolveLiveConfig } from './config';
22
+ export type { LiveConfig } from './config';
23
+
24
+ // Scope
25
+ export { ConnectionScopeResolver } from './transport/scope-resolver';
26
+ export type { LiveHandshake, LiveScopeResolver } from './transport/scope-resolver';
27
+ export type { LiveExecutionContext, LiveInputs, LiveResourceExecutor, LiveScope } from './resource/types';
28
+ export { defaultScopeWarning } from './scope-warning';
29
+ export type { DefaultScopeWarningInput } from './scope-warning';
30
+
31
+ // Authorization
32
+ export { AllowAllAuthorizer, authKeysOf, isAuthKey } from './auth/authorizer';
33
+ export type { LiveAuthorizationRequest, LiveAuthorizer } from './auth/authorizer';
34
+
35
+ // Invalidation
36
+ export { InProcessBus } from './bus/InProcessBus';
37
+ export type { InvalidationBus, InvalidationHandler } from './bus/InvalidationBus';
38
+ export type { Dependency, InvalidationEvent } from './graph/types';
39
+ export { ancestorsOf, rowKey, tableKey } from './graph/dep-key';
40
+ export type { DepKey } from './graph/dep-key';
41
+ export { WriteDuringComputeError } from './emitters/AppEmitter';
42
+ export { PgNotifyBus, chunkEvents } from './bus/PgNotifyBus';
43
+ export type { PgNotifyBusOptions } from './bus/PgNotifyBus';
44
+ export { PgNotifyEmitter, eventsFromPayload } from './emitters/pg-notify-emitter';
45
+ export type { PgNotifyEmitterOptions, PgNotifyTable } from './emitters/pg-notify-emitter';
46
+ export { PgListener } from './emitters/pg-listener';
47
+ export type { ListenableSql, PgListenerOptions } from './emitters/pg-listener';
48
+ export { tableOfKey } from './graph/dep-key';
49
+
50
+ // Protocol and patches, shared with the client
51
+ export * from './shared/protocol';
52
+ export type { PatchOp, PathSegment } from './patch/types';
53
+ export { PatchEngine } from './patch/PatchEngine';
54
+ export { canonical, NonSerializableInputError } from './shared/canonical';
55
+ export { fnv1a64 } from './shared/hash';
56
+ export { normalizeLiveInputs, resourceIdOf } from './shared/descriptor';
57
+ export type { LiveDataOf, LiveDescriptor, LiveInputsOf } from './shared/descriptor';
58
+ export type { OptimisticEntry, OptimisticList } from './client/optimistic';
59
+
60
+ // Framework-free client adapter
61
+ export { liveStore, liveStoreOf, liveIdentity, LiveSlot } from './client/vanilla';
62
+ export type { LiveHandle } from './client/vanilla';
63
+
64
+ export { WebSocketTransport, LadderTransport, PollingTransport, SseClientTransport, routeIndex } from './client/transport';
65
+ export type { ClientTransport, TransportHandlers, EventSourceLike, RoutePath } from './client/transport';
66
+
67
+ // Transports
68
+ export { FanTransport } from './transport/FanTransport';
69
+ export type { OwnedTransport } from './transport/FanTransport';
70
+ export { SseTransport } from './transport/SseTransport';
71
+ export type { SseTransportOptions } from './transport/SseTransport';
72
+ export { createSseRoutes } from './transport/sse-routes';
73
+ export type { SseRouteOptions } from './transport/sse-routes';
74
+
75
+ // Conditional GET
76
+ export { LiveETagMiddleware, pathMatcher } from './http/etag';
77
+ export type { LivePollingGuard, LivePollingRequest, LiveRoutePath } from './http/etag';
78
+
79
+ // First paint
80
+ export { prefetchLive } from './resource/prefetch';
81
+ export type { LivePayload } from './resource/prefetch';
82
+ export { LiveRouteExecutionError } from './resource/route-executor';
83
+ export { hydrationKey, toHydrateMap, readHydrationPayload, HYDRATION_ATTRIBUTE } from './client/hydrate';