@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,27 @@
1
+ /**
2
+ * The SQL the Postgres emitter installs.
3
+ *
4
+ * It lives in its own module with no I/O so it can be asserted on without a
5
+ * database, and so the one place that concatenates identifiers into SQL is one
6
+ * short file you can read end to end.
7
+ */
8
+ export declare class UnsafeIdentifierError extends Error {
9
+ constructor(kind: string, value: string);
10
+ }
11
+ /** Every identifier interpolated into the DDL below passes through here. */
12
+ export declare function assertIdentifier(kind: string, value: string): string;
13
+ export declare const TRIGGER_FUNCTION_NAME = "carno_live_notify";
14
+ export declare function triggerNameOf(table: string): string;
15
+ /**
16
+ * The shared trigger function. Installed once; every table's trigger passes it
17
+ * the primary key column and the channel as arguments.
18
+ *
19
+ * Two decisions are baked in here rather than on the JavaScript side:
20
+ * an UPDATE whose jsonb diff is empty does not notify at all, because a write
21
+ * that changed nothing must not wake a single subscriber; and a payload over
22
+ * the ceiling degrades to the whole table instead of being truncated into
23
+ * something that would parse as a different row.
24
+ */
25
+ export declare function createFunctionSql(maxPayloadBytes: number): string;
26
+ export declare function createTriggerSql(table: string, primaryKey: string, channel: string): string;
27
+ export declare function dropTriggerSql(table: string): string;
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+ /**
3
+ * The SQL the Postgres emitter installs.
4
+ *
5
+ * It lives in its own module with no I/O so it can be asserted on without a
6
+ * database, and so the one place that concatenates identifiers into SQL is one
7
+ * short file you can read end to end.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.TRIGGER_FUNCTION_NAME = exports.UnsafeIdentifierError = void 0;
11
+ exports.assertIdentifier = assertIdentifier;
12
+ exports.triggerNameOf = triggerNameOf;
13
+ exports.createFunctionSql = createFunctionSql;
14
+ exports.createTriggerSql = createTriggerSql;
15
+ exports.dropTriggerSql = dropTriggerSql;
16
+ const BARE_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
17
+ class UnsafeIdentifierError extends Error {
18
+ constructor(kind, value) {
19
+ super(`Refusing to build SQL with an unsafe ${kind}: ${JSON.stringify(value)}. ` +
20
+ `Only bare names matching [A-Za-z_][A-Za-z0-9_]* are accepted.`);
21
+ this.name = 'UnsafeIdentifierError';
22
+ }
23
+ }
24
+ exports.UnsafeIdentifierError = UnsafeIdentifierError;
25
+ /** Every identifier interpolated into the DDL below passes through here. */
26
+ function assertIdentifier(kind, value) {
27
+ if (!BARE_IDENTIFIER.test(value)) {
28
+ throw new UnsafeIdentifierError(kind, value);
29
+ }
30
+ return value;
31
+ }
32
+ exports.TRIGGER_FUNCTION_NAME = 'carno_live_notify';
33
+ function triggerNameOf(table) {
34
+ return `carno_live_${assertIdentifier('table', table)}`;
35
+ }
36
+ /**
37
+ * The shared trigger function. Installed once; every table's trigger passes it
38
+ * the primary key column and the channel as arguments.
39
+ *
40
+ * Two decisions are baked in here rather than on the JavaScript side:
41
+ * an UPDATE whose jsonb diff is empty does not notify at all, because a write
42
+ * that changed nothing must not wake a single subscriber; and a payload over
43
+ * the ceiling degrades to the whole table instead of being truncated into
44
+ * something that would parse as a different row.
45
+ */
46
+ function createFunctionSql(maxPayloadBytes) {
47
+ if (!Number.isInteger(maxPayloadBytes) || maxPayloadBytes <= 0) {
48
+ throw new Error(`maxPayloadBytes must be a positive integer, got ${maxPayloadBytes}.`);
49
+ }
50
+ return `
51
+ CREATE OR REPLACE FUNCTION ${exports.TRIGGER_FUNCTION_NAME}() RETURNS trigger AS $carno$
52
+ DECLARE
53
+ pk_column text := TG_ARGV[0];
54
+ channel text := TG_ARGV[1];
55
+ new_row jsonb;
56
+ old_row jsonb;
57
+ changed text[];
58
+ row_id text;
59
+ payload text;
60
+ BEGIN
61
+ IF TG_OP = 'DELETE' THEN
62
+ new_row := to_jsonb(OLD);
63
+ ELSE
64
+ new_row := to_jsonb(NEW);
65
+ END IF;
66
+
67
+ row_id := new_row ->> pk_column;
68
+
69
+ IF TG_OP = 'UPDATE' THEN
70
+ old_row := to_jsonb(OLD);
71
+
72
+ SELECT array_agg(entry.key ORDER BY entry.key) INTO changed
73
+ FROM jsonb_each(new_row) AS entry
74
+ WHERE entry.value IS DISTINCT FROM (old_row -> entry.key);
75
+
76
+ IF changed IS NULL THEN
77
+ RETURN NULL;
78
+ END IF;
79
+ END IF;
80
+
81
+ payload := json_build_object('t', TG_TABLE_NAME, 'i', row_id, 'c', changed)::text;
82
+
83
+ IF octet_length(payload) > ${maxPayloadBytes} THEN
84
+ payload := json_build_object('t', TG_TABLE_NAME, 'i', NULL, 'c', NULL)::text;
85
+ END IF;
86
+
87
+ PERFORM pg_notify(channel, payload);
88
+ RETURN NULL;
89
+ END;
90
+ $carno$ LANGUAGE plpgsql;
91
+ `.trim();
92
+ }
93
+ function createTriggerSql(table, primaryKey, channel) {
94
+ const safeTable = assertIdentifier('table', table);
95
+ const safeKey = assertIdentifier('primary key column', primaryKey);
96
+ const safeChannel = assertIdentifier('channel', channel);
97
+ const trigger = triggerNameOf(safeTable);
98
+ return `
99
+ DROP TRIGGER IF EXISTS ${trigger} ON ${safeTable};
100
+ CREATE TRIGGER ${trigger}
101
+ AFTER INSERT OR UPDATE OR DELETE ON ${safeTable}
102
+ FOR EACH ROW EXECUTE FUNCTION ${exports.TRIGGER_FUNCTION_NAME}('${safeKey}', '${safeChannel}');
103
+ `.trim();
104
+ }
105
+ function dropTriggerSql(table) {
106
+ return `DROP TRIGGER IF EXISTS ${triggerNameOf(table)} ON ${assertIdentifier('table', table)};`;
107
+ }
@@ -0,0 +1,16 @@
1
+ import type { Statement } from '@carno.js/orm';
2
+ import type { Dependency, InvalidationEvent } from '../graph/types';
3
+ /**
4
+ * Turn the column list the ORM generated into bare column names.
5
+ * Returns null when the list is absent or contains an expression/star.
6
+ */
7
+ export declare function normalizeColumns(columns: string[] | undefined): string[] | null;
8
+ /**
9
+ * Extract primary-key values only when the WHERE clause is exactly a primary
10
+ * key equality or a primary-key IN list. Anything else degrades to the table.
11
+ */
12
+ export declare function extractRowIds(where: string | undefined, primaryKeyColumn: string | undefined): (string | number)[] | null;
13
+ /** Dependencies registered by one read. Empty for writes. */
14
+ export declare function readDependencies(statement: Statement<any>, maxKeysPerRead: number): Dependency[];
15
+ /** Invalidation events announced by one write. Empty for reads. */
16
+ export declare function writeEvents(statement: Statement<any>, maxKeysPerRead: number): InvalidationEvent[];
@@ -0,0 +1,182 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.normalizeColumns = normalizeColumns;
4
+ exports.extractRowIds = extractRowIds;
5
+ exports.readDependencies = readDependencies;
6
+ exports.writeEvents = writeEvents;
7
+ const dep_key_1 = require("../graph/dep-key");
8
+ const WRITE_STATEMENTS = new Set(['insert', 'update', 'delete']);
9
+ const READ_STATEMENTS = new Set(['select', 'count']);
10
+ /**
11
+ * Turn the column list the ORM generated into bare column names.
12
+ * Returns null when the list is absent or contains an expression/star.
13
+ */
14
+ function normalizeColumns(columns) {
15
+ if (!columns || columns.length === 0) {
16
+ return null;
17
+ }
18
+ const names = new Set();
19
+ for (const raw of columns) {
20
+ const beforeAlias = raw.split(/\s+as\s+/i)[0].trim();
21
+ const lastDot = beforeAlias.lastIndexOf('.');
22
+ const bare = (lastDot === -1 ? beforeAlias : beforeAlias.slice(lastDot + 1))
23
+ .replace(/["`\[\]]/g, '')
24
+ .trim();
25
+ if (bare === '' || bare === '*' || bare.includes('(')) {
26
+ return null;
27
+ }
28
+ names.add(bare);
29
+ }
30
+ return [...names].sort();
31
+ }
32
+ function unquote(value) {
33
+ return value.replace(/["`\[\]]/g, '');
34
+ }
35
+ /** SQL statements carry a qualified and quoted table; keys use the table only. */
36
+ function tableNameOf(table) {
37
+ const lastPart = table.trim().split('.').pop() ?? table;
38
+ return unquote(lastPart.trim());
39
+ }
40
+ function parseLiteral(raw) {
41
+ const trimmed = raw.trim();
42
+ if (trimmed.startsWith("'") && trimmed.endsWith("'")) {
43
+ return trimmed.slice(1, -1);
44
+ }
45
+ return Number(trimmed);
46
+ }
47
+ function escapeRegExp(value) {
48
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
49
+ }
50
+ function unwrapOuterParentheses(value) {
51
+ let current = value.trim();
52
+ while (current.startsWith('(') && current.endsWith(')')) {
53
+ let depth = 0;
54
+ let enclosesWholeExpression = true;
55
+ for (let index = 0; index < current.length; index++) {
56
+ if (current[index] === '(') {
57
+ depth++;
58
+ }
59
+ else if (current[index] === ')') {
60
+ depth--;
61
+ if (depth === 0 && index !== current.length - 1) {
62
+ enclosesWholeExpression = false;
63
+ break;
64
+ }
65
+ }
66
+ }
67
+ if (!enclosesWholeExpression || depth !== 0) {
68
+ break;
69
+ }
70
+ current = current.slice(1, -1).trim();
71
+ }
72
+ return current;
73
+ }
74
+ /**
75
+ * Extract primary-key values only when the WHERE clause is exactly a primary
76
+ * key equality or a primary-key IN list. Anything else degrades to the table.
77
+ */
78
+ function extractRowIds(where, primaryKeyColumn) {
79
+ if (!where || !primaryKeyColumn) {
80
+ return null;
81
+ }
82
+ const trimmed = unwrapOuterParentheses(where.trim());
83
+ const column = `(?:[\\w"\`\\[\\]]+\\.)?["\`\\[]?${escapeRegExp(primaryKeyColumn)}["\`\\]]?`;
84
+ const equality = new RegExp(`^${column}\\s*=\\s*(\\d+|'[^']*')$`, 'i').exec(trimmed);
85
+ if (equality) {
86
+ return [parseLiteral(equality[1])];
87
+ }
88
+ const inList = new RegExp(`^${column}\\s+IN\\s*\\(([^()]*)\\)$`, 'i').exec(trimmed);
89
+ if (inList) {
90
+ const items = inList[1].split(',').map(item => item.trim()).filter(item => item !== '');
91
+ if (items.length === 0 || items.some(item => !/^(\d+|'[^']*')$/.test(item))) {
92
+ return null;
93
+ }
94
+ return items.map(parseLiteral);
95
+ }
96
+ return null;
97
+ }
98
+ /** Dependencies registered by one read. Empty for writes. */
99
+ function readDependencies(statement, maxKeysPerRead) {
100
+ if (!READ_STATEMENTS.has(statement.statement ?? '') || !statement.table) {
101
+ return [];
102
+ }
103
+ const table = tableNameOf(statement.table);
104
+ const columns = normalizeColumns(statement.columns);
105
+ const ids = extractRowIds(statement.where, statement.primaryKeyColumnName);
106
+ const deps = [];
107
+ if (ids && ids.length <= maxKeysPerRead) {
108
+ for (const id of ids) {
109
+ deps.push({ key: (0, dep_key_1.rowKey)(table, id), columns });
110
+ }
111
+ }
112
+ else {
113
+ deps.push({ key: (0, dep_key_1.tableKey)(table), columns });
114
+ }
115
+ // A joined read depends on every joined table too. We cannot attribute the
116
+ // selected columns per table, so joins are wildcard.
117
+ for (const join of statement.join ?? []) {
118
+ if (join.joinTable) {
119
+ deps.push({ key: (0, dep_key_1.tableKey)(tableNameOf(join.joinTable)), columns: null });
120
+ }
121
+ }
122
+ // Select-strategy relations execute these child statements directly via
123
+ // SqlJoinManager, so they never pass through the ORM's root observer hook.
124
+ // Their filters depend on the root rows and are not stable primary-key
125
+ // predicates; track each child table as a wildcard instead.
126
+ for (const selectJoin of statement.selectJoin ?? []) {
127
+ if (selectJoin.table) {
128
+ deps.push({ key: (0, dep_key_1.tableKey)(tableNameOf(selectJoin.table)), columns: null });
129
+ }
130
+ }
131
+ return deps;
132
+ }
133
+ function writtenColumns(statement) {
134
+ if (statement.statement === 'delete') {
135
+ return null;
136
+ }
137
+ const values = statement.values;
138
+ if (!values) {
139
+ return null;
140
+ }
141
+ const rows = Array.isArray(values) ? values : [values];
142
+ const names = new Set();
143
+ for (const row of rows) {
144
+ for (const name of Object.keys(row)) {
145
+ names.add(unquote(name));
146
+ }
147
+ }
148
+ return names.size === 0 ? null : [...names].sort();
149
+ }
150
+ function insertedIds(statement) {
151
+ const pk = statement.primaryKeyColumnName;
152
+ if (!pk || !statement.values) {
153
+ return null;
154
+ }
155
+ const rows = Array.isArray(statement.values)
156
+ ? statement.values
157
+ : [statement.values];
158
+ const ids = [];
159
+ for (const row of rows) {
160
+ const value = row[pk];
161
+ if (typeof value !== 'string' && typeof value !== 'number') {
162
+ return null;
163
+ }
164
+ ids.push(value);
165
+ }
166
+ return ids.length === 0 ? null : ids;
167
+ }
168
+ /** Invalidation events announced by one write. Empty for reads. */
169
+ function writeEvents(statement, maxKeysPerRead) {
170
+ if (!WRITE_STATEMENTS.has(statement.statement ?? '') || !statement.table) {
171
+ return [];
172
+ }
173
+ const table = tableNameOf(statement.table);
174
+ const columns = writtenColumns(statement);
175
+ const ids = statement.statement === 'insert'
176
+ ? insertedIds(statement)
177
+ : extractRowIds(statement.where, statement.primaryKeyColumnName);
178
+ if (ids && ids.length <= maxKeysPerRead) {
179
+ return ids.map(id => ({ key: (0, dep_key_1.rowKey)(table, id), columns }));
180
+ }
181
+ return [{ key: (0, dep_key_1.tableKey)(table), columns }];
182
+ }
@@ -0,0 +1,27 @@
1
+ import type { Dependency, InvalidationEvent } from './types';
2
+ /**
3
+ * Key ↔ instance index with ancestor resolution and column filtering.
4
+ *
5
+ * Knows nothing about WebSocket, the ORM, or resources — it is a pure data
6
+ * structure, which is why the hard part of invalidation is testable without
7
+ * a server, a database or a socket.
8
+ */
9
+ export declare class DependencyGraph {
10
+ private readonly byKey;
11
+ private readonly byInstance;
12
+ /** Replace every dependency held by this instance. */
13
+ setDependencies(instanceId: string, deps: Dependency[]): void;
14
+ /** Forget the instance entirely. */
15
+ remove(instanceId: string): void;
16
+ /**
17
+ * Instances concerned by this write.
18
+ *
19
+ * Both directions of the hierarchy matter. A row write wakes table
20
+ * subscribers, while a table write wakes row subscribers because a
21
+ * predicate write may have touched that row.
22
+ */
23
+ resolve(event: InvalidationEvent): string[];
24
+ keyCount(): number;
25
+ instanceCount(): number;
26
+ private collect;
27
+ }
@@ -0,0 +1,118 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DependencyGraph = void 0;
4
+ const dep_key_1 = require("./dep-key");
5
+ /**
6
+ * Key ↔ instance index with ancestor resolution and column filtering.
7
+ *
8
+ * Knows nothing about WebSocket, the ORM, or resources — it is a pure data
9
+ * structure, which is why the hard part of invalidation is testable without
10
+ * a server, a database or a socket.
11
+ */
12
+ class DependencyGraph {
13
+ constructor() {
14
+ this.byKey = new Map();
15
+ this.byInstance = new Map();
16
+ }
17
+ /** Replace every dependency held by this instance. */
18
+ setDependencies(instanceId, deps) {
19
+ this.remove(instanceId);
20
+ if (deps.length === 0) {
21
+ return;
22
+ }
23
+ const keys = new Set();
24
+ for (const dep of deps) {
25
+ keys.add(dep.key);
26
+ let holders = this.byKey.get(dep.key);
27
+ if (!holders) {
28
+ holders = new Map();
29
+ this.byKey.set(dep.key, holders);
30
+ }
31
+ if (!holders.has(instanceId)) {
32
+ holders.set(instanceId, dep.columns === null ? null : new Set(dep.columns));
33
+ continue;
34
+ }
35
+ const existing = holders.get(instanceId);
36
+ if (existing === null) {
37
+ continue;
38
+ }
39
+ if (dep.columns === null) {
40
+ holders.set(instanceId, null);
41
+ continue;
42
+ }
43
+ for (const column of dep.columns) {
44
+ existing.add(column);
45
+ }
46
+ }
47
+ this.byInstance.set(instanceId, keys);
48
+ }
49
+ /** Forget the instance entirely. */
50
+ remove(instanceId) {
51
+ const keys = this.byInstance.get(instanceId);
52
+ if (!keys) {
53
+ return;
54
+ }
55
+ for (const key of keys) {
56
+ const holders = this.byKey.get(key);
57
+ if (!holders) {
58
+ continue;
59
+ }
60
+ holders.delete(instanceId);
61
+ if (holders.size === 0) {
62
+ this.byKey.delete(key);
63
+ }
64
+ }
65
+ this.byInstance.delete(instanceId);
66
+ }
67
+ /**
68
+ * Instances concerned by this write.
69
+ *
70
+ * Both directions of the hierarchy matter. A row write wakes table
71
+ * subscribers, while a table write wakes row subscribers because a
72
+ * predicate write may have touched that row.
73
+ */
74
+ resolve(event) {
75
+ const matched = new Set();
76
+ for (const key of (0, dep_key_1.ancestorsOf)(event.key)) {
77
+ this.collect(key, event.columns, matched);
78
+ }
79
+ const descendantPrefix = `${event.key}#`;
80
+ if (!event.key.includes('#')) {
81
+ for (const key of this.byKey.keys()) {
82
+ if (key.startsWith(descendantPrefix)) {
83
+ this.collect(key, event.columns, matched);
84
+ }
85
+ }
86
+ }
87
+ return [...matched];
88
+ }
89
+ keyCount() {
90
+ return this.byKey.size;
91
+ }
92
+ instanceCount() {
93
+ return this.byInstance.size;
94
+ }
95
+ collect(key, writtenColumns, into) {
96
+ const holders = this.byKey.get(key);
97
+ if (!holders) {
98
+ return;
99
+ }
100
+ for (const [instanceId, readColumns] of holders) {
101
+ if (intersects(readColumns, writtenColumns)) {
102
+ into.add(instanceId);
103
+ }
104
+ }
105
+ }
106
+ }
107
+ exports.DependencyGraph = DependencyGraph;
108
+ function intersects(readColumns, writtenColumns) {
109
+ if (readColumns === null || writtenColumns === null) {
110
+ return true;
111
+ }
112
+ for (const column of writtenColumns) {
113
+ if (readColumns.has(column)) {
114
+ return true;
115
+ }
116
+ }
117
+ return false;
118
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Instance ↔ connection index with per-pair refcounting.
3
+ *
4
+ * The refcount is per (connection, instance) pair because one page can mount
5
+ * two components bound to the same resource with the same inputs: unmounting
6
+ * one must not unsubscribe the other.
7
+ */
8
+ export declare class SubscriptionRegistry {
9
+ private readonly byInstance;
10
+ private readonly byConnection;
11
+ /** Returns the connection's refcount on this instance after the call. */
12
+ subscribe(connectionId: string, instanceId: string): number;
13
+ /** Returns the connection's refcount on this instance after the call. */
14
+ unsubscribe(connectionId: string, instanceId: string): number;
15
+ /** Drop the connection; returns instances now left with no subscriber. */
16
+ dropConnection(connectionId: string): string[];
17
+ connectionsOf(instanceId: string): string[];
18
+ hasSubscribers(instanceId: string): boolean;
19
+ /** Distinct instances held by the connection — the per-connection ceiling. */
20
+ countForConnection(connectionId: string): number;
21
+ instanceCount(): number;
22
+ }
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SubscriptionRegistry = void 0;
4
+ /**
5
+ * Instance ↔ connection index with per-pair refcounting.
6
+ *
7
+ * The refcount is per (connection, instance) pair because one page can mount
8
+ * two components bound to the same resource with the same inputs: unmounting
9
+ * one must not unsubscribe the other.
10
+ */
11
+ class SubscriptionRegistry {
12
+ constructor() {
13
+ this.byInstance = new Map();
14
+ this.byConnection = new Map();
15
+ }
16
+ /** Returns the connection's refcount on this instance after the call. */
17
+ subscribe(connectionId, instanceId) {
18
+ let holders = this.byInstance.get(instanceId);
19
+ if (!holders) {
20
+ holders = new Map();
21
+ this.byInstance.set(instanceId, holders);
22
+ }
23
+ const next = (holders.get(connectionId) ?? 0) + 1;
24
+ holders.set(connectionId, next);
25
+ let owned = this.byConnection.get(connectionId);
26
+ if (!owned) {
27
+ owned = new Set();
28
+ this.byConnection.set(connectionId, owned);
29
+ }
30
+ owned.add(instanceId);
31
+ return next;
32
+ }
33
+ /** Returns the connection's refcount on this instance after the call. */
34
+ unsubscribe(connectionId, instanceId) {
35
+ const holders = this.byInstance.get(instanceId);
36
+ const current = holders?.get(connectionId) ?? 0;
37
+ if (!holders || current === 0) {
38
+ return 0;
39
+ }
40
+ const next = current - 1;
41
+ if (next === 0) {
42
+ holders.delete(connectionId);
43
+ const owned = this.byConnection.get(connectionId);
44
+ owned?.delete(instanceId);
45
+ if (owned?.size === 0) {
46
+ this.byConnection.delete(connectionId);
47
+ }
48
+ if (holders.size === 0) {
49
+ this.byInstance.delete(instanceId);
50
+ }
51
+ }
52
+ else {
53
+ holders.set(connectionId, next);
54
+ }
55
+ return next;
56
+ }
57
+ /** Drop the connection; returns instances now left with no subscriber. */
58
+ dropConnection(connectionId) {
59
+ const owned = this.byConnection.get(connectionId);
60
+ if (!owned) {
61
+ return [];
62
+ }
63
+ const orphaned = [];
64
+ for (const instanceId of owned) {
65
+ const holders = this.byInstance.get(instanceId);
66
+ if (!holders) {
67
+ continue;
68
+ }
69
+ holders.delete(connectionId);
70
+ if (holders.size === 0) {
71
+ this.byInstance.delete(instanceId);
72
+ orphaned.push(instanceId);
73
+ }
74
+ }
75
+ this.byConnection.delete(connectionId);
76
+ return orphaned;
77
+ }
78
+ connectionsOf(instanceId) {
79
+ return [...(this.byInstance.get(instanceId)?.keys() ?? [])];
80
+ }
81
+ hasSubscribers(instanceId) {
82
+ return this.byInstance.has(instanceId);
83
+ }
84
+ /** Distinct instances held by the connection — the per-connection ceiling. */
85
+ countForConnection(connectionId) {
86
+ return this.byConnection.get(connectionId)?.size ?? 0;
87
+ }
88
+ instanceCount() {
89
+ return this.byInstance.size;
90
+ }
91
+ }
92
+ exports.SubscriptionRegistry = SubscriptionRegistry;
@@ -0,0 +1,14 @@
1
+ export type DepKey = string;
2
+ /** Key naming a whole table: every row and every column of it. */
3
+ export declare function tableKey(table: string): DepKey;
4
+ /** Key naming one row by primary key. */
5
+ export declare function rowKey(table: string, id: string | number): DepKey;
6
+ /**
7
+ * The key itself plus every key that contains it.
8
+ *
9
+ * The hierarchy is exactly two levels deep and the only separator is `#`:
10
+ * `orm:users#42` is contained by `orm:users`. Colons are not hierarchical.
11
+ */
12
+ export declare function ancestorsOf(key: DepKey): DepKey[];
13
+ /** The table an ORM key names, or null for a key from another namespace. */
14
+ export declare function tableOfKey(key: DepKey): string | null;
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.tableKey = tableKey;
4
+ exports.rowKey = rowKey;
5
+ exports.ancestorsOf = ancestorsOf;
6
+ exports.tableOfKey = tableOfKey;
7
+ const ROW_SEPARATOR = '#';
8
+ /** Key naming a whole table: every row and every column of it. */
9
+ function tableKey(table) {
10
+ return `orm:${table}`;
11
+ }
12
+ /** Key naming one row by primary key. */
13
+ function rowKey(table, id) {
14
+ return `orm:${table}${ROW_SEPARATOR}${id}`;
15
+ }
16
+ /**
17
+ * The key itself plus every key that contains it.
18
+ *
19
+ * The hierarchy is exactly two levels deep and the only separator is `#`:
20
+ * `orm:users#42` is contained by `orm:users`. Colons are not hierarchical.
21
+ */
22
+ function ancestorsOf(key) {
23
+ const index = key.indexOf(ROW_SEPARATOR);
24
+ if (index === -1) {
25
+ return [key];
26
+ }
27
+ return [key, key.slice(0, index)];
28
+ }
29
+ /** The table an ORM key names, or null for a key from another namespace. */
30
+ function tableOfKey(key) {
31
+ if (!key.startsWith('orm:')) {
32
+ return null;
33
+ }
34
+ const rest = key.slice('orm:'.length);
35
+ const separator = rest.indexOf(ROW_SEPARATOR);
36
+ return separator === -1 ? rest : rest.slice(0, separator);
37
+ }
@@ -0,0 +1,13 @@
1
+ import type { DepKey } from './dep-key';
2
+ /** A read registered by one resource compute. */
3
+ export interface Dependency {
4
+ key: DepKey;
5
+ /** Null is a wildcard for an unenumerated column set. */
6
+ columns: string[] | null;
7
+ }
8
+ /** A write announced by an invalidation emitter. */
9
+ export interface InvalidationEvent {
10
+ key: DepKey;
11
+ /** Null is a wildcard for a whole-row write. */
12
+ columns: string[] | null;
13
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });