@terreno/api 0.13.2 → 0.14.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 (175) hide show
  1. package/dist/__tests__/versionCheckPlugin.test.js +53 -3
  2. package/dist/api.arrayOperations.test.js +1 -0
  3. package/dist/api.asyncHandler.test.d.ts +1 -0
  4. package/dist/api.asyncHandler.test.js +236 -0
  5. package/dist/api.d.ts +15 -4
  6. package/dist/api.errors.test.js +1 -0
  7. package/dist/api.hooks.test.js +1 -0
  8. package/dist/api.js +153 -104
  9. package/dist/api.query.test.js +1 -0
  10. package/dist/api.test.js +174 -0
  11. package/dist/auth.d.ts +10 -5
  12. package/dist/auth.js +163 -90
  13. package/dist/auth.test.js +159 -0
  14. package/dist/betterAuthApp.test.js +1 -0
  15. package/dist/betterAuthSetup.d.ts +5 -6
  16. package/dist/betterAuthSetup.js +17 -14
  17. package/dist/betterAuthSetup.test.js +1 -0
  18. package/dist/config.d.ts +48 -0
  19. package/dist/config.js +248 -0
  20. package/dist/config.test.d.ts +1 -0
  21. package/dist/config.test.js +328 -0
  22. package/dist/configuration.test.js +1 -0
  23. package/dist/configurationApp.d.ts +1 -1
  24. package/dist/configurationApp.js +17 -13
  25. package/dist/configurationPlugin.test.js +1 -0
  26. package/dist/consentApp.test.js +1 -0
  27. package/dist/envConfigurationPlugin.d.ts +2 -0
  28. package/dist/envConfigurationPlugin.js +173 -0
  29. package/dist/envConfigurationPlugin.test.d.ts +1 -0
  30. package/dist/envConfigurationPlugin.test.js +322 -0
  31. package/dist/errors.d.ts +18 -7
  32. package/dist/errors.js +106 -10
  33. package/dist/errors.test.js +16 -1
  34. package/dist/example.js +16 -7
  35. package/dist/expressServer.d.ts +10 -9
  36. package/dist/expressServer.js +62 -53
  37. package/dist/expressServer.test.js +53 -2
  38. package/dist/githubAuth.d.ts +2 -1
  39. package/dist/githubAuth.js +41 -26
  40. package/dist/githubAuth.test.js +1 -0
  41. package/dist/index.d.ts +4 -0
  42. package/dist/index.js +4 -0
  43. package/dist/logger.d.ts +1 -1
  44. package/dist/logger.js +42 -20
  45. package/dist/models/versionConfig.d.ts +2 -0
  46. package/dist/models/versionConfig.js +8 -0
  47. package/dist/notifiers/googleChatNotifier.js +14 -16
  48. package/dist/notifiers/googleChatNotifier.test.js +1 -0
  49. package/dist/notifiers/slackNotifier.js +16 -14
  50. package/dist/notifiers/slackNotifier.test.js +41 -3
  51. package/dist/notifiers/zoomNotifier.js +7 -10
  52. package/dist/notifiers/zoomNotifier.test.js +1 -0
  53. package/dist/openApi.d.ts +1 -1
  54. package/dist/openApi.test.js +1 -0
  55. package/dist/openApiBuilder.d.ts +39 -6
  56. package/dist/openApiBuilder.js +1 -31
  57. package/dist/openApiBuilder.test.js +1 -0
  58. package/dist/openApiValidator.js +1 -0
  59. package/dist/openApiValidator.test.js +65 -0
  60. package/dist/permissions.d.ts +4 -4
  61. package/dist/permissions.js +67 -65
  62. package/dist/permissions.middleware.test.js +1 -0
  63. package/dist/permissions.test.js +1 -0
  64. package/dist/plugins.d.ts +5 -5
  65. package/dist/plugins.js +18 -9
  66. package/dist/plugins.test.js +1 -1
  67. package/dist/populate.d.ts +15 -8
  68. package/dist/populate.js +23 -24
  69. package/dist/populate.test.js +1 -0
  70. package/dist/realtime/changeStreamWatcher.d.ts +73 -0
  71. package/dist/realtime/changeStreamWatcher.js +720 -0
  72. package/dist/realtime/index.d.ts +6 -0
  73. package/dist/realtime/index.js +27 -0
  74. package/dist/realtime/queryMatcher.d.ts +14 -0
  75. package/dist/realtime/queryMatcher.js +250 -0
  76. package/dist/realtime/queryStore.d.ts +37 -0
  77. package/dist/realtime/queryStore.js +195 -0
  78. package/dist/realtime/realtime.test.d.ts +10 -0
  79. package/dist/realtime/realtime.test.js +2158 -0
  80. package/dist/realtime/realtimeApp.d.ts +93 -0
  81. package/dist/realtime/realtimeApp.js +560 -0
  82. package/dist/realtime/registry.d.ts +40 -0
  83. package/dist/realtime/registry.js +38 -0
  84. package/dist/realtime/socketUser.d.ts +10 -0
  85. package/dist/realtime/socketUser.js +17 -0
  86. package/dist/realtime/types.d.ts +100 -0
  87. package/dist/realtime/types.js +2 -0
  88. package/dist/requestContext.d.ts +37 -0
  89. package/dist/requestContext.js +344 -0
  90. package/dist/requestContext.test.d.ts +1 -0
  91. package/dist/requestContext.test.js +241 -0
  92. package/dist/terrenoApp.d.ts +8 -0
  93. package/dist/terrenoApp.js +50 -13
  94. package/dist/terrenoApp.test.js +194 -21
  95. package/dist/terrenoPlugin.d.ts +11 -0
  96. package/dist/tests/bunSetup.js +1 -0
  97. package/dist/tests.js +1 -1
  98. package/dist/transformers.d.ts +2 -2
  99. package/dist/transformers.js +5 -3
  100. package/dist/transformers.test.js +90 -0
  101. package/dist/types/consentResponse.d.ts +6 -3
  102. package/dist/versionCheckPlugin.d.ts +2 -0
  103. package/dist/versionCheckPlugin.js +18 -12
  104. package/package.json +4 -2
  105. package/src/__tests__/versionCheckPlugin.test.ts +37 -3
  106. package/src/api.arrayOperations.test.ts +1 -0
  107. package/src/api.asyncHandler.test.ts +177 -0
  108. package/src/api.errors.test.ts +1 -0
  109. package/src/api.hooks.test.ts +1 -0
  110. package/src/api.query.test.ts +1 -0
  111. package/src/api.test.ts +132 -0
  112. package/src/api.ts +199 -84
  113. package/src/auth.test.ts +160 -0
  114. package/src/auth.ts +120 -50
  115. package/src/betterAuthApp.test.ts +1 -0
  116. package/src/betterAuthSetup.test.ts +1 -0
  117. package/src/betterAuthSetup.ts +46 -19
  118. package/src/config.test.ts +255 -0
  119. package/src/config.ts +206 -0
  120. package/src/configuration.test.ts +1 -0
  121. package/src/configurationApp.ts +59 -24
  122. package/src/configurationPlugin.test.ts +1 -0
  123. package/src/consentApp.test.ts +1 -0
  124. package/src/envConfigurationPlugin.test.ts +143 -0
  125. package/src/envConfigurationPlugin.ts +100 -0
  126. package/src/errors.test.ts +19 -1
  127. package/src/errors.ts +94 -20
  128. package/src/example.ts +46 -21
  129. package/src/express.d.ts +18 -1
  130. package/src/expressServer.test.ts +50 -2
  131. package/src/expressServer.ts +80 -50
  132. package/src/githubAuth.test.ts +1 -0
  133. package/src/githubAuth.ts +59 -38
  134. package/src/index.ts +4 -0
  135. package/src/logger.ts +47 -17
  136. package/src/models/versionConfig.ts +13 -2
  137. package/src/notifiers/googleChatNotifier.test.ts +1 -0
  138. package/src/notifiers/googleChatNotifier.ts +7 -9
  139. package/src/notifiers/slackNotifier.test.ts +29 -3
  140. package/src/notifiers/slackNotifier.ts +9 -7
  141. package/src/notifiers/zoomNotifier.test.ts +1 -0
  142. package/src/notifiers/zoomNotifier.ts +8 -11
  143. package/src/openApi.test.ts +1 -0
  144. package/src/openApi.ts +4 -4
  145. package/src/openApiBuilder.test.ts +1 -0
  146. package/src/openApiBuilder.ts +14 -11
  147. package/src/openApiValidator.test.ts +59 -0
  148. package/src/openApiValidator.ts +3 -2
  149. package/src/permissions.middleware.test.ts +1 -0
  150. package/src/permissions.test.ts +1 -0
  151. package/src/permissions.ts +30 -25
  152. package/src/plugins.test.ts +1 -1
  153. package/src/plugins.ts +21 -14
  154. package/src/populate.test.ts +1 -0
  155. package/src/populate.ts +44 -36
  156. package/src/realtime/changeStreamWatcher.ts +568 -0
  157. package/src/realtime/index.ts +34 -0
  158. package/src/realtime/queryMatcher.ts +179 -0
  159. package/src/realtime/queryStore.ts +132 -0
  160. package/src/realtime/realtime.test.ts +1755 -0
  161. package/src/realtime/realtimeApp.ts +478 -0
  162. package/src/realtime/registry.ts +64 -0
  163. package/src/realtime/socketUser.ts +25 -0
  164. package/src/realtime/types.ts +112 -0
  165. package/src/requestContext.test.ts +196 -0
  166. package/src/requestContext.ts +368 -0
  167. package/src/terrenoApp.test.ts +137 -11
  168. package/src/terrenoApp.ts +64 -17
  169. package/src/terrenoPlugin.ts +12 -0
  170. package/src/tests/bunSetup.ts +1 -0
  171. package/src/tests.ts +7 -2
  172. package/src/transformers.test.ts +70 -2
  173. package/src/transformers.ts +15 -7
  174. package/src/types/consentResponse.ts +8 -10
  175. package/src/versionCheckPlugin.ts +15 -7
@@ -0,0 +1,2158 @@
1
+ "use strict";
2
+ // biome-ignore-all lint/suspicious/noExplicitAny: test mocks use dynamic shapes for registry entries and documents
3
+ /**
4
+ * Tests for the realtime module's pure functions and classes:
5
+ * - queryMatcher.ts (matchesQuery)
6
+ * - queryStore.ts (addQuerySubscription, removeQuerySubscription, etc.)
7
+ * - registry.ts (registerRealtime, getRealtimeRegistry, etc.)
8
+ * - realtimeApp.ts (RealtimeApp class — register, getIo, close)
9
+ * - realtimeApp.ts (installRealtimeSocketHandlers — permission and rate-limit logic)
10
+ * - changeStreamWatcher.ts (serializeDoc — responseHandler fallback)
11
+ */
12
+ var __assign = (this && this.__assign) || function () {
13
+ __assign = Object.assign || function(t) {
14
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
15
+ s = arguments[i];
16
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
17
+ t[p] = s[p];
18
+ }
19
+ return t;
20
+ };
21
+ return __assign.apply(this, arguments);
22
+ };
23
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
24
+ if (k2 === undefined) k2 = k;
25
+ var desc = Object.getOwnPropertyDescriptor(m, k);
26
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
27
+ desc = { enumerable: true, get: function() { return m[k]; } };
28
+ }
29
+ Object.defineProperty(o, k2, desc);
30
+ }) : (function(o, m, k, k2) {
31
+ if (k2 === undefined) k2 = k;
32
+ o[k2] = m[k];
33
+ }));
34
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
35
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
36
+ }) : function(o, v) {
37
+ o["default"] = v;
38
+ });
39
+ var __importStar = (this && this.__importStar) || (function () {
40
+ var ownKeys = function(o) {
41
+ ownKeys = Object.getOwnPropertyNames || function (o) {
42
+ var ar = [];
43
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
44
+ return ar;
45
+ };
46
+ return ownKeys(o);
47
+ };
48
+ return function (mod) {
49
+ if (mod && mod.__esModule) return mod;
50
+ var result = {};
51
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
52
+ __setModuleDefault(result, mod);
53
+ return result;
54
+ };
55
+ })();
56
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
57
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
58
+ return new (P || (P = Promise))(function (resolve, reject) {
59
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
60
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
61
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
62
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
63
+ });
64
+ };
65
+ var __generator = (this && this.__generator) || function (thisArg, body) {
66
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
67
+ return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
68
+ function verb(n) { return function (v) { return step([n, v]); }; }
69
+ function step(op) {
70
+ if (f) throw new TypeError("Generator is already executing.");
71
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
72
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
73
+ if (y = 0, t) op = [op[0] & 2, t.value];
74
+ switch (op[0]) {
75
+ case 0: case 1: t = op; break;
76
+ case 4: _.label++; return { value: op[1], done: false };
77
+ case 5: _.label++; y = op[1]; op = [0]; continue;
78
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
79
+ default:
80
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
81
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
82
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
83
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
84
+ if (t[2]) _.ops.pop();
85
+ _.trys.pop(); continue;
86
+ }
87
+ op = body.call(thisArg, _);
88
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
89
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
90
+ }
91
+ };
92
+ var __rest = (this && this.__rest) || function (s, e) {
93
+ var t = {};
94
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
95
+ t[p] = s[p];
96
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
97
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
98
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
99
+ t[p[i]] = s[p[i]];
100
+ }
101
+ return t;
102
+ };
103
+ var __read = (this && this.__read) || function (o, n) {
104
+ var m = typeof Symbol === "function" && o[Symbol.iterator];
105
+ if (!m) return o;
106
+ var i = m.call(o), r, ar = [], e;
107
+ try {
108
+ while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
109
+ }
110
+ catch (error) { e = { error: error }; }
111
+ finally {
112
+ try {
113
+ if (r && !r.done && (m = i["return"])) m.call(i);
114
+ }
115
+ finally { if (e) throw e.error; }
116
+ }
117
+ return ar;
118
+ };
119
+ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
120
+ if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
121
+ if (ar || !(i in from)) {
122
+ if (!ar) ar = Array.prototype.slice.call(from, 0, i);
123
+ ar[i] = from[i];
124
+ }
125
+ }
126
+ return to.concat(ar || Array.prototype.slice.call(from));
127
+ };
128
+ var __importDefault = (this && this.__importDefault) || function (mod) {
129
+ return (mod && mod.__esModule) ? mod : { "default": mod };
130
+ };
131
+ Object.defineProperty(exports, "__esModule", { value: true });
132
+ var bun_test_1 = require("bun:test");
133
+ var express_1 = __importDefault(require("express"));
134
+ var changeStreamWatcher_1 = require("./changeStreamWatcher");
135
+ var queryMatcher_1 = require("./queryMatcher");
136
+ var queryStore_1 = require("./queryStore");
137
+ var realtimeApp_1 = require("./realtimeApp");
138
+ var registry_1 = require("./registry");
139
+ // ─────────────────────────────────────────────────────────────────────────────
140
+ // queryMatcher tests
141
+ // ─────────────────────────────────────────────────────────────────────────────
142
+ (0, bun_test_1.describe)("matchesQuery", function () {
143
+ (0, bun_test_1.describe)("direct equality", function () {
144
+ (0, bun_test_1.it)("matches string equality", function () {
145
+ (0, bun_test_1.expect)((0, queryMatcher_1.matchesQuery)({ name: "Alice" }, { name: "Alice" })).toBe(true);
146
+ });
147
+ (0, bun_test_1.it)("returns false for non-matching string", function () {
148
+ (0, bun_test_1.expect)((0, queryMatcher_1.matchesQuery)({ name: "Alice" }, { name: "Bob" })).toBe(false);
149
+ });
150
+ (0, bun_test_1.it)("matches numeric equality", function () {
151
+ (0, bun_test_1.expect)((0, queryMatcher_1.matchesQuery)({ count: 5 }, { count: 5 })).toBe(true);
152
+ });
153
+ (0, bun_test_1.it)("returns false for non-matching number", function () {
154
+ (0, bun_test_1.expect)((0, queryMatcher_1.matchesQuery)({ count: 5 }, { count: 6 })).toBe(false);
155
+ });
156
+ (0, bun_test_1.it)("matches boolean true", function () {
157
+ (0, bun_test_1.expect)((0, queryMatcher_1.matchesQuery)({ active: true }, { active: true })).toBe(true);
158
+ });
159
+ (0, bun_test_1.it)("returns false for non-matching boolean", function () {
160
+ (0, bun_test_1.expect)((0, queryMatcher_1.matchesQuery)({ active: true }, { active: false })).toBe(false);
161
+ });
162
+ (0, bun_test_1.it)("matches null value", function () {
163
+ (0, bun_test_1.expect)((0, queryMatcher_1.matchesQuery)({ deleted: null }, { deleted: null })).toBe(true);
164
+ });
165
+ (0, bun_test_1.it)("matches undefined/missing field", function () {
166
+ (0, bun_test_1.expect)((0, queryMatcher_1.matchesQuery)({}, { deleted: undefined })).toBe(true);
167
+ });
168
+ });
169
+ (0, bun_test_1.describe)("nested field access", function () {
170
+ (0, bun_test_1.it)("accesses nested fields via dot notation", function () {
171
+ var doc = { user: { age: 30, name: "Alice" } };
172
+ (0, bun_test_1.expect)((0, queryMatcher_1.matchesQuery)(doc, { "user.name": "Alice" })).toBe(true);
173
+ });
174
+ (0, bun_test_1.it)("returns false when nested path doesn't match", function () {
175
+ var doc = { user: { name: "Alice" } };
176
+ (0, bun_test_1.expect)((0, queryMatcher_1.matchesQuery)(doc, { "user.name": "Bob" })).toBe(false);
177
+ });
178
+ (0, bun_test_1.it)("returns false when nested path is undefined", function () {
179
+ var doc = { user: {} };
180
+ (0, bun_test_1.expect)((0, queryMatcher_1.matchesQuery)(doc, { "user.name": "Alice" })).toBe(false);
181
+ });
182
+ });
183
+ (0, bun_test_1.describe)("$eq operator", function () {
184
+ (0, bun_test_1.it)("matches with $eq", function () {
185
+ (0, bun_test_1.expect)((0, queryMatcher_1.matchesQuery)({ count: 5 }, { count: { $eq: 5 } })).toBe(true);
186
+ });
187
+ (0, bun_test_1.it)("returns false with $eq mismatch", function () {
188
+ (0, bun_test_1.expect)((0, queryMatcher_1.matchesQuery)({ count: 5 }, { count: { $eq: 6 } })).toBe(false);
189
+ });
190
+ });
191
+ (0, bun_test_1.describe)("$ne operator", function () {
192
+ (0, bun_test_1.it)("matches when value is not equal", function () {
193
+ (0, bun_test_1.expect)((0, queryMatcher_1.matchesQuery)({ status: "active" }, { status: { $ne: "inactive" } })).toBe(true);
194
+ });
195
+ (0, bun_test_1.it)("returns false when value equals $ne operand", function () {
196
+ (0, bun_test_1.expect)((0, queryMatcher_1.matchesQuery)({ status: "active" }, { status: { $ne: "active" } })).toBe(false);
197
+ });
198
+ });
199
+ (0, bun_test_1.describe)("$gt, $gte operators", function () {
200
+ (0, bun_test_1.it)("matches $gt", function () {
201
+ (0, bun_test_1.expect)((0, queryMatcher_1.matchesQuery)({ score: 10 }, { score: { $gt: 5 } })).toBe(true);
202
+ });
203
+ (0, bun_test_1.it)("returns false when not $gt", function () {
204
+ (0, bun_test_1.expect)((0, queryMatcher_1.matchesQuery)({ score: 5 }, { score: { $gt: 5 } })).toBe(false);
205
+ });
206
+ (0, bun_test_1.it)("matches $gte for equal value", function () {
207
+ (0, bun_test_1.expect)((0, queryMatcher_1.matchesQuery)({ score: 5 }, { score: { $gte: 5 } })).toBe(true);
208
+ });
209
+ (0, bun_test_1.it)("matches $gte for greater value", function () {
210
+ (0, bun_test_1.expect)((0, queryMatcher_1.matchesQuery)({ score: 6 }, { score: { $gte: 5 } })).toBe(true);
211
+ });
212
+ (0, bun_test_1.it)("returns false when not $gte", function () {
213
+ (0, bun_test_1.expect)((0, queryMatcher_1.matchesQuery)({ score: 4 }, { score: { $gte: 5 } })).toBe(false);
214
+ });
215
+ });
216
+ (0, bun_test_1.describe)("$lt, $lte operators", function () {
217
+ (0, bun_test_1.it)("matches $lt", function () {
218
+ (0, bun_test_1.expect)((0, queryMatcher_1.matchesQuery)({ score: 3 }, { score: { $lt: 5 } })).toBe(true);
219
+ });
220
+ (0, bun_test_1.it)("returns false when not $lt", function () {
221
+ (0, bun_test_1.expect)((0, queryMatcher_1.matchesQuery)({ score: 5 }, { score: { $lt: 5 } })).toBe(false);
222
+ });
223
+ (0, bun_test_1.it)("matches $lte for equal value", function () {
224
+ (0, bun_test_1.expect)((0, queryMatcher_1.matchesQuery)({ score: 5 }, { score: { $lte: 5 } })).toBe(true);
225
+ });
226
+ (0, bun_test_1.it)("matches $lte for lesser value", function () {
227
+ (0, bun_test_1.expect)((0, queryMatcher_1.matchesQuery)({ score: 4 }, { score: { $lte: 5 } })).toBe(true);
228
+ });
229
+ (0, bun_test_1.it)("returns false when not $lte", function () {
230
+ (0, bun_test_1.expect)((0, queryMatcher_1.matchesQuery)({ score: 6 }, { score: { $lte: 5 } })).toBe(false);
231
+ });
232
+ });
233
+ (0, bun_test_1.describe)("$in operator", function () {
234
+ (0, bun_test_1.it)("matches when value is in array", function () {
235
+ (0, bun_test_1.expect)((0, queryMatcher_1.matchesQuery)({ status: "active" }, { status: { $in: ["active", "pending"] } })).toBe(true);
236
+ });
237
+ (0, bun_test_1.it)("returns false when value is not in array", function () {
238
+ (0, bun_test_1.expect)((0, queryMatcher_1.matchesQuery)({ status: "inactive" }, { status: { $in: ["active", "pending"] } })).toBe(false);
239
+ });
240
+ (0, bun_test_1.it)("returns false when operand is not an array", function () {
241
+ (0, bun_test_1.expect)((0, queryMatcher_1.matchesQuery)({ status: "active" }, { status: { $in: "active" } })).toBe(false);
242
+ });
243
+ });
244
+ (0, bun_test_1.describe)("$nin operator", function () {
245
+ (0, bun_test_1.it)("matches when value is not in array", function () {
246
+ (0, bun_test_1.expect)((0, queryMatcher_1.matchesQuery)({ status: "inactive" }, { status: { $nin: ["active", "pending"] } })).toBe(true);
247
+ });
248
+ (0, bun_test_1.it)("returns false when value is in array", function () {
249
+ (0, bun_test_1.expect)((0, queryMatcher_1.matchesQuery)({ status: "active" }, { status: { $nin: ["active", "pending"] } })).toBe(false);
250
+ });
251
+ (0, bun_test_1.it)("returns false when operand is not an array", function () {
252
+ (0, bun_test_1.expect)((0, queryMatcher_1.matchesQuery)({ status: "active" }, { status: { $nin: "active" } })).toBe(false);
253
+ });
254
+ });
255
+ (0, bun_test_1.describe)("$exists operator", function () {
256
+ (0, bun_test_1.it)("matches when field exists and $exists is true", function () {
257
+ (0, bun_test_1.expect)((0, queryMatcher_1.matchesQuery)({ name: "Alice" }, { name: { $exists: true } })).toBe(true);
258
+ });
259
+ (0, bun_test_1.it)("returns false when field is missing and $exists is true", function () {
260
+ (0, bun_test_1.expect)((0, queryMatcher_1.matchesQuery)({}, { name: { $exists: true } })).toBe(false);
261
+ });
262
+ (0, bun_test_1.it)("matches when field is missing and $exists is false", function () {
263
+ (0, bun_test_1.expect)((0, queryMatcher_1.matchesQuery)({}, { name: { $exists: false } })).toBe(true);
264
+ });
265
+ (0, bun_test_1.it)("returns false when field exists and $exists is false", function () {
266
+ (0, bun_test_1.expect)((0, queryMatcher_1.matchesQuery)({ name: "Alice" }, { name: { $exists: false } })).toBe(false);
267
+ });
268
+ });
269
+ (0, bun_test_1.describe)("$not operator", function () {
270
+ (0, bun_test_1.it)("negates a condition with $not", function () {
271
+ (0, bun_test_1.expect)((0, queryMatcher_1.matchesQuery)({ count: 5 }, { count: { $not: { $gt: 10 } } })).toBe(true);
272
+ });
273
+ (0, bun_test_1.it)("returns false when negated condition matches", function () {
274
+ (0, bun_test_1.expect)((0, queryMatcher_1.matchesQuery)({ count: 15 }, { count: { $not: { $gt: 10 } } })).toBe(false);
275
+ });
276
+ });
277
+ (0, bun_test_1.describe)("$and operator", function () {
278
+ (0, bun_test_1.it)("matches when all conditions are true", function () {
279
+ (0, bun_test_1.expect)((0, queryMatcher_1.matchesQuery)({ count: 5, status: "active" }, { $and: [{ status: "active" }, { count: 5 }] })).toBe(true);
280
+ });
281
+ (0, bun_test_1.it)("returns false when one condition fails", function () {
282
+ (0, bun_test_1.expect)((0, queryMatcher_1.matchesQuery)({ count: 3, status: "active" }, { $and: [{ status: "active" }, { count: 5 }] })).toBe(false);
283
+ });
284
+ (0, bun_test_1.it)("returns false when $and operand is not an array", function () {
285
+ (0, bun_test_1.expect)((0, queryMatcher_1.matchesQuery)({ status: "active" }, { $and: "invalid" })).toBe(false);
286
+ });
287
+ });
288
+ (0, bun_test_1.describe)("$or operator", function () {
289
+ (0, bun_test_1.it)("matches when any condition is true", function () {
290
+ (0, bun_test_1.expect)((0, queryMatcher_1.matchesQuery)({ status: "inactive" }, { $or: [{ status: "active" }, { status: "inactive" }] })).toBe(true);
291
+ });
292
+ (0, bun_test_1.it)("returns false when no conditions match", function () {
293
+ (0, bun_test_1.expect)((0, queryMatcher_1.matchesQuery)({ status: "pending" }, { $or: [{ status: "active" }, { status: "inactive" }] })).toBe(false);
294
+ });
295
+ (0, bun_test_1.it)("returns false when $or operand is not an array", function () {
296
+ (0, bun_test_1.expect)((0, queryMatcher_1.matchesQuery)({ status: "active" }, { $or: "invalid" })).toBe(false);
297
+ });
298
+ });
299
+ (0, bun_test_1.describe)("unknown operator", function () {
300
+ (0, bun_test_1.it)("returns false for unknown operators (fail closed)", function () {
301
+ (0, bun_test_1.expect)((0, queryMatcher_1.matchesQuery)({ name: "Alice" }, { name: { $regex: "Al" } })).toBe(false);
302
+ });
303
+ });
304
+ (0, bun_test_1.describe)("ObjectId-like values", function () {
305
+ (0, bun_test_1.it)("matches ObjectId-like objects via toString", function () {
306
+ var fakeObjectId = {
307
+ constructor: { name: "ObjectId" },
308
+ toString: function () { return "507f1f77bcf86cd799439011"; },
309
+ };
310
+ var doc = { ownerId: fakeObjectId };
311
+ (0, bun_test_1.expect)((0, queryMatcher_1.matchesQuery)(doc, { ownerId: "507f1f77bcf86cd799439011" })).toBe(true);
312
+ });
313
+ });
314
+ (0, bun_test_1.describe)("array equality", function () {
315
+ (0, bun_test_1.it)("matches array conditions by JSON serialization", function () {
316
+ (0, bun_test_1.expect)((0, queryMatcher_1.matchesQuery)({ tags: ["a", "b"] }, { tags: ["a", "b"] })).toBe(true);
317
+ });
318
+ (0, bun_test_1.it)("returns false for different arrays", function () {
319
+ (0, bun_test_1.expect)((0, queryMatcher_1.matchesQuery)({ tags: ["a", "b"] }, { tags: ["a", "c"] })).toBe(false);
320
+ });
321
+ });
322
+ (0, bun_test_1.describe)("empty query", function () {
323
+ (0, bun_test_1.it)("matches any document with empty query", function () {
324
+ (0, bun_test_1.expect)((0, queryMatcher_1.matchesQuery)({ count: 5, name: "Alice" }, {})).toBe(true);
325
+ });
326
+ });
327
+ });
328
+ // ─────────────────────────────────────────────────────────────────────────────
329
+ // queryStore tests
330
+ // ─────────────────────────────────────────────────────────────────────────────
331
+ (0, bun_test_1.describe)("queryStore", function () {
332
+ (0, bun_test_1.beforeEach)(function () {
333
+ (0, queryStore_1.clearQueryStore)();
334
+ });
335
+ (0, bun_test_1.afterEach)(function () {
336
+ (0, queryStore_1.clearQueryStore)();
337
+ });
338
+ (0, bun_test_1.describe)("computeQueryId", function () {
339
+ (0, bun_test_1.it)("produces a deterministic id from collection and query", function () {
340
+ var id1 = (0, queryStore_1.computeQueryId)("todos", { completed: false });
341
+ var id2 = (0, queryStore_1.computeQueryId)("todos", { completed: false });
342
+ (0, bun_test_1.expect)(id1).toBe(id2);
343
+ });
344
+ (0, bun_test_1.it)("normalizes key order before computing id", function () {
345
+ var id1 = (0, queryStore_1.computeQueryId)("todos", { completed: false, ownerId: "abc" });
346
+ var id2 = (0, queryStore_1.computeQueryId)("todos", { completed: false, ownerId: "abc" });
347
+ (0, bun_test_1.expect)(id1).toBe(id2);
348
+ });
349
+ (0, bun_test_1.it)("includes the collection name in the id", function () {
350
+ var id = (0, queryStore_1.computeQueryId)("todos", { completed: false });
351
+ (0, bun_test_1.expect)(id.startsWith("todos:")).toBe(true);
352
+ });
353
+ (0, bun_test_1.it)("produces different ids for different collections", function () {
354
+ var id1 = (0, queryStore_1.computeQueryId)("todos", { completed: false });
355
+ var id2 = (0, queryStore_1.computeQueryId)("items", { completed: false });
356
+ (0, bun_test_1.expect)(id1).not.toBe(id2);
357
+ });
358
+ (0, bun_test_1.it)("produces different ids for different queries", function () {
359
+ var id1 = (0, queryStore_1.computeQueryId)("todos", { completed: false });
360
+ var id2 = (0, queryStore_1.computeQueryId)("todos", { completed: true });
361
+ (0, bun_test_1.expect)(id1).not.toBe(id2);
362
+ });
363
+ });
364
+ (0, bun_test_1.describe)("addQuerySubscription", function () {
365
+ (0, bun_test_1.it)("stores a subscription and makes it retrievable", function () {
366
+ (0, queryStore_1.addQuerySubscription)("socket1", "todos", { completed: false }, "todos:q1");
367
+ var subs = (0, queryStore_1.getQuerySubscriptionsForCollection)("todos");
368
+ (0, bun_test_1.expect)(subs).toHaveLength(1);
369
+ (0, bun_test_1.expect)(subs[0].queryId).toBe("todos:q1");
370
+ });
371
+ (0, bun_test_1.it)("allows the same socket to subscribe to multiple queries", function () {
372
+ (0, queryStore_1.addQuerySubscription)("socket1", "todos", { completed: false }, "todos:q1");
373
+ (0, queryStore_1.addQuerySubscription)("socket1", "todos", { completed: true }, "todos:q2");
374
+ var subs = (0, queryStore_1.getQuerySubscriptionsForCollection)("todos");
375
+ (0, bun_test_1.expect)(subs).toHaveLength(2);
376
+ });
377
+ (0, bun_test_1.it)("allows multiple sockets to subscribe to the same query", function () {
378
+ (0, queryStore_1.addQuerySubscription)("socket1", "todos", { completed: false }, "todos:q1");
379
+ (0, queryStore_1.addQuerySubscription)("socket2", "todos", { completed: false }, "todos:q1");
380
+ var subs = (0, queryStore_1.getQuerySubscriptionsForCollection)("todos");
381
+ // Same queryId — deduplicated in the store
382
+ (0, bun_test_1.expect)(subs).toHaveLength(1);
383
+ });
384
+ });
385
+ (0, bun_test_1.describe)("removeQuerySubscription", function () {
386
+ (0, bun_test_1.it)("removes a specific query for a socket", function () {
387
+ (0, queryStore_1.addQuerySubscription)("socket1", "todos", { completed: false }, "todos:q1");
388
+ (0, queryStore_1.removeQuerySubscription)("socket1", "todos:q1");
389
+ var subs = (0, queryStore_1.getQuerySubscriptionsForCollection)("todos");
390
+ (0, bun_test_1.expect)(subs).toHaveLength(0);
391
+ });
392
+ (0, bun_test_1.it)("does not remove a query if another socket still uses it", function () {
393
+ (0, queryStore_1.addQuerySubscription)("socket1", "todos", { completed: false }, "todos:q1");
394
+ (0, queryStore_1.addQuerySubscription)("socket2", "todos", { completed: false }, "todos:q1");
395
+ (0, queryStore_1.removeQuerySubscription)("socket1", "todos:q1");
396
+ var subs = (0, queryStore_1.getQuerySubscriptionsForCollection)("todos");
397
+ (0, bun_test_1.expect)(subs).toHaveLength(1);
398
+ });
399
+ (0, bun_test_1.it)("is safe to call for a socket that has no subscriptions", function () {
400
+ (0, bun_test_1.expect)(function () { return (0, queryStore_1.removeQuerySubscription)("nonexistent", "todos:q1"); }).not.toThrow();
401
+ });
402
+ });
403
+ (0, bun_test_1.describe)("removeAllSocketQueries", function () {
404
+ (0, bun_test_1.it)("removes all subscriptions for a socket", function () {
405
+ (0, queryStore_1.addQuerySubscription)("socket1", "todos", { completed: false }, "todos:q1");
406
+ (0, queryStore_1.addQuerySubscription)("socket1", "todos", { completed: true }, "todos:q2");
407
+ (0, queryStore_1.removeAllSocketQueries)("socket1");
408
+ var subs = (0, queryStore_1.getQuerySubscriptionsForCollection)("todos");
409
+ (0, bun_test_1.expect)(subs).toHaveLength(0);
410
+ });
411
+ (0, bun_test_1.it)("preserves subscriptions for other sockets", function () {
412
+ (0, queryStore_1.addQuerySubscription)("socket1", "todos", { completed: false }, "todos:q1");
413
+ (0, queryStore_1.addQuerySubscription)("socket2", "todos", { completed: true }, "todos:q2");
414
+ (0, queryStore_1.removeAllSocketQueries)("socket1");
415
+ var subs = (0, queryStore_1.getQuerySubscriptionsForCollection)("todos");
416
+ (0, bun_test_1.expect)(subs).toHaveLength(1);
417
+ (0, bun_test_1.expect)(subs[0].queryId).toBe("todos:q2");
418
+ });
419
+ (0, bun_test_1.it)("is safe to call for a socket that has no subscriptions", function () {
420
+ (0, bun_test_1.expect)(function () { return (0, queryStore_1.removeAllSocketQueries)("nonexistent"); }).not.toThrow();
421
+ });
422
+ (0, bun_test_1.it)("only removes a shared query if no other sockets use it", function () {
423
+ (0, queryStore_1.addQuerySubscription)("socket1", "todos", { completed: false }, "todos:q1");
424
+ (0, queryStore_1.addQuerySubscription)("socket2", "todos", { completed: false }, "todos:q1");
425
+ (0, queryStore_1.removeAllSocketQueries)("socket1");
426
+ // socket2 still subscribes — query should remain
427
+ var subs = (0, queryStore_1.getQuerySubscriptionsForCollection)("todos");
428
+ (0, bun_test_1.expect)(subs).toHaveLength(1);
429
+ });
430
+ });
431
+ (0, bun_test_1.describe)("getQuerySubscriptionsForCollection", function () {
432
+ (0, bun_test_1.it)("returns only subscriptions for the given collection", function () {
433
+ (0, queryStore_1.addQuerySubscription)("socket1", "todos", { completed: false }, "todos:q1");
434
+ (0, queryStore_1.addQuerySubscription)("socket2", "items", { status: "active" }, "items:q1");
435
+ var todoSubs = (0, queryStore_1.getQuerySubscriptionsForCollection)("todos");
436
+ (0, bun_test_1.expect)(todoSubs).toHaveLength(1);
437
+ (0, bun_test_1.expect)(todoSubs[0].queryId).toBe("todos:q1");
438
+ });
439
+ (0, bun_test_1.it)("returns an empty array when no subscriptions exist for collection", function () {
440
+ var subs = (0, queryStore_1.getQuerySubscriptionsForCollection)("nonexistent");
441
+ (0, bun_test_1.expect)(subs).toHaveLength(0);
442
+ });
443
+ (0, bun_test_1.it)("returns query data for each subscription", function () {
444
+ var query = { completed: false, ownerId: "user1" };
445
+ (0, queryStore_1.addQuerySubscription)("socket1", "todos", query, "todos:q1");
446
+ var subs = (0, queryStore_1.getQuerySubscriptionsForCollection)("todos");
447
+ (0, bun_test_1.expect)(subs[0].query).toEqual(query);
448
+ });
449
+ });
450
+ (0, bun_test_1.describe)("clearQueryStore", function () {
451
+ (0, bun_test_1.it)("removes all subscriptions", function () {
452
+ (0, queryStore_1.addQuerySubscription)("socket1", "todos", { completed: false }, "todos:q1");
453
+ (0, queryStore_1.addQuerySubscription)("socket2", "items", { status: "active" }, "items:q1");
454
+ (0, queryStore_1.clearQueryStore)();
455
+ (0, bun_test_1.expect)((0, queryStore_1.getQuerySubscriptionsForCollection)("todos")).toHaveLength(0);
456
+ (0, bun_test_1.expect)((0, queryStore_1.getQuerySubscriptionsForCollection)("items")).toHaveLength(0);
457
+ });
458
+ });
459
+ });
460
+ // ─────────────────────────────────────────────────────────────────────────────
461
+ // registry tests
462
+ // ─────────────────────────────────────────────────────────────────────────────
463
+ (0, bun_test_1.describe)("realtimeRegistry", function () {
464
+ var makeEntry = function (overrides) {
465
+ if (overrides === void 0) { overrides = {}; }
466
+ return (__assign({ collectionName: "todos", config: {
467
+ methods: ["create", "update", "delete"],
468
+ roomStrategy: "owner",
469
+ }, modelName: "Todo", options: {}, routePath: "/todos" }, overrides));
470
+ };
471
+ (0, bun_test_1.beforeEach)(function () {
472
+ (0, registry_1.clearRealtimeRegistry)();
473
+ });
474
+ (0, bun_test_1.afterEach)(function () {
475
+ (0, registry_1.clearRealtimeRegistry)();
476
+ });
477
+ (0, bun_test_1.describe)("registerRealtime", function () {
478
+ (0, bun_test_1.it)("adds an entry to the registry", function () {
479
+ (0, registry_1.registerRealtime)(makeEntry());
480
+ (0, bun_test_1.expect)((0, registry_1.getRealtimeRegistry)()).toHaveLength(1);
481
+ });
482
+ (0, bun_test_1.it)("allows multiple entries", function () {
483
+ (0, registry_1.registerRealtime)(makeEntry({ modelName: "Todo", routePath: "/todos" }));
484
+ (0, registry_1.registerRealtime)(makeEntry({ collectionName: "items", modelName: "Item", routePath: "/items" }));
485
+ (0, bun_test_1.expect)((0, registry_1.getRealtimeRegistry)()).toHaveLength(2);
486
+ });
487
+ });
488
+ (0, bun_test_1.describe)("getRealtimeRegistry", function () {
489
+ (0, bun_test_1.it)("returns all registered entries", function () {
490
+ (0, registry_1.registerRealtime)(makeEntry({ modelName: "Todo" }));
491
+ (0, registry_1.registerRealtime)(makeEntry({ collectionName: "items", modelName: "Item", routePath: "/items" }));
492
+ var registry = (0, registry_1.getRealtimeRegistry)();
493
+ (0, bun_test_1.expect)(registry).toHaveLength(2);
494
+ (0, bun_test_1.expect)(registry.map(function (e) { return e.modelName; })).toEqual(["Todo", "Item"]);
495
+ });
496
+ (0, bun_test_1.it)("returns empty array when nothing is registered", function () {
497
+ (0, bun_test_1.expect)((0, registry_1.getRealtimeRegistry)()).toHaveLength(0);
498
+ });
499
+ });
500
+ (0, bun_test_1.describe)("findRegistryEntryByCollection", function () {
501
+ (0, bun_test_1.it)("finds an entry by collection name", function () {
502
+ (0, registry_1.registerRealtime)(makeEntry({ collectionName: "todos" }));
503
+ var entry = (0, registry_1.findRegistryEntryByCollection)("todos");
504
+ (0, bun_test_1.expect)(entry).toBeDefined();
505
+ (0, bun_test_1.expect)(entry === null || entry === void 0 ? void 0 : entry.collectionName).toBe("todos");
506
+ });
507
+ (0, bun_test_1.it)("returns undefined for unknown collection", function () {
508
+ var entry = (0, registry_1.findRegistryEntryByCollection)("nonexistent");
509
+ (0, bun_test_1.expect)(entry).toBeUndefined();
510
+ });
511
+ (0, bun_test_1.it)("returns first match when multiple entries exist", function () {
512
+ (0, registry_1.registerRealtime)(makeEntry({ collectionName: "todos", modelName: "Todo1" }));
513
+ (0, registry_1.registerRealtime)(makeEntry({ collectionName: "todos", modelName: "Todo2" }));
514
+ var entry = (0, registry_1.findRegistryEntryByCollection)("todos");
515
+ (0, bun_test_1.expect)(entry === null || entry === void 0 ? void 0 : entry.modelName).toBe("Todo1");
516
+ });
517
+ });
518
+ (0, bun_test_1.describe)("findRegistryEntryByRoutePath", function () {
519
+ (0, bun_test_1.it)("finds an entry by exact route path with leading slash", function () {
520
+ (0, registry_1.registerRealtime)(makeEntry({ routePath: "/todos" }));
521
+ var entry = (0, registry_1.findRegistryEntryByRoutePath)("todos");
522
+ (0, bun_test_1.expect)(entry).toBeDefined();
523
+ (0, bun_test_1.expect)(entry === null || entry === void 0 ? void 0 : entry.routePath).toBe("/todos");
524
+ });
525
+ (0, bun_test_1.it)("finds an entry when collection matches routePath exactly", function () {
526
+ (0, registry_1.registerRealtime)(makeEntry({ routePath: "todos" }));
527
+ var entry = (0, registry_1.findRegistryEntryByRoutePath)("todos");
528
+ (0, bun_test_1.expect)(entry).toBeDefined();
529
+ });
530
+ (0, bun_test_1.it)("returns undefined for unknown route path", function () {
531
+ (0, registry_1.registerRealtime)(makeEntry({ routePath: "/todos" }));
532
+ var entry = (0, registry_1.findRegistryEntryByRoutePath)("items");
533
+ (0, bun_test_1.expect)(entry).toBeUndefined();
534
+ });
535
+ });
536
+ (0, bun_test_1.describe)("clearRealtimeRegistry", function () {
537
+ (0, bun_test_1.it)("removes all entries", function () {
538
+ (0, registry_1.registerRealtime)(makeEntry());
539
+ (0, registry_1.clearRealtimeRegistry)();
540
+ (0, bun_test_1.expect)((0, registry_1.getRealtimeRegistry)()).toHaveLength(0);
541
+ });
542
+ });
543
+ });
544
+ // ─────────────────────────────────────────────────────────────────────────────
545
+ // RealtimeApp tests
546
+ // ─────────────────────────────────────────────────────────────────────────────
547
+ (0, bun_test_1.describe)("RealtimeApp", function () {
548
+ (0, bun_test_1.describe)("constructor", function () {
549
+ (0, bun_test_1.it)("creates an instance with empty config", function () {
550
+ var app = new realtimeApp_1.RealtimeApp();
551
+ (0, bun_test_1.expect)(app).toBeDefined();
552
+ });
553
+ (0, bun_test_1.it)("creates an instance with provided config", function () {
554
+ var app = new realtimeApp_1.RealtimeApp({ adapter: "none", debug: true });
555
+ (0, bun_test_1.expect)(app).toBeDefined();
556
+ });
557
+ });
558
+ (0, bun_test_1.describe)("getIo", function () {
559
+ (0, bun_test_1.it)("returns null before the server is created", function () {
560
+ var app = new realtimeApp_1.RealtimeApp();
561
+ (0, bun_test_1.expect)(app.getIo()).toBeNull();
562
+ });
563
+ });
564
+ (0, bun_test_1.describe)("register", function () {
565
+ (0, bun_test_1.it)("health endpoint returns status not_started when io is not initialized", function () { return __awaiter(void 0, void 0, void 0, function () {
566
+ var expressApp, app, supertest, st, res;
567
+ return __generator(this, function (_a) {
568
+ switch (_a.label) {
569
+ case 0:
570
+ expressApp = (0, express_1.default)();
571
+ app = new realtimeApp_1.RealtimeApp();
572
+ app.register(expressApp);
573
+ return [4 /*yield*/, Promise.resolve().then(function () { return __importStar(require("supertest")); })];
574
+ case 1:
575
+ supertest = _a.sent();
576
+ st = supertest.default(expressApp);
577
+ return [4 /*yield*/, st.get("/realtime/health").expect(200)];
578
+ case 2:
579
+ res = _a.sent();
580
+ (0, bun_test_1.expect)(res.body.status).toBe("not_started");
581
+ (0, bun_test_1.expect)(res.body.clients).toBe(0);
582
+ return [2 /*return*/];
583
+ }
584
+ });
585
+ }); });
586
+ });
587
+ (0, bun_test_1.describe)("close", function () {
588
+ (0, bun_test_1.it)("closes gracefully when io is null", function () { return __awaiter(void 0, void 0, void 0, function () {
589
+ var app;
590
+ return __generator(this, function (_a) {
591
+ switch (_a.label) {
592
+ case 0:
593
+ app = new realtimeApp_1.RealtimeApp();
594
+ // Should not throw
595
+ return [4 /*yield*/, (0, bun_test_1.expect)(app.close()).resolves.toBeUndefined()];
596
+ case 1:
597
+ // Should not throw
598
+ _a.sent();
599
+ return [2 /*return*/];
600
+ }
601
+ });
602
+ }); });
603
+ });
604
+ });
605
+ var createMockSocket = function (decodedToken) {
606
+ var rooms = new Set();
607
+ var emitted = [];
608
+ var listeners = new Map();
609
+ var socket = {
610
+ decodedToken: decodedToken,
611
+ emit: function (event, payload) {
612
+ emitted.push({ event: event, payload: payload });
613
+ },
614
+ emitted: emitted,
615
+ id: "socket-".concat(Math.random().toString(36).slice(2, 9)),
616
+ join: function (room) { return __awaiter(void 0, void 0, void 0, function () {
617
+ return __generator(this, function (_a) {
618
+ rooms.add(room);
619
+ return [2 /*return*/];
620
+ });
621
+ }); },
622
+ leave: function (room) { return __awaiter(void 0, void 0, void 0, function () {
623
+ return __generator(this, function (_a) {
624
+ rooms.delete(room);
625
+ return [2 /*return*/];
626
+ });
627
+ }); },
628
+ listeners: listeners,
629
+ on: function (event, handler) {
630
+ listeners.set(event, handler);
631
+ },
632
+ rooms: rooms,
633
+ trigger: function (event) {
634
+ var args = [];
635
+ for (var _i = 1; _i < arguments.length; _i++) {
636
+ args[_i - 1] = arguments[_i];
637
+ }
638
+ return __awaiter(void 0, void 0, void 0, function () {
639
+ var handler;
640
+ return __generator(this, function (_a) {
641
+ switch (_a.label) {
642
+ case 0:
643
+ handler = listeners.get(event);
644
+ if (!handler) return [3 /*break*/, 2];
645
+ return [4 /*yield*/, handler.apply(void 0, __spreadArray([], __read(args), false))];
646
+ case 1:
647
+ _a.sent();
648
+ _a.label = 2;
649
+ case 2: return [2 /*return*/];
650
+ }
651
+ });
652
+ });
653
+ },
654
+ };
655
+ return socket;
656
+ };
657
+ (0, bun_test_1.describe)("installRealtimeSocketHandlers", function () {
658
+ (0, bun_test_1.beforeEach)(function () {
659
+ (0, registry_1.clearRealtimeRegistry)();
660
+ (0, queryStore_1.clearQueryStore)();
661
+ });
662
+ (0, bun_test_1.afterEach)(function () {
663
+ (0, registry_1.clearRealtimeRegistry)();
664
+ (0, queryStore_1.clearQueryStore)();
665
+ });
666
+ var registerOwnerCollection = function () {
667
+ (0, registry_1.registerRealtime)({
668
+ collectionName: "todos",
669
+ config: { methods: ["create", "update", "delete"], roomStrategy: "owner" },
670
+ modelName: "Todo",
671
+ options: {
672
+ permissions: {
673
+ create: [function () { return true; }],
674
+ delete: [function () { return true; }],
675
+ list: [function () { return true; }],
676
+ read: [function () { return true; }],
677
+ update: [function () { return true; }],
678
+ },
679
+ },
680
+ routePath: "/todos",
681
+ });
682
+ };
683
+ var registerModelCollection = function () {
684
+ (0, registry_1.registerRealtime)({
685
+ collectionName: "broadcasts",
686
+ config: { methods: ["create", "update", "delete"], roomStrategy: "model" },
687
+ modelName: "Broadcast",
688
+ options: {
689
+ permissions: {
690
+ create: [function () { return true; }],
691
+ delete: [function () { return true; }],
692
+ list: [function () { return true; }],
693
+ read: [function () { return true; }],
694
+ update: [function () { return true; }],
695
+ },
696
+ },
697
+ routePath: "/broadcasts",
698
+ });
699
+ };
700
+ var registerAdminOnlyCollection = function () {
701
+ (0, registry_1.registerRealtime)({
702
+ collectionName: "secrets",
703
+ config: { methods: ["create", "update", "delete"], roomStrategy: "model" },
704
+ modelName: "Secret",
705
+ options: {
706
+ permissions: {
707
+ create: [function (_method, user) { return (user === null || user === void 0 ? void 0 : user.admin) === true; }],
708
+ delete: [function (_method, user) { return (user === null || user === void 0 ? void 0 : user.admin) === true; }],
709
+ list: [function (_method, user) { return (user === null || user === void 0 ? void 0 : user.admin) === true; }],
710
+ read: [function (_method, user) { return (user === null || user === void 0 ? void 0 : user.admin) === true; }],
711
+ update: [function (_method, user) { return (user === null || user === void 0 ? void 0 : user.admin) === true; }],
712
+ },
713
+ },
714
+ routePath: "/secrets",
715
+ });
716
+ };
717
+ (0, bun_test_1.describe)("connection setup", function () {
718
+ (0, bun_test_1.it)("joins user-specific and authenticated rooms when token has userId", function () { return __awaiter(void 0, void 0, void 0, function () {
719
+ var socket;
720
+ return __generator(this, function (_a) {
721
+ switch (_a.label) {
722
+ case 0:
723
+ socket = createMockSocket({ id: "user1" });
724
+ (0, realtimeApp_1.installRealtimeSocketHandlers)(socket);
725
+ // joinUserRooms is fire-and-forget — give microtasks a chance to flush.
726
+ return [4 /*yield*/, Promise.resolve()];
727
+ case 1:
728
+ // joinUserRooms is fire-and-forget — give microtasks a chance to flush.
729
+ _a.sent();
730
+ return [4 /*yield*/, Promise.resolve()];
731
+ case 2:
732
+ _a.sent();
733
+ (0, bun_test_1.expect)(socket.rooms.has("user:user1")).toBe(true);
734
+ (0, bun_test_1.expect)(socket.rooms.has("authenticated")).toBe(true);
735
+ return [2 /*return*/];
736
+ }
737
+ });
738
+ }); });
739
+ (0, bun_test_1.it)("joins admin room for admin tokens", function () { return __awaiter(void 0, void 0, void 0, function () {
740
+ var socket;
741
+ return __generator(this, function (_a) {
742
+ switch (_a.label) {
743
+ case 0:
744
+ socket = createMockSocket({ admin: true, id: "admin1" });
745
+ (0, realtimeApp_1.installRealtimeSocketHandlers)(socket);
746
+ return [4 /*yield*/, Promise.resolve()];
747
+ case 1:
748
+ _a.sent();
749
+ return [4 /*yield*/, Promise.resolve()];
750
+ case 2:
751
+ _a.sent();
752
+ (0, bun_test_1.expect)(socket.rooms.has("admin")).toBe(true);
753
+ return [2 /*return*/];
754
+ }
755
+ });
756
+ }); });
757
+ (0, bun_test_1.it)("does not join admin room for non-admin tokens", function () { return __awaiter(void 0, void 0, void 0, function () {
758
+ var socket;
759
+ return __generator(this, function (_a) {
760
+ switch (_a.label) {
761
+ case 0:
762
+ socket = createMockSocket({ admin: false, id: "user1" });
763
+ (0, realtimeApp_1.installRealtimeSocketHandlers)(socket);
764
+ return [4 /*yield*/, Promise.resolve()];
765
+ case 1:
766
+ _a.sent();
767
+ return [4 /*yield*/, Promise.resolve()];
768
+ case 2:
769
+ _a.sent();
770
+ (0, bun_test_1.expect)(socket.rooms.has("admin")).toBe(false);
771
+ return [2 /*return*/];
772
+ }
773
+ });
774
+ }); });
775
+ });
776
+ (0, bun_test_1.describe)("subscribe:model permission", function () {
777
+ (0, bun_test_1.it)("denies unregistered collections", function () { return __awaiter(void 0, void 0, void 0, function () {
778
+ var socket;
779
+ return __generator(this, function (_a) {
780
+ switch (_a.label) {
781
+ case 0:
782
+ socket = createMockSocket({ id: "user1" });
783
+ (0, realtimeApp_1.installRealtimeSocketHandlers)(socket);
784
+ return [4 /*yield*/, socket.trigger("subscribe:model", "nonexistent")];
785
+ case 1:
786
+ _a.sent();
787
+ (0, bun_test_1.expect)(socket.rooms.has("model:nonexistent")).toBe(false);
788
+ return [2 /*return*/];
789
+ }
790
+ });
791
+ }); });
792
+ (0, bun_test_1.it)("denies owner-strategy model room for non-admin users", function () { return __awaiter(void 0, void 0, void 0, function () {
793
+ var socket;
794
+ return __generator(this, function (_a) {
795
+ switch (_a.label) {
796
+ case 0:
797
+ registerOwnerCollection();
798
+ socket = createMockSocket({ admin: false, id: "user1" });
799
+ (0, realtimeApp_1.installRealtimeSocketHandlers)(socket);
800
+ return [4 /*yield*/, socket.trigger("subscribe:model", "todos")];
801
+ case 1:
802
+ _a.sent();
803
+ (0, bun_test_1.expect)(socket.rooms.has("model:todos")).toBe(false);
804
+ return [2 /*return*/];
805
+ }
806
+ });
807
+ }); });
808
+ (0, bun_test_1.it)("allows owner-strategy model room for admins", function () { return __awaiter(void 0, void 0, void 0, function () {
809
+ var socket;
810
+ return __generator(this, function (_a) {
811
+ switch (_a.label) {
812
+ case 0:
813
+ registerOwnerCollection();
814
+ socket = createMockSocket({ admin: true, id: "admin1" });
815
+ (0, realtimeApp_1.installRealtimeSocketHandlers)(socket);
816
+ return [4 /*yield*/, socket.trigger("subscribe:model", "todos")];
817
+ case 1:
818
+ _a.sent();
819
+ (0, bun_test_1.expect)(socket.rooms.has("model:todos")).toBe(true);
820
+ return [2 /*return*/];
821
+ }
822
+ });
823
+ }); });
824
+ (0, bun_test_1.it)("allows non-admins to subscribe to model-strategy collections", function () { return __awaiter(void 0, void 0, void 0, function () {
825
+ var socket;
826
+ return __generator(this, function (_a) {
827
+ switch (_a.label) {
828
+ case 0:
829
+ registerModelCollection();
830
+ socket = createMockSocket({ admin: false, id: "user1" });
831
+ (0, realtimeApp_1.installRealtimeSocketHandlers)(socket);
832
+ return [4 /*yield*/, socket.trigger("subscribe:model", "broadcasts")];
833
+ case 1:
834
+ _a.sent();
835
+ (0, bun_test_1.expect)(socket.rooms.has("model:broadcasts")).toBe(true);
836
+ return [2 /*return*/];
837
+ }
838
+ });
839
+ }); });
840
+ (0, bun_test_1.it)("denies model-strategy collections when modelRouter list permission fails", function () { return __awaiter(void 0, void 0, void 0, function () {
841
+ var socket;
842
+ return __generator(this, function (_a) {
843
+ switch (_a.label) {
844
+ case 0:
845
+ registerAdminOnlyCollection();
846
+ socket = createMockSocket({ admin: false, id: "user1" });
847
+ (0, realtimeApp_1.installRealtimeSocketHandlers)(socket);
848
+ return [4 /*yield*/, socket.trigger("subscribe:model", "secrets")];
849
+ case 1:
850
+ _a.sent();
851
+ (0, bun_test_1.expect)(socket.rooms.has("model:secrets")).toBe(false);
852
+ return [2 /*return*/];
853
+ }
854
+ });
855
+ }); });
856
+ (0, bun_test_1.it)("allows model-strategy collections when modelRouter list permission passes", function () { return __awaiter(void 0, void 0, void 0, function () {
857
+ var socket;
858
+ return __generator(this, function (_a) {
859
+ switch (_a.label) {
860
+ case 0:
861
+ registerAdminOnlyCollection();
862
+ socket = createMockSocket({ admin: true, id: "admin1" });
863
+ (0, realtimeApp_1.installRealtimeSocketHandlers)(socket);
864
+ return [4 /*yield*/, socket.trigger("subscribe:model", "secrets")];
865
+ case 1:
866
+ _a.sent();
867
+ (0, bun_test_1.expect)(socket.rooms.has("model:secrets")).toBe(true);
868
+ return [2 /*return*/];
869
+ }
870
+ });
871
+ }); });
872
+ (0, bun_test_1.it)("ignores empty or non-string model names", function () { return __awaiter(void 0, void 0, void 0, function () {
873
+ var socket, modelRooms;
874
+ return __generator(this, function (_a) {
875
+ switch (_a.label) {
876
+ case 0:
877
+ registerModelCollection();
878
+ socket = createMockSocket({ id: "user1" });
879
+ (0, realtimeApp_1.installRealtimeSocketHandlers)(socket);
880
+ return [4 /*yield*/, socket.trigger("subscribe:model", "")];
881
+ case 1:
882
+ _a.sent();
883
+ return [4 /*yield*/, socket.trigger("subscribe:model", 123)];
884
+ case 2:
885
+ _a.sent();
886
+ return [4 /*yield*/, socket.trigger("subscribe:model", null)];
887
+ case 3:
888
+ _a.sent();
889
+ modelRooms = Array.from(socket.rooms).filter(function (r) { return r.startsWith("model:"); });
890
+ (0, bun_test_1.expect)(modelRooms).toHaveLength(0);
891
+ return [2 /*return*/];
892
+ }
893
+ });
894
+ }); });
895
+ (0, bun_test_1.it)("enforces MAX_MODEL_SUBSCRIPTIONS cap", function () { return __awaiter(void 0, void 0, void 0, function () {
896
+ var i, socket, i, modelRooms;
897
+ return __generator(this, function (_a) {
898
+ switch (_a.label) {
899
+ case 0:
900
+ // Register MAX + 5 model-strategy collections.
901
+ for (i = 0; i < realtimeApp_1.MAX_MODEL_SUBSCRIPTIONS + 5; i++) {
902
+ (0, registry_1.registerRealtime)({
903
+ collectionName: "coll".concat(i),
904
+ config: { methods: ["create"], roomStrategy: "model" },
905
+ modelName: "Coll".concat(i),
906
+ options: {
907
+ permissions: {
908
+ create: [function () { return true; }],
909
+ delete: [function () { return true; }],
910
+ list: [function () { return true; }],
911
+ read: [function () { return true; }],
912
+ update: [function () { return true; }],
913
+ },
914
+ },
915
+ routePath: "/coll".concat(i),
916
+ });
917
+ }
918
+ socket = createMockSocket({ id: "user1" });
919
+ (0, realtimeApp_1.installRealtimeSocketHandlers)(socket);
920
+ i = 0;
921
+ _a.label = 1;
922
+ case 1:
923
+ if (!(i < realtimeApp_1.MAX_MODEL_SUBSCRIPTIONS + 5)) return [3 /*break*/, 4];
924
+ return [4 /*yield*/, socket.trigger("subscribe:model", "coll".concat(i))];
925
+ case 2:
926
+ _a.sent();
927
+ _a.label = 3;
928
+ case 3:
929
+ i++;
930
+ return [3 /*break*/, 1];
931
+ case 4:
932
+ modelRooms = Array.from(socket.rooms).filter(function (r) { return r.startsWith("model:"); });
933
+ (0, bun_test_1.expect)(modelRooms.length).toBe(realtimeApp_1.MAX_MODEL_SUBSCRIPTIONS);
934
+ return [2 /*return*/];
935
+ }
936
+ });
937
+ }); });
938
+ });
939
+ (0, bun_test_1.describe)("subscribe:document permission", function () {
940
+ (0, bun_test_1.it)("denies unregistered collections", function () { return __awaiter(void 0, void 0, void 0, function () {
941
+ var socket, docRooms;
942
+ return __generator(this, function (_a) {
943
+ switch (_a.label) {
944
+ case 0:
945
+ socket = createMockSocket({ id: "user1" });
946
+ (0, realtimeApp_1.installRealtimeSocketHandlers)(socket);
947
+ return [4 /*yield*/, socket.trigger("subscribe:document", { collection: "nonexistent", id: "abc" })];
948
+ case 1:
949
+ _a.sent();
950
+ docRooms = Array.from(socket.rooms).filter(function (r) { return r.startsWith("document:"); });
951
+ (0, bun_test_1.expect)(docRooms).toHaveLength(0);
952
+ return [2 /*return*/];
953
+ }
954
+ });
955
+ }); });
956
+ (0, bun_test_1.it)("denies owner-strategy document subscription for non-admin", function () { return __awaiter(void 0, void 0, void 0, function () {
957
+ var socket;
958
+ return __generator(this, function (_a) {
959
+ switch (_a.label) {
960
+ case 0:
961
+ registerOwnerCollection();
962
+ socket = createMockSocket({ admin: false, id: "user1" });
963
+ (0, realtimeApp_1.installRealtimeSocketHandlers)(socket);
964
+ return [4 /*yield*/, socket.trigger("subscribe:document", { collection: "todos", id: "doc1" })];
965
+ case 1:
966
+ _a.sent();
967
+ (0, bun_test_1.expect)(socket.rooms.has("document:todos:doc1")).toBe(false);
968
+ return [2 /*return*/];
969
+ }
970
+ });
971
+ }); });
972
+ (0, bun_test_1.it)("allows owner-strategy document subscription for admins", function () { return __awaiter(void 0, void 0, void 0, function () {
973
+ var socket;
974
+ return __generator(this, function (_a) {
975
+ switch (_a.label) {
976
+ case 0:
977
+ registerOwnerCollection();
978
+ socket = createMockSocket({ admin: true, id: "admin1" });
979
+ (0, realtimeApp_1.installRealtimeSocketHandlers)(socket);
980
+ return [4 /*yield*/, socket.trigger("subscribe:document", { collection: "todos", id: "doc1" })];
981
+ case 1:
982
+ _a.sent();
983
+ (0, bun_test_1.expect)(socket.rooms.has("document:todos:doc1")).toBe(true);
984
+ return [2 /*return*/];
985
+ }
986
+ });
987
+ }); });
988
+ (0, bun_test_1.it)("allows model-strategy document subscription for non-admin", function () { return __awaiter(void 0, void 0, void 0, function () {
989
+ var socket;
990
+ return __generator(this, function (_a) {
991
+ switch (_a.label) {
992
+ case 0:
993
+ registerModelCollection();
994
+ socket = createMockSocket({ admin: false, id: "user1" });
995
+ (0, realtimeApp_1.installRealtimeSocketHandlers)(socket);
996
+ return [4 /*yield*/, socket.trigger("subscribe:document", { collection: "broadcasts", id: "doc1" })];
997
+ case 1:
998
+ _a.sent();
999
+ (0, bun_test_1.expect)(socket.rooms.has("document:broadcasts:doc1")).toBe(true);
1000
+ return [2 /*return*/];
1001
+ }
1002
+ });
1003
+ }); });
1004
+ (0, bun_test_1.it)("denies document subscriptions when modelRouter read permission fails", function () { return __awaiter(void 0, void 0, void 0, function () {
1005
+ var socket;
1006
+ return __generator(this, function (_a) {
1007
+ switch (_a.label) {
1008
+ case 0:
1009
+ registerAdminOnlyCollection();
1010
+ socket = createMockSocket({ admin: false, id: "user1" });
1011
+ (0, realtimeApp_1.installRealtimeSocketHandlers)(socket);
1012
+ return [4 /*yield*/, socket.trigger("subscribe:document", { collection: "secrets", id: "doc1" })];
1013
+ case 1:
1014
+ _a.sent();
1015
+ (0, bun_test_1.expect)(socket.rooms.has("document:secrets:doc1")).toBe(false);
1016
+ return [2 /*return*/];
1017
+ }
1018
+ });
1019
+ }); });
1020
+ (0, bun_test_1.it)("ignores malformed payloads", function () { return __awaiter(void 0, void 0, void 0, function () {
1021
+ var socket, docRooms;
1022
+ return __generator(this, function (_a) {
1023
+ switch (_a.label) {
1024
+ case 0:
1025
+ registerModelCollection();
1026
+ socket = createMockSocket({ id: "user1" });
1027
+ (0, realtimeApp_1.installRealtimeSocketHandlers)(socket);
1028
+ return [4 /*yield*/, socket.trigger("subscribe:document", null)];
1029
+ case 1:
1030
+ _a.sent();
1031
+ return [4 /*yield*/, socket.trigger("subscribe:document", {})];
1032
+ case 2:
1033
+ _a.sent();
1034
+ return [4 /*yield*/, socket.trigger("subscribe:document", { collection: "broadcasts" })];
1035
+ case 3:
1036
+ _a.sent();
1037
+ return [4 /*yield*/, socket.trigger("subscribe:document", { id: "doc1" })];
1038
+ case 4:
1039
+ _a.sent();
1040
+ return [4 /*yield*/, socket.trigger("subscribe:document", { collection: 123, id: "doc1" })];
1041
+ case 5:
1042
+ _a.sent();
1043
+ docRooms = Array.from(socket.rooms).filter(function (r) { return r.startsWith("document:"); });
1044
+ (0, bun_test_1.expect)(docRooms).toHaveLength(0);
1045
+ return [2 /*return*/];
1046
+ }
1047
+ });
1048
+ }); });
1049
+ });
1050
+ (0, bun_test_1.describe)("subscribe:query permission", function () {
1051
+ (0, bun_test_1.it)("denies unregistered collections", function () { return __awaiter(void 0, void 0, void 0, function () {
1052
+ var socket, queryRooms;
1053
+ return __generator(this, function (_a) {
1054
+ switch (_a.label) {
1055
+ case 0:
1056
+ socket = createMockSocket({ id: "user1" });
1057
+ (0, realtimeApp_1.installRealtimeSocketHandlers)(socket);
1058
+ return [4 /*yield*/, socket.trigger("subscribe:query", { collection: "nope", query: { a: 1 } })];
1059
+ case 1:
1060
+ _a.sent();
1061
+ queryRooms = Array.from(socket.rooms).filter(function (r) { return r.startsWith("query:"); });
1062
+ (0, bun_test_1.expect)(queryRooms).toHaveLength(0);
1063
+ return [2 /*return*/];
1064
+ }
1065
+ });
1066
+ }); });
1067
+ (0, bun_test_1.it)("injects ownerId for owner-strategy non-admin subscribers", function () { return __awaiter(void 0, void 0, void 0, function () {
1068
+ var socket, subscribed;
1069
+ return __generator(this, function (_a) {
1070
+ switch (_a.label) {
1071
+ case 0:
1072
+ registerOwnerCollection();
1073
+ socket = createMockSocket({ admin: false, id: "user1" });
1074
+ (0, realtimeApp_1.installRealtimeSocketHandlers)(socket);
1075
+ return [4 /*yield*/, socket.trigger("subscribe:query", {
1076
+ collection: "todos",
1077
+ query: { completed: false },
1078
+ })];
1079
+ case 1:
1080
+ _a.sent();
1081
+ subscribed = socket.emitted.find(function (e) { return e.event === "query:subscribed"; });
1082
+ (0, bun_test_1.expect)(subscribed).toBeDefined();
1083
+ (0, bun_test_1.expect)((subscribed === null || subscribed === void 0 ? void 0 : subscribed.payload).queryId).toContain("user1");
1084
+ return [2 /*return*/];
1085
+ }
1086
+ });
1087
+ }); });
1088
+ (0, bun_test_1.it)("does NOT inject ownerId for admins (admins see all)", function () { return __awaiter(void 0, void 0, void 0, function () {
1089
+ var socket, subscribed;
1090
+ return __generator(this, function (_a) {
1091
+ switch (_a.label) {
1092
+ case 0:
1093
+ registerOwnerCollection();
1094
+ socket = createMockSocket({ admin: true, id: "admin1" });
1095
+ (0, realtimeApp_1.installRealtimeSocketHandlers)(socket);
1096
+ return [4 /*yield*/, socket.trigger("subscribe:query", {
1097
+ collection: "todos",
1098
+ query: { completed: false },
1099
+ })];
1100
+ case 1:
1101
+ _a.sent();
1102
+ subscribed = socket.emitted.find(function (e) { return e.event === "query:subscribed"; });
1103
+ (0, bun_test_1.expect)(subscribed).toBeDefined();
1104
+ (0, bun_test_1.expect)((subscribed === null || subscribed === void 0 ? void 0 : subscribed.payload).queryId).not.toContain("admin1");
1105
+ return [2 /*return*/];
1106
+ }
1107
+ });
1108
+ }); });
1109
+ (0, bun_test_1.it)("ignores subscriptions when user has no id (anonymous) for owner strategy", function () { return __awaiter(void 0, void 0, void 0, function () {
1110
+ var socket, queryRooms;
1111
+ return __generator(this, function (_a) {
1112
+ switch (_a.label) {
1113
+ case 0:
1114
+ registerOwnerCollection();
1115
+ socket = createMockSocket({ admin: false });
1116
+ (0, realtimeApp_1.installRealtimeSocketHandlers)(socket);
1117
+ return [4 /*yield*/, socket.trigger("subscribe:query", {
1118
+ collection: "todos",
1119
+ query: { completed: false },
1120
+ })];
1121
+ case 1:
1122
+ _a.sent();
1123
+ queryRooms = Array.from(socket.rooms).filter(function (r) { return r.startsWith("query:"); });
1124
+ (0, bun_test_1.expect)(queryRooms).toHaveLength(0);
1125
+ return [2 /*return*/];
1126
+ }
1127
+ });
1128
+ }); });
1129
+ (0, bun_test_1.it)("computes the queryId server-side regardless of client-provided value", function () { return __awaiter(void 0, void 0, void 0, function () {
1130
+ var socket, subscribed, payload;
1131
+ return __generator(this, function (_a) {
1132
+ switch (_a.label) {
1133
+ case 0:
1134
+ registerOwnerCollection();
1135
+ socket = createMockSocket({ admin: true, id: "admin1" });
1136
+ (0, realtimeApp_1.installRealtimeSocketHandlers)(socket);
1137
+ return [4 /*yield*/, socket.trigger("subscribe:query", {
1138
+ collection: "todos",
1139
+ query: { completed: false },
1140
+ queryId: "EVIL_HIJACK", // client-provided is ignored
1141
+ })];
1142
+ case 1:
1143
+ _a.sent();
1144
+ subscribed = socket.emitted.find(function (e) { return e.event === "query:subscribed"; });
1145
+ payload = subscribed === null || subscribed === void 0 ? void 0 : subscribed.payload;
1146
+ (0, bun_test_1.expect)(payload.queryId).not.toBe("EVIL_HIJACK");
1147
+ // Must match what the server computes
1148
+ (0, bun_test_1.expect)(payload.queryId).toBe((0, queryStore_1.computeQueryId)("todos", { completed: false }));
1149
+ return [2 /*return*/];
1150
+ }
1151
+ });
1152
+ }); });
1153
+ (0, bun_test_1.it)("denies query subscriptions when modelRouter list permission fails", function () { return __awaiter(void 0, void 0, void 0, function () {
1154
+ var socket, queryRooms;
1155
+ return __generator(this, function (_a) {
1156
+ switch (_a.label) {
1157
+ case 0:
1158
+ registerAdminOnlyCollection();
1159
+ socket = createMockSocket({ admin: false, id: "user1" });
1160
+ (0, realtimeApp_1.installRealtimeSocketHandlers)(socket);
1161
+ return [4 /*yield*/, socket.trigger("subscribe:query", {
1162
+ collection: "secrets",
1163
+ query: { classification: "restricted" },
1164
+ })];
1165
+ case 1:
1166
+ _a.sent();
1167
+ queryRooms = Array.from(socket.rooms).filter(function (r) { return r.startsWith("query:"); });
1168
+ (0, bun_test_1.expect)(queryRooms).toHaveLength(0);
1169
+ (0, bun_test_1.expect)((0, queryStore_1.getQuerySubscriptionsForCollection)("secrets")).toHaveLength(0);
1170
+ return [2 /*return*/];
1171
+ }
1172
+ });
1173
+ }); });
1174
+ (0, bun_test_1.it)("applies modelRouter queryFilter before storing a query subscription", function () { return __awaiter(void 0, void 0, void 0, function () {
1175
+ var socket, subs;
1176
+ return __generator(this, function (_a) {
1177
+ switch (_a.label) {
1178
+ case 0:
1179
+ (0, registry_1.registerRealtime)({
1180
+ collectionName: "filtered",
1181
+ config: { methods: ["create"], roomStrategy: "model" },
1182
+ modelName: "Filtered",
1183
+ options: {
1184
+ permissions: {
1185
+ create: [function () { return true; }],
1186
+ delete: [function () { return true; }],
1187
+ list: [function () { return true; }],
1188
+ read: [function () { return true; }],
1189
+ update: [function () { return true; }],
1190
+ },
1191
+ queryFilter: function () { return ({ tenantId: "tenant-1" }); },
1192
+ },
1193
+ routePath: "/filtered",
1194
+ });
1195
+ socket = createMockSocket({ id: "user1" });
1196
+ (0, realtimeApp_1.installRealtimeSocketHandlers)(socket);
1197
+ return [4 /*yield*/, socket.trigger("subscribe:query", {
1198
+ collection: "filtered",
1199
+ query: { status: "active" },
1200
+ })];
1201
+ case 1:
1202
+ _a.sent();
1203
+ subs = (0, queryStore_1.getQuerySubscriptionsForCollection)("filtered");
1204
+ (0, bun_test_1.expect)(subs).toHaveLength(1);
1205
+ (0, bun_test_1.expect)(subs[0].query).toEqual({ status: "active", tenantId: "tenant-1" });
1206
+ return [2 /*return*/];
1207
+ }
1208
+ });
1209
+ }); });
1210
+ (0, bun_test_1.it)("denies query subscriptions when modelRouter queryFilter throws", function () { return __awaiter(void 0, void 0, void 0, function () {
1211
+ var socket, queryRooms;
1212
+ return __generator(this, function (_a) {
1213
+ switch (_a.label) {
1214
+ case 0:
1215
+ (0, registry_1.registerRealtime)({
1216
+ collectionName: "filtered",
1217
+ config: { methods: ["create"], roomStrategy: "model" },
1218
+ modelName: "Filtered",
1219
+ options: {
1220
+ permissions: {
1221
+ create: [function () { return true; }],
1222
+ delete: [function () { return true; }],
1223
+ list: [function () { return true; }],
1224
+ read: [function () { return true; }],
1225
+ update: [function () { return true; }],
1226
+ },
1227
+ queryFilter: function () {
1228
+ throw new Error("tenant lookup failed");
1229
+ },
1230
+ },
1231
+ routePath: "/filtered",
1232
+ });
1233
+ socket = createMockSocket({ id: "user1" });
1234
+ (0, realtimeApp_1.installRealtimeSocketHandlers)(socket);
1235
+ return [4 /*yield*/, socket.trigger("subscribe:query", {
1236
+ collection: "filtered",
1237
+ query: { status: "active" },
1238
+ })];
1239
+ case 1:
1240
+ _a.sent();
1241
+ queryRooms = Array.from(socket.rooms).filter(function (r) { return r.startsWith("query:"); });
1242
+ (0, bun_test_1.expect)(queryRooms).toHaveLength(0);
1243
+ (0, bun_test_1.expect)((0, queryStore_1.getQuerySubscriptionsForCollection)("filtered")).toHaveLength(0);
1244
+ (0, bun_test_1.expect)(socket.emitted.some(function (e) { return e.event === "query:subscribed"; })).toBe(false);
1245
+ return [2 /*return*/];
1246
+ }
1247
+ });
1248
+ }); });
1249
+ (0, bun_test_1.it)("ignores malformed query payloads", function () { return __awaiter(void 0, void 0, void 0, function () {
1250
+ var socket, queryRooms;
1251
+ return __generator(this, function (_a) {
1252
+ switch (_a.label) {
1253
+ case 0:
1254
+ registerOwnerCollection();
1255
+ socket = createMockSocket({ id: "user1" });
1256
+ (0, realtimeApp_1.installRealtimeSocketHandlers)(socket);
1257
+ return [4 /*yield*/, socket.trigger("subscribe:query", null)];
1258
+ case 1:
1259
+ _a.sent();
1260
+ return [4 /*yield*/, socket.trigger("subscribe:query", {})];
1261
+ case 2:
1262
+ _a.sent();
1263
+ return [4 /*yield*/, socket.trigger("subscribe:query", { collection: "todos" })];
1264
+ case 3:
1265
+ _a.sent();
1266
+ return [4 /*yield*/, socket.trigger("subscribe:query", { collection: "todos", query: [1, 2, 3] })];
1267
+ case 4:
1268
+ _a.sent();
1269
+ queryRooms = Array.from(socket.rooms).filter(function (r) { return r.startsWith("query:"); });
1270
+ (0, bun_test_1.expect)(queryRooms).toHaveLength(0);
1271
+ return [2 /*return*/];
1272
+ }
1273
+ });
1274
+ }); });
1275
+ (0, bun_test_1.it)("enforces MAX_QUERY_SUBSCRIPTIONS cap", function () { return __awaiter(void 0, void 0, void 0, function () {
1276
+ var socket, i, queryRooms;
1277
+ return __generator(this, function (_a) {
1278
+ switch (_a.label) {
1279
+ case 0:
1280
+ registerOwnerCollection();
1281
+ socket = createMockSocket({ admin: true, id: "admin1" });
1282
+ (0, realtimeApp_1.installRealtimeSocketHandlers)(socket);
1283
+ i = 0;
1284
+ _a.label = 1;
1285
+ case 1:
1286
+ if (!(i < realtimeApp_1.MAX_QUERY_SUBSCRIPTIONS + 5)) return [3 /*break*/, 4];
1287
+ return [4 /*yield*/, socket.trigger("subscribe:query", {
1288
+ collection: "todos",
1289
+ query: { priority: i },
1290
+ })];
1291
+ case 2:
1292
+ _a.sent();
1293
+ _a.label = 3;
1294
+ case 3:
1295
+ i++;
1296
+ return [3 /*break*/, 1];
1297
+ case 4:
1298
+ queryRooms = Array.from(socket.rooms).filter(function (r) { return r.startsWith("query:"); });
1299
+ (0, bun_test_1.expect)(queryRooms.length).toBe(realtimeApp_1.MAX_QUERY_SUBSCRIPTIONS);
1300
+ return [2 /*return*/];
1301
+ }
1302
+ });
1303
+ }); });
1304
+ });
1305
+ (0, bun_test_1.describe)("unsubscribe and counters", function () {
1306
+ (0, bun_test_1.it)("unsubscribe:model frees a slot so further subscriptions are allowed", function () { return __awaiter(void 0, void 0, void 0, function () {
1307
+ var socket;
1308
+ return __generator(this, function (_a) {
1309
+ switch (_a.label) {
1310
+ case 0:
1311
+ registerModelCollection();
1312
+ (0, registry_1.registerRealtime)({
1313
+ collectionName: "other",
1314
+ config: { methods: ["create"], roomStrategy: "model" },
1315
+ modelName: "Other",
1316
+ options: {
1317
+ permissions: {
1318
+ create: [function () { return true; }],
1319
+ delete: [function () { return true; }],
1320
+ list: [function () { return true; }],
1321
+ read: [function () { return true; }],
1322
+ update: [function () { return true; }],
1323
+ },
1324
+ },
1325
+ routePath: "/other",
1326
+ });
1327
+ socket = createMockSocket({ id: "user1" });
1328
+ (0, realtimeApp_1.installRealtimeSocketHandlers)(socket);
1329
+ return [4 /*yield*/, socket.trigger("subscribe:model", "broadcasts")];
1330
+ case 1:
1331
+ _a.sent();
1332
+ (0, bun_test_1.expect)(socket.rooms.has("model:broadcasts")).toBe(true);
1333
+ return [4 /*yield*/, socket.trigger("unsubscribe:model", "broadcasts")];
1334
+ case 2:
1335
+ _a.sent();
1336
+ (0, bun_test_1.expect)(socket.rooms.has("model:broadcasts")).toBe(false);
1337
+ // can re-subscribe
1338
+ return [4 /*yield*/, socket.trigger("subscribe:model", "other")];
1339
+ case 3:
1340
+ // can re-subscribe
1341
+ _a.sent();
1342
+ (0, bun_test_1.expect)(socket.rooms.has("model:other")).toBe(true);
1343
+ return [2 /*return*/];
1344
+ }
1345
+ });
1346
+ }); });
1347
+ (0, bun_test_1.it)("unsubscribe:document removes the room and decrements the counter", function () { return __awaiter(void 0, void 0, void 0, function () {
1348
+ var socket;
1349
+ return __generator(this, function (_a) {
1350
+ switch (_a.label) {
1351
+ case 0:
1352
+ registerModelCollection();
1353
+ socket = createMockSocket({ id: "user1" });
1354
+ (0, realtimeApp_1.installRealtimeSocketHandlers)(socket);
1355
+ return [4 /*yield*/, socket.trigger("subscribe:document", { collection: "broadcasts", id: "doc1" })];
1356
+ case 1:
1357
+ _a.sent();
1358
+ (0, bun_test_1.expect)(socket.rooms.has("document:broadcasts:doc1")).toBe(true);
1359
+ return [4 /*yield*/, socket.trigger("unsubscribe:document", { collection: "broadcasts", id: "doc1" })];
1360
+ case 2:
1361
+ _a.sent();
1362
+ (0, bun_test_1.expect)(socket.rooms.has("document:broadcasts:doc1")).toBe(false);
1363
+ return [2 /*return*/];
1364
+ }
1365
+ });
1366
+ }); });
1367
+ (0, bun_test_1.it)("unsubscribe:document ignores malformed payloads", function () { return __awaiter(void 0, void 0, void 0, function () {
1368
+ var socket, docRooms;
1369
+ return __generator(this, function (_a) {
1370
+ switch (_a.label) {
1371
+ case 0:
1372
+ socket = createMockSocket({ id: "user1" });
1373
+ (0, realtimeApp_1.installRealtimeSocketHandlers)(socket);
1374
+ // None of these should throw or affect rooms
1375
+ return [4 /*yield*/, socket.trigger("unsubscribe:document", null)];
1376
+ case 1:
1377
+ // None of these should throw or affect rooms
1378
+ _a.sent();
1379
+ return [4 /*yield*/, socket.trigger("unsubscribe:document", {})];
1380
+ case 2:
1381
+ _a.sent();
1382
+ return [4 /*yield*/, socket.trigger("unsubscribe:document", { collection: "broadcasts" })];
1383
+ case 3:
1384
+ _a.sent();
1385
+ return [4 /*yield*/, socket.trigger("unsubscribe:document", { id: "doc1" })];
1386
+ case 4:
1387
+ _a.sent();
1388
+ docRooms = Array.from(socket.rooms).filter(function (r) { return r.startsWith("document:"); });
1389
+ (0, bun_test_1.expect)(docRooms).toHaveLength(0);
1390
+ return [2 /*return*/];
1391
+ }
1392
+ });
1393
+ }); });
1394
+ (0, bun_test_1.it)("unsubscribe:query removes the query subscription and leaves the room", function () { return __awaiter(void 0, void 0, void 0, function () {
1395
+ var socket, subscribed, queryId;
1396
+ return __generator(this, function (_a) {
1397
+ switch (_a.label) {
1398
+ case 0:
1399
+ registerModelCollection();
1400
+ socket = createMockSocket({ id: "user1" });
1401
+ (0, realtimeApp_1.installRealtimeSocketHandlers)(socket);
1402
+ return [4 /*yield*/, socket.trigger("subscribe:query", { collection: "broadcasts", query: { priority: 1 } })];
1403
+ case 1:
1404
+ _a.sent();
1405
+ subscribed = socket.emitted.find(function (e) { return e.event === "query:subscribed"; });
1406
+ (0, bun_test_1.expect)(subscribed).toBeDefined();
1407
+ queryId = (subscribed === null || subscribed === void 0 ? void 0 : subscribed.payload).queryId;
1408
+ (0, bun_test_1.expect)(socket.rooms.has("query:".concat(queryId))).toBe(true);
1409
+ (0, bun_test_1.expect)((0, queryStore_1.getQuerySubscriptionsForCollection)("broadcasts").length).toBe(1);
1410
+ return [4 /*yield*/, socket.trigger("unsubscribe:query", { queryId: queryId })];
1411
+ case 2:
1412
+ _a.sent();
1413
+ (0, bun_test_1.expect)(socket.rooms.has("query:".concat(queryId))).toBe(false);
1414
+ (0, bun_test_1.expect)((0, queryStore_1.getQuerySubscriptionsForCollection)("broadcasts").length).toBe(0);
1415
+ return [2 /*return*/];
1416
+ }
1417
+ });
1418
+ }); });
1419
+ (0, bun_test_1.it)("unsubscribe:query ignores malformed payloads", function () { return __awaiter(void 0, void 0, void 0, function () {
1420
+ var socket;
1421
+ return __generator(this, function (_a) {
1422
+ switch (_a.label) {
1423
+ case 0:
1424
+ socket = createMockSocket({ id: "user1" });
1425
+ (0, realtimeApp_1.installRealtimeSocketHandlers)(socket);
1426
+ return [4 /*yield*/, socket.trigger("unsubscribe:query", null)];
1427
+ case 1:
1428
+ _a.sent();
1429
+ return [4 /*yield*/, socket.trigger("unsubscribe:query", {})];
1430
+ case 2:
1431
+ _a.sent();
1432
+ return [2 /*return*/];
1433
+ }
1434
+ });
1435
+ }); });
1436
+ (0, bun_test_1.it)("disconnect removes all query subscriptions for the socket", function () { return __awaiter(void 0, void 0, void 0, function () {
1437
+ var socket;
1438
+ return __generator(this, function (_a) {
1439
+ switch (_a.label) {
1440
+ case 0:
1441
+ registerOwnerCollection();
1442
+ socket = createMockSocket({ admin: true, id: "admin1" });
1443
+ (0, realtimeApp_1.installRealtimeSocketHandlers)(socket);
1444
+ return [4 /*yield*/, socket.trigger("subscribe:query", { collection: "todos", query: { priority: 1 } })];
1445
+ case 1:
1446
+ _a.sent();
1447
+ return [4 /*yield*/, socket.trigger("subscribe:query", { collection: "todos", query: { priority: 2 } })];
1448
+ case 2:
1449
+ _a.sent();
1450
+ (0, bun_test_1.expect)((0, queryStore_1.getQuerySubscriptionsForCollection)("todos").length).toBeGreaterThan(0);
1451
+ return [4 /*yield*/, socket.trigger("disconnect")];
1452
+ case 3:
1453
+ _a.sent();
1454
+ // The mock leaves rooms intact but the store should be cleared for this socket.
1455
+ // Other sockets aren't subscribed, so the store should be empty.
1456
+ (0, bun_test_1.expect)((0, queryStore_1.getQuerySubscriptionsForCollection)("todos").length).toBe(0);
1457
+ return [2 /*return*/];
1458
+ }
1459
+ });
1460
+ }); });
1461
+ });
1462
+ });
1463
+ // ─────────────────────────────────────────────────────────────────────────────
1464
+ // serializeDoc — responseHandler fallback for change stream events
1465
+ // ─────────────────────────────────────────────────────────────────────────────
1466
+ (0, bun_test_1.describe)("serializeDoc (change stream serializer)", function () {
1467
+ var makeEntry = function (overrides) {
1468
+ if (overrides === void 0) { overrides = {}; }
1469
+ return (__assign({ collectionName: "users", config: { methods: ["create", "update", "delete"], roomStrategy: "model" }, modelName: "User", options: {}, routePath: "/users" }, overrides));
1470
+ };
1471
+ (0, bun_test_1.it)("prefers realtimeResponseHandler when provided", function () { return __awaiter(void 0, void 0, void 0, function () {
1472
+ var entry, result;
1473
+ return __generator(this, function (_a) {
1474
+ switch (_a.label) {
1475
+ case 0:
1476
+ entry = makeEntry({
1477
+ config: {
1478
+ methods: ["update"],
1479
+ realtimeResponseHandler: function (doc) { return ({ customized: doc.name }); },
1480
+ roomStrategy: "model",
1481
+ },
1482
+ });
1483
+ return [4 /*yield*/, (0, changeStreamWatcher_1.serializeDoc)(entry, { name: "Alice", secret: "x" }, "update")];
1484
+ case 1:
1485
+ result = _a.sent();
1486
+ (0, bun_test_1.expect)(result).toEqual({ customized: "Alice" });
1487
+ return [2 /*return*/];
1488
+ }
1489
+ });
1490
+ }); });
1491
+ (0, bun_test_1.it)("falls back to modelRouter responseHandler when no realtime handler is set", function () { return __awaiter(void 0, void 0, void 0, function () {
1492
+ var responseHandler, entry, result;
1493
+ return __generator(this, function (_a) {
1494
+ switch (_a.label) {
1495
+ case 0:
1496
+ responseHandler = (0, bun_test_1.mock)(function (doc) { return __awaiter(void 0, void 0, void 0, function () {
1497
+ var hash, salt, rest;
1498
+ return __generator(this, function (_a) {
1499
+ hash = doc.hash, salt = doc.salt, rest = __rest(doc, ["hash", "salt"]);
1500
+ return [2 /*return*/, rest];
1501
+ });
1502
+ }); });
1503
+ entry = makeEntry({ options: { responseHandler: responseHandler } });
1504
+ return [4 /*yield*/, (0, changeStreamWatcher_1.serializeDoc)(entry, { email: "a@b.com", hash: "h", name: "Alice", salt: "s" }, "update")];
1505
+ case 1:
1506
+ result = _a.sent();
1507
+ (0, bun_test_1.expect)(result).toEqual({ email: "a@b.com", name: "Alice" });
1508
+ (0, bun_test_1.expect)(responseHandler).toHaveBeenCalled();
1509
+ return [2 /*return*/];
1510
+ }
1511
+ });
1512
+ }); });
1513
+ (0, bun_test_1.it)("maps 'delete' method to 'read' when invoking the REST responseHandler", function () { return __awaiter(void 0, void 0, void 0, function () {
1514
+ var observedMethod, responseHandler, entry;
1515
+ return __generator(this, function (_a) {
1516
+ switch (_a.label) {
1517
+ case 0:
1518
+ responseHandler = function (doc, method) { return __awaiter(void 0, void 0, void 0, function () {
1519
+ return __generator(this, function (_a) {
1520
+ observedMethod = method;
1521
+ return [2 /*return*/, doc];
1522
+ });
1523
+ }); };
1524
+ entry = makeEntry({ options: { responseHandler: responseHandler } });
1525
+ return [4 /*yield*/, (0, changeStreamWatcher_1.serializeDoc)(entry, { name: "Alice" }, "delete")];
1526
+ case 1:
1527
+ _a.sent();
1528
+ (0, bun_test_1.expect)(observedMethod).toBe("read");
1529
+ return [2 /*return*/];
1530
+ }
1531
+ });
1532
+ }); });
1533
+ (0, bun_test_1.it)("re-throws when modelRouter responseHandler throws (event dropped, no leak)", function () { return __awaiter(void 0, void 0, void 0, function () {
1534
+ var responseHandler, entry, doc;
1535
+ return __generator(this, function (_a) {
1536
+ switch (_a.label) {
1537
+ case 0:
1538
+ responseHandler = function () { return __awaiter(void 0, void 0, void 0, function () {
1539
+ return __generator(this, function (_a) {
1540
+ throw new Error("boom");
1541
+ });
1542
+ }); };
1543
+ entry = makeEntry({ options: { responseHandler: responseHandler } });
1544
+ doc = { hash: "h", name: "Alice", salt: "s", toJSON: function () { return ({ hash: "h", name: "Alice" }); } };
1545
+ return [4 /*yield*/, (0, bun_test_1.expect)((0, changeStreamWatcher_1.serializeDoc)(entry, doc, "update")).rejects.toThrow("boom")];
1546
+ case 1:
1547
+ _a.sent();
1548
+ return [2 /*return*/];
1549
+ }
1550
+ });
1551
+ }); });
1552
+ (0, bun_test_1.it)("re-throws when realtimeResponseHandler throws (event dropped, no leak)", function () { return __awaiter(void 0, void 0, void 0, function () {
1553
+ var entry, doc;
1554
+ return __generator(this, function (_a) {
1555
+ switch (_a.label) {
1556
+ case 0:
1557
+ entry = makeEntry({
1558
+ config: {
1559
+ methods: ["update"],
1560
+ realtimeResponseHandler: function () {
1561
+ throw new Error("boom");
1562
+ },
1563
+ roomStrategy: "model",
1564
+ },
1565
+ });
1566
+ doc = { name: "Alice", toJSON: function () { return ({ name: "Alice-json" }); } };
1567
+ return [4 /*yield*/, (0, bun_test_1.expect)((0, changeStreamWatcher_1.serializeDoc)(entry, doc, "update")).rejects.toThrow("boom")];
1568
+ case 1:
1569
+ _a.sent();
1570
+ return [2 /*return*/];
1571
+ }
1572
+ });
1573
+ }); });
1574
+ (0, bun_test_1.it)("returns toJSON output when no handlers are configured", function () { return __awaiter(void 0, void 0, void 0, function () {
1575
+ var entry, doc, result;
1576
+ return __generator(this, function (_a) {
1577
+ switch (_a.label) {
1578
+ case 0:
1579
+ entry = makeEntry();
1580
+ doc = { name: "Alice", toJSON: function () { return ({ id: "1", name: "Alice" }); } };
1581
+ return [4 /*yield*/, (0, changeStreamWatcher_1.serializeDoc)(entry, doc, "create")];
1582
+ case 1:
1583
+ result = _a.sent();
1584
+ (0, bun_test_1.expect)(result).toEqual({ id: "1", name: "Alice" });
1585
+ return [2 /*return*/];
1586
+ }
1587
+ });
1588
+ }); });
1589
+ (0, bun_test_1.it)("returns raw doc when toJSON is missing and no handlers configured", function () { return __awaiter(void 0, void 0, void 0, function () {
1590
+ var entry, result;
1591
+ return __generator(this, function (_a) {
1592
+ switch (_a.label) {
1593
+ case 0:
1594
+ entry = makeEntry();
1595
+ return [4 /*yield*/, (0, changeStreamWatcher_1.serializeDoc)(entry, { name: "Alice" }, "create")];
1596
+ case 1:
1597
+ result = _a.sent();
1598
+ (0, bun_test_1.expect)(result).toEqual({ name: "Alice" });
1599
+ return [2 /*return*/];
1600
+ }
1601
+ });
1602
+ }); });
1603
+ (0, bun_test_1.it)("adds id from _id when handlers omit it (change stream raw document shape)", function () { return __awaiter(void 0, void 0, void 0, function () {
1604
+ var entry, result;
1605
+ return __generator(this, function (_a) {
1606
+ switch (_a.label) {
1607
+ case 0:
1608
+ entry = makeEntry();
1609
+ return [4 /*yield*/, (0, changeStreamWatcher_1.serializeDoc)(entry, { _id: "507f1f77bcf86cd799439011", name: "Alice" }, "create")];
1610
+ case 1:
1611
+ result = _a.sent();
1612
+ (0, bun_test_1.expect)(result).toEqual({
1613
+ _id: "507f1f77bcf86cd799439011",
1614
+ id: "507f1f77bcf86cd799439011",
1615
+ name: "Alice",
1616
+ });
1617
+ return [2 /*return*/];
1618
+ }
1619
+ });
1620
+ }); });
1621
+ });
1622
+ // ─────────────────────────────────────────────────────────────────────────────
1623
+ // changeStreamWatcher — internal helpers
1624
+ // ─────────────────────────────────────────────────────────────────────────────
1625
+ (0, bun_test_1.describe)("mapOperationType", function () {
1626
+ (0, bun_test_1.it)("maps insert to create", function () {
1627
+ (0, bun_test_1.expect)((0, changeStreamWatcher_1.mapOperationType)("insert", {})).toBe("create");
1628
+ });
1629
+ (0, bun_test_1.it)("maps update to update by default", function () {
1630
+ (0, bun_test_1.expect)((0, changeStreamWatcher_1.mapOperationType)("update", {
1631
+ updateDescription: { updatedFields: { title: "x" } },
1632
+ })).toBe("update");
1633
+ });
1634
+ (0, bun_test_1.it)("maps replace to update", function () {
1635
+ (0, bun_test_1.expect)((0, changeStreamWatcher_1.mapOperationType)("replace", {})).toBe("update");
1636
+ });
1637
+ (0, bun_test_1.it)("maps update with deleted=true to delete (soft delete) when delete is enabled", function () {
1638
+ (0, bun_test_1.expect)((0, changeStreamWatcher_1.mapOperationType)("update", { updateDescription: { updatedFields: { deleted: true } } }, [
1639
+ "create",
1640
+ "update",
1641
+ "delete",
1642
+ ])).toBe("delete");
1643
+ });
1644
+ (0, bun_test_1.it)("keeps soft delete as update when delete is NOT in the enabled methods", function () {
1645
+ // A model configured with methods: ["create", "update"] must still see
1646
+ // soft-delete events as updates — otherwise they'd be silently dropped.
1647
+ (0, bun_test_1.expect)((0, changeStreamWatcher_1.mapOperationType)("update", { updateDescription: { updatedFields: { deleted: true } } }, [
1648
+ "create",
1649
+ "update",
1650
+ ])).toBe("update");
1651
+ });
1652
+ (0, bun_test_1.it)("maps delete to delete", function () {
1653
+ (0, bun_test_1.expect)((0, changeStreamWatcher_1.mapOperationType)("delete", {})).toBe("delete");
1654
+ });
1655
+ (0, bun_test_1.it)("returns null for unknown operation types", function () {
1656
+ (0, bun_test_1.expect)((0, changeStreamWatcher_1.mapOperationType)("invalidate", {})).toBeNull();
1657
+ (0, bun_test_1.expect)((0, changeStreamWatcher_1.mapOperationType)("drop", {})).toBeNull();
1658
+ });
1659
+ });
1660
+ (0, bun_test_1.describe)("resolveRooms", function () {
1661
+ var baseEntry = {
1662
+ collectionName: "todos",
1663
+ config: { methods: ["create", "update", "delete"] },
1664
+ modelName: "Todo",
1665
+ options: {},
1666
+ routePath: "/todos",
1667
+ };
1668
+ (0, bun_test_1.it)("returns user-specific room for owner strategy with ownerId", function () {
1669
+ var entry = __assign(__assign({}, baseEntry), { config: __assign(__assign({}, baseEntry.config), { roomStrategy: "owner" }) });
1670
+ var rooms = (0, changeStreamWatcher_1.resolveRooms)(entry, { ownerId: "user-1" }, "create");
1671
+ (0, bun_test_1.expect)(rooms).toEqual(["user:user-1"]);
1672
+ });
1673
+ (0, bun_test_1.it)("converts ObjectId-like ownerId to string", function () {
1674
+ var entry = __assign(__assign({}, baseEntry), { config: __assign(__assign({}, baseEntry.config), { roomStrategy: "owner" }) });
1675
+ var ownerId = { toString: function () { return "owner-from-obj"; } };
1676
+ var rooms = (0, changeStreamWatcher_1.resolveRooms)(entry, { ownerId: ownerId }, "create");
1677
+ (0, bun_test_1.expect)(rooms).toEqual(["user:owner-from-obj"]);
1678
+ });
1679
+ (0, bun_test_1.it)("falls back to model room when owner strategy has no ownerId", function () {
1680
+ var entry = __assign(__assign({}, baseEntry), { config: __assign(__assign({}, baseEntry.config), { roomStrategy: "owner" }) });
1681
+ var rooms = (0, changeStreamWatcher_1.resolveRooms)(entry, {}, "create");
1682
+ (0, bun_test_1.expect)(rooms).toEqual(["model:todos"]);
1683
+ });
1684
+ (0, bun_test_1.it)("returns model room for model strategy", function () {
1685
+ var entry = __assign(__assign({}, baseEntry), { config: __assign(__assign({}, baseEntry.config), { roomStrategy: "model" }) });
1686
+ var rooms = (0, changeStreamWatcher_1.resolveRooms)(entry, {}, "create");
1687
+ (0, bun_test_1.expect)(rooms).toEqual(["model:todos"]);
1688
+ });
1689
+ (0, bun_test_1.it)("returns authenticated room for broadcast strategy", function () {
1690
+ var entry = __assign(__assign({}, baseEntry), { config: __assign(__assign({}, baseEntry.config), { roomStrategy: "broadcast" }) });
1691
+ var rooms = (0, changeStreamWatcher_1.resolveRooms)(entry, {}, "create");
1692
+ (0, bun_test_1.expect)(rooms).toEqual(["authenticated"]);
1693
+ });
1694
+ (0, bun_test_1.it)("defaults to model room for unknown strategy", function () {
1695
+ var entry = __assign(__assign({}, baseEntry), { config: __assign(__assign({}, baseEntry.config), { roomStrategy: "unknown" }) });
1696
+ var rooms = (0, changeStreamWatcher_1.resolveRooms)(entry, {}, "create");
1697
+ (0, bun_test_1.expect)(rooms).toEqual(["model:todos"]);
1698
+ });
1699
+ (0, bun_test_1.it)("invokes custom function room resolver", function () {
1700
+ var entry = __assign(__assign({}, baseEntry), { config: __assign(__assign({}, baseEntry.config), { roomStrategy: function (doc, method) { return ["custom:".concat(method, ":").concat(doc.id)]; } }) });
1701
+ var rooms = (0, changeStreamWatcher_1.resolveRooms)(entry, { id: "42" }, "update");
1702
+ (0, bun_test_1.expect)(rooms).toEqual(["custom:update:42"]);
1703
+ });
1704
+ });
1705
+ (0, bun_test_1.describe)("emitToDocumentAndQueryRooms", function () {
1706
+ var permissiveOptions = {
1707
+ permissions: {
1708
+ create: [function () { return true; }],
1709
+ delete: [function () { return true; }],
1710
+ list: [function () { return true; }],
1711
+ read: [function () { return true; }],
1712
+ update: [function () { return true; }],
1713
+ },
1714
+ };
1715
+ var makeIo = function () {
1716
+ var emissions = [];
1717
+ var roomSockets = new Map();
1718
+ var sockets = new Map();
1719
+ var nextSocketId = 1;
1720
+ var addSocketToRoom = function (room, decodedToken) {
1721
+ var _a;
1722
+ if (decodedToken === void 0) { decodedToken = { admin: true, id: "admin" }; }
1723
+ var socketId = "socket-".concat(nextSocketId);
1724
+ nextSocketId += 1;
1725
+ if (!roomSockets.has(room)) {
1726
+ roomSockets.set(room, new Set());
1727
+ }
1728
+ (_a = roomSockets.get(room)) === null || _a === void 0 ? void 0 : _a.add(socketId);
1729
+ sockets.set(socketId, {
1730
+ decodedToken: decodedToken,
1731
+ emit: function (event, payload) {
1732
+ emissions.push({ event: event, payload: payload, room: room });
1733
+ },
1734
+ id: socketId,
1735
+ });
1736
+ };
1737
+ var io = {
1738
+ sockets: {
1739
+ adapter: { rooms: roomSockets },
1740
+ sockets: sockets,
1741
+ },
1742
+ to: function (room) { return ({
1743
+ emit: function (event, payload) {
1744
+ emissions.push({ event: event, payload: payload, room: room });
1745
+ },
1746
+ }); },
1747
+ };
1748
+ return { addSocketToRoom: addSocketToRoom, emissions: emissions, io: io };
1749
+ };
1750
+ (0, bun_test_1.beforeEach)(function () {
1751
+ (0, queryStore_1.clearQueryStore)();
1752
+ });
1753
+ (0, bun_test_1.afterEach)(function () {
1754
+ (0, queryStore_1.clearQueryStore)();
1755
+ });
1756
+ (0, bun_test_1.it)("emits to the document room", function () { return __awaiter(void 0, void 0, void 0, function () {
1757
+ var _a, emissions, io, event;
1758
+ return __generator(this, function (_b) {
1759
+ switch (_b.label) {
1760
+ case 0:
1761
+ _a = makeIo(), emissions = _a.emissions, io = _a.io;
1762
+ event = {
1763
+ collection: "todos",
1764
+ id: "doc-1",
1765
+ method: "update",
1766
+ model: "Todo",
1767
+ timestamp: 1,
1768
+ };
1769
+ return [4 /*yield*/, (0, changeStreamWatcher_1.emitToDocumentAndQueryRooms)(io, "todos", event, {}, function () { })];
1770
+ case 1:
1771
+ _b.sent();
1772
+ (0, bun_test_1.expect)(emissions.some(function (e) { return e.room === "document:todos:doc-1"; })).toBe(true);
1773
+ return [2 /*return*/];
1774
+ }
1775
+ });
1776
+ }); });
1777
+ (0, bun_test_1.it)("forwards hard deletes to every query room for non-owner strategies", function () { return __awaiter(void 0, void 0, void 0, function () {
1778
+ var queryId, _a, addSocketToRoom, emissions, io, event, entry;
1779
+ return __generator(this, function (_b) {
1780
+ switch (_b.label) {
1781
+ case 0:
1782
+ queryId = (0, queryStore_1.computeQueryId)("todos", { priority: 1 });
1783
+ (0, queryStore_1.addQuerySubscription)("socket-a", "todos", { priority: 1 }, queryId);
1784
+ _a = makeIo(), addSocketToRoom = _a.addSocketToRoom, emissions = _a.emissions, io = _a.io;
1785
+ addSocketToRoom("query:".concat(queryId));
1786
+ event = {
1787
+ collection: "todos",
1788
+ id: "doc-1",
1789
+ method: "delete",
1790
+ model: "Todo",
1791
+ timestamp: 1,
1792
+ };
1793
+ entry = {
1794
+ collectionName: "todos",
1795
+ config: { methods: ["delete"], roomStrategy: "model" },
1796
+ modelName: "Todo",
1797
+ options: permissiveOptions,
1798
+ routePath: "/todos",
1799
+ };
1800
+ return [4 /*yield*/, (0, changeStreamWatcher_1.emitToDocumentAndQueryRooms)(io, "todos", event, undefined, function () { }, entry)];
1801
+ case 1:
1802
+ _b.sent();
1803
+ (0, bun_test_1.expect)(emissions.some(function (e) { return e.room === "query:".concat(queryId) && e.event === "sync"; })).toBe(true);
1804
+ return [2 /*return*/];
1805
+ }
1806
+ });
1807
+ }); });
1808
+ (0, bun_test_1.it)("does NOT forward hard deletes to query rooms for owner-strategy collections", function () { return __awaiter(void 0, void 0, void 0, function () {
1809
+ var queryId, _a, addSocketToRoom, emissions, io, event, entry;
1810
+ return __generator(this, function (_b) {
1811
+ switch (_b.label) {
1812
+ case 0:
1813
+ queryId = (0, queryStore_1.computeQueryId)("todos", { ownerId: "user-1" });
1814
+ (0, queryStore_1.addQuerySubscription)("socket-a", "todos", { ownerId: "user-1" }, queryId);
1815
+ _a = makeIo(), addSocketToRoom = _a.addSocketToRoom, emissions = _a.emissions, io = _a.io;
1816
+ addSocketToRoom("document:todos:doc-1");
1817
+ addSocketToRoom("query:".concat(queryId));
1818
+ event = {
1819
+ collection: "todos",
1820
+ id: "doc-1",
1821
+ method: "delete",
1822
+ model: "Todo",
1823
+ timestamp: 1,
1824
+ };
1825
+ entry = {
1826
+ collectionName: "todos",
1827
+ config: { methods: ["delete"], roomStrategy: "owner" },
1828
+ modelName: "Todo",
1829
+ options: permissiveOptions,
1830
+ routePath: "/todos",
1831
+ };
1832
+ return [4 /*yield*/, (0, changeStreamWatcher_1.emitToDocumentAndQueryRooms)(io, "todos", event, undefined, function () { }, entry)];
1833
+ case 1:
1834
+ _b.sent();
1835
+ // Document room still receives the event, but query rooms must not — otherwise users
1836
+ // would see deletes for docs they don't own.
1837
+ (0, bun_test_1.expect)(emissions.some(function (e) { return e.room === "query:".concat(queryId); })).toBe(false);
1838
+ (0, bun_test_1.expect)(emissions.some(function (e) { return e.room === "document:todos:doc-1"; })).toBe(true);
1839
+ return [2 /*return*/];
1840
+ }
1841
+ });
1842
+ }); });
1843
+ (0, bun_test_1.it)("forwards soft deletes only to query rooms whose filter the document satisfies", function () { return __awaiter(void 0, void 0, void 0, function () {
1844
+ var matchingQueryId, nonMatchingQueryId, _a, addSocketToRoom, emissions, io, event, entry;
1845
+ return __generator(this, function (_b) {
1846
+ switch (_b.label) {
1847
+ case 0:
1848
+ matchingQueryId = (0, queryStore_1.computeQueryId)("todos", { priority: 1 });
1849
+ nonMatchingQueryId = (0, queryStore_1.computeQueryId)("todos", { priority: 9 });
1850
+ (0, queryStore_1.addQuerySubscription)("socket-a", "todos", { priority: 1 }, matchingQueryId);
1851
+ (0, queryStore_1.addQuerySubscription)("socket-b", "todos", { priority: 9 }, nonMatchingQueryId);
1852
+ _a = makeIo(), addSocketToRoom = _a.addSocketToRoom, emissions = _a.emissions, io = _a.io;
1853
+ addSocketToRoom("query:".concat(matchingQueryId));
1854
+ addSocketToRoom("query:".concat(nonMatchingQueryId));
1855
+ event = {
1856
+ collection: "todos",
1857
+ data: { deleted: true, priority: 1 },
1858
+ id: "doc-1",
1859
+ method: "delete",
1860
+ model: "Todo",
1861
+ timestamp: 1,
1862
+ };
1863
+ entry = {
1864
+ collectionName: "todos",
1865
+ config: { methods: ["delete"], roomStrategy: "owner" },
1866
+ modelName: "Todo",
1867
+ options: permissiveOptions,
1868
+ routePath: "/todos",
1869
+ };
1870
+ return [4 /*yield*/, (0, changeStreamWatcher_1.emitToDocumentAndQueryRooms)(io, "todos", event, { deleted: true, priority: 1 }, function () { }, entry)];
1871
+ case 1:
1872
+ _b.sent();
1873
+ (0, bun_test_1.expect)(emissions.some(function (e) { return e.room === "query:".concat(matchingQueryId); })).toBe(true);
1874
+ (0, bun_test_1.expect)(emissions.some(function (e) { return e.room === "query:".concat(nonMatchingQueryId); })).toBe(false);
1875
+ return [2 /*return*/];
1876
+ }
1877
+ });
1878
+ }); });
1879
+ (0, bun_test_1.it)("skips matching when fullDocument is missing on non-delete events", function () { return __awaiter(void 0, void 0, void 0, function () {
1880
+ var queryId, _a, emissions, io, event;
1881
+ return __generator(this, function (_b) {
1882
+ switch (_b.label) {
1883
+ case 0:
1884
+ queryId = (0, queryStore_1.computeQueryId)("todos", { priority: 1 });
1885
+ (0, queryStore_1.addQuerySubscription)("socket-a", "todos", { priority: 1 }, queryId);
1886
+ _a = makeIo(), emissions = _a.emissions, io = _a.io;
1887
+ event = {
1888
+ collection: "todos",
1889
+ id: "doc-1",
1890
+ method: "create",
1891
+ model: "Todo",
1892
+ timestamp: 1,
1893
+ };
1894
+ return [4 /*yield*/, (0, changeStreamWatcher_1.emitToDocumentAndQueryRooms)(io, "todos", event, undefined, function () { })];
1895
+ case 1:
1896
+ _b.sent();
1897
+ (0, bun_test_1.expect)(emissions.some(function (e) { return e.room === "query:".concat(queryId); })).toBe(false);
1898
+ return [2 /*return*/];
1899
+ }
1900
+ });
1901
+ }); });
1902
+ (0, bun_test_1.it)("emits create events to query rooms when the doc matches", function () { return __awaiter(void 0, void 0, void 0, function () {
1903
+ var queryId, _a, emissions, io, event, queryEmissions;
1904
+ return __generator(this, function (_b) {
1905
+ switch (_b.label) {
1906
+ case 0:
1907
+ queryId = (0, queryStore_1.computeQueryId)("todos", { priority: 1 });
1908
+ (0, queryStore_1.addQuerySubscription)("socket-a", "todos", { priority: 1 }, queryId);
1909
+ _a = makeIo(), emissions = _a.emissions, io = _a.io;
1910
+ event = {
1911
+ collection: "todos",
1912
+ id: "doc-1",
1913
+ method: "create",
1914
+ model: "Todo",
1915
+ timestamp: 1,
1916
+ };
1917
+ return [4 /*yield*/, (0, changeStreamWatcher_1.emitToDocumentAndQueryRooms)(io, "todos", event, { priority: 1 }, function () { })];
1918
+ case 1:
1919
+ _b.sent();
1920
+ queryEmissions = emissions.filter(function (e) { return e.room === "query:".concat(queryId); });
1921
+ (0, bun_test_1.expect)(queryEmissions.length).toBe(1);
1922
+ (0, bun_test_1.expect)(queryEmissions[0].payload).toMatchObject({ method: "create" });
1923
+ return [2 /*return*/];
1924
+ }
1925
+ });
1926
+ }); });
1927
+ (0, bun_test_1.it)("does not emit create events to query rooms when the doc does not match", function () { return __awaiter(void 0, void 0, void 0, function () {
1928
+ var queryId, _a, emissions, io, event;
1929
+ return __generator(this, function (_b) {
1930
+ switch (_b.label) {
1931
+ case 0:
1932
+ queryId = (0, queryStore_1.computeQueryId)("todos", { priority: 1 });
1933
+ (0, queryStore_1.addQuerySubscription)("socket-a", "todos", { priority: 1 }, queryId);
1934
+ _a = makeIo(), emissions = _a.emissions, io = _a.io;
1935
+ event = {
1936
+ collection: "todos",
1937
+ id: "doc-1",
1938
+ method: "create",
1939
+ model: "Todo",
1940
+ timestamp: 1,
1941
+ };
1942
+ return [4 /*yield*/, (0, changeStreamWatcher_1.emitToDocumentAndQueryRooms)(io, "todos", event, { priority: 9 }, function () { })];
1943
+ case 1:
1944
+ _b.sent();
1945
+ (0, bun_test_1.expect)(emissions.some(function (e) { return e.room === "query:".concat(queryId); })).toBe(false);
1946
+ return [2 /*return*/];
1947
+ }
1948
+ });
1949
+ }); });
1950
+ (0, bun_test_1.it)("emits update as-is when the document still matches the query", function () { return __awaiter(void 0, void 0, void 0, function () {
1951
+ var queryId, _a, emissions, io, event, queryEmissions;
1952
+ return __generator(this, function (_b) {
1953
+ switch (_b.label) {
1954
+ case 0:
1955
+ queryId = (0, queryStore_1.computeQueryId)("todos", { priority: 1 });
1956
+ (0, queryStore_1.addQuerySubscription)("socket-a", "todos", { priority: 1 }, queryId);
1957
+ _a = makeIo(), emissions = _a.emissions, io = _a.io;
1958
+ event = {
1959
+ collection: "todos",
1960
+ id: "doc-1",
1961
+ method: "update",
1962
+ model: "Todo",
1963
+ timestamp: 1,
1964
+ };
1965
+ return [4 /*yield*/, (0, changeStreamWatcher_1.emitToDocumentAndQueryRooms)(io, "todos", event, { priority: 1 }, function () { })];
1966
+ case 1:
1967
+ _b.sent();
1968
+ queryEmissions = emissions.filter(function (e) { return e.room === "query:".concat(queryId); });
1969
+ (0, bun_test_1.expect)(queryEmissions.length).toBe(1);
1970
+ (0, bun_test_1.expect)(queryEmissions[0].payload).toMatchObject({ method: "update" });
1971
+ return [2 /*return*/];
1972
+ }
1973
+ });
1974
+ }); });
1975
+ (0, bun_test_1.it)("converts updates that no longer match into delete events for the query", function () { return __awaiter(void 0, void 0, void 0, function () {
1976
+ var queryId, _a, emissions, io, event, queryEmissions;
1977
+ return __generator(this, function (_b) {
1978
+ switch (_b.label) {
1979
+ case 0:
1980
+ queryId = (0, queryStore_1.computeQueryId)("todos", { priority: 1 });
1981
+ (0, queryStore_1.addQuerySubscription)("socket-a", "todos", { priority: 1 }, queryId);
1982
+ _a = makeIo(), emissions = _a.emissions, io = _a.io;
1983
+ event = {
1984
+ collection: "todos",
1985
+ id: "doc-1",
1986
+ method: "update",
1987
+ model: "Todo",
1988
+ timestamp: 1,
1989
+ };
1990
+ return [4 /*yield*/, (0, changeStreamWatcher_1.emitToDocumentAndQueryRooms)(io, "todos", event, { priority: 9 }, function () { })];
1991
+ case 1:
1992
+ _b.sent();
1993
+ queryEmissions = emissions.filter(function (e) { return e.room === "query:".concat(queryId); });
1994
+ (0, bun_test_1.expect)(queryEmissions.length).toBe(1);
1995
+ (0, bun_test_1.expect)(queryEmissions[0].payload).toMatchObject({ method: "delete" });
1996
+ return [2 /*return*/];
1997
+ }
1998
+ });
1999
+ }); });
2000
+ (0, bun_test_1.it)("filters socket emissions by object read permission and responseHandler", function () { return __awaiter(void 0, void 0, void 0, function () {
2001
+ var _a, addSocketToRoom, emissions, io, entry;
2002
+ return __generator(this, function (_b) {
2003
+ switch (_b.label) {
2004
+ case 0:
2005
+ _a = makeIo(), addSocketToRoom = _a.addSocketToRoom, emissions = _a.emissions, io = _a.io;
2006
+ addSocketToRoom("model:todos", { id: "owner-1" });
2007
+ addSocketToRoom("model:todos", { id: "other-user" });
2008
+ entry = {
2009
+ collectionName: "todos",
2010
+ config: { methods: ["update"], roomStrategy: "model" },
2011
+ modelName: "Todo",
2012
+ options: {
2013
+ permissions: {
2014
+ create: [function () { return true; }],
2015
+ delete: [function () { return true; }],
2016
+ list: [function () { return true; }],
2017
+ read: [
2018
+ function (_method, user, obj) {
2019
+ return (user === null || user === void 0 ? void 0 : user.admin) === true || (user === null || user === void 0 ? void 0 : user.id) === (obj === null || obj === void 0 ? void 0 : obj.ownerId);
2020
+ },
2021
+ ],
2022
+ update: [function () { return true; }],
2023
+ },
2024
+ responseHandler: function (doc, _method, req) {
2025
+ var _a;
2026
+ return ({
2027
+ id: doc._id,
2028
+ title: doc.title,
2029
+ visibleTo: (_a = req.user) === null || _a === void 0 ? void 0 : _a.id,
2030
+ });
2031
+ },
2032
+ },
2033
+ routePath: "/todos",
2034
+ };
2035
+ return [4 /*yield*/, (0, changeStreamWatcher_1.emitToAuthorizedRoom)(io, "model:todos", {
2036
+ collection: "todos",
2037
+ id: "todo-1",
2038
+ method: "update",
2039
+ model: "Todo",
2040
+ timestamp: 1,
2041
+ }, entry, { _id: "todo-1", ownerId: "owner-1", secret: "hidden", title: "Visible" }, function () { })];
2042
+ case 1:
2043
+ _b.sent();
2044
+ (0, bun_test_1.expect)(emissions).toHaveLength(1);
2045
+ (0, bun_test_1.expect)(emissions[0].payload).toMatchObject({
2046
+ data: { id: "todo-1", title: "Visible", visibleTo: "owner-1" },
2047
+ });
2048
+ return [2 /*return*/];
2049
+ }
2050
+ });
2051
+ }); });
2052
+ (0, bun_test_1.it)("continues emitting to other sockets when per-socket serialization fails", function () { return __awaiter(void 0, void 0, void 0, function () {
2053
+ var _a, addSocketToRoom, emissions, io, entry;
2054
+ return __generator(this, function (_b) {
2055
+ switch (_b.label) {
2056
+ case 0:
2057
+ _a = makeIo(), addSocketToRoom = _a.addSocketToRoom, emissions = _a.emissions, io = _a.io;
2058
+ addSocketToRoom("model:todos", { id: "bad-user" });
2059
+ addSocketToRoom("model:todos", { id: "good-user" });
2060
+ entry = {
2061
+ collectionName: "todos",
2062
+ config: { methods: ["update"], roomStrategy: "model" },
2063
+ modelName: "Todo",
2064
+ options: {
2065
+ permissions: {
2066
+ create: [function () { return true; }],
2067
+ delete: [function () { return true; }],
2068
+ list: [function () { return true; }],
2069
+ read: [function () { return true; }],
2070
+ update: [function () { return true; }],
2071
+ },
2072
+ responseHandler: function (doc, _method, req) {
2073
+ var _a, _b;
2074
+ if (((_a = req.user) === null || _a === void 0 ? void 0 : _a.id) === "bad-user") {
2075
+ throw new Error("cannot serialize for bad user");
2076
+ }
2077
+ return __assign(__assign({}, doc), { visibleTo: (_b = req.user) === null || _b === void 0 ? void 0 : _b.id });
2078
+ },
2079
+ },
2080
+ routePath: "/todos",
2081
+ };
2082
+ return [4 /*yield*/, (0, changeStreamWatcher_1.emitToAuthorizedRoom)(io, "model:todos", {
2083
+ collection: "todos",
2084
+ id: "todo-1",
2085
+ method: "update",
2086
+ model: "Todo",
2087
+ timestamp: 1,
2088
+ }, entry, { _id: "todo-1", title: "Visible" }, function () { })];
2089
+ case 1:
2090
+ _b.sent();
2091
+ (0, bun_test_1.expect)(emissions).toHaveLength(1);
2092
+ (0, bun_test_1.expect)(emissions[0].payload).toMatchObject({
2093
+ data: { id: "todo-1", title: "Visible", visibleTo: "good-user" },
2094
+ });
2095
+ return [2 /*return*/];
2096
+ }
2097
+ });
2098
+ }); });
2099
+ (0, bun_test_1.it)("does not emit hard delete metadata when read permission requires an object owner", function () { return __awaiter(void 0, void 0, void 0, function () {
2100
+ var _a, addSocketToRoom, emissions, io, entry;
2101
+ return __generator(this, function (_b) {
2102
+ switch (_b.label) {
2103
+ case 0:
2104
+ _a = makeIo(), addSocketToRoom = _a.addSocketToRoom, emissions = _a.emissions, io = _a.io;
2105
+ addSocketToRoom("model:todos", { id: "other-user" });
2106
+ entry = {
2107
+ collectionName: "todos",
2108
+ config: { methods: ["delete"], roomStrategy: "model" },
2109
+ modelName: "Todo",
2110
+ options: {
2111
+ permissions: {
2112
+ create: [function () { return true; }],
2113
+ delete: [function () { return true; }],
2114
+ list: [function () { return true; }],
2115
+ read: [
2116
+ function (_method, user, obj) {
2117
+ return (user === null || user === void 0 ? void 0 : user.admin) === true || (user === null || user === void 0 ? void 0 : user.id) === (obj === null || obj === void 0 ? void 0 : obj.ownerId);
2118
+ },
2119
+ ],
2120
+ update: [function () { return true; }],
2121
+ },
2122
+ },
2123
+ routePath: "/todos",
2124
+ };
2125
+ return [4 /*yield*/, (0, changeStreamWatcher_1.emitToAuthorizedRoom)(io, "model:todos", {
2126
+ collection: "todos",
2127
+ id: "todo-1",
2128
+ method: "delete",
2129
+ model: "Todo",
2130
+ timestamp: 1,
2131
+ }, entry, undefined, function () { })];
2132
+ case 1:
2133
+ _b.sent();
2134
+ (0, bun_test_1.expect)(emissions).toEqual([]);
2135
+ return [2 /*return*/];
2136
+ }
2137
+ });
2138
+ }); });
2139
+ });
2140
+ // ─────────────────────────────────────────────────────────────────────────────
2141
+ // redactCredentials — Redis URL logging
2142
+ // ─────────────────────────────────────────────────────────────────────────────
2143
+ (0, bun_test_1.describe)("redactCredentials", function () {
2144
+ (0, bun_test_1.it)("redacts user:password@ in a redis URL", function () {
2145
+ (0, bun_test_1.expect)((0, realtimeApp_1.redactCredentials)("redis://user:secret@host:6379/0")).toBe("redis://***@host:6379/0");
2146
+ });
2147
+ (0, bun_test_1.it)("redacts password-only userinfo", function () {
2148
+ (0, bun_test_1.expect)((0, realtimeApp_1.redactCredentials)("redis://:secret@host:6379")).toBe("redis://***@host:6379");
2149
+ });
2150
+ (0, bun_test_1.it)("returns the URL unchanged when there are no credentials", function () {
2151
+ (0, bun_test_1.expect)((0, realtimeApp_1.redactCredentials)("redis://host:6379/0")).toBe("redis://host:6379/0");
2152
+ });
2153
+ (0, bun_test_1.it)("falls back to regex replacement on unparsable URLs", function () {
2154
+ // Some non-URL strings still match the userinfo regex.
2155
+ (0, bun_test_1.expect)((0, realtimeApp_1.redactCredentials)("rediss://u:p@example.com")).toContain("***@");
2156
+ (0, bun_test_1.expect)((0, realtimeApp_1.redactCredentials)("rediss://u:p@example.com")).not.toContain("u:p");
2157
+ });
2158
+ });