@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,204 @@
1
+ import {beforeEach, describe, expect, it} from "bun:test";
2
+ import {configureStore} from "@reduxjs/toolkit";
3
+ import {REHYDRATE} from "redux-persist";
4
+
5
+ import {
6
+ addConflict,
7
+ type ConflictRecord,
8
+ clearConflicts,
9
+ clearQueue,
10
+ dequeue,
11
+ dismissConflict,
12
+ enqueue,
13
+ offlineReducer,
14
+ type QueuedMutation,
15
+ selectConflicts,
16
+ selectIsOnline,
17
+ selectIsOnlineSafe,
18
+ selectIsSyncing,
19
+ selectOfflineQueue,
20
+ selectQueueLength,
21
+ selectUndismissedConflicts,
22
+ setOnlineStatus,
23
+ setSyncing,
24
+ } from "./offlineSlice";
25
+
26
+ const createTestStore = () =>
27
+ configureStore({
28
+ reducer: {offline: offlineReducer},
29
+ });
30
+
31
+ describe("offlineSlice", () => {
32
+ let store: ReturnType<typeof createTestStore>;
33
+
34
+ beforeEach(() => {
35
+ store = createTestStore();
36
+ });
37
+
38
+ describe("setOnlineStatus", () => {
39
+ it("sets online status to false", () => {
40
+ store.dispatch(setOnlineStatus(false));
41
+ expect(selectIsOnline(store.getState())).toBe(false);
42
+ });
43
+
44
+ it("sets online status to true", () => {
45
+ store.dispatch(setOnlineStatus(false));
46
+ store.dispatch(setOnlineStatus(true));
47
+ expect(selectIsOnline(store.getState())).toBe(true);
48
+ });
49
+
50
+ it("defaults to online", () => {
51
+ expect(selectIsOnline(store.getState())).toBe(true);
52
+ });
53
+ });
54
+
55
+ describe("mutation queue", () => {
56
+ const mutation1: QueuedMutation = {
57
+ args: {body: {title: "Updated"}, id: "123"},
58
+ endpointName: "patchTodosById",
59
+ id: "m1",
60
+ timestamp: "2026-04-15T10:00:00.000Z",
61
+ type: "update",
62
+ };
63
+
64
+ const mutation2: QueuedMutation = {
65
+ args: {body: {title: "New Todo"}},
66
+ endpointName: "postTodos",
67
+ id: "m2",
68
+ timestamp: "2026-04-15T10:01:00.000Z",
69
+ type: "create",
70
+ };
71
+
72
+ it("enqueues mutations in FIFO order", () => {
73
+ store.dispatch(enqueue(mutation1));
74
+ store.dispatch(enqueue(mutation2));
75
+
76
+ const queue = selectOfflineQueue(store.getState());
77
+ expect(queue).toHaveLength(2);
78
+ expect(queue[0].id).toBe("m1");
79
+ expect(queue[1].id).toBe("m2");
80
+ });
81
+
82
+ it("dequeues by id", () => {
83
+ store.dispatch(enqueue(mutation1));
84
+ store.dispatch(enqueue(mutation2));
85
+ store.dispatch(dequeue("m1"));
86
+
87
+ const queue = selectOfflineQueue(store.getState());
88
+ expect(queue).toHaveLength(1);
89
+ expect(queue[0].id).toBe("m2");
90
+ });
91
+
92
+ it("clears entire queue", () => {
93
+ store.dispatch(enqueue(mutation1));
94
+ store.dispatch(enqueue(mutation2));
95
+ store.dispatch(clearQueue());
96
+
97
+ expect(selectOfflineQueue(store.getState())).toHaveLength(0);
98
+ });
99
+
100
+ it("reports queue length", () => {
101
+ expect(selectQueueLength(store.getState())).toBe(0);
102
+ store.dispatch(enqueue(mutation1));
103
+ expect(selectQueueLength(store.getState())).toBe(1);
104
+ store.dispatch(enqueue(mutation2));
105
+ expect(selectQueueLength(store.getState())).toBe(2);
106
+ });
107
+
108
+ it("preserves mutation args", () => {
109
+ store.dispatch(enqueue(mutation1));
110
+ const queued = selectOfflineQueue(store.getState())[0];
111
+ expect(queued.args).toEqual({body: {title: "Updated"}, id: "123"});
112
+ expect(queued.endpointName).toBe("patchTodosById");
113
+ expect(queued.type).toBe("update");
114
+ });
115
+ });
116
+
117
+ describe("conflicts", () => {
118
+ const conflict1: ConflictRecord = {
119
+ args: {body: {title: "Offline"}, id: "123"},
120
+ dismissed: false,
121
+ endpointName: "patchTodosById",
122
+ id: "c1",
123
+ serverDocument: {_id: "123", title: "Server Version", updated: "2026-04-15T11:00:00.000Z"},
124
+ timestamp: "2026-04-15T10:30:00.000Z",
125
+ };
126
+
127
+ const conflict2: ConflictRecord = {
128
+ args: {body: {completed: true}, id: "456"},
129
+ dismissed: false,
130
+ endpointName: "patchTodosById",
131
+ id: "c2",
132
+ serverDocument: {_id: "456", completed: false, title: "Other"},
133
+ timestamp: "2026-04-15T10:31:00.000Z",
134
+ };
135
+
136
+ it("adds conflict records", () => {
137
+ store.dispatch(addConflict(conflict1));
138
+ expect(selectConflicts(store.getState())).toHaveLength(1);
139
+ expect(selectConflicts(store.getState())[0].id).toBe("c1");
140
+ });
141
+
142
+ it("dismisses a conflict", () => {
143
+ store.dispatch(addConflict(conflict1));
144
+ store.dispatch(addConflict(conflict2));
145
+ store.dispatch(dismissConflict("c1"));
146
+
147
+ const conflicts = selectConflicts(store.getState());
148
+ expect(conflicts[0].dismissed).toBe(true);
149
+ expect(conflicts[1].dismissed).toBe(false);
150
+ });
151
+
152
+ it("filters undismissed conflicts", () => {
153
+ store.dispatch(addConflict(conflict1));
154
+ store.dispatch(addConflict(conflict2));
155
+ store.dispatch(dismissConflict("c1"));
156
+
157
+ const undismissed = selectUndismissedConflicts(store.getState());
158
+ expect(undismissed).toHaveLength(1);
159
+ expect(undismissed[0].id).toBe("c2");
160
+ });
161
+
162
+ it("clears all conflicts", () => {
163
+ store.dispatch(addConflict(conflict1));
164
+ store.dispatch(addConflict(conflict2));
165
+ store.dispatch(clearConflicts());
166
+
167
+ expect(selectConflicts(store.getState())).toHaveLength(0);
168
+ });
169
+ });
170
+
171
+ describe("selectIsOnlineSafe", () => {
172
+ it("defaults to online when offline reducer is absent", () => {
173
+ expect(selectIsOnlineSafe({})).toBe(true);
174
+ });
175
+
176
+ it("reflects offline state when reducer is mounted", () => {
177
+ store.dispatch(setOnlineStatus(false));
178
+ expect(selectIsOnlineSafe(store.getState())).toBe(false);
179
+ });
180
+ });
181
+
182
+ describe("syncing", () => {
183
+ it("defaults to not syncing", () => {
184
+ expect(selectIsSyncing(store.getState())).toBe(false);
185
+ });
186
+
187
+ it("sets syncing state", () => {
188
+ store.dispatch(setSyncing(true));
189
+ expect(selectIsSyncing(store.getState())).toBe(true);
190
+ });
191
+
192
+ it("clears syncing state", () => {
193
+ store.dispatch(setSyncing(true));
194
+ store.dispatch(setSyncing(false));
195
+ expect(selectIsSyncing(store.getState())).toBe(false);
196
+ });
197
+
198
+ it("resets syncing on redux-persist rehydrate", () => {
199
+ store.dispatch(setSyncing(true));
200
+ store.dispatch({payload: undefined, type: REHYDRATE});
201
+ expect(selectIsSyncing(store.getState())).toBe(false);
202
+ });
203
+ });
204
+ });
@@ -0,0 +1,130 @@
1
+ import {createSlice, type PayloadAction} from "@reduxjs/toolkit";
2
+ import {REHYDRATE} from "redux-persist";
3
+
4
+ import {IsWeb} from "./platform";
5
+
6
+ const getInitialOnlineStatus = (): boolean => {
7
+ if (IsWeb && typeof navigator !== "undefined" && typeof navigator.onLine === "boolean") {
8
+ return navigator.onLine;
9
+ }
10
+ return true;
11
+ };
12
+
13
+ export interface QueuedMutation {
14
+ /** Unique identifier for this queued mutation */
15
+ id: string;
16
+ /** RTK Query endpoint name, e.g. "patchTodosById" */
17
+ endpointName: string;
18
+ /** Original mutation arguments */
19
+ args: unknown;
20
+ /** ISO timestamp of when the mutation was queued */
21
+ timestamp: string;
22
+ /** Document `_updatedAt` at queue time (used for If-Unmodified-Since on replay) */
23
+ baseUpdatedAt?: string;
24
+ /** The type of CRUD operation */
25
+ type: "create" | "update" | "delete";
26
+ /** Auth user ID when the mutation was queued; replay is skipped if it does not match the current user */
27
+ userId?: string;
28
+ }
29
+
30
+ export interface ConflictRecord {
31
+ /** Unique identifier for this conflict */
32
+ id: string;
33
+ /** RTK Query endpoint name that caused the conflict */
34
+ endpointName: string;
35
+ /** Original mutation arguments that were rejected */
36
+ args: unknown;
37
+ /** The current server version of the document */
38
+ serverDocument: unknown;
39
+ /** ISO timestamp of when the conflict was detected */
40
+ timestamp: string;
41
+ /** Whether the user has dismissed this conflict notification */
42
+ dismissed: boolean;
43
+ }
44
+
45
+ export interface OfflineState {
46
+ isOnline: boolean;
47
+ queue: QueuedMutation[];
48
+ conflicts: ConflictRecord[];
49
+ isSyncing: boolean;
50
+ }
51
+
52
+ const initialState: OfflineState = {
53
+ conflicts: [],
54
+ isOnline: getInitialOnlineStatus(),
55
+ isSyncing: false,
56
+ queue: [],
57
+ };
58
+
59
+ export const offlineSlice = createSlice({
60
+ extraReducers: (builder) => {
61
+ builder.addCase(REHYDRATE, (state) => {
62
+ state.isSyncing = false;
63
+ });
64
+ },
65
+ initialState,
66
+ name: "offline",
67
+ reducers: {
68
+ addConflict(state, action: PayloadAction<ConflictRecord>) {
69
+ state.conflicts.push(action.payload);
70
+ },
71
+ clearConflicts(state) {
72
+ state.conflicts = [];
73
+ },
74
+ clearQueue(state) {
75
+ state.queue = [];
76
+ },
77
+ dequeue(state, action: PayloadAction<string>) {
78
+ state.queue = state.queue.filter((m) => m.id !== action.payload);
79
+ },
80
+ dismissConflict(state, action: PayloadAction<string>) {
81
+ const conflict = state.conflicts.find((c) => c.id === action.payload);
82
+ if (conflict) {
83
+ conflict.dismissed = true;
84
+ }
85
+ },
86
+ enqueue(state, action: PayloadAction<QueuedMutation>) {
87
+ state.queue.push(action.payload);
88
+ },
89
+ setOnlineStatus(state, action: PayloadAction<boolean>) {
90
+ state.isOnline = action.payload;
91
+ },
92
+ setSyncing(state, action: PayloadAction<boolean>) {
93
+ state.isSyncing = action.payload;
94
+ },
95
+ },
96
+ });
97
+
98
+ export const {
99
+ setOnlineStatus,
100
+ enqueue,
101
+ dequeue,
102
+ clearQueue,
103
+ addConflict,
104
+ dismissConflict,
105
+ clearConflicts,
106
+ setSyncing,
107
+ } = offlineSlice.actions;
108
+
109
+ export const offlineReducer = offlineSlice.reducer;
110
+
111
+ // Selectors
112
+ export const selectIsOnline = (state: {offline: OfflineState}): boolean => state.offline.isOnline;
113
+
114
+ /** Safe online check when the offline reducer may not be mounted (defaults to online). */
115
+ export const selectIsOnlineSafe = (state: {offline?: OfflineState}): boolean =>
116
+ state.offline?.isOnline ?? true;
117
+
118
+ export const selectOfflineQueue = (state: {offline: OfflineState}): QueuedMutation[] =>
119
+ state.offline.queue;
120
+
121
+ export const selectQueueLength = (state: {offline: OfflineState}): number =>
122
+ state.offline.queue.length;
123
+
124
+ export const selectConflicts = (state: {offline: OfflineState}): ConflictRecord[] =>
125
+ state.offline.conflicts;
126
+
127
+ export const selectUndismissedConflicts = (state: {offline: OfflineState}): ConflictRecord[] =>
128
+ state.offline.conflicts.filter((c) => !c.dismissed);
129
+
130
+ export const selectIsSyncing = (state: {offline: OfflineState}): boolean => state.offline.isSyncing;