tetherdb 0.1.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 (194) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +277 -0
  3. package/bin/tetherdb.js +7 -0
  4. package/dist/cli/args.d.cts +20 -0
  5. package/dist/cli/args.d.ts +20 -0
  6. package/dist/cli/args.d.ts.map +1 -0
  7. package/dist/cli/backend.d.cts +15 -0
  8. package/dist/cli/backend.d.ts +15 -0
  9. package/dist/cli/backend.d.ts.map +1 -0
  10. package/dist/cli/cli.d.cts +8 -0
  11. package/dist/cli/cli.d.ts +8 -0
  12. package/dist/cli/cli.d.ts.map +1 -0
  13. package/dist/cli/commands/apps.d.cts +9 -0
  14. package/dist/cli/commands/apps.d.ts +9 -0
  15. package/dist/cli/commands/apps.d.ts.map +1 -0
  16. package/dist/cli/commands/help.d.cts +5 -0
  17. package/dist/cli/commands/help.d.ts +5 -0
  18. package/dist/cli/commands/help.d.ts.map +1 -0
  19. package/dist/cli/commands/index.d.cts +8 -0
  20. package/dist/cli/commands/index.d.ts +8 -0
  21. package/dist/cli/commands/index.d.ts.map +1 -0
  22. package/dist/cli/commands/maintenance.d.cts +9 -0
  23. package/dist/cli/commands/maintenance.d.ts +9 -0
  24. package/dist/cli/commands/maintenance.d.ts.map +1 -0
  25. package/dist/cli/commands/serve.d.cts +14 -0
  26. package/dist/cli/commands/serve.d.ts +14 -0
  27. package/dist/cli/commands/serve.d.ts.map +1 -0
  28. package/dist/cli/commands/status.d.cts +9 -0
  29. package/dist/cli/commands/status.d.ts +9 -0
  30. package/dist/cli/commands/status.d.ts.map +1 -0
  31. package/dist/cli/commands/tables.d.cts +9 -0
  32. package/dist/cli/commands/tables.d.ts +9 -0
  33. package/dist/cli/commands/tables.d.ts.map +1 -0
  34. package/dist/cli/commands/users.d.cts +9 -0
  35. package/dist/cli/commands/users.d.ts +9 -0
  36. package/dist/cli/commands/users.d.ts.map +1 -0
  37. package/dist/cli/index.cjs +3999 -0
  38. package/dist/cli/index.cjs.map +1 -0
  39. package/dist/cli/index.d.cts +7 -0
  40. package/dist/cli/index.d.ts +7 -0
  41. package/dist/cli/index.d.ts.map +1 -0
  42. package/dist/cli/index.js +3958 -0
  43. package/dist/cli/index.js.map +1 -0
  44. package/dist/client/auth.d.cts +155 -0
  45. package/dist/client/auth.d.ts +155 -0
  46. package/dist/client/auth.d.ts.map +1 -0
  47. package/dist/client/client.d.cts +122 -0
  48. package/dist/client/client.d.ts +122 -0
  49. package/dist/client/client.d.ts.map +1 -0
  50. package/dist/client/errors.d.cts +39 -0
  51. package/dist/client/errors.d.ts +39 -0
  52. package/dist/client/errors.d.ts.map +1 -0
  53. package/dist/client/index.cjs +1742 -0
  54. package/dist/client/index.cjs.map +1 -0
  55. package/dist/client/index.d.cts +12 -0
  56. package/dist/client/index.d.ts +12 -0
  57. package/dist/client/index.d.ts.map +1 -0
  58. package/dist/client/index.js +1707 -0
  59. package/dist/client/index.js.map +1 -0
  60. package/dist/client/shared/event.d.cts +29 -0
  61. package/dist/client/shared/event.d.ts +29 -0
  62. package/dist/client/shared/event.d.ts.map +1 -0
  63. package/dist/client/shared/id.d.cts +5 -0
  64. package/dist/client/shared/id.d.ts +5 -0
  65. package/dist/client/shared/id.d.ts.map +1 -0
  66. package/dist/client/storage.d.cts +183 -0
  67. package/dist/client/storage.d.ts +183 -0
  68. package/dist/client/storage.d.ts.map +1 -0
  69. package/dist/client/sync.d.cts +126 -0
  70. package/dist/client/sync.d.ts +126 -0
  71. package/dist/client/sync.d.ts.map +1 -0
  72. package/dist/client/table.d.cts +146 -0
  73. package/dist/client/table.d.ts +146 -0
  74. package/dist/client/table.d.ts.map +1 -0
  75. package/dist/index.cjs +1742 -0
  76. package/dist/index.cjs.map +1 -0
  77. package/dist/index.d.cts +7 -0
  78. package/dist/index.d.ts +7 -0
  79. package/dist/index.d.ts.map +1 -0
  80. package/dist/index.js +1707 -0
  81. package/dist/index.js.map +1 -0
  82. package/dist/server/crypto.d.cts +55 -0
  83. package/dist/server/crypto.d.ts +55 -0
  84. package/dist/server/crypto.d.ts.map +1 -0
  85. package/dist/server/errors.d.cts +39 -0
  86. package/dist/server/errors.d.ts +39 -0
  87. package/dist/server/errors.d.ts.map +1 -0
  88. package/dist/server/index.cjs +3621 -0
  89. package/dist/server/index.cjs.map +1 -0
  90. package/dist/server/index.d.cts +10 -0
  91. package/dist/server/index.d.ts +10 -0
  92. package/dist/server/index.d.ts.map +1 -0
  93. package/dist/server/index.js +3569 -0
  94. package/dist/server/index.js.map +1 -0
  95. package/dist/server/lock.d.cts +56 -0
  96. package/dist/server/lock.d.ts +56 -0
  97. package/dist/server/lock.d.ts.map +1 -0
  98. package/dist/server/rate-limiter.d.cts +81 -0
  99. package/dist/server/rate-limiter.d.ts +81 -0
  100. package/dist/server/rate-limiter.d.ts.map +1 -0
  101. package/dist/server/server.d.cts +206 -0
  102. package/dist/server/server.d.ts +206 -0
  103. package/dist/server/server.d.ts.map +1 -0
  104. package/dist/server/storage/app.d.cts +70 -0
  105. package/dist/server/storage/app.d.ts +70 -0
  106. package/dist/server/storage/app.d.ts.map +1 -0
  107. package/dist/server/storage/base/app.d.cts +42 -0
  108. package/dist/server/storage/base/app.d.ts +42 -0
  109. package/dist/server/storage/base/app.d.ts.map +1 -0
  110. package/dist/server/storage/base/index.d.cts +5 -0
  111. package/dist/server/storage/base/index.d.ts +5 -0
  112. package/dist/server/storage/base/index.d.ts.map +1 -0
  113. package/dist/server/storage/base/storage.d.cts +40 -0
  114. package/dist/server/storage/base/storage.d.ts +40 -0
  115. package/dist/server/storage/base/storage.d.ts.map +1 -0
  116. package/dist/server/storage/base/table.d.cts +32 -0
  117. package/dist/server/storage/base/table.d.ts +32 -0
  118. package/dist/server/storage/base/table.d.ts.map +1 -0
  119. package/dist/server/storage/base/user.d.cts +31 -0
  120. package/dist/server/storage/base/user.d.ts +31 -0
  121. package/dist/server/storage/base/user.d.ts.map +1 -0
  122. package/dist/server/storage/file/app.d.cts +50 -0
  123. package/dist/server/storage/file/app.d.ts +50 -0
  124. package/dist/server/storage/file/app.d.ts.map +1 -0
  125. package/dist/server/storage/file/index.d.cts +5 -0
  126. package/dist/server/storage/file/index.d.ts +5 -0
  127. package/dist/server/storage/file/index.d.ts.map +1 -0
  128. package/dist/server/storage/file/storage.d.cts +67 -0
  129. package/dist/server/storage/file/storage.d.ts +67 -0
  130. package/dist/server/storage/file/storage.d.ts.map +1 -0
  131. package/dist/server/storage/file/table.d.cts +14 -0
  132. package/dist/server/storage/file/table.d.ts +14 -0
  133. package/dist/server/storage/file/table.d.ts.map +1 -0
  134. package/dist/server/storage/file/user.d.cts +14 -0
  135. package/dist/server/storage/file/user.d.ts +14 -0
  136. package/dist/server/storage/file/user.d.ts.map +1 -0
  137. package/dist/server/storage/index.d.cts +14 -0
  138. package/dist/server/storage/index.d.ts +14 -0
  139. package/dist/server/storage/index.d.ts.map +1 -0
  140. package/dist/server/storage/memory/app.d.cts +29 -0
  141. package/dist/server/storage/memory/app.d.ts +29 -0
  142. package/dist/server/storage/memory/app.d.ts.map +1 -0
  143. package/dist/server/storage/memory/index.d.cts +5 -0
  144. package/dist/server/storage/memory/index.d.ts +5 -0
  145. package/dist/server/storage/memory/index.d.ts.map +1 -0
  146. package/dist/server/storage/memory/storage.d.cts +47 -0
  147. package/dist/server/storage/memory/storage.d.ts +47 -0
  148. package/dist/server/storage/memory/storage.d.ts.map +1 -0
  149. package/dist/server/storage/memory/table.d.cts +17 -0
  150. package/dist/server/storage/memory/table.d.ts +17 -0
  151. package/dist/server/storage/memory/table.d.ts.map +1 -0
  152. package/dist/server/storage/memory/user.d.cts +21 -0
  153. package/dist/server/storage/memory/user.d.ts +21 -0
  154. package/dist/server/storage/memory/user.d.ts.map +1 -0
  155. package/dist/server/storage/sqlite/app.d.cts +31 -0
  156. package/dist/server/storage/sqlite/app.d.ts +31 -0
  157. package/dist/server/storage/sqlite/app.d.ts.map +1 -0
  158. package/dist/server/storage/sqlite/index.d.cts +5 -0
  159. package/dist/server/storage/sqlite/index.d.ts +5 -0
  160. package/dist/server/storage/sqlite/index.d.ts.map +1 -0
  161. package/dist/server/storage/sqlite/storage.d.cts +92 -0
  162. package/dist/server/storage/sqlite/storage.d.ts +92 -0
  163. package/dist/server/storage/sqlite/storage.d.ts.map +1 -0
  164. package/dist/server/storage/sqlite/table.d.cts +15 -0
  165. package/dist/server/storage/sqlite/table.d.ts +15 -0
  166. package/dist/server/storage/sqlite/table.d.ts.map +1 -0
  167. package/dist/server/storage/sqlite/user.d.cts +14 -0
  168. package/dist/server/storage/sqlite/user.d.ts +14 -0
  169. package/dist/server/storage/sqlite/user.d.ts.map +1 -0
  170. package/dist/server/storage/storage.d.cts +152 -0
  171. package/dist/server/storage/storage.d.ts +152 -0
  172. package/dist/server/storage/storage.d.ts.map +1 -0
  173. package/dist/server/storage/table.d.cts +45 -0
  174. package/dist/server/storage/table.d.ts +45 -0
  175. package/dist/server/storage/table.d.ts.map +1 -0
  176. package/dist/server/storage/user.d.cts +45 -0
  177. package/dist/server/storage/user.d.ts +45 -0
  178. package/dist/server/storage/user.d.ts.map +1 -0
  179. package/dist/server/sync.d.cts +67 -0
  180. package/dist/server/sync.d.ts +67 -0
  181. package/dist/server/sync.d.ts.map +1 -0
  182. package/dist/server/validate.d.cts +113 -0
  183. package/dist/server/validate.d.ts +113 -0
  184. package/dist/server/validate.d.ts.map +1 -0
  185. package/dist/shared/clock.d.cts +22 -0
  186. package/dist/shared/clock.d.ts +22 -0
  187. package/dist/shared/clock.d.ts.map +1 -0
  188. package/dist/shared/path.d.cts +12 -0
  189. package/dist/shared/path.d.ts +12 -0
  190. package/dist/shared/path.d.ts.map +1 -0
  191. package/dist/shared/types.d.cts +234 -0
  192. package/dist/shared/types.d.ts +234 -0
  193. package/dist/shared/types.d.ts.map +1 -0
  194. package/package.json +133 -0
@@ -0,0 +1,1742 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/client/index.ts
21
+ var client_exports = {};
22
+ __export(client_exports, {
23
+ AuthStatus: () => AuthStatus,
24
+ DataMode: () => DataMode,
25
+ OperationType: () => OperationType,
26
+ PROTOCOL_VERSION: () => PROTOCOL_VERSION,
27
+ SyncStatus: () => SyncStatus,
28
+ Table: () => Table,
29
+ TetherClient: () => TetherClient,
30
+ TetherClientError: () => TetherClientError,
31
+ TetherClientErrorCode: () => TetherClientErrorCode
32
+ });
33
+ module.exports = __toCommonJS(client_exports);
34
+
35
+ // src/shared/types.ts
36
+ var OperationType = /* @__PURE__ */ ((OperationType2) => {
37
+ OperationType2["Put"] = "put";
38
+ OperationType2["Delete"] = "delete";
39
+ return OperationType2;
40
+ })(OperationType || {});
41
+ var PROTOCOL_VERSION = 1;
42
+
43
+ // src/client/errors.ts
44
+ var TetherClientErrorCode = /* @__PURE__ */ ((TetherClientErrorCode2) => {
45
+ TetherClientErrorCode2[TetherClientErrorCode2["MissingConfiguration"] = 0] = "MissingConfiguration";
46
+ TetherClientErrorCode2[TetherClientErrorCode2["InvalidInput"] = 1] = "InvalidInput";
47
+ TetherClientErrorCode2[TetherClientErrorCode2["FetchUnavailable"] = 2] = "FetchUnavailable";
48
+ TetherClientErrorCode2[TetherClientErrorCode2["NetworkError"] = 3] = "NetworkError";
49
+ TetherClientErrorCode2[TetherClientErrorCode2["MissingCredentials"] = 4] = "MissingCredentials";
50
+ TetherClientErrorCode2[TetherClientErrorCode2["RegistrationFailed"] = 5] = "RegistrationFailed";
51
+ TetherClientErrorCode2[TetherClientErrorCode2["AuthenticationFailed"] = 6] = "AuthenticationFailed";
52
+ TetherClientErrorCode2[TetherClientErrorCode2["StorageError"] = 7] = "StorageError";
53
+ TetherClientErrorCode2[TetherClientErrorCode2["SyncError"] = 8] = "SyncError";
54
+ return TetherClientErrorCode2;
55
+ })(TetherClientErrorCode || {});
56
+ var TetherClientError = class extends Error {
57
+ /** Error category code identifying the broad error type. */
58
+ code;
59
+ /**
60
+ * Initializes a new `TetherClientError`.
61
+ *
62
+ * @param code - The error category code.
63
+ * @param message - User-safe error description message.
64
+ */
65
+ constructor(code, message) {
66
+ super(message ?? getDefaultClientErrorMessage(code));
67
+ this.name = "TetherClientError";
68
+ this.code = code;
69
+ Object.setPrototypeOf(this, new.target.prototype);
70
+ }
71
+ };
72
+ function getDefaultClientErrorMessage(code) {
73
+ switch (code) {
74
+ case 0 /* MissingConfiguration */:
75
+ return "Missing required configuration option";
76
+ case 1 /* InvalidInput */:
77
+ return "Invalid input parameter provided";
78
+ case 2 /* FetchUnavailable */:
79
+ return "No fetch implementation available";
80
+ case 3 /* NetworkError */:
81
+ return "Network communication error";
82
+ case 4 /* MissingCredentials */:
83
+ return "Missing authentication credentials";
84
+ case 5 /* RegistrationFailed */:
85
+ return "Registration failed";
86
+ case 6 /* AuthenticationFailed */:
87
+ return "Authentication failed";
88
+ case 7 /* StorageError */:
89
+ return "Local storage operation failed";
90
+ case 8 /* SyncError */:
91
+ return "Synchronization error";
92
+ }
93
+ }
94
+
95
+ // src/client/shared/event.ts
96
+ var EventRegistry = class {
97
+ listeners = /* @__PURE__ */ new Set();
98
+ /**
99
+ * Registers a callback listener to be notified when events are published.
100
+ *
101
+ * @param listener - Callback function receiving event payloads.
102
+ * @returns An unregister function that removes the listener.
103
+ */
104
+ register(listener) {
105
+ this.listeners.add(listener);
106
+ return () => {
107
+ this.listeners.delete(listener);
108
+ };
109
+ }
110
+ /**
111
+ * Dispatches an event payload to all registered listeners in registration order.
112
+ * Catches and logs listener exceptions to prevent aborting dispatch to remaining listeners.
113
+ *
114
+ * @param args - The event data payload (optional if payload type is void).
115
+ */
116
+ publish(...args) {
117
+ const event = args[0];
118
+ for (const listener of this.listeners) {
119
+ try {
120
+ listener(event);
121
+ } catch (err) {
122
+ console.error("Unhandled error:", err);
123
+ }
124
+ }
125
+ }
126
+ };
127
+
128
+ // src/client/auth.ts
129
+ var DataMode = /* @__PURE__ */ ((DataMode2) => {
130
+ DataMode2[DataMode2["Remote"] = 0] = "Remote";
131
+ DataMode2[DataMode2["Local"] = 1] = "Local";
132
+ DataMode2[DataMode2["Merge"] = 2] = "Merge";
133
+ DataMode2[DataMode2["Clear"] = 3] = "Clear";
134
+ return DataMode2;
135
+ })(DataMode || {});
136
+ var AuthStatus = /* @__PURE__ */ ((AuthStatus2) => {
137
+ AuthStatus2[AuthStatus2["SignedOut"] = 0] = "SignedOut";
138
+ AuthStatus2[AuthStatus2["SigningIn"] = 1] = "SigningIn";
139
+ AuthStatus2[AuthStatus2["SignedIn"] = 2] = "SignedIn";
140
+ AuthStatus2[AuthStatus2["Error"] = 3] = "Error";
141
+ return AuthStatus2;
142
+ })(AuthStatus || {});
143
+ var Auth = class {
144
+ /** Base URL for remote authentication REST API endpoints. */
145
+ baseUrl;
146
+ /** Reactive event registry triggered whenever the authentication status changes. */
147
+ onStatusChange = new EventRegistry();
148
+ storage;
149
+ fetchFn;
150
+ currentAuthStatus = 0 /* SignedOut */;
151
+ currentUsername;
152
+ currentUserId;
153
+ currentToken;
154
+ autoRestorePromise;
155
+ constructor(dependencies) {
156
+ this.baseUrl = dependencies.baseUrl;
157
+ this.storage = dependencies.storage;
158
+ const rawFetch = dependencies.fetchFn ?? (typeof globalThis !== "undefined" && globalThis.fetch ? globalThis.fetch : typeof fetch !== "undefined" ? fetch : void 0);
159
+ if (!rawFetch) {
160
+ throw new TetherClientError(
161
+ 2 /* FetchUnavailable */,
162
+ "No fetch implementation available"
163
+ );
164
+ }
165
+ this.fetchFn = rawFetch.bind(globalThis);
166
+ this.autoRestorePromise = this.restoreSession();
167
+ }
168
+ /**
169
+ * Current authentication lifecycle status.
170
+ */
171
+ get status() {
172
+ return this.currentAuthStatus;
173
+ }
174
+ /**
175
+ * The authenticated user's username, or `undefined` if signed out.
176
+ */
177
+ get username() {
178
+ return this.currentUsername;
179
+ }
180
+ /**
181
+ * The authenticated user's ID, or `undefined` if signed out.
182
+ */
183
+ get userId() {
184
+ return this.currentUserId;
185
+ }
186
+ /**
187
+ * The active authentication session token, or `undefined` if signed out.
188
+ */
189
+ get token() {
190
+ return this.currentToken;
191
+ }
192
+ /**
193
+ * Restores any remembered session from IndexedDB metadata on startup.
194
+ */
195
+ async restoreSession() {
196
+ try {
197
+ const session = await this.storage.getMeta("auth");
198
+ if (session?.token && session.username) {
199
+ this.currentUserId = session.userId;
200
+ this.currentUsername = session.username;
201
+ this.currentToken = session.token;
202
+ this.setStatus(2 /* SignedIn */);
203
+ }
204
+ } catch {
205
+ }
206
+ }
207
+ /**
208
+ * Registers a new user account, applies local data mode, and notifies listeners.
209
+ */
210
+ async register(options) {
211
+ if (!options?.username || !options?.password) {
212
+ throw new TetherClientError(
213
+ 4 /* MissingCredentials */,
214
+ "Registration requires username and password"
215
+ );
216
+ }
217
+ const previousStatus = this.currentAuthStatus;
218
+ this.setStatus(1 /* SigningIn */);
219
+ try {
220
+ const defaultDataMode = previousStatus === 2 /* SignedIn */ ? 3 /* Clear */ : 1 /* Local */;
221
+ const dataMode = options.dataMode ?? defaultDataMode;
222
+ const data = await this.sendAuthRequest(
223
+ "/auth/register",
224
+ { username: options.username, password: options.password },
225
+ 5 /* RegistrationFailed */,
226
+ "Registration failed"
227
+ );
228
+ this.currentUserId = data.userId;
229
+ this.currentUsername = data.username;
230
+ this.currentToken = data.token;
231
+ await this.applyDataMode(dataMode);
232
+ await this.updateStoredAuth(options.remember ?? false, data);
233
+ this.setStatus(2 /* SignedIn */);
234
+ return true;
235
+ } catch {
236
+ this.setStatus(3 /* Error */);
237
+ return false;
238
+ }
239
+ }
240
+ /**
241
+ * Logs into an account (or reconnects with remembered credentials), applies data mode, and notifies listeners.
242
+ */
243
+ async login(options = {}) {
244
+ await this.autoRestorePromise;
245
+ this.setStatus(1 /* SigningIn */);
246
+ try {
247
+ let token = this.currentToken;
248
+ let username = this.currentUsername;
249
+ let userId = this.currentUserId;
250
+ if (options.username && options.password) {
251
+ const data = await this.sendAuthRequest(
252
+ "/auth/login",
253
+ { username: options.username, password: options.password },
254
+ 6 /* AuthenticationFailed */,
255
+ "Authentication failed"
256
+ );
257
+ token = data.token;
258
+ username = data.username;
259
+ userId = data.userId;
260
+ await this.updateStoredAuth(options.remember ?? false, data);
261
+ } else if (!token) {
262
+ const stored = await this.storage.getMeta("auth");
263
+ if (stored?.token) {
264
+ token = stored.token;
265
+ username = stored.username;
266
+ userId = stored.userId;
267
+ }
268
+ }
269
+ if (!token) {
270
+ throw new TetherClientError(
271
+ 4 /* MissingCredentials */,
272
+ "No login credentials or saved session available"
273
+ );
274
+ }
275
+ this.currentToken = token;
276
+ this.currentUsername = username;
277
+ this.currentUserId = userId;
278
+ await this.applyDataMode(options.dataMode ?? 0 /* Remote */);
279
+ this.setStatus(2 /* SignedIn */);
280
+ return true;
281
+ } catch {
282
+ this.setStatus(3 /* Error */);
283
+ return false;
284
+ }
285
+ }
286
+ /**
287
+ * Logs out of the current session, removes stored credentials, and applies data mode.
288
+ */
289
+ async logout(options = {}) {
290
+ this.currentUserId = void 0;
291
+ this.currentUsername = void 0;
292
+ this.currentToken = void 0;
293
+ await this.storage.deleteMeta("auth");
294
+ await this.applyDataMode(options.dataMode ?? 3 /* Clear */);
295
+ this.setStatus(0 /* SignedOut */);
296
+ return true;
297
+ }
298
+ /**
299
+ * Updates the in-memory and persisted session token when refreshed by the server.
300
+ *
301
+ * @param token - The new refreshed session token.
302
+ */
303
+ async handleTokenRefresh(token) {
304
+ this.currentToken = token;
305
+ const session = await this.storage.getMeta("auth");
306
+ if (session) {
307
+ await this.storage.setMeta("auth", {
308
+ ...session,
309
+ token
310
+ });
311
+ }
312
+ }
313
+ /**
314
+ * Cleans up local session and transitions to SignedOut when authentication fails or expires.
315
+ *
316
+ * @param _message - Optional error message from the server.
317
+ */
318
+ async handleAuthError(_message) {
319
+ this.currentUserId = void 0;
320
+ this.currentUsername = void 0;
321
+ this.currentToken = void 0;
322
+ await this.storage.deleteMeta("auth");
323
+ this.setStatus(0 /* SignedOut */);
324
+ }
325
+ // -- Private Helpers ------------------------------------------------------
326
+ async sendAuthRequest(path, credentials, errorCode, defaultErrorMessage) {
327
+ const res = await this.fetchFn(`${this.baseUrl}${path}`, {
328
+ method: "POST",
329
+ headers: { "Content-Type": "application/json" },
330
+ body: JSON.stringify(credentials)
331
+ });
332
+ const data = await res.json();
333
+ if (!res.ok) {
334
+ throw new TetherClientError(errorCode, data.error ?? defaultErrorMessage);
335
+ }
336
+ return data;
337
+ }
338
+ async applyDataMode(dataMode) {
339
+ if (dataMode === 3 /* Clear */ || dataMode === 0 /* Remote */) {
340
+ await this.storage.clearTables(true);
341
+ await this.storage.setMeta("lastSyncSeq", 0);
342
+ }
343
+ }
344
+ async updateStoredAuth(remember, auth) {
345
+ if (remember) {
346
+ await this.storage.setMeta("auth", {
347
+ token: auth.token,
348
+ userId: auth.userId,
349
+ username: auth.username
350
+ });
351
+ } else {
352
+ await this.storage.deleteMeta("auth");
353
+ }
354
+ }
355
+ setStatus(status) {
356
+ if (this.currentAuthStatus === status) return;
357
+ this.currentAuthStatus = status;
358
+ this.onStatusChange.publish(status);
359
+ }
360
+ };
361
+
362
+ // src/shared/path.ts
363
+ function normalizeBasePath(path) {
364
+ if (path === "" || path === "/") return "";
365
+ if (path.endsWith("/")) path = path.slice(0, path.length - 1);
366
+ if (!path.startsWith("/")) path = `/${path}`;
367
+ return path === "/" ? "" : path;
368
+ }
369
+
370
+ // src/shared/clock.ts
371
+ function shouldOverwrite(incoming, existing) {
372
+ if (!existing) return true;
373
+ if (incoming.timestamp > existing.timestamp) {
374
+ return true;
375
+ }
376
+ if (incoming.timestamp < existing.timestamp) {
377
+ return false;
378
+ }
379
+ const incomingClient = incoming.clientId ?? "";
380
+ const existingClient = existing.clientId ?? "";
381
+ return incomingClient >= existingClient;
382
+ }
383
+
384
+ // src/client/shared/id.ts
385
+ function randomUUID() {
386
+ if (typeof crypto !== "undefined") {
387
+ if (typeof crypto.randomUUID === "function") {
388
+ return crypto.randomUUID();
389
+ }
390
+ if (typeof crypto.getRandomValues === "function") {
391
+ return "10000000-1000-4000-8000-100000000000".replace(
392
+ /[018]/g,
393
+ (char) => {
394
+ const num = Number(char);
395
+ const randomByte = crypto.getRandomValues(new Uint8Array(1))[0];
396
+ return (num ^ randomByte & 15 >> num / 4).toString(16);
397
+ }
398
+ );
399
+ }
400
+ }
401
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (char) => {
402
+ const randomByte = Math.random() * 16 | 0;
403
+ return (char === "x" ? randomByte : randomByte & 3 | 8).toString(16);
404
+ });
405
+ }
406
+
407
+ // src/client/table.ts
408
+ var Table = class {
409
+ /** Reactive event registry triggered when records in this table are created, updated, or deleted. */
410
+ onChange = new EventRegistry();
411
+ tableName;
412
+ storage;
413
+ /**
414
+ * Creates a new Table instance.
415
+ *
416
+ * @param tableName - Name of the table.
417
+ * @param storage - Local storage coordinator.
418
+ */
419
+ constructor(tableName, storage) {
420
+ this.tableName = tableName;
421
+ this.storage = storage;
422
+ }
423
+ /**
424
+ * The name of the table.
425
+ */
426
+ get name() {
427
+ return this.tableName;
428
+ }
429
+ /**
430
+ * The client identifier for local mutations.
431
+ */
432
+ get clientId() {
433
+ return this.storage.clientId;
434
+ }
435
+ /**
436
+ * Retrieves a single record by its identifier.
437
+ *
438
+ * @param id - The unique record identifier.
439
+ * @returns A promise resolving to the record data, or `undefined` if not found.
440
+ */
441
+ async get(id) {
442
+ const record = await this.storage.getRecord(this.tableName, id);
443
+ return record?.data;
444
+ }
445
+ /**
446
+ * Retrieves records stored in this table, optionally filtered by a list of IDs.
447
+ *
448
+ * @param ids - Optional list of record identifiers to fetch. If omitted, retrieves all records.
449
+ * @returns A promise resolving to an array of record data objects.
450
+ */
451
+ async getAll(ids) {
452
+ if (ids !== void 0) {
453
+ if (ids.length === 0) return [];
454
+ const map = await this.storage.getRecords(this.tableName, ids);
455
+ const results = [];
456
+ for (const id of ids) {
457
+ const rec = map.get(id);
458
+ if (rec) {
459
+ results.push(rec.data);
460
+ }
461
+ }
462
+ return results;
463
+ }
464
+ const records = await this.storage.getAllRecords(this.tableName);
465
+ return records.map((r) => r.data);
466
+ }
467
+ /**
468
+ * Deletes all records in this table and queues tombstones for synchronization.
469
+ *
470
+ * @returns A promise resolving to the number of deleted records.
471
+ */
472
+ async clear() {
473
+ const records = await this.storage.getAllRecords(this.tableName);
474
+ const ids = records.map((r) => r.id);
475
+ return this.deleteAll(ids);
476
+ }
477
+ /**
478
+ * Retrieves a single stored record including metadata (version, timestamp).
479
+ *
480
+ * @param id - The unique record identifier.
481
+ * @returns A promise resolving to the stored record with metadata, or `undefined` if not found.
482
+ */
483
+ async getWithMetadata(id) {
484
+ return this.storage.getRecord(this.tableName, id);
485
+ }
486
+ /**
487
+ * Retrieves all records with storage metadata (version, timestamp).
488
+ *
489
+ * @returns A promise resolving to an array of stored records with metadata.
490
+ */
491
+ async getAllWithMetadata() {
492
+ return this.storage.getAllRecords(this.tableName);
493
+ }
494
+ /**
495
+ * Stores or updates a record locally and triggers synchronization.
496
+ *
497
+ * @param id - The unique record identifier.
498
+ * @param data - The data payload to save.
499
+ * @returns A promise resolving to the saved data.
500
+ */
501
+ async put(id, data) {
502
+ await this.putAll([{ id, data }]);
503
+ return data;
504
+ }
505
+ /**
506
+ * Atomically stores or updates multiple records locally in a single batch,
507
+ * queues outbox mutations, notifies subscribers with an event list, and triggers synchronization.
508
+ *
509
+ * @param entries - Array of record entries to save.
510
+ * @returns A promise resolving to the array of saved data objects.
511
+ */
512
+ async putAll(entries) {
513
+ if (entries.length === 0) return [];
514
+ const ids = entries.map((e) => e.id);
515
+ const existingMap = await this.storage.getRecords(this.tableName, ids);
516
+ const now = Date.now();
517
+ const mutations = [];
518
+ const events = [];
519
+ const savedData = [];
520
+ for (const entry of entries) {
521
+ const { mutation, event } = this.createMutationItem(
522
+ entry.id,
523
+ "put" /* Put */,
524
+ existingMap.get(entry.id),
525
+ now,
526
+ entry.data
527
+ );
528
+ mutations.push(mutation);
529
+ events.push(event);
530
+ savedData.push(entry.data);
531
+ }
532
+ await this.storage.applyLocalChanges(this.tableName, mutations);
533
+ this.onChange.publish(events);
534
+ return savedData;
535
+ }
536
+ /**
537
+ * Deletes a record locally by creating a tombstone and triggers synchronization.
538
+ *
539
+ * @param id - The unique record identifier to delete.
540
+ * @returns A promise resolving to `true` if the record existed and was deleted; otherwise `false`.
541
+ */
542
+ async delete(id) {
543
+ const deletedCount = await this.deleteAll([id]);
544
+ return deletedCount > 0;
545
+ }
546
+ /**
547
+ * Atomically deletes multiple records locally in a single batch,
548
+ * creates tombstones, notifies subscribers with an event list, and triggers synchronization.
549
+ *
550
+ * @param ids - Array of record identifiers to delete.
551
+ * @returns A promise resolving to the count of records that existed and were deleted.
552
+ */
553
+ async deleteAll(ids) {
554
+ if (ids.length === 0) return 0;
555
+ const existingMap = await this.storage.getRecords(this.tableName, ids);
556
+ const now = Date.now();
557
+ const mutations = [];
558
+ const events = [];
559
+ for (const id of ids) {
560
+ const existing = existingMap.get(id);
561
+ if (!existing || existing.deleted) continue;
562
+ const { mutation, event } = this.createMutationItem(
563
+ id,
564
+ "delete" /* Delete */,
565
+ existing,
566
+ now
567
+ );
568
+ mutations.push(mutation);
569
+ events.push(event);
570
+ }
571
+ if (mutations.length > 0) {
572
+ await this.storage.applyLocalChanges(this.tableName, mutations);
573
+ this.onChange.publish(events);
574
+ }
575
+ return mutations.length;
576
+ }
577
+ /**
578
+ * Subscribes to the complete list of records in this table.
579
+ * Immediately invokes the listener with the current records, and re-invokes it
580
+ * whenever any local or remote mutations occur on this table.
581
+ *
582
+ * @param listener - Callback receiving the latest array of records.
583
+ * @returns An unsubscribe function.
584
+ */
585
+ subscribeAll(listener) {
586
+ let isActive = true;
587
+ let currentVersion = 0;
588
+ const fetchAndNotify = () => {
589
+ const version = ++currentVersion;
590
+ this.getAll().then((items) => {
591
+ if (isActive && version === currentVersion) {
592
+ listener(items);
593
+ }
594
+ }).catch(() => {
595
+ });
596
+ };
597
+ fetchAndNotify();
598
+ const unsubscribe = this.onChange.register(() => {
599
+ fetchAndNotify();
600
+ });
601
+ return () => {
602
+ isActive = false;
603
+ unsubscribe();
604
+ };
605
+ }
606
+ /**
607
+ * Notifies registered subscribers of remote change events.
608
+ *
609
+ * @param events - The list of remote change events.
610
+ */
611
+ notifyRemoteChanges(events) {
612
+ if (events.length === 0) return;
613
+ this.onChange.publish(events);
614
+ }
615
+ // -- Private Helpers ------------------------------------------------------
616
+ createMutationItem(id, op, existing, now, data) {
617
+ const version = (existing?.version ?? 0) + 1;
618
+ const timestamp = Math.max(now, (existing?.timestamp ?? 0) + 1);
619
+ const change = {
620
+ table: this.tableName,
621
+ id,
622
+ op,
623
+ data,
624
+ timestamp,
625
+ clientId: this.clientId,
626
+ version
627
+ };
628
+ return {
629
+ mutation: { id, op, data, change },
630
+ event: { op, id, data, isRemote: false }
631
+ };
632
+ }
633
+ };
634
+
635
+ // src/client/storage.ts
636
+ var Storage = class {
637
+ /** Name of the local IndexedDB database. */
638
+ name;
639
+ /** Unique client instance identifier used for monotonic logical clock tie-breaking. */
640
+ clientId;
641
+ /** Reactive event registry triggered when mutations are committed to the local database. */
642
+ onLocalChange = new EventRegistry();
643
+ databasePromise = null;
644
+ schemaMutex = Promise.resolve();
645
+ tables = /* @__PURE__ */ new Map();
646
+ /**
647
+ * Creates a new Storage instance.
648
+ *
649
+ * @param name - Name of the IndexedDB database.
650
+ */
651
+ constructor(name) {
652
+ this.name = name;
653
+ this.clientId = randomUUID();
654
+ }
655
+ /**
656
+ * Obtains a typed table reference for reading, mutating, and subscribing to records.
657
+ * Tables are created dynamically on-demand if not already declared.
658
+ *
659
+ * @typeParam T - Data payload model type for records in this table.
660
+ * @param name - The table name.
661
+ * @returns A typed `Table<T>` instance.
662
+ */
663
+ table(name) {
664
+ let table = this.tables.get(name);
665
+ if (!table) {
666
+ table = new Table(name, this);
667
+ this.tables.set(name, table);
668
+ }
669
+ return table;
670
+ }
671
+ /**
672
+ * Opens or returns the active IndexedDB connection, executing schema upgrades when needed.
673
+ *
674
+ * @returns A promise resolving to the open IDBDatabase instance.
675
+ */
676
+ async getDatabase() {
677
+ if (this.databasePromise) return this.databasePromise;
678
+ this.databasePromise = this.openDatabase().catch((err) => {
679
+ this.databasePromise = null;
680
+ throw err;
681
+ });
682
+ return this.databasePromise;
683
+ }
684
+ /**
685
+ * Dynamically ensures that multiple application tables exist in IndexedDB.
686
+ *
687
+ * @param tableNames - Array of table names to ensure.
688
+ */
689
+ async ensureTables(tableNames) {
690
+ this.schemaMutex = this.schemaMutex.then(async () => {
691
+ const database = await this.getDatabase();
692
+ const missing = tableNames.filter(
693
+ (s) => !database.objectStoreNames.contains(s)
694
+ );
695
+ if (missing.length === 0) return;
696
+ const nextVersion = database.version + 1;
697
+ this.databasePromise = this.upgradeDatabase(
698
+ database,
699
+ nextVersion,
700
+ missing
701
+ ).catch((err) => {
702
+ this.databasePromise = null;
703
+ throw err;
704
+ });
705
+ await this.databasePromise;
706
+ });
707
+ return this.schemaMutex;
708
+ }
709
+ /**
710
+ * Ensures a single application table exists in IndexedDB.
711
+ *
712
+ * @param tableName - Name of the table to ensure.
713
+ */
714
+ async ensureTable(tableName) {
715
+ await this.ensureTables([tableName]);
716
+ }
717
+ /**
718
+ * Retrieves a metadata value from the internal metadata store.
719
+ *
720
+ * @typeParam T - Expected value type.
721
+ * @param key - The metadata key identifier.
722
+ * @returns The stored metadata value, or `undefined` if not set.
723
+ */
724
+ async getMeta(key) {
725
+ return this.withDatabase(async (database) => {
726
+ const entry = await promisifyRequest(
727
+ database.transaction(META_STORE, "readonly").objectStore(META_STORE).get(key)
728
+ );
729
+ return entry?.value;
730
+ });
731
+ }
732
+ /**
733
+ * Sets a metadata value in the internal metadata store.
734
+ *
735
+ * @param key - The metadata key identifier.
736
+ * @param value - The value to store.
737
+ */
738
+ async setMeta(key, value) {
739
+ return this.withDatabase(async (database) => {
740
+ await promisifyRequest(
741
+ database.transaction(META_STORE, "readwrite").objectStore(META_STORE).put({ key, value })
742
+ );
743
+ });
744
+ }
745
+ /**
746
+ * Deletes a metadata entry from the internal metadata store.
747
+ *
748
+ * @param key - The metadata key identifier.
749
+ */
750
+ async deleteMeta(key) {
751
+ return this.withDatabase(async (database) => {
752
+ await promisifyRequest(
753
+ database.transaction(META_STORE, "readwrite").objectStore(META_STORE).delete(key)
754
+ );
755
+ });
756
+ }
757
+ /**
758
+ * Retrieves a single stored record by table and identifier.
759
+ *
760
+ * @typeParam T - Expected payload type.
761
+ * @param tableName - Table name.
762
+ * @param id - Record identifier.
763
+ * @returns Stored record or `undefined` if not found.
764
+ */
765
+ async getRecord(tableName, id) {
766
+ await this.ensureTable(tableName);
767
+ return this.withDatabase(async (database) => {
768
+ return promisifyRequest(
769
+ database.transaction(tableName, "readonly").objectStore(tableName).get(id)
770
+ );
771
+ });
772
+ }
773
+ /**
774
+ * Retrieves multiple stored records by their identifiers in a single readonly transaction.
775
+ *
776
+ * @typeParam T - Expected payload type.
777
+ * @param tableName - Table name.
778
+ * @param ids - Array of record identifiers.
779
+ * @returns Map of found stored records keyed by id.
780
+ */
781
+ async getRecords(tableName, ids) {
782
+ if (ids.length === 0) return /* @__PURE__ */ new Map();
783
+ await this.ensureTable(tableName);
784
+ return this.withDatabase(async (database) => {
785
+ const tx = database.transaction(tableName, "readonly");
786
+ const store = tx.objectStore(tableName);
787
+ const results = /* @__PURE__ */ new Map();
788
+ for (const id of ids) {
789
+ const request = store.get(id);
790
+ request.onsuccess = () => {
791
+ if (request.result) {
792
+ results.set(id, request.result);
793
+ }
794
+ };
795
+ }
796
+ await promisifyTransaction(tx);
797
+ return results;
798
+ });
799
+ }
800
+ /**
801
+ * Retrieves stored records from a specified table.
802
+ *
803
+ * @typeParam T - Data payload type.
804
+ * @param tableName - Table name.
805
+ * @returns Array of stored records with metadata.
806
+ */
807
+ async getAllRecords(tableName) {
808
+ await this.ensureTable(tableName);
809
+ return this.withDatabase(async (database) => {
810
+ const records = await promisifyRequest(
811
+ database.transaction(tableName, "readonly").objectStore(tableName).getAll()
812
+ );
813
+ return records ?? [];
814
+ });
815
+ }
816
+ /**
817
+ * Atomically persists a batch of local mutations alongside outbox changelog entries
818
+ * within a single IndexedDB transaction.
819
+ *
820
+ * @typeParam T - Data payload type.
821
+ * @param tableName - Target table name.
822
+ * @param mutations - List of mutations to apply and queue into outbox.
823
+ * @returns Array of newly stored records with updated metadata.
824
+ */
825
+ async applyLocalChanges(tableName, mutations) {
826
+ if (mutations.length === 0) return [];
827
+ await this.ensureTable(tableName);
828
+ return this.withDatabase(async (database) => {
829
+ const tx = database.transaction([tableName, OUTBOX_STORE], "readwrite");
830
+ const dataStore = tx.objectStore(tableName);
831
+ const outboxStore = tx.objectStore(OUTBOX_STORE);
832
+ const records = [];
833
+ const now = Date.now();
834
+ for (const item of mutations) {
835
+ const isDelete = item.op === "delete" /* Delete */;
836
+ const record = {
837
+ id: item.id,
838
+ data: item.data ?? null,
839
+ timestamp: item.change.timestamp,
840
+ version: item.change.version ?? 1,
841
+ clientId: item.change.clientId,
842
+ deleted: isDelete
843
+ };
844
+ if (isDelete) {
845
+ dataStore.delete(item.id);
846
+ } else {
847
+ dataStore.put(record);
848
+ }
849
+ outboxStore.add({
850
+ change: item.change,
851
+ createdAt: now
852
+ });
853
+ records.push(record);
854
+ }
855
+ await promisifyTransaction(tx);
856
+ this.onLocalChange.publish();
857
+ return records;
858
+ });
859
+ }
860
+ /**
861
+ * Retrieves all currently queued outbox entries awaiting sync.
862
+ *
863
+ * @param limit - Optional maximum number of entries to retrieve.
864
+ * @returns Array of pending outbox queue items.
865
+ */
866
+ async getPendingOutbox(limit) {
867
+ return this.withDatabase(async (database) => {
868
+ const store = database.transaction(OUTBOX_STORE, "readonly").objectStore(OUTBOX_STORE);
869
+ const req = limit ? store.getAll(null, limit) : store.getAll();
870
+ const results = await promisifyRequest(req);
871
+ return results ?? [];
872
+ });
873
+ }
874
+ /**
875
+ * Clears acknowledged changes from the outbox by their local database IDs.
876
+ *
877
+ * @param localIds - Array of auto-incremented local outbox IDs to remove.
878
+ */
879
+ async removeOutboxEntries(localIds) {
880
+ if (localIds.length === 0) return;
881
+ return this.withDatabase(async (database) => {
882
+ const tx = database.transaction(OUTBOX_STORE, "readwrite");
883
+ const store = tx.objectStore(OUTBOX_STORE);
884
+ for (const id of localIds) {
885
+ store.delete(id);
886
+ }
887
+ await promisifyTransaction(tx);
888
+ });
889
+ }
890
+ /**
891
+ * Applies an entire snapshot batch across tables atomically in a single transaction.
892
+ *
893
+ * @param snapshot - Array of record items to persist.
894
+ * @param seq - Global sequence number corresponding to this snapshot.
895
+ */
896
+ async applySnapshotBatch(snapshot, seq) {
897
+ await this.applyBatchRecords(snapshot, seq);
898
+ }
899
+ /**
900
+ * Applies a batch of incoming remote delta mutations atomically across tables
901
+ * in a single IndexedDB transaction without generating local outbox echo entries.
902
+ *
903
+ * @param changes - Array of change records received from the server.
904
+ * @param seq - New sequence number to record upon transaction commit.
905
+ */
906
+ async applyRemoteChangesBatch(changes, seq) {
907
+ if (changes.length === 0) {
908
+ await this.setMeta("lastSyncSeq", seq);
909
+ return;
910
+ }
911
+ const records = changes.map((change) => ({
912
+ table: change.table,
913
+ id: change.id,
914
+ data: change.data,
915
+ timestamp: change.timestamp,
916
+ version: change.version ?? 1,
917
+ deleted: change.op === "delete" /* Delete */,
918
+ clientId: change.clientId
919
+ }));
920
+ await this.applyBatchRecords(records, seq);
921
+ }
922
+ /**
923
+ * Clears only user table stores, leaving internal metadata and outbox intact if desired.
924
+ *
925
+ * @param clearOutbox - Whether to clear the pending outbox queue as well (defaults to `true`).
926
+ */
927
+ async clearTables(clearOutbox = true) {
928
+ return this.withDatabase(async (db) => {
929
+ const storeNames = Array.from(db.objectStoreNames).filter((name) => {
930
+ if (name === META_STORE) return false;
931
+ if (name === OUTBOX_STORE && !clearOutbox) return false;
932
+ return true;
933
+ });
934
+ await this.clearStores(db, storeNames);
935
+ });
936
+ }
937
+ /**
938
+ * Clears all local table stores, outbox changelog entries, and metadata.
939
+ */
940
+ async clearAllData() {
941
+ return this.withDatabase(async (db) => {
942
+ await this.clearStores(db, Array.from(db.objectStoreNames));
943
+ });
944
+ }
945
+ /**
946
+ * Closes the active IndexedDB connection.
947
+ */
948
+ async close() {
949
+ if (this.databasePromise) {
950
+ const db = await this.databasePromise;
951
+ db.close();
952
+ this.databasePromise = null;
953
+ }
954
+ }
955
+ // -- Private Helpers ------------------------------------------------------
956
+ async withDatabase(fn) {
957
+ return this.schemaMutex.then(async () => {
958
+ const db = await this.getDatabase();
959
+ return fn(db);
960
+ });
961
+ }
962
+ async applyBatchRecords(records, seq) {
963
+ if (records.length === 0) {
964
+ await this.setMeta("lastSyncSeq", seq);
965
+ return;
966
+ }
967
+ const tableNames = Array.from(
968
+ new Set(records.map((item) => item.table))
969
+ ).filter(Boolean);
970
+ await this.ensureTables(tableNames);
971
+ await this.withDatabase(async (database) => {
972
+ const tx = database.transaction([...tableNames, META_STORE], "readwrite");
973
+ const tableStores = /* @__PURE__ */ new Map();
974
+ for (const name of tableNames) {
975
+ tableStores.set(name, tx.objectStore(name));
976
+ }
977
+ for (const item of records) {
978
+ const store = tableStores.get(item.table);
979
+ if (!store) continue;
980
+ const getReq = store.get(item.id);
981
+ getReq.onsuccess = () => {
982
+ const existing = getReq.result;
983
+ if (!existing || shouldOverwrite(item, existing)) {
984
+ if (item.deleted) {
985
+ store.delete(item.id);
986
+ } else {
987
+ const record = {
988
+ id: item.id,
989
+ data: item.data,
990
+ timestamp: item.timestamp,
991
+ version: item.version ?? 1,
992
+ clientId: item.clientId
993
+ };
994
+ store.put(record);
995
+ }
996
+ }
997
+ };
998
+ }
999
+ const metaStore = tx.objectStore(META_STORE);
1000
+ metaStore.put({ key: "lastSyncSeq", value: seq });
1001
+ metaStore.put({ key: "lastSyncTimestamp", value: Date.now() });
1002
+ await promisifyTransaction(tx);
1003
+ });
1004
+ }
1005
+ async clearStores(database, storeNames) {
1006
+ if (storeNames.length === 0) return;
1007
+ const tx = database.transaction(storeNames, "readwrite");
1008
+ for (const name of storeNames) {
1009
+ tx.objectStore(name).clear();
1010
+ }
1011
+ await promisifyTransaction(tx);
1012
+ }
1013
+ async openDatabase() {
1014
+ return new Promise((resolve, reject) => {
1015
+ const request = indexedDB.open(this.name);
1016
+ request.onupgradeneeded = () => {
1017
+ this.createInternalStores(request.result);
1018
+ };
1019
+ request.onsuccess = () => {
1020
+ const database = request.result;
1021
+ if (!database.objectStoreNames.contains(OUTBOX_STORE) || !database.objectStoreNames.contains(META_STORE)) {
1022
+ const nextVersion = database.version + 1;
1023
+ this.databasePromise = this.upgradeDatabase(
1024
+ database,
1025
+ nextVersion,
1026
+ []
1027
+ );
1028
+ this.databasePromise.then(resolve).catch(reject);
1029
+ } else {
1030
+ resolve(database);
1031
+ }
1032
+ };
1033
+ request.onerror = () => reject(request.error);
1034
+ });
1035
+ }
1036
+ async upgradeDatabase(currentDb, nextVersion, newTables) {
1037
+ currentDb.close();
1038
+ return new Promise((resolve, reject) => {
1039
+ const request = indexedDB.open(this.name, nextVersion);
1040
+ request.onupgradeneeded = () => {
1041
+ const database = request.result;
1042
+ this.createInternalStores(database);
1043
+ for (const tableName of newTables) {
1044
+ if (!database.objectStoreNames.contains(tableName)) {
1045
+ database.createObjectStore(tableName, { keyPath: "id" });
1046
+ }
1047
+ }
1048
+ };
1049
+ request.onsuccess = () => resolve(request.result);
1050
+ request.onerror = () => reject(request.error);
1051
+ });
1052
+ }
1053
+ createInternalStores(db) {
1054
+ if (!db.objectStoreNames.contains(OUTBOX_STORE)) {
1055
+ db.createObjectStore(OUTBOX_STORE, {
1056
+ keyPath: "localId",
1057
+ autoIncrement: true
1058
+ });
1059
+ }
1060
+ if (!db.objectStoreNames.contains(META_STORE)) {
1061
+ db.createObjectStore(META_STORE, { keyPath: "key" });
1062
+ }
1063
+ }
1064
+ };
1065
+ var OUTBOX_STORE = "__tether_outbox";
1066
+ var META_STORE = "__tether_meta";
1067
+ function promisifyRequest(request) {
1068
+ return new Promise((resolve, reject) => {
1069
+ request.onsuccess = () => resolve(request.result);
1070
+ request.onerror = () => reject(request.error);
1071
+ });
1072
+ }
1073
+ function promisifyTransaction(tx) {
1074
+ return new Promise((resolve, reject) => {
1075
+ tx.oncomplete = () => resolve();
1076
+ tx.onerror = () => reject(tx.error);
1077
+ tx.onabort = () => reject(tx.error ?? new Error("IndexedDB transaction aborted"));
1078
+ });
1079
+ }
1080
+
1081
+ // src/client/sync.ts
1082
+ var SyncStatus = /* @__PURE__ */ ((SyncStatus2) => {
1083
+ SyncStatus2[SyncStatus2["Disconnected"] = 0] = "Disconnected";
1084
+ SyncStatus2[SyncStatus2["Connecting"] = 1] = "Connecting";
1085
+ SyncStatus2[SyncStatus2["Connected"] = 2] = "Connected";
1086
+ SyncStatus2[SyncStatus2["Error"] = 3] = "Error";
1087
+ return SyncStatus2;
1088
+ })(SyncStatus || {});
1089
+ var Sync = class {
1090
+ /** Remote WebSocket endpoint URL. */
1091
+ url;
1092
+ /** Application namespace identifier for partitioning synchronization channels. */
1093
+ appId;
1094
+ /** Client identifier used for conflict resolution tie-breaking. */
1095
+ clientId;
1096
+ /** Reactive event registry triggered whenever synchronization status transitions. */
1097
+ onStatusChange = new EventRegistry();
1098
+ /** Reactive event registry triggered whenever background sync or network errors occur. */
1099
+ onError = new EventRegistry();
1100
+ token;
1101
+ storage;
1102
+ options;
1103
+ webSocket = null;
1104
+ currentStatus = 0 /* Disconnected */;
1105
+ reconnectTimer = null;
1106
+ reconnectAttempts = 0;
1107
+ pingTimer = null;
1108
+ pushTimer = null;
1109
+ isPushing = false;
1110
+ isDestroyed = false;
1111
+ pendingBatches = /* @__PURE__ */ new Map();
1112
+ // batchId -> localIds
1113
+ messageQueue = Promise.resolve();
1114
+ /**
1115
+ * Creates a new Sync instance.
1116
+ *
1117
+ * @param storage - Local storage coordinator.
1118
+ * @param options - Configuration options for sync and connection.
1119
+ */
1120
+ constructor(storage, options) {
1121
+ if (!options.appId) {
1122
+ throw new TetherClientError(
1123
+ 0 /* MissingConfiguration */,
1124
+ "Missing required appId in SyncOptions"
1125
+ );
1126
+ }
1127
+ if (!options.clientId) {
1128
+ throw new TetherClientError(
1129
+ 0 /* MissingConfiguration */,
1130
+ "Missing required clientId in SyncOptions"
1131
+ );
1132
+ }
1133
+ this.url = options.url;
1134
+ this.token = options.token;
1135
+ this.storage = storage;
1136
+ this.appId = options.appId;
1137
+ this.clientId = options.clientId;
1138
+ this.options = {
1139
+ reconnectIntervalMs: 1e3,
1140
+ maxReconnectIntervalMs: 3e4,
1141
+ pingIntervalMs: 3e4,
1142
+ ...options
1143
+ };
1144
+ if (typeof window !== "undefined" && typeof window.addEventListener === "function") {
1145
+ window.addEventListener("online", this.handleOnline);
1146
+ window.addEventListener("offline", this.handleOffline);
1147
+ }
1148
+ if (this.token && this.url) {
1149
+ this.connect();
1150
+ }
1151
+ }
1152
+ /**
1153
+ * Current operational status of the sync coordinator.
1154
+ */
1155
+ get status() {
1156
+ return this.currentStatus;
1157
+ }
1158
+ /**
1159
+ * Initiates a WebSocket connection to the sync endpoint and sends authentication.
1160
+ *
1161
+ * @param token - Optional session token to connect with.
1162
+ * @param url - Optional WebSocket URL override.
1163
+ */
1164
+ connect(token, url) {
1165
+ if (token) this.token = token;
1166
+ if (url) this.url = url;
1167
+ if (!this.url) {
1168
+ return;
1169
+ }
1170
+ if (this.webSocket) {
1171
+ this.disconnect();
1172
+ }
1173
+ this.setStatus(1 /* Connecting */);
1174
+ const wsUrl = this.url;
1175
+ const WebSocketImpl = this.options.webSocketClass ?? (typeof WebSocket !== "undefined" ? WebSocket : null);
1176
+ if (!WebSocketImpl) {
1177
+ this.setStatus(3 /* Error */);
1178
+ this.onError.publish(
1179
+ new TetherClientError(
1180
+ 0 /* MissingConfiguration */,
1181
+ "No WebSocket implementation available in this environment"
1182
+ )
1183
+ );
1184
+ return;
1185
+ }
1186
+ try {
1187
+ this.webSocket = new WebSocketImpl(wsUrl);
1188
+ } catch (err) {
1189
+ this.setStatus(3 /* Error */);
1190
+ this.onError.publish(
1191
+ new TetherClientError(
1192
+ 3 /* NetworkError */,
1193
+ err instanceof Error ? err.message : "Failed to construct WebSocket connection"
1194
+ )
1195
+ );
1196
+ this.scheduleReconnect();
1197
+ return;
1198
+ }
1199
+ this.webSocket.onopen = () => {
1200
+ this.reconnectAttempts = 0;
1201
+ this.startPing();
1202
+ this.sendAuth();
1203
+ };
1204
+ this.webSocket.onmessage = (event) => {
1205
+ try {
1206
+ const raw = typeof event.data === "string" ? event.data : event.data.toString("utf8");
1207
+ const msg = JSON.parse(raw);
1208
+ this.messageQueue = this.messageQueue.then(() => this.handleServerMessage(msg)).catch((err) => {
1209
+ this.onError.publish(
1210
+ new TetherClientError(
1211
+ 8 /* SyncError */,
1212
+ err instanceof Error ? err.message : "Failed to process incoming WebSocket message"
1213
+ )
1214
+ );
1215
+ });
1216
+ } catch (err) {
1217
+ this.onError.publish(
1218
+ new TetherClientError(
1219
+ 8 /* SyncError */,
1220
+ err instanceof Error ? err.message : "Failed to parse incoming WebSocket message"
1221
+ )
1222
+ );
1223
+ }
1224
+ };
1225
+ this.webSocket.onerror = () => {
1226
+ this.onError.publish(
1227
+ new TetherClientError(
1228
+ 3 /* NetworkError */,
1229
+ "WebSocket connection encountered an error"
1230
+ )
1231
+ );
1232
+ };
1233
+ this.webSocket.onclose = (event) => {
1234
+ this.stopPing();
1235
+ this.pendingBatches.clear();
1236
+ this.messageQueue = Promise.resolve();
1237
+ this.webSocket = null;
1238
+ if (!this.isDestroyed) {
1239
+ this.setStatus(0 /* Disconnected */);
1240
+ if (event.code !== 1e3 && event.code !== 1005) {
1241
+ this.scheduleReconnect();
1242
+ }
1243
+ }
1244
+ };
1245
+ }
1246
+ /**
1247
+ * Disconnects the active WebSocket connection.
1248
+ */
1249
+ disconnect() {
1250
+ this.stopPing();
1251
+ this.pendingBatches.clear();
1252
+ this.messageQueue = Promise.resolve();
1253
+ if (this.reconnectTimer) {
1254
+ clearTimeout(this.reconnectTimer);
1255
+ this.reconnectTimer = null;
1256
+ }
1257
+ if (this.webSocket) {
1258
+ const ws = this.webSocket;
1259
+ this.webSocket = null;
1260
+ ws.onopen = null;
1261
+ ws.onmessage = null;
1262
+ ws.onerror = () => {
1263
+ };
1264
+ ws.onclose = null;
1265
+ try {
1266
+ const wsWithTerminate = ws;
1267
+ if (typeof wsWithTerminate.terminate === "function") {
1268
+ wsWithTerminate.terminate();
1269
+ } else {
1270
+ ws.close();
1271
+ }
1272
+ } catch {
1273
+ }
1274
+ }
1275
+ this.setStatus(0 /* Disconnected */);
1276
+ }
1277
+ /**
1278
+ * Permanently tears down the sync coordinator and cancels reconnection timers.
1279
+ */
1280
+ destroy() {
1281
+ this.isDestroyed = true;
1282
+ if (typeof window !== "undefined" && typeof window.removeEventListener === "function") {
1283
+ window.removeEventListener("online", this.handleOnline);
1284
+ window.removeEventListener("offline", this.handleOffline);
1285
+ }
1286
+ this.disconnect();
1287
+ }
1288
+ /**
1289
+ * Schedules a debounced push of pending outbox mutations.
1290
+ *
1291
+ * @param delayMs - Optional override for debounce delay in milliseconds.
1292
+ */
1293
+ schedulePush(delayMs) {
1294
+ const delay = delayMs ?? this.options.pushDebounceMs ?? 10;
1295
+ if (this.pushTimer) clearTimeout(this.pushTimer);
1296
+ this.pushTimer = setTimeout(() => {
1297
+ this.pushTimer = null;
1298
+ this.pushOutbox();
1299
+ }, delay);
1300
+ }
1301
+ /**
1302
+ * Immediately extracts queued outbox changes and transmits them to the server.
1303
+ */
1304
+ async pushOutbox() {
1305
+ if (this.isPushing || this.pendingBatches.size > 0 || this.currentStatus !== 2 /* Connected */ || !this.webSocket || this.webSocket.readyState !== (this.webSocket.OPEN ?? 1)) {
1306
+ return;
1307
+ }
1308
+ this.isPushing = true;
1309
+ try {
1310
+ const pending = await this.storage.getPendingOutbox(500);
1311
+ if (pending.length === 0) return;
1312
+ const batchId = `batch_${Math.random().toString(36).substring(2, 10)}`;
1313
+ const localIds = [];
1314
+ const changes = [];
1315
+ for (const entry of pending) {
1316
+ if (entry.localId !== void 0) {
1317
+ localIds.push(entry.localId);
1318
+ }
1319
+ changes.push(entry.change);
1320
+ }
1321
+ this.pendingBatches.set(batchId, localIds);
1322
+ this.send({
1323
+ type: "change_batch" /* ChangeBatch */,
1324
+ clientId: this.clientId,
1325
+ batchId,
1326
+ changes
1327
+ });
1328
+ } catch (err) {
1329
+ this.onError.publish(
1330
+ new TetherClientError(
1331
+ 8 /* SyncError */,
1332
+ err instanceof Error ? err.message : "Failed to push outbox batch to server"
1333
+ )
1334
+ );
1335
+ } finally {
1336
+ this.isPushing = false;
1337
+ }
1338
+ }
1339
+ // -- Private Helpers ------------------------------------------------------
1340
+ setStatus(newStatus) {
1341
+ if (this.currentStatus === newStatus) return;
1342
+ this.currentStatus = newStatus;
1343
+ this.onStatusChange.publish(newStatus);
1344
+ }
1345
+ startPing() {
1346
+ this.stopPing();
1347
+ const interval = this.options.pingIntervalMs ?? 3e4;
1348
+ if (interval <= 0) return;
1349
+ this.pingTimer = setInterval(() => {
1350
+ this.send({ type: "ping" /* Ping */ });
1351
+ }, interval);
1352
+ }
1353
+ stopPing() {
1354
+ if (this.pingTimer) {
1355
+ clearInterval(this.pingTimer);
1356
+ this.pingTimer = null;
1357
+ }
1358
+ }
1359
+ isOnline() {
1360
+ if (typeof navigator !== "undefined" && typeof navigator.onLine === "boolean") {
1361
+ return navigator.onLine;
1362
+ }
1363
+ return true;
1364
+ }
1365
+ handleOnline = () => {
1366
+ if (this.isDestroyed || !this.token || !this.url) return;
1367
+ this.reconnectAttempts = 0;
1368
+ if (this.reconnectTimer) {
1369
+ clearTimeout(this.reconnectTimer);
1370
+ this.reconnectTimer = null;
1371
+ }
1372
+ this.connect();
1373
+ };
1374
+ handleOffline = () => {
1375
+ if (this.isDestroyed) return;
1376
+ if (this.reconnectTimer) {
1377
+ clearTimeout(this.reconnectTimer);
1378
+ this.reconnectTimer = null;
1379
+ }
1380
+ if (this.webSocket) {
1381
+ this.disconnect();
1382
+ }
1383
+ };
1384
+ scheduleReconnect() {
1385
+ if (this.reconnectTimer || this.isDestroyed || !this.isOnline()) return;
1386
+ const base = this.options.reconnectIntervalMs ?? 1e3;
1387
+ const max = this.options.maxReconnectIntervalMs ?? 3e4;
1388
+ const delay = Math.min(base * 2 ** this.reconnectAttempts, max);
1389
+ this.reconnectAttempts++;
1390
+ this.reconnectTimer = setTimeout(() => {
1391
+ this.reconnectTimer = null;
1392
+ this.connect();
1393
+ }, delay);
1394
+ }
1395
+ send(msg) {
1396
+ if (this.webSocket && this.webSocket.readyState === (this.webSocket.OPEN ?? 1)) {
1397
+ this.webSocket.send(JSON.stringify(msg));
1398
+ }
1399
+ }
1400
+ async sendAuth() {
1401
+ const lastSyncSeq = await this.storage.getMeta("lastSyncSeq") ?? 0;
1402
+ this.send({
1403
+ type: "auth" /* Auth */,
1404
+ protocolVersion: PROTOCOL_VERSION,
1405
+ token: this.token ?? "",
1406
+ appId: this.options.appId,
1407
+ clientId: this.clientId,
1408
+ lastSyncSeq
1409
+ });
1410
+ }
1411
+ async handleServerMessage(msg) {
1412
+ switch (msg.type) {
1413
+ case "auth_success" /* AuthSuccess */: {
1414
+ this.reconnectAttempts = 0;
1415
+ this.setStatus(2 /* Connected */);
1416
+ if (msg.token) {
1417
+ this.token = msg.token;
1418
+ this.options.onTokenRefresh?.(msg.token);
1419
+ }
1420
+ this.startPing();
1421
+ await this.pushOutbox();
1422
+ break;
1423
+ }
1424
+ case "auth_error" /* AuthError */: {
1425
+ this.setStatus(3 /* Error */);
1426
+ this.disconnect();
1427
+ this.options.onAuthError?.(msg.message);
1428
+ this.onError.publish(
1429
+ new TetherClientError(
1430
+ 6 /* AuthenticationFailed */,
1431
+ msg.message
1432
+ )
1433
+ );
1434
+ break;
1435
+ }
1436
+ case "sync_snapshot" /* SyncSnapshot */: {
1437
+ await this.handleSnapshot(msg.snapshot, msg.seq);
1438
+ await this.pushOutbox();
1439
+ break;
1440
+ }
1441
+ case "sync_diff" /* SyncDiff */: {
1442
+ await this.handleDiff(msg.changes, msg.toSeq);
1443
+ await this.pushOutbox();
1444
+ break;
1445
+ }
1446
+ case "broadcast_changes" /* BroadcastChanges */: {
1447
+ await this.handleDiff(msg.changes, msg.seq);
1448
+ break;
1449
+ }
1450
+ case "change_ack" /* ChangeAck */: {
1451
+ const localIds = this.pendingBatches.get(msg.batchId);
1452
+ if (localIds) {
1453
+ this.pendingBatches.delete(msg.batchId);
1454
+ await this.storage.removeOutboxEntries(localIds);
1455
+ }
1456
+ if (msg.appliedSeq !== void 0) {
1457
+ await this.storage.setMeta("lastSyncSeq", msg.appliedSeq);
1458
+ }
1459
+ await this.pushOutbox();
1460
+ break;
1461
+ }
1462
+ case "pong" /* Pong */:
1463
+ break;
1464
+ case "error" /* Error */:
1465
+ this.onError.publish(
1466
+ new TetherClientError(8 /* SyncError */, msg.message)
1467
+ );
1468
+ break;
1469
+ }
1470
+ }
1471
+ async handleSnapshot(records, seq) {
1472
+ await this.storage.applySnapshotBatch(records, seq);
1473
+ this.notifyTableRemoteEvents(
1474
+ records.map((item) => ({
1475
+ table: item.table,
1476
+ id: item.id,
1477
+ isDelete: item.deleted ?? false,
1478
+ data: item.data
1479
+ }))
1480
+ );
1481
+ }
1482
+ async handleDiff(changes, seq) {
1483
+ await this.storage.applyRemoteChangesBatch(changes, seq);
1484
+ this.notifyTableRemoteEvents(
1485
+ changes.map((change) => ({
1486
+ table: change.table,
1487
+ id: change.id,
1488
+ isDelete: change.op === "delete" /* Delete */,
1489
+ data: change.data
1490
+ }))
1491
+ );
1492
+ }
1493
+ notifyTableRemoteEvents(items) {
1494
+ const tableEvents = /* @__PURE__ */ new Map();
1495
+ for (const item of items) {
1496
+ let events = tableEvents.get(item.table);
1497
+ if (!events) {
1498
+ events = [];
1499
+ tableEvents.set(item.table, events);
1500
+ }
1501
+ events.push({
1502
+ op: item.isDelete ? "delete" /* Delete */ : "put" /* Put */,
1503
+ id: item.id,
1504
+ data: item.isDelete ? void 0 : item.data,
1505
+ isRemote: true
1506
+ });
1507
+ }
1508
+ for (const [tableName, events] of tableEvents.entries()) {
1509
+ const table = this.storage.table(tableName);
1510
+ table.notifyRemoteChanges(events);
1511
+ }
1512
+ }
1513
+ };
1514
+
1515
+ // src/client/client.ts
1516
+ var TetherClient = class {
1517
+ /** Reactive event registry triggered whenever the authentication status changes. */
1518
+ onAuthStatusChange = new EventRegistry();
1519
+ /** Reactive event registry triggered whenever the synchronization status changes. */
1520
+ onSyncStatusChange = new EventRegistry();
1521
+ /** Reactive event registry triggered whenever background sync or network errors occur. */
1522
+ onError = new EventRegistry();
1523
+ storage;
1524
+ auth;
1525
+ sync;
1526
+ /**
1527
+ * Initializes a new TetherClient instance and wires reactive auth & sync coordination.
1528
+ *
1529
+ * @param name - Name of the local IndexedDB database.
1530
+ * @param options - Configuration options for the server connection and sync behavior.
1531
+ */
1532
+ constructor(name, options = {}) {
1533
+ this.storage = this.createStorage(name);
1534
+ this.auth = this.createAuth(options, this.storage);
1535
+ this.sync = this.createSync(name, options, this.storage);
1536
+ this.storage.onLocalChange.register(() => {
1537
+ this.sync.schedulePush();
1538
+ });
1539
+ this.auth.onStatusChange.register((status) => {
1540
+ this.onAuthStatusChange.publish(status);
1541
+ if (status === 2 /* SignedIn */ && this.auth.token) {
1542
+ this.sync.connect(this.auth.token);
1543
+ } else {
1544
+ this.sync.disconnect();
1545
+ }
1546
+ });
1547
+ this.sync.onStatusChange.register((status) => {
1548
+ this.onSyncStatusChange.publish(status);
1549
+ });
1550
+ this.sync.onError.register((err) => {
1551
+ this.onError.publish(err);
1552
+ });
1553
+ }
1554
+ // -- Database / Storage ------------------------------------------------------
1555
+ /**
1556
+ * Obtains a typed table reference for reading, mutating, and subscribing to records.
1557
+ * Tables are created dynamically on-demand if not already declared.
1558
+ *
1559
+ * @typeParam T - Data payload model type for records in this table.
1560
+ * @param name - The table name.
1561
+ * @returns A typed `Table<T>` instance.
1562
+ */
1563
+ table(name) {
1564
+ return this.storage.table(name);
1565
+ }
1566
+ /**
1567
+ * Clears all local application tables, outbox entries, and sync metadata.
1568
+ */
1569
+ async clear() {
1570
+ await this.storage.clearAllData();
1571
+ }
1572
+ /**
1573
+ * Closes active synchronization connections and closes the IndexedDB database handle.
1574
+ */
1575
+ async close() {
1576
+ this.sync.destroy();
1577
+ await this.storage.close();
1578
+ }
1579
+ // -- Authentication ------------------------------------------------------
1580
+ /**
1581
+ * Current authentication lifecycle status.
1582
+ */
1583
+ get authStatus() {
1584
+ return this.auth.status;
1585
+ }
1586
+ /**
1587
+ * The authenticated user's username, or `undefined` if signed out.
1588
+ */
1589
+ get username() {
1590
+ return this.auth.username;
1591
+ }
1592
+ /**
1593
+ * Registers a new user account, applies local data mode, and connects synchronization.
1594
+ *
1595
+ * @param options - Registration credentials and data reconciliation settings.
1596
+ * @returns `true` if registration succeeded; `false` otherwise.
1597
+ */
1598
+ async register(options) {
1599
+ return this.auth.register(options);
1600
+ }
1601
+ /**
1602
+ * Logs into an account (or reconnects using remembered credentials), applies data mode, and connects synchronization.
1603
+ *
1604
+ * @param options - Optional login credentials and data reconciliation mode.
1605
+ * @returns `true` if authentication succeeded; `false` otherwise.
1606
+ */
1607
+ async login(options = {}) {
1608
+ return this.auth.login(options);
1609
+ }
1610
+ /**
1611
+ * Logs out of the current session, disconnects sync, and optionally clears local tables.
1612
+ *
1613
+ * @param options - Options controlling whether local data should be wiped.
1614
+ * @returns `true` on successful logout.
1615
+ */
1616
+ async logout(options = {}) {
1617
+ return this.auth.logout(options);
1618
+ }
1619
+ // -- Synchronization ------------------------------------------------------
1620
+ /**
1621
+ * Retrieves the current synchronization status (e.g. Connected, Connecting, Disconnected, Error).
1622
+ */
1623
+ get syncStatus() {
1624
+ return this.sync.status;
1625
+ }
1626
+ // -- Private Helpers ------------------------------------------------------
1627
+ createStorage(name) {
1628
+ return new Storage(name);
1629
+ }
1630
+ createAuth(options, storage) {
1631
+ return new Auth({
1632
+ baseUrl: this.resolveBaseUrl(options),
1633
+ storage,
1634
+ fetchFn: options.fetch
1635
+ });
1636
+ }
1637
+ createSync(name, options, storage) {
1638
+ return new Sync(storage, {
1639
+ url: this.resolveWebSocketUrl(options),
1640
+ appId: options.appId ?? name,
1641
+ clientId: storage.clientId,
1642
+ webSocketClass: options.webSocketClass,
1643
+ pushDebounceMs: options.pushDebounceMs,
1644
+ reconnectIntervalMs: options.reconnectIntervalMs,
1645
+ maxReconnectIntervalMs: options.maxReconnectIntervalMs,
1646
+ pingIntervalMs: options.pingIntervalMs,
1647
+ onTokenRefresh: (token) => {
1648
+ this.auth.handleTokenRefresh(token);
1649
+ },
1650
+ onAuthError: (message) => {
1651
+ this.auth.handleAuthError(message);
1652
+ }
1653
+ });
1654
+ }
1655
+ parseUrl(urlStr) {
1656
+ if (!urlStr) return {};
1657
+ try {
1658
+ const isWs = /^wss?:/i.test(urlStr);
1659
+ const isSecure = /^https:|^wss:/i.test(urlStr);
1660
+ const httpNormalized = urlStr.replace(/^ws(s)?:/i, "http$1:");
1661
+ const parsed = new URL(httpNormalized);
1662
+ const host = parsed.hostname || void 0;
1663
+ const port = parsed.port ? Number.parseInt(parsed.port, 10) : void 0;
1664
+ const pathname = parsed.pathname;
1665
+ let basePath;
1666
+ let webSocketPath;
1667
+ if (isWs) {
1668
+ if (pathname === "/sync" || pathname === "/sync/") {
1669
+ basePath = "";
1670
+ webSocketPath = "/sync";
1671
+ } else if (pathname.endsWith("/sync") || pathname.endsWith("/sync/")) {
1672
+ basePath = pathname.replace(/\/sync\/?$/, "");
1673
+ webSocketPath = pathname.replace(/\/$/, "");
1674
+ } else if (pathname && pathname !== "/") {
1675
+ basePath = pathname.replace(/\/$/, "");
1676
+ webSocketPath = `${basePath}/sync`;
1677
+ }
1678
+ } else {
1679
+ if (pathname && pathname !== "/") {
1680
+ basePath = pathname.replace(/\/$/, "");
1681
+ } else {
1682
+ basePath = "";
1683
+ }
1684
+ }
1685
+ return {
1686
+ host,
1687
+ port,
1688
+ secure: isSecure,
1689
+ basePath,
1690
+ webSocketPath
1691
+ };
1692
+ } catch {
1693
+ return {};
1694
+ }
1695
+ }
1696
+ resolveHostHeader(options) {
1697
+ const urlConfig = this.parseUrl(options.url);
1698
+ const isBrowser = typeof window !== "undefined" && Boolean(window.location);
1699
+ const host = options.host ?? urlConfig.host ?? (isBrowser && window.location.hostname !== "" ? window.location.hostname : void 0);
1700
+ const port = options.port ?? urlConfig.port ?? (isBrowser && window.location.port ? Number.parseInt(window.location.port, 10) : void 0);
1701
+ const secure = options.secure ?? urlConfig.secure ?? (isBrowser ? window.location.protocol === "https:" : false);
1702
+ if (!host) {
1703
+ return { host: void 0, secure };
1704
+ }
1705
+ const hostHeader = port !== void 0 && !host.includes(":") ? `${host}:${port}` : host;
1706
+ return { host: hostHeader, secure };
1707
+ }
1708
+ resolveBaseUrl(options) {
1709
+ const urlConfig = this.parseUrl(options.url);
1710
+ const basePath = normalizeBasePath(
1711
+ options.basePath ?? urlConfig.basePath ?? ""
1712
+ );
1713
+ const { host, secure } = this.resolveHostHeader(options);
1714
+ if (!host) return basePath;
1715
+ const proto = secure ? "https" : "http";
1716
+ return `${proto}://${host}${basePath}`;
1717
+ }
1718
+ resolveWebSocketUrl(options) {
1719
+ const urlConfig = this.parseUrl(options.url);
1720
+ const { host, secure } = this.resolveHostHeader(options);
1721
+ if (!host) return void 0;
1722
+ const basePath = normalizeBasePath(
1723
+ options.basePath ?? urlConfig.basePath ?? ""
1724
+ );
1725
+ const webSocketPath = options.webSocketPath ?? urlConfig.webSocketPath ?? `${basePath}/sync`;
1726
+ const proto = secure ? "wss" : "ws";
1727
+ return `${proto}://${host}${webSocketPath}`;
1728
+ }
1729
+ };
1730
+ // Annotate the CommonJS export names for ESM import in node:
1731
+ 0 && (module.exports = {
1732
+ AuthStatus,
1733
+ DataMode,
1734
+ OperationType,
1735
+ PROTOCOL_VERSION,
1736
+ SyncStatus,
1737
+ Table,
1738
+ TetherClient,
1739
+ TetherClientError,
1740
+ TetherClientErrorCode
1741
+ });
1742
+ //# sourceMappingURL=index.cjs.map