@basictech/react 0.7.0-beta.6 → 0.7.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.
package/dist/index.mjs CHANGED
@@ -1,135 +1,181 @@
1
- // src/AuthContext.tsx
2
- import { createContext, useContext, useEffect, useState, useRef } from "react";
3
- import { jwtDecode } from "jwt-decode";
4
-
5
- // src/sync/index.ts
6
- import { v7 as uuidv7 } from "uuid";
7
- import { Dexie as Dexie2 } from "dexie";
8
- import "dexie-syncable";
9
- import "dexie-observable";
10
-
11
- // src/sync/syncProtocol.js
12
- import { Dexie } from "dexie";
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res) => function __init() {
4
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
5
+ };
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
13
10
 
14
11
  // src/config.ts
15
- var log = (...args) => {
16
- try {
17
- if (localStorage.getItem("basic_debug") === "true") {
18
- console.log("[basic]", ...args);
19
- }
20
- } catch (e) {
12
+ var log;
13
+ var init_config = __esm({
14
+ "src/config.ts"() {
15
+ "use strict";
16
+ log = (...args) => {
17
+ try {
18
+ if (localStorage.getItem("basic_debug") === "true") {
19
+ console.log("[basic]", ...args);
20
+ }
21
+ } catch (e) {
22
+ }
23
+ };
21
24
  }
22
- };
25
+ });
23
26
 
24
27
  // src/sync/syncProtocol.js
25
- var syncProtocol = function() {
26
- log("Initializing syncProtocol");
27
- var RECONNECT_DELAY = 5e3;
28
- Dexie.Syncable.registerSyncProtocol("websocket", {
29
- sync: function(context, url, options, baseRevision, syncedRevision, changes, partial, applyRemoteChanges, onChangesAccepted, onSuccess, onError) {
30
- var requestId = 0;
31
- var acceptCallbacks = {};
32
- log("Connecting to", url);
33
- var ws = new WebSocket(url);
34
- function sendChanges(changes2, baseRevision2, partial2, onChangesAccepted2) {
35
- log("sendChanges", changes2.length, baseRevision2);
36
- ++requestId;
37
- acceptCallbacks[requestId.toString()] = onChangesAccepted2;
38
- ws.send(
39
- JSON.stringify({
40
- type: "changes",
41
- changes: changes2,
42
- partial: partial2,
43
- baseRevision: baseRevision2,
44
- requestId
45
- })
46
- );
47
- }
48
- ws.onopen = function(event) {
49
- log("Opening socket - sending clientIdentity", context.clientIdentity);
50
- ws.send(
51
- JSON.stringify({
52
- type: "clientIdentity",
53
- clientIdentity: context.clientIdentity || null,
54
- authToken: options.authToken,
55
- schema: options.schema
56
- })
57
- );
58
- };
59
- ws.onerror = function(event) {
60
- ws.close();
61
- log("ws.onerror", event);
62
- onError(event?.message, RECONNECT_DELAY);
63
- };
64
- ws.onclose = function(event) {
65
- onError("Socket closed: " + event.reason, RECONNECT_DELAY);
66
- };
67
- var isFirstRound = true;
68
- ws.onmessage = function(event) {
69
- try {
70
- var requestFromServer = JSON.parse(event.data);
71
- log("requestFromServer", requestFromServer, { acceptCallback, isFirstRound });
72
- if (requestFromServer.type == "clientIdentity") {
73
- context.clientIdentity = requestFromServer.clientIdentity;
74
- context.save();
75
- sendChanges(changes, baseRevision, partial, onChangesAccepted);
28
+ var syncProtocol_exports = {};
29
+ __export(syncProtocol_exports, {
30
+ syncProtocol: () => syncProtocol
31
+ });
32
+ import { Dexie } from "dexie";
33
+ var syncProtocol;
34
+ var init_syncProtocol = __esm({
35
+ "src/sync/syncProtocol.js"() {
36
+ "use strict";
37
+ "use client";
38
+ init_config();
39
+ syncProtocol = function() {
40
+ log("Initializing syncProtocol");
41
+ var RECONNECT_DELAY = 5e3;
42
+ Dexie.Syncable.registerSyncProtocol("websocket", {
43
+ sync: function(context, url, options, baseRevision, syncedRevision, changes, partial, applyRemoteChanges, onChangesAccepted, onSuccess, onError) {
44
+ var requestId = 0;
45
+ var acceptCallbacks = {};
46
+ log("Connecting to", url);
47
+ var ws = new WebSocket(url);
48
+ function sendChanges(changes2, baseRevision2, partial2, onChangesAccepted2) {
49
+ log("sendChanges", changes2.length, baseRevision2);
50
+ ++requestId;
51
+ acceptCallbacks[requestId.toString()] = onChangesAccepted2;
76
52
  ws.send(
77
53
  JSON.stringify({
78
- type: "subscribe",
79
- syncedRevision
54
+ type: "changes",
55
+ changes: changes2,
56
+ partial: partial2,
57
+ baseRevision: baseRevision2,
58
+ requestId
80
59
  })
81
60
  );
82
- } else if (requestFromServer.type == "changes") {
83
- applyRemoteChanges(
84
- requestFromServer.changes,
85
- requestFromServer.currentRevision,
86
- requestFromServer.partial
61
+ }
62
+ ws.onopen = function(event) {
63
+ log("Opening socket - sending clientIdentity", context.clientIdentity);
64
+ ws.send(
65
+ JSON.stringify({
66
+ type: "clientIdentity",
67
+ clientIdentity: context.clientIdentity || null,
68
+ authToken: options.authToken,
69
+ schema: options.schema
70
+ })
87
71
  );
88
- if (isFirstRound && !requestFromServer.partial) {
89
- onSuccess({
90
- // Specify a react function that will react on additional client changes
91
- react: function(changes2, baseRevision2, partial2, onChangesAccepted2) {
92
- sendChanges(
93
- changes2,
94
- baseRevision2,
95
- partial2,
96
- onChangesAccepted2
97
- );
98
- },
99
- // Specify a disconnect function that will close our socket so that we dont continue to monitor changes.
100
- disconnect: function() {
101
- ws.close();
72
+ };
73
+ ws.onerror = function(event) {
74
+ ws.close();
75
+ log("ws.onerror", event);
76
+ onError(event?.message, RECONNECT_DELAY);
77
+ };
78
+ ws.onclose = function(event) {
79
+ onError("Socket closed: " + event.reason, RECONNECT_DELAY);
80
+ };
81
+ var isFirstRound = true;
82
+ ws.onmessage = function(event) {
83
+ try {
84
+ var requestFromServer = JSON.parse(event.data);
85
+ log("requestFromServer", requestFromServer, { acceptCallback, isFirstRound });
86
+ if (requestFromServer.type == "clientIdentity") {
87
+ context.clientIdentity = requestFromServer.clientIdentity;
88
+ context.save();
89
+ sendChanges(changes, baseRevision, partial, onChangesAccepted);
90
+ ws.send(
91
+ JSON.stringify({
92
+ type: "subscribe",
93
+ syncedRevision
94
+ })
95
+ );
96
+ } else if (requestFromServer.type == "changes") {
97
+ applyRemoteChanges(
98
+ requestFromServer.changes,
99
+ requestFromServer.currentRevision,
100
+ requestFromServer.partial
101
+ );
102
+ if (isFirstRound && !requestFromServer.partial) {
103
+ onSuccess({
104
+ // Specify a react function that will react on additional client changes
105
+ react: function(changes2, baseRevision2, partial2, onChangesAccepted2) {
106
+ sendChanges(
107
+ changes2,
108
+ baseRevision2,
109
+ partial2,
110
+ onChangesAccepted2
111
+ );
112
+ },
113
+ // Specify a disconnect function that will close our socket so that we dont continue to monitor changes.
114
+ disconnect: function() {
115
+ ws.close();
116
+ }
117
+ });
118
+ isFirstRound = false;
102
119
  }
103
- });
104
- isFirstRound = false;
120
+ } else if (requestFromServer.type == "ack") {
121
+ var requestId2 = requestFromServer.requestId;
122
+ var acceptCallback = acceptCallbacks[requestId2.toString()];
123
+ acceptCallback();
124
+ delete acceptCallbacks[requestId2.toString()];
125
+ } else if (requestFromServer.type == "error") {
126
+ var requestId2 = requestFromServer.requestId;
127
+ ws.close();
128
+ onError(requestFromServer.message, Infinity);
129
+ } else {
130
+ log("unknown message", requestFromServer);
131
+ ws.close();
132
+ onError("unknown message", Infinity);
133
+ }
134
+ } catch (e) {
135
+ ws.close();
136
+ log("caught error", e);
137
+ onError(e, Infinity);
105
138
  }
106
- } else if (requestFromServer.type == "ack") {
107
- var requestId2 = requestFromServer.requestId;
108
- var acceptCallback = acceptCallbacks[requestId2.toString()];
109
- acceptCallback();
110
- delete acceptCallbacks[requestId2.toString()];
111
- } else if (requestFromServer.type == "error") {
112
- var requestId2 = requestFromServer.requestId;
113
- ws.close();
114
- onError(requestFromServer.message, Infinity);
115
- } else {
116
- log("unknown message", requestFromServer);
117
- ws.close();
118
- onError("unknown message", Infinity);
119
- }
120
- } catch (e) {
121
- ws.close();
122
- log("caught error", e);
123
- onError(e, Infinity);
139
+ };
124
140
  }
125
- };
126
- }
127
- });
128
- };
141
+ });
142
+ };
143
+ }
144
+ });
145
+
146
+ // src/AuthContext.tsx
147
+ import { createContext, useContext, useEffect, useState, useRef } from "react";
148
+ import { jwtDecode } from "jwt-decode";
129
149
 
130
150
  // src/sync/index.ts
151
+ init_config();
152
+ import { v7 as uuidv7 } from "uuid";
153
+ import { Dexie as Dexie2 } from "dexie";
131
154
  import { validateData } from "@basictech/schema";
132
- syncProtocol();
155
+ var dexieExtensionsLoaded = false;
156
+ var initPromise = null;
157
+ async function initDexieExtensions() {
158
+ if (dexieExtensionsLoaded)
159
+ return;
160
+ if (typeof window === "undefined")
161
+ return;
162
+ if (initPromise)
163
+ return initPromise;
164
+ initPromise = (async () => {
165
+ try {
166
+ await import("dexie-syncable");
167
+ await import("dexie-observable");
168
+ const { syncProtocol: syncProtocol2 } = await Promise.resolve().then(() => (init_syncProtocol(), syncProtocol_exports));
169
+ syncProtocol2();
170
+ dexieExtensionsLoaded = true;
171
+ log("Dexie extensions loaded successfully");
172
+ } catch (error) {
173
+ console.error("Failed to load Dexie extensions:", error);
174
+ throw error;
175
+ }
176
+ })();
177
+ return initPromise;
178
+ }
133
179
  var BasicSync = class extends Dexie2 {
134
180
  basic_schema;
135
181
  constructor(name, options) {
@@ -195,63 +241,388 @@ var BasicSync = class extends Dexie2 {
195
241
  return this.syncable;
196
242
  }
197
243
  collection(name) {
244
+ if (this.basic_schema?.tables && !this.basic_schema.tables[name]) {
245
+ throw new Error(`Table "${name}" not found in schema`);
246
+ }
247
+ const table = this.table(name);
198
248
  return {
199
249
  /**
200
250
  * Returns the underlying Dexie table
201
251
  * @type {Dexie.Table}
202
252
  */
203
- ref: this.table(name),
253
+ ref: table,
204
254
  // --- WRITE ---- //
205
- add: (data) => {
255
+ /**
256
+ * Add a new record - returns the full object with generated id
257
+ */
258
+ add: async (data) => {
206
259
  const valid = validateData(this.basic_schema, name, data);
207
260
  if (!valid.valid) {
208
261
  log("Invalid data", valid);
209
- return Promise.reject({ ...valid });
262
+ throw new Error(valid.message || "Data validation failed");
210
263
  }
211
- return this.table(name).add({
212
- id: uuidv7(),
213
- ...data
214
- });
264
+ const id = uuidv7();
265
+ const fullData = { id, ...data };
266
+ await table.add(fullData);
267
+ return fullData;
215
268
  },
216
- put: (data) => {
269
+ /**
270
+ * Put (upsert) a record - returns the full object
271
+ */
272
+ put: async (data) => {
273
+ if (!data.id) {
274
+ throw new Error("put() requires an id field");
275
+ }
217
276
  const valid = validateData(this.basic_schema, name, data);
218
277
  if (!valid.valid) {
219
278
  log("Invalid data", valid);
220
- return Promise.reject({ ...valid });
279
+ throw new Error(valid.message || "Data validation failed");
221
280
  }
222
- return this.table(name).put({
223
- id: uuidv7(),
224
- ...data
225
- });
281
+ await table.put(data);
282
+ return data;
226
283
  },
227
- update: (id, data) => {
284
+ /**
285
+ * Update an existing record - returns updated object or null
286
+ */
287
+ update: async (id, data) => {
288
+ if (!id) {
289
+ throw new Error("update() requires an id");
290
+ }
228
291
  const valid = validateData(this.basic_schema, name, data, false);
229
292
  if (!valid.valid) {
230
293
  log("Invalid data", valid);
231
- return Promise.reject({ ...valid });
294
+ throw new Error(valid.message || "Data validation failed");
232
295
  }
233
- return this.table(name).update(id, data);
296
+ const updated = await table.update(id, data);
297
+ if (updated === 0) {
298
+ return null;
299
+ }
300
+ const record = await table.get(id);
301
+ return record || null;
234
302
  },
235
- delete: (id) => {
236
- return this.table(name).delete(id);
303
+ /**
304
+ * Delete a record - returns true if deleted, false if not found
305
+ */
306
+ delete: async (id) => {
307
+ if (!id) {
308
+ throw new Error("delete() requires an id");
309
+ }
310
+ const exists = await table.get(id);
311
+ if (!exists) {
312
+ return false;
313
+ }
314
+ await table.delete(id);
315
+ return true;
237
316
  },
238
317
  // --- READ ---- //
318
+ /**
319
+ * Get a single record by id - returns null if not found
320
+ */
239
321
  get: async (id) => {
240
- return this.table(name).get(id);
322
+ if (!id) {
323
+ throw new Error("get() requires an id");
324
+ }
325
+ const record = await table.get(id);
326
+ return record || null;
241
327
  },
328
+ /**
329
+ * Get all records in the collection
330
+ */
242
331
  getAll: async () => {
243
- return this.table(name).toArray();
332
+ return table.toArray();
244
333
  },
245
334
  // --- QUERY ---- //
246
- // TODO: lots to do here. simplifing creating querie, filtering/ordering/limit, and execute
247
- query: () => this.table(name),
248
- filter: (fn) => this.table(name).filter(fn).toArray()
335
+ /**
336
+ * Filter records using a predicate function
337
+ */
338
+ filter: async (fn) => {
339
+ return table.filter(fn).toArray();
340
+ },
341
+ /**
342
+ * Get the raw Dexie table for advanced queries
343
+ * @deprecated Use ref instead
344
+ */
345
+ query: () => table
249
346
  };
250
347
  }
251
348
  };
252
349
 
350
+ // src/core/db/types.ts
351
+ var RemoteDBError = class extends Error {
352
+ status;
353
+ response;
354
+ constructor(message, status, response) {
355
+ super(message);
356
+ this.name = "RemoteDBError";
357
+ this.status = status;
358
+ this.response = response;
359
+ }
360
+ };
361
+
362
+ // src/core/db/RemoteCollection.ts
363
+ import { validateData as validateData2 } from "@basictech/schema";
364
+ var NotAuthenticatedError = class extends Error {
365
+ constructor(message = "Not authenticated") {
366
+ super(message);
367
+ this.name = "NotAuthenticatedError";
368
+ }
369
+ };
370
+ var RemoteCollection = class {
371
+ tableName;
372
+ config;
373
+ constructor(tableName, config) {
374
+ this.tableName = tableName;
375
+ this.config = config;
376
+ }
377
+ log(...args) {
378
+ if (this.config.debug) {
379
+ console.log("[RemoteDB]", ...args);
380
+ }
381
+ }
382
+ /**
383
+ * Check if an error is a "not authenticated" error
384
+ */
385
+ isNotAuthenticatedError(error) {
386
+ if (error instanceof Error) {
387
+ const message = error.message.toLowerCase();
388
+ return message.includes("no token") || message.includes("not authenticated") || message.includes("please sign in");
389
+ }
390
+ return false;
391
+ }
392
+ /**
393
+ * Helper to make authenticated API requests
394
+ * Automatically retries once on 401 (token expired) by refreshing the token
395
+ */
396
+ async request(method, path, body, isRetry = false) {
397
+ const token = await this.config.getToken();
398
+ const url = `${this.config.serverUrl}${path}`;
399
+ this.log(`${method} ${url}`, body ? JSON.stringify(body) : "");
400
+ const response = await fetch(url, {
401
+ method,
402
+ headers: {
403
+ "Content-Type": "application/json",
404
+ "Authorization": `Bearer ${token}`
405
+ },
406
+ ...body ? { body: JSON.stringify(body) } : {}
407
+ });
408
+ const responseData = await response.json().catch(() => ({}));
409
+ if (!response.ok) {
410
+ if (response.status === 401 && !isRetry) {
411
+ this.log("Got 401, retrying with fresh token...");
412
+ return this.request(method, path, body, true);
413
+ }
414
+ if (this.config.debug) {
415
+ console.error(`[RemoteDB] Error ${response.status}:`, responseData);
416
+ }
417
+ if (response.status === 401 && this.config.onAuthError) {
418
+ this.config.onAuthError({
419
+ status: response.status,
420
+ message: "Authentication failed",
421
+ response: responseData
422
+ });
423
+ }
424
+ const errorMessage = responseData.message || responseData.error || responseData.detail || (typeof responseData === "string" ? responseData : `API request failed: ${response.status}`);
425
+ throw new RemoteDBError(errorMessage, response.status, responseData);
426
+ }
427
+ this.log("Response:", responseData);
428
+ return responseData;
429
+ }
430
+ /**
431
+ * Validate data against schema if available
432
+ */
433
+ validateData(data, checkRequired = true) {
434
+ if (this.config.schema) {
435
+ const result = validateData2(this.config.schema, this.tableName, data, checkRequired);
436
+ if (!result.valid) {
437
+ throw new Error(result.message || "Data validation failed");
438
+ }
439
+ }
440
+ }
441
+ /**
442
+ * Get the base path for this collection
443
+ */
444
+ get basePath() {
445
+ return `/account/${this.config.projectId}/db/${this.tableName}`;
446
+ }
447
+ /**
448
+ * Add a new record to the collection
449
+ * The server generates the ID
450
+ * Requires authentication - throws NotAuthenticatedError if not signed in
451
+ */
452
+ async add(data) {
453
+ this.validateData(data, true);
454
+ try {
455
+ const result = await this.request(
456
+ "POST",
457
+ this.basePath,
458
+ { value: data }
459
+ );
460
+ return result.data;
461
+ } catch (error) {
462
+ if (this.isNotAuthenticatedError(error)) {
463
+ throw new NotAuthenticatedError("Sign in required to add items");
464
+ }
465
+ throw error;
466
+ }
467
+ }
468
+ /**
469
+ * Put (upsert) a record - requires id
470
+ * Requires authentication - throws NotAuthenticatedError if not signed in
471
+ */
472
+ async put(data) {
473
+ if (!data.id) {
474
+ throw new Error("put() requires an id field");
475
+ }
476
+ const { id, ...rest } = data;
477
+ this.validateData(rest, true);
478
+ try {
479
+ const result = await this.request(
480
+ "PUT",
481
+ `${this.basePath}/${id}`,
482
+ { value: rest }
483
+ );
484
+ return result.data || data;
485
+ } catch (error) {
486
+ if (this.isNotAuthenticatedError(error)) {
487
+ throw new NotAuthenticatedError("Sign in required to update items");
488
+ }
489
+ throw error;
490
+ }
491
+ }
492
+ /**
493
+ * Update an existing record by id
494
+ * Requires authentication - throws NotAuthenticatedError if not signed in
495
+ */
496
+ async update(id, data) {
497
+ if (!id) {
498
+ throw new Error("update() requires an id");
499
+ }
500
+ this.validateData(data, false);
501
+ try {
502
+ const result = await this.request(
503
+ "PATCH",
504
+ `${this.basePath}/${id}`,
505
+ { value: data }
506
+ );
507
+ return result.data || null;
508
+ } catch (error) {
509
+ if (error instanceof RemoteDBError && error.status === 404) {
510
+ return null;
511
+ }
512
+ if (this.isNotAuthenticatedError(error)) {
513
+ throw new NotAuthenticatedError("Sign in required to update items");
514
+ }
515
+ throw error;
516
+ }
517
+ }
518
+ /**
519
+ * Delete a record by id
520
+ * Requires authentication - throws NotAuthenticatedError if not signed in
521
+ */
522
+ async delete(id) {
523
+ if (!id) {
524
+ throw new Error("delete() requires an id");
525
+ }
526
+ try {
527
+ await this.request(
528
+ "DELETE",
529
+ `${this.basePath}/${id}`
530
+ );
531
+ return true;
532
+ } catch (error) {
533
+ if (error instanceof RemoteDBError && error.status === 404) {
534
+ return false;
535
+ }
536
+ if (this.isNotAuthenticatedError(error)) {
537
+ throw new NotAuthenticatedError("Sign in required to delete items");
538
+ }
539
+ throw error;
540
+ }
541
+ }
542
+ /**
543
+ * Get a single record by id
544
+ * Returns null if not authenticated (graceful degradation for read operations)
545
+ */
546
+ async get(id) {
547
+ if (!id) {
548
+ throw new Error("get() requires an id");
549
+ }
550
+ try {
551
+ const result = await this.request(
552
+ "GET",
553
+ `${this.basePath}?id=${id}`
554
+ );
555
+ return result.data?.[0] || null;
556
+ } catch (error) {
557
+ if (this.isNotAuthenticatedError(error)) {
558
+ this.log("Not authenticated - returning null for get()");
559
+ }
560
+ return null;
561
+ }
562
+ }
563
+ /**
564
+ * Get all records in the collection
565
+ * Returns empty array if not authenticated (graceful degradation for read operations)
566
+ */
567
+ async getAll() {
568
+ try {
569
+ const result = await this.request(
570
+ "GET",
571
+ this.basePath
572
+ );
573
+ return result.data || [];
574
+ } catch (error) {
575
+ if (this.isNotAuthenticatedError(error)) {
576
+ this.log("Not authenticated - returning empty array for getAll()");
577
+ return [];
578
+ }
579
+ throw error;
580
+ }
581
+ }
582
+ /**
583
+ * Filter records using a predicate function
584
+ * Note: This fetches all records and filters client-side
585
+ * Returns empty array if not authenticated (graceful degradation for read operations)
586
+ */
587
+ async filter(fn) {
588
+ const all = await this.getAll();
589
+ return all.filter(fn);
590
+ }
591
+ /**
592
+ * ref is not available for remote collections
593
+ */
594
+ ref = void 0;
595
+ };
596
+
597
+ // src/core/db/RemoteDB.ts
598
+ var RemoteDB = class {
599
+ config;
600
+ collections = /* @__PURE__ */ new Map();
601
+ constructor(config) {
602
+ this.config = config;
603
+ }
604
+ /**
605
+ * Get a collection by name
606
+ * Collections are cached for reuse
607
+ */
608
+ collection(name) {
609
+ if (this.collections.has(name)) {
610
+ return this.collections.get(name);
611
+ }
612
+ if (this.config.schema?.tables && !this.config.schema.tables[name]) {
613
+ throw new Error(`Table "${name}" not found in schema`);
614
+ }
615
+ const collection = new RemoteCollection(name, this.config);
616
+ this.collections.set(name, collection);
617
+ return collection;
618
+ }
619
+ };
620
+
621
+ // src/AuthContext.tsx
622
+ init_config();
623
+
253
624
  // package.json
254
- var version = "0.7.0-beta.4";
625
+ var version = "0.7.0-beta.6";
255
626
 
256
627
  // src/updater/versionUpdater.ts
257
628
  var VersionUpdater = class {
@@ -433,6 +804,7 @@ function clearCookie(name) {
433
804
  }
434
805
 
435
806
  // src/utils/network.ts
807
+ init_config();
436
808
  function isDevelopment(debug) {
437
809
  return window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1" || window.location.hostname.includes("localhost") || window.location.hostname.includes("127.0.0.1") || window.location.hostname.includes(".local") || process.env.NODE_ENV === "development" || debug === true;
438
810
  }
@@ -495,6 +867,7 @@ function getSyncStatus(statusCode) {
495
867
  }
496
868
 
497
869
  // src/utils/schema.ts
870
+ init_config();
498
871
  import { validateSchema as validateSchema2, compareSchemas } from "@basictech/schema";
499
872
  async function getSchemaStatus(schema) {
500
873
  const projectId = schema.project_id;
@@ -598,29 +971,44 @@ var DEFAULT_AUTH_CONFIG = {
598
971
  server_url: "https://api.basic.tech",
599
972
  ws_url: "wss://pds.basic.id/ws"
600
973
  };
974
+ var noDb = {
975
+ collection: () => {
976
+ throw new Error("no basicdb found - initialization failed. double check your schema.");
977
+ }
978
+ };
601
979
  var BasicContext = createContext({
602
- unicorn: "\u{1F984}",
603
- isAuthReady: false,
980
+ // Auth state
981
+ isReady: false,
604
982
  isSignedIn: false,
605
983
  user: null,
606
- signout: () => Promise.resolve(),
984
+ // Auth actions
985
+ signIn: () => Promise.resolve(),
986
+ signOut: () => Promise.resolve(),
987
+ signInWithCode: () => Promise.resolve({ success: false }),
988
+ // Token management
989
+ getToken: () => Promise.reject(new Error("no token")),
990
+ getSignInUrl: () => Promise.resolve(""),
991
+ // DB access
992
+ db: noDb,
993
+ dbStatus: "LOADING" /* LOADING */,
994
+ dbMode: "sync",
995
+ // Legacy aliases
996
+ isAuthReady: false,
607
997
  signin: () => Promise.resolve(),
608
- signinWithCode: () => new Promise(() => {
609
- }),
610
- getToken: () => new Promise(() => {
611
- }),
612
- getSignInLink: () => Promise.resolve(""),
613
- db: {},
614
- dbStatus: "LOADING" /* LOADING */
998
+ signout: () => Promise.resolve(),
999
+ signinWithCode: () => Promise.resolve({ success: false }),
1000
+ getSignInLink: () => Promise.resolve("")
615
1001
  });
616
1002
  function BasicProvider({
617
1003
  children,
618
- project_id,
1004
+ project_id: project_id_prop,
619
1005
  schema,
620
1006
  debug = false,
621
1007
  storage,
622
- auth
1008
+ auth,
1009
+ dbMode = "sync"
623
1010
  }) {
1011
+ const project_id = schema?.project_id || project_id_prop;
624
1012
  const [isAuthReady, setIsAuthReady] = useState(false);
625
1013
  const [isSignedIn, setIsSignedIn] = useState(false);
626
1014
  const [token, setToken] = useState(null);
@@ -632,6 +1020,7 @@ function BasicProvider({
632
1020
  const [isOnline, setIsOnline] = useState(navigator.onLine);
633
1021
  const [pendingRefresh, setPendingRefresh] = useState(false);
634
1022
  const syncRef = useRef(null);
1023
+ const remoteDbRef = useRef(null);
635
1024
  const storageAdapter = storage || new LocalStorageAdapter();
636
1025
  const authConfig = {
637
1026
  scopes: auth?.scopes || DEFAULT_AUTH_CONFIG.scopes,
@@ -671,9 +1060,10 @@ function BasicProvider({
671
1060
  };
672
1061
  }, [pendingRefresh, token]);
673
1062
  useEffect(() => {
674
- function initDb(options) {
1063
+ async function initSyncDb(options) {
675
1064
  if (!syncRef.current) {
676
- log("Initializing Basic DB");
1065
+ log("Initializing Basic Sync DB");
1066
+ await initDexieExtensions();
677
1067
  syncRef.current = new BasicSync("basicdb", { schema });
678
1068
  syncRef.current.syncable.on("statusChanged", (status, url) => {
679
1069
  setDbStatus(getSyncStatus(status));
@@ -686,6 +1076,33 @@ function BasicProvider({
686
1076
  setIsReady(true);
687
1077
  }
688
1078
  }
1079
+ function initRemoteDb() {
1080
+ if (!remoteDbRef.current) {
1081
+ if (!project_id) {
1082
+ setError({
1083
+ code: "missing_project_id",
1084
+ title: "Project ID Required",
1085
+ message: "Remote mode requires a project_id. Provide it via schema.project_id or the project_id prop."
1086
+ });
1087
+ setIsReady(true);
1088
+ return;
1089
+ }
1090
+ log("Initializing Basic Remote DB");
1091
+ remoteDbRef.current = new RemoteDB({
1092
+ serverUrl: authConfig.server_url,
1093
+ projectId: project_id,
1094
+ getToken,
1095
+ schema,
1096
+ debug,
1097
+ onAuthError: (error2) => {
1098
+ log("RemoteDB auth error:", error2);
1099
+ signout();
1100
+ }
1101
+ });
1102
+ setDbStatus("ONLINE" /* ONLINE */);
1103
+ setIsReady(true);
1104
+ }
1105
+ }
689
1106
  async function checkSchema() {
690
1107
  const result = await validateAndCheckSchema(schema);
691
1108
  if (!result.isValid) {
@@ -704,18 +1121,26 @@ function BasicProvider({
704
1121
  setIsReady(true);
705
1122
  return null;
706
1123
  }
707
- if (result.schemaStatus.valid) {
708
- initDb({ shouldConnect: true });
1124
+ if (dbMode === "remote") {
1125
+ initRemoteDb();
709
1126
  } else {
710
- log("Schema is invalid!", result.schemaStatus);
711
- initDb({ shouldConnect: false });
1127
+ if (result.schemaStatus.valid) {
1128
+ await initSyncDb({ shouldConnect: true });
1129
+ } else {
1130
+ log("Schema is invalid!", result.schemaStatus);
1131
+ await initSyncDb({ shouldConnect: false });
1132
+ }
712
1133
  }
713
1134
  checkForNewVersion();
714
1135
  }
715
1136
  if (schema) {
716
1137
  checkSchema();
717
1138
  } else {
718
- setIsReady(true);
1139
+ if (dbMode === "remote" && project_id) {
1140
+ initRemoteDb();
1141
+ } else {
1142
+ setIsReady(true);
1143
+ }
719
1144
  }
720
1145
  }, []);
721
1146
  useEffect(() => {
@@ -862,8 +1287,14 @@ function BasicProvider({
862
1287
  const isExpired = decoded.exp && decoded.exp < Date.now() / 1e3 + expirationBuffer;
863
1288
  if (isExpired) {
864
1289
  log("token is expired - refreshing ...");
1290
+ const refreshToken = token?.refresh_token;
1291
+ if (!refreshToken) {
1292
+ log("Error: No refresh token available for expired token");
1293
+ setIsAuthReady(true);
1294
+ return;
1295
+ }
865
1296
  try {
866
- const newToken = await fetchToken(token?.refresh_token || "", true);
1297
+ const newToken = await fetchToken(refreshToken, true);
867
1298
  fetchUser(newToken?.access_token || "");
868
1299
  } catch (error2) {
869
1300
  log("Failed to refresh token in checkToken:", error2);
@@ -1070,6 +1501,11 @@ function BasicProvider({
1070
1501
  return token?.access_token || "";
1071
1502
  };
1072
1503
  const fetchToken = async (codeOrRefreshToken, isRefreshToken = false) => {
1504
+ if (!codeOrRefreshToken || codeOrRefreshToken.trim() === "") {
1505
+ const errorMsg = isRefreshToken ? "Refresh token is empty or undefined" : "Authorization code is empty or undefined";
1506
+ log("Error:", errorMsg);
1507
+ throw new Error(errorMsg);
1508
+ }
1073
1509
  if (isRefreshToken && refreshPromiseRef.current) {
1074
1510
  log("Reusing in-flight refresh token request");
1075
1511
  return refreshPromiseRef.current;
@@ -1182,24 +1618,36 @@ function BasicProvider({
1182
1618
  }
1183
1619
  return refreshPromise;
1184
1620
  };
1185
- const noDb = {
1186
- collection: () => {
1187
- throw new Error("no basicdb found - initialization failed. double check your schema.");
1621
+ const getCurrentDb = () => {
1622
+ if (dbMode === "remote") {
1623
+ return remoteDbRef.current || noDb;
1188
1624
  }
1625
+ return syncRef.current || noDb;
1189
1626
  };
1190
- return /* @__PURE__ */ jsxs(BasicContext.Provider, { value: {
1191
- unicorn: "\u{1F984}",
1192
- isAuthReady,
1627
+ const contextValue = {
1628
+ // Auth state (new naming)
1629
+ isReady: isAuthReady,
1193
1630
  isSignedIn,
1194
1631
  user,
1195
- signout,
1632
+ // Auth actions (new camelCase naming)
1633
+ signIn: signin,
1634
+ signOut: signout,
1635
+ signInWithCode: signinWithCode,
1636
+ // Token management
1637
+ getToken,
1638
+ getSignInUrl: getSignInLink,
1639
+ // DB access
1640
+ db: getCurrentDb(),
1641
+ dbStatus,
1642
+ dbMode,
1643
+ // Legacy aliases (deprecated)
1644
+ isAuthReady,
1196
1645
  signin,
1646
+ signout,
1197
1647
  signinWithCode,
1198
- getToken,
1199
- getSignInLink,
1200
- db: syncRef.current ? syncRef.current : noDb,
1201
- dbStatus
1202
- }, children: [
1648
+ getSignInLink
1649
+ };
1650
+ return /* @__PURE__ */ jsxs(BasicContext.Provider, { value: contextValue, children: [
1203
1651
  error && isDevMode() && /* @__PURE__ */ jsx(ErrorDisplay, { error }),
1204
1652
  isReady && children
1205
1653
  ] });
@@ -1235,6 +1683,11 @@ function useBasic() {
1235
1683
  import { useLiveQuery as useQuery } from "dexie-react-hooks";
1236
1684
  export {
1237
1685
  BasicProvider,
1686
+ NotAuthenticatedError,
1687
+ RemoteCollection,
1688
+ RemoteDB,
1689
+ RemoteDBError,
1690
+ STORAGE_KEYS,
1238
1691
  useBasic,
1239
1692
  useQuery
1240
1693
  };