@terreno/rtk 0.13.3 → 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 (144) hide show
  1. package/dist/authSlice.test.js +2 -10
  2. package/dist/authSlice.test.js.map +1 -1
  3. package/dist/authSliceNative.test.js +11 -3
  4. package/dist/authSliceNative.test.js.map +1 -1
  5. package/dist/betterAuthSlice.test.js +1 -25
  6. package/dist/betterAuthSlice.test.js.map +1 -1
  7. package/dist/constants.d.ts +3 -0
  8. package/dist/constants.d.ts.map +1 -1
  9. package/dist/constants.js +11 -3
  10. package/dist/constants.js.map +1 -1
  11. package/dist/constants.test.js +44 -2
  12. package/dist/constants.test.js.map +1 -1
  13. package/dist/emptyApi.d.ts +2 -0
  14. package/dist/emptyApi.d.ts.map +1 -1
  15. package/dist/emptyApi.js +5 -0
  16. package/dist/emptyApi.js.map +1 -1
  17. package/dist/index.d.ts +8 -0
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +8 -0
  20. package/dist/index.js.map +1 -1
  21. package/dist/isolated/constants.isolated.js +7 -1
  22. package/dist/isolated/constants.isolated.js.map +1 -1
  23. package/dist/isolated/offlineMiddleware.isolated.d.ts +2 -0
  24. package/dist/isolated/offlineMiddleware.isolated.d.ts.map +1 -0
  25. package/dist/isolated/offlineMiddleware.isolated.js +516 -0
  26. package/dist/isolated/offlineMiddleware.isolated.js.map +1 -0
  27. package/dist/isolated/offlineMiddleware.native.isolated.d.ts +2 -0
  28. package/dist/isolated/offlineMiddleware.native.isolated.d.ts.map +1 -0
  29. package/dist/isolated/offlineMiddleware.native.isolated.js +70 -0
  30. package/dist/isolated/offlineMiddleware.native.isolated.js.map +1 -0
  31. package/dist/isolated/useUpgradeCheck.mobile.isolated.d.ts +2 -0
  32. package/dist/isolated/useUpgradeCheck.mobile.isolated.d.ts.map +1 -0
  33. package/dist/{useUpgradeCheck.mobile.test.js → isolated/useUpgradeCheck.mobile.isolated.js} +13 -5
  34. package/dist/isolated/useUpgradeCheck.mobile.isolated.js.map +1 -0
  35. package/dist/isolated/useUpgradeCheck.web.isolated.d.ts +2 -0
  36. package/dist/isolated/useUpgradeCheck.web.isolated.d.ts.map +1 -0
  37. package/dist/{useUpgradeCheck.test.js → isolated/useUpgradeCheck.web.isolated.js} +37 -7
  38. package/dist/isolated/useUpgradeCheck.web.isolated.js.map +1 -0
  39. package/dist/mongooseSlice.test.js +1 -1
  40. package/dist/mongooseSlice.test.js.map +1 -1
  41. package/dist/offlineGate.d.ts +8 -0
  42. package/dist/offlineGate.d.ts.map +1 -0
  43. package/dist/offlineGate.js +20 -0
  44. package/dist/offlineGate.js.map +1 -0
  45. package/dist/offlineGate.test.d.ts +2 -0
  46. package/dist/offlineGate.test.d.ts.map +1 -0
  47. package/dist/offlineGate.test.js +46 -0
  48. package/dist/offlineGate.test.js.map +1 -0
  49. package/dist/offlineMiddleware.d.ts +46 -0
  50. package/dist/offlineMiddleware.d.ts.map +1 -0
  51. package/dist/offlineMiddleware.js +591 -0
  52. package/dist/offlineMiddleware.js.map +1 -0
  53. package/dist/offlineMiddleware.test.d.ts +2 -0
  54. package/dist/offlineMiddleware.test.d.ts.map +1 -0
  55. package/dist/offlineMiddleware.test.js +145 -0
  56. package/dist/offlineMiddleware.test.js.map +1 -0
  57. package/dist/offlineSlice.d.ts +232 -0
  58. package/dist/offlineSlice.d.ts.map +1 -0
  59. package/dist/offlineSlice.js +65 -0
  60. package/dist/offlineSlice.js.map +1 -0
  61. package/dist/offlineSlice.test.d.ts +2 -0
  62. package/dist/offlineSlice.test.d.ts.map +1 -0
  63. package/dist/offlineSlice.test.js +153 -0
  64. package/dist/offlineSlice.test.js.map +1 -0
  65. package/dist/realtime.d.ts +123 -0
  66. package/dist/realtime.d.ts.map +1 -0
  67. package/dist/realtime.js +299 -0
  68. package/dist/realtime.js.map +1 -0
  69. package/dist/realtime.test.d.ts +2 -0
  70. package/dist/realtime.test.d.ts.map +1 -0
  71. package/dist/realtime.test.js +339 -0
  72. package/dist/realtime.test.js.map +1 -0
  73. package/dist/socket.d.ts.map +1 -1
  74. package/dist/socket.js +29 -28
  75. package/dist/socket.js.map +1 -1
  76. package/dist/sync.d.ts +35 -0
  77. package/dist/sync.d.ts.map +1 -0
  78. package/dist/sync.js +205 -0
  79. package/dist/sync.js.map +1 -0
  80. package/dist/tagGenerator.d.ts +20 -1
  81. package/dist/tagGenerator.d.ts.map +1 -1
  82. package/dist/tagGenerator.js +1 -2
  83. package/dist/tagGenerator.js.map +1 -1
  84. package/dist/useOfflineStatus.d.ts +33 -0
  85. package/dist/useOfflineStatus.d.ts.map +1 -0
  86. package/dist/useOfflineStatus.js +43 -0
  87. package/dist/useOfflineStatus.js.map +1 -0
  88. package/dist/useOfflineStatus.test.d.ts +2 -0
  89. package/dist/useOfflineStatus.test.d.ts.map +1 -0
  90. package/dist/useOfflineStatus.test.js +119 -0
  91. package/dist/useOfflineStatus.test.js.map +1 -0
  92. package/dist/useRealtimeDebug.d.ts +6 -0
  93. package/dist/useRealtimeDebug.d.ts.map +1 -0
  94. package/dist/useRealtimeDebug.js +41 -0
  95. package/dist/useRealtimeDebug.js.map +1 -0
  96. package/dist/useServerStatus.d.ts +29 -0
  97. package/dist/useServerStatus.d.ts.map +1 -0
  98. package/dist/useServerStatus.js +89 -0
  99. package/dist/useServerStatus.js.map +1 -0
  100. package/dist/useUpgradeCheck.d.ts +7 -3
  101. package/dist/useUpgradeCheck.d.ts.map +1 -1
  102. package/dist/useUpgradeCheck.js +27 -13
  103. package/dist/useUpgradeCheck.js.map +1 -1
  104. package/package.json +2 -2
  105. package/src/authSlice.test.ts +2 -11
  106. package/src/authSliceNative.test.ts +11 -4
  107. package/src/betterAuthSlice.test.ts +1 -25
  108. package/src/constants.test.ts +54 -1
  109. package/src/constants.ts +14 -3
  110. package/src/emptyApi.ts +8 -0
  111. package/src/index.ts +8 -0
  112. package/src/isolated/constants.isolated.ts +7 -3
  113. package/src/isolated/offlineMiddleware.isolated.ts +696 -0
  114. package/src/isolated/offlineMiddleware.native.isolated.ts +80 -0
  115. package/src/{useUpgradeCheck.mobile.test.ts → isolated/useUpgradeCheck.mobile.isolated.ts} +13 -4
  116. package/src/{useUpgradeCheck.test.ts → isolated/useUpgradeCheck.web.isolated.ts} +48 -6
  117. package/src/mongooseSlice.test.ts +1 -1
  118. package/src/offlineGate.test.ts +55 -0
  119. package/src/offlineGate.ts +25 -0
  120. package/src/offlineMiddleware.test.ts +187 -0
  121. package/src/offlineMiddleware.ts +718 -0
  122. package/src/offlineSlice.test.ts +204 -0
  123. package/src/offlineSlice.ts +130 -0
  124. package/src/realtime.test.ts +404 -0
  125. package/src/realtime.ts +408 -0
  126. package/src/socket.ts +33 -32
  127. package/src/sync.ts +310 -0
  128. package/src/tagGenerator.ts +29 -11
  129. package/src/useOfflineStatus.test.ts +157 -0
  130. package/src/useOfflineStatus.ts +82 -0
  131. package/src/useRealtimeDebug.ts +51 -0
  132. package/src/useServerStatus.ts +122 -0
  133. package/src/useUpgradeCheck.ts +35 -13
  134. package/dist/isolated/useUpgradeCheck.isolated.d.ts +0 -2
  135. package/dist/isolated/useUpgradeCheck.isolated.d.ts.map +0 -1
  136. package/dist/isolated/useUpgradeCheck.isolated.js +0 -153
  137. package/dist/isolated/useUpgradeCheck.isolated.js.map +0 -1
  138. package/dist/useUpgradeCheck.mobile.test.d.ts +0 -2
  139. package/dist/useUpgradeCheck.mobile.test.d.ts.map +0 -1
  140. package/dist/useUpgradeCheck.mobile.test.js.map +0 -1
  141. package/dist/useUpgradeCheck.test.d.ts +0 -2
  142. package/dist/useUpgradeCheck.test.d.ts.map +0 -1
  143. package/dist/useUpgradeCheck.test.js.map +0 -1
  144. package/src/isolated/useUpgradeCheck.isolated.ts +0 -201
@@ -0,0 +1,404 @@
1
+ // biome-ignore-all lint/suspicious/noExplicitAny: realtime RTK tests mock Socket.io and RTK Query runtime shapes
2
+ import {afterEach, describe, expect, it} from "bun:test";
3
+ import type {Socket} from "socket.io-client";
4
+
5
+ const {realtimeDocument, realtimeList, setRealtimeSocket} = await import("./realtime");
6
+
7
+ interface MockSocket {
8
+ emitted: Array<{event: string; payload: any}>;
9
+ emit: (event: string, payload: any) => void;
10
+ off: (event: string, handler: (payload: any) => void) => void;
11
+ on: (event: string, handler: (payload: any) => void) => void;
12
+ trigger: (event: string, payload: any) => void;
13
+ }
14
+
15
+ const createDeferred = (): {promise: Promise<void>; resolve: () => void} => {
16
+ let resolve!: () => void;
17
+ const promise = new Promise<void>((res) => {
18
+ resolve = res;
19
+ });
20
+ return {promise, resolve};
21
+ };
22
+
23
+ const createMockSocket = (canonicalQueryId?: string): MockSocket => {
24
+ const listeners = new Map<string, Set<(payload: any) => void>>();
25
+ const socket: MockSocket = {
26
+ emit: (event, payload) => {
27
+ socket.emitted.push({event, payload});
28
+ if (event !== "subscribe:query" || !canonicalQueryId) {
29
+ return;
30
+ }
31
+ queueMicrotask(() => {
32
+ const handlers = listeners.get("query:subscribed") ?? new Set();
33
+ for (const handler of handlers) {
34
+ handler({collection: payload.collection, queryId: canonicalQueryId});
35
+ }
36
+ });
37
+ },
38
+ emitted: [],
39
+ off: (event, handler) => {
40
+ listeners.get(event)?.delete(handler);
41
+ },
42
+ on: (event, handler) => {
43
+ if (!listeners.has(event)) {
44
+ listeners.set(event, new Set());
45
+ }
46
+ listeners.get(event)?.add(handler);
47
+ },
48
+ trigger: (event, payload) => {
49
+ const handlers = listeners.get(event) ?? new Set();
50
+ for (const handler of handlers) {
51
+ handler(payload);
52
+ }
53
+ },
54
+ };
55
+ return socket;
56
+ };
57
+
58
+ const createCacheApi = (
59
+ draft: any
60
+ ): {
61
+ cacheDataLoaded: Promise<void>;
62
+ cacheEntryRemoved: Promise<void>;
63
+ remove: () => void;
64
+ updateCachedData: (callback: (draft: any) => void) => void;
65
+ } => {
66
+ const cacheEntryRemoved = createDeferred();
67
+ return {
68
+ cacheDataLoaded: Promise.resolve(),
69
+ cacheEntryRemoved: cacheEntryRemoved.promise,
70
+ remove: cacheEntryRemoved.resolve,
71
+ updateCachedData: (callback): void => {
72
+ callback(draft);
73
+ },
74
+ };
75
+ };
76
+
77
+ const flushPromises = async (): Promise<void> => {
78
+ await Promise.resolve();
79
+ await Promise.resolve();
80
+ };
81
+
82
+ describe("realtimeDocument", () => {
83
+ afterEach(() => {
84
+ setRealtimeSocket(null);
85
+ });
86
+
87
+ it("waits for a socket, patches matching update payloads, and unsubscribes", async () => {
88
+ const draft: any = {id: "doc-1", title: "Old"};
89
+ const api = createCacheApi(draft);
90
+ const task = realtimeDocument("todos")("doc-1", api);
91
+
92
+ await Promise.resolve();
93
+ const socket = createMockSocket();
94
+ setRealtimeSocket(socket as any);
95
+ await flushPromises();
96
+
97
+ socket.trigger("sync", {
98
+ collection: "other",
99
+ data: {_id: "doc-1", title: "Ignored"},
100
+ id: "doc-1",
101
+ method: "update",
102
+ model: "Todo",
103
+ timestamp: 1,
104
+ });
105
+ socket.trigger("sync", {
106
+ collection: "todos",
107
+ data: {_id: "doc-2", title: "Ignored"},
108
+ id: "doc-2",
109
+ method: "update",
110
+ model: "Todo",
111
+ timestamp: 1,
112
+ });
113
+ socket.trigger("sync", {
114
+ collection: "todos",
115
+ data: {_id: "doc-1", title: "New"},
116
+ id: "doc-1",
117
+ method: "update",
118
+ model: "Todo",
119
+ timestamp: 1,
120
+ });
121
+ socket.trigger("sync", {
122
+ collection: "todos",
123
+ id: "doc-1",
124
+ method: "delete",
125
+ model: "Todo",
126
+ timestamp: 1,
127
+ });
128
+
129
+ expect(draft).toEqual({_id: "doc-1", id: "doc-1", title: "New"});
130
+ api.remove();
131
+ await task;
132
+
133
+ expect(socket.emitted).toContainEqual({
134
+ event: "subscribe:document",
135
+ payload: {collection: "todos", id: "doc-1"},
136
+ });
137
+ expect(socket.emitted).toContainEqual({
138
+ event: "unsubscribe:document",
139
+ payload: {collection: "todos", id: "doc-1"},
140
+ });
141
+ });
142
+
143
+ it("returns before subscribing when id or cache load is unavailable", async () => {
144
+ const socket = createMockSocket();
145
+ setRealtimeSocket(socket as any);
146
+
147
+ await realtimeDocument("todos")({}, createCacheApi({}));
148
+ await realtimeDocument("todos")("doc-1", {
149
+ cacheDataLoaded: Promise.reject(new Error("missing")),
150
+ cacheEntryRemoved: Promise.resolve(),
151
+ updateCachedData: () => undefined,
152
+ });
153
+
154
+ expect(socket.emitted).toEqual([]);
155
+ });
156
+
157
+ it("returns early when socket resolves to null before subscribing", async () => {
158
+ setRealtimeSocket(null);
159
+ const removed = createDeferred();
160
+ const task = realtimeDocument("todos")("doc-1", {
161
+ cacheDataLoaded: Promise.resolve(),
162
+ cacheEntryRemoved: removed.promise,
163
+ updateCachedData: () => undefined,
164
+ });
165
+ await Promise.resolve();
166
+ removed.resolve();
167
+ await task;
168
+ // No socket → no subscription events
169
+ });
170
+
171
+ it("logs sync events when websockets debug is enabled", async () => {
172
+ const {setRealtimeDebug} = await import("./constants");
173
+ const originalInfo = console.info;
174
+ const calls: unknown[][] = [];
175
+ console.info = (...args: unknown[]): void => {
176
+ calls.push(args);
177
+ };
178
+
179
+ setRealtimeDebug(true);
180
+ try {
181
+ const draft = {id: "doc-1", title: "Old"};
182
+ const api = createCacheApi(draft);
183
+ const socket = createMockSocket();
184
+ setRealtimeSocket(socket as unknown as Socket);
185
+ const task = realtimeDocument("todos")("doc-1", api);
186
+ await flushPromises();
187
+
188
+ socket.trigger("sync", {
189
+ collection: "todos",
190
+ data: {_id: "doc-1", title: "New"},
191
+ id: "doc-1",
192
+ method: "update",
193
+ model: "Todo",
194
+ timestamp: 1,
195
+ });
196
+
197
+ expect(calls.some((c) => (c[0] as string)?.includes("[websocket]"))).toBe(true);
198
+
199
+ api.remove();
200
+ await task;
201
+ } finally {
202
+ setRealtimeDebug(false);
203
+ console.info = originalInfo;
204
+ }
205
+ });
206
+ });
207
+
208
+ describe("realtimeList", () => {
209
+ afterEach(() => {
210
+ setRealtimeSocket(null);
211
+ });
212
+
213
+ it("unsubscribes from the server-canonical queryId", async () => {
214
+ const cacheEntryRemoved = createDeferred();
215
+ const socket = createMockSocket('todos:{"completed":false,"ownerId":"user1"}');
216
+ setRealtimeSocket(socket as any);
217
+
218
+ const task = realtimeList("todos")(
219
+ {completed: false, limit: 20},
220
+ {
221
+ cacheDataLoaded: Promise.resolve(),
222
+ cacheEntryRemoved: cacheEntryRemoved.promise,
223
+ updateCachedData: () => undefined,
224
+ }
225
+ );
226
+
227
+ await Promise.resolve();
228
+ await Promise.resolve();
229
+ cacheEntryRemoved.resolve();
230
+ await task;
231
+
232
+ const unsubscribed = socket.emitted.find((item) => item.event === "unsubscribe:query");
233
+ expect(unsubscribed?.payload).toEqual({
234
+ queryId: 'todos:{"completed":false,"ownerId":"user1"}',
235
+ });
236
+ });
237
+
238
+ it("handles model-room create, update, stale update, insert-on-update, and delete events", async () => {
239
+ const socket = createMockSocket();
240
+ setRealtimeSocket(socket as any);
241
+ const draft = {
242
+ data: [{id: "todo-1", title: "Existing", updated: "2026-01-02T00:00:00.000Z"}],
243
+ total: 1,
244
+ };
245
+ const api = createCacheApi(draft);
246
+ const task = realtimeList("todos")({limit: 20}, api);
247
+
248
+ await flushPromises();
249
+
250
+ socket.trigger("sync", {
251
+ collection: "other",
252
+ data: {_id: "ignored"},
253
+ id: "ignored",
254
+ method: "create",
255
+ model: "Todo",
256
+ timestamp: 1,
257
+ });
258
+ socket.trigger("sync", {
259
+ collection: "todos",
260
+ data: {_id: "todo-2", title: "Created"},
261
+ id: "todo-2",
262
+ method: "create",
263
+ model: "Todo",
264
+ timestamp: 1,
265
+ });
266
+ socket.trigger("sync", {
267
+ collection: "todos",
268
+ data: {id: "todo-1", title: "Stale", updated: "2026-01-01T00:00:00.000Z"},
269
+ id: "todo-1",
270
+ method: "update",
271
+ model: "Todo",
272
+ timestamp: 1,
273
+ });
274
+ socket.trigger("sync", {
275
+ collection: "todos",
276
+ data: {id: "todo-1", title: "Updated", updated: "2026-01-03T00:00:00.000Z"},
277
+ id: "todo-1",
278
+ method: "update",
279
+ model: "Todo",
280
+ timestamp: 1,
281
+ });
282
+ socket.trigger("sync", {
283
+ collection: "todos",
284
+ data: {id: "todo-3", title: "New match"},
285
+ id: "todo-3",
286
+ method: "update",
287
+ model: "Todo",
288
+ timestamp: 1,
289
+ });
290
+ socket.trigger("sync", {
291
+ collection: "todos",
292
+ id: "todo-2",
293
+ method: "delete",
294
+ model: "Todo",
295
+ timestamp: 1,
296
+ });
297
+
298
+ expect(draft.data.map((item: any) => item.id)).toEqual(["todo-3", "todo-1"]);
299
+ expect(draft.data[1].title).toBe("Updated");
300
+ expect(draft.total).toBe(2);
301
+
302
+ api.remove();
303
+ await task;
304
+ expect(socket.emitted).toContainEqual({event: "subscribe:model", payload: "todos"});
305
+ expect(socket.emitted).toContainEqual({event: "unsubscribe:model", payload: "todos"});
306
+ });
307
+
308
+ it("correlates query subscription acknowledgements before using canonical queryId", async () => {
309
+ const socket = createMockSocket();
310
+ setRealtimeSocket(socket as any);
311
+ const api = createCacheApi({data: [], total: 0});
312
+ const task = realtimeList("todos")({completed: false}, api);
313
+
314
+ await flushPromises();
315
+
316
+ socket.trigger("query:subscribed", {
317
+ collection: "other",
318
+ queryId: "wrong-collection",
319
+ });
320
+ socket.trigger("query:subscribed", {
321
+ clientQueryId: 'todos:{"completed":true}',
322
+ collection: "todos",
323
+ queryId: "wrong-client",
324
+ });
325
+ socket.trigger("query:subscribed", {
326
+ clientQueryId: 'todos:{"completed":false}',
327
+ collection: "todos",
328
+ queryId: 'todos:{"completed":false,"ownerId":"owner-1"}',
329
+ });
330
+
331
+ api.remove();
332
+ await task;
333
+
334
+ expect(socket.emitted).toContainEqual({
335
+ event: "unsubscribe:query",
336
+ payload: {queryId: 'todos:{"completed":false,"ownerId":"owner-1"}'},
337
+ });
338
+ });
339
+
340
+ it("returns before subscribing when cache load fails or socket never arrives", async () => {
341
+ const socket = createMockSocket();
342
+ setRealtimeSocket(socket as any);
343
+ await realtimeList("todos")(
344
+ {},
345
+ {
346
+ cacheDataLoaded: Promise.reject(new Error("missing")),
347
+ cacheEntryRemoved: Promise.resolve(),
348
+ updateCachedData: () => undefined,
349
+ }
350
+ );
351
+ expect(socket.emitted).toEqual([]);
352
+
353
+ setRealtimeSocket(null);
354
+ const removed = createDeferred();
355
+ const task = realtimeList("todos")(
356
+ {},
357
+ {
358
+ cacheDataLoaded: Promise.resolve(),
359
+ cacheEntryRemoved: removed.promise,
360
+ updateCachedData: () => undefined,
361
+ }
362
+ );
363
+ await Promise.resolve();
364
+ removed.resolve();
365
+ await task;
366
+ });
367
+
368
+ it("logs sync events when websockets debug is enabled", async () => {
369
+ const {setRealtimeDebug} = await import("./constants");
370
+ const originalInfo = console.info;
371
+ const calls: unknown[][] = [];
372
+ console.info = (...args: unknown[]): void => {
373
+ calls.push(args);
374
+ };
375
+
376
+ setRealtimeDebug(true);
377
+ try {
378
+ const socket = createMockSocket();
379
+ setRealtimeSocket(socket as unknown as Socket);
380
+ const draft = {data: [{id: "todo-1", title: "Existing"}], total: 1};
381
+ const api = createCacheApi(draft);
382
+ const task = realtimeList("todos")({limit: 20}, api);
383
+
384
+ await flushPromises();
385
+
386
+ socket.trigger("sync", {
387
+ collection: "todos",
388
+ data: {_id: "todo-2", title: "Created"},
389
+ id: "todo-2",
390
+ method: "create",
391
+ model: "Todo",
392
+ timestamp: 1,
393
+ });
394
+
395
+ expect(calls.some((c) => (c[0] as string)?.includes("[websocket]"))).toBe(true);
396
+
397
+ api.remove();
398
+ await task;
399
+ } finally {
400
+ setRealtimeDebug(false);
401
+ console.info = originalInfo;
402
+ }
403
+ });
404
+ });