live-cache 0.1.0 → 0.2.1

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.cjs CHANGED
@@ -449,6 +449,34 @@ class Collection {
449
449
  }
450
450
  }
451
451
 
452
+ class Invalidator {
453
+ constructor() {
454
+ this.invalidator = (...data) => {
455
+ throw Error("The invalidator needs to be binded from the controller");
456
+ };
457
+ }
458
+ bind(invalidator) {
459
+ this.invalidator = invalidator;
460
+ }
461
+ registerInvalidation() {
462
+ throw Error("Method not implemented");
463
+ }
464
+ unregisterInvalidation() {
465
+ throw Error("Method not implemented");
466
+ }
467
+ }
468
+ class DefaultInvalidator extends Invalidator {
469
+ bind(invalidator) {
470
+ super.bind(invalidator);
471
+ }
472
+ registerInvalidation() {
473
+ // No-op
474
+ }
475
+ unregisterInvalidation() {
476
+ return () => { };
477
+ }
478
+ }
479
+
452
480
  /**
453
481
  * Storage adapter used by `Controller` to persist and hydrate snapshots.
454
482
  *
@@ -456,19 +484,25 @@ class Collection {
456
484
  * Implementations should be resilient: reads should return `[]` on failure.
457
485
  */
458
486
  class StorageManager {
487
+ constructor(prefix) {
488
+ this.prefix = prefix;
489
+ }
459
490
  }
460
491
  /**
461
492
  * No-op storage manager.
462
493
  *
463
494
  * Useful in environments where you don’t want persistence (tests, ephemeral caches, etc).
464
495
  */
465
- class DefaultStorageManager {
466
- get(_name) {
496
+ class DefaultStorageManager extends StorageManager {
497
+ constructor(prefix) {
498
+ super(prefix);
499
+ }
500
+ get(name) {
467
501
  return __awaiter(this, void 0, void 0, function* () {
468
- return Promise.resolve([]);
502
+ return Promise.resolve(null);
469
503
  });
470
504
  }
471
- set(_name, _models) {
505
+ set(name, models) {
472
506
  return __awaiter(this, void 0, void 0, function* () {
473
507
  return Promise.resolve();
474
508
  });
@@ -478,48 +512,13 @@ class DefaultStorageManager {
478
512
  return Promise.resolve();
479
513
  });
480
514
  }
515
+ getParams() {
516
+ return __awaiter(this, void 0, void 0, function* () {
517
+ return Promise.resolve([]);
518
+ });
519
+ }
481
520
  }
482
521
 
483
- /**
484
- * Controller is the recommended integration layer for server-backed resources.
485
- *
486
- * It wraps a `Collection` with:
487
- * - hydration (`initialise()`)
488
- * - persistence (`commit()` writes a full snapshot using the configured `StorageManager`)
489
- * - subscriptions (`publish()`)
490
- * - invalidation hooks (`invalidate()`, `refetch()`)
491
- *
492
- * The intended mutation pattern is:
493
- * 1) mutate `this.collection` (insert/update/delete)
494
- * 2) call `await this.commit()` so subscribers update and storage persists
495
- *
496
- * @typeParam TVariable - the “data” shape stored in the collection (without `_id`)
497
- * @typeParam TName - a stable, string-literal name for this controller/collection
498
- *
499
- * @example
500
- * ```ts
501
- * type User = { id: number; name: string };
502
- *
503
- * class UsersController extends Controller<User, "users"> {
504
- * async fetchAll(): Promise<[User[], number]> {
505
- * const res = await fetch("/api/users");
506
- * const data = (await res.json()) as User[];
507
- * return [data, data.length];
508
- * }
509
- *
510
- * invalidate(): () => void {
511
- * this.abort();
512
- * void this.refetch();
513
- * return () => {};
514
- * }
515
- *
516
- * async renameUser(id: number, name: string) {
517
- * this.collection.findOneAndUpdate({ id }, { name });
518
- * await this.commit();
519
- * }
520
- * }
521
- * ```
522
- */
523
522
  class Controller {
524
523
  /**
525
524
  * Abort any in-flight work owned by this controller (typically network fetches).
@@ -562,12 +561,16 @@ class Controller {
562
561
  */
563
562
  initialise() {
564
563
  return __awaiter(this, void 0, void 0, function* () {
564
+ var _a;
565
+ if (this.loading)
566
+ return;
565
567
  // If the collection is not empty, return.
566
568
  let data = this.collection.find().map((doc) => doc.toData());
567
- if (data.length !== 0)
569
+ if (data.length !== 0) {
568
570
  return;
571
+ }
569
572
  // If the collection is empty, check the storage manager.
570
- data = yield this.storageManager.get(this.name);
573
+ data = (_a = (yield this.storageManager.get(this.name))) !== null && _a !== void 0 ? _a : [];
571
574
  if (data.length !== 0) {
572
575
  this.updateTotal(this.collection.find().length);
573
576
  this.collection.insertMany(data);
@@ -580,6 +583,7 @@ class Controller {
580
583
  const [_data, total] = yield this.fetchAll();
581
584
  this.collection.insertMany(_data);
582
585
  this.updateTotal(total);
586
+ yield this.commit();
583
587
  }
584
588
  catch (error) {
585
589
  this.error = error;
@@ -587,7 +591,6 @@ class Controller {
587
591
  finally {
588
592
  this.loading = false;
589
593
  }
590
- yield this.commit();
591
594
  });
592
595
  }
593
596
  /**
@@ -673,23 +676,24 @@ class Controller {
673
676
  * Create a controller.
674
677
  *
675
678
  * @param name - stable controller/collection name
676
- * @param initialise - whether to run `initialise()` immediately
677
679
  * @param storageManager - where snapshots are persisted (defaults to no-op)
678
680
  * @param pageSize - optional pagination hint (userland)
679
681
  */
680
- constructor(name, initialise = true, storageManager = new DefaultStorageManager(), pageSize = -1) {
682
+ constructor(name, { storageManager = new DefaultStorageManager("live-cache:"), pageSize = -1, invalidator = new DefaultInvalidator(), initialiseOnMount = true, }) {
681
683
  this.subscribers = new Set();
682
684
  this.loading = false;
683
685
  this.error = null;
684
686
  this.total = -1;
685
687
  this.pageSize = -1;
686
688
  this.abortController = null;
689
+ this.name = name;
687
690
  this.collection = new Collection(name);
688
691
  this.storageManager = storageManager;
689
- this.name = name;
690
692
  this.pageSize = pageSize;
691
- if (initialise) {
692
- void this.initialise();
693
+ this.invalidator = invalidator;
694
+ this.invalidator.bind(this.invalidate.bind(this));
695
+ if (initialiseOnMount) {
696
+ this.initialise();
693
697
  }
694
698
  }
695
699
  }
@@ -865,6 +869,28 @@ function join(from, where = {}, select) {
865
869
  return out;
866
870
  });
867
871
  }
872
+ // // Type-check-only example (kept unreachable to avoid runtime side effects).
873
+ // if (false) {
874
+ // join(
875
+ // [
876
+ // new Controller<{ name: string; age: number; city: string }, "users">("users"),
877
+ // new Controller<{ name: string; title: string; body: string; creator: string }, "posts">("posts"),
878
+ // ],
879
+ // {
880
+ // $and: {
881
+ // users: {
882
+ // name: {
883
+ // $ref: {
884
+ // controller: "posts",
885
+ // field: "creator",
886
+ // },
887
+ // },
888
+ // },
889
+ // },
890
+ // } as const,
891
+ // ["posts._id"]
892
+ // )[0];
893
+ // }
868
894
 
869
895
  /**
870
896
  * Registry for controllers, keyed by `controller.name`.
@@ -882,6 +908,7 @@ function join(from, where = {}, select) {
882
908
  class ObjectStore {
883
909
  constructor() {
884
910
  this.store = new Map();
911
+ this.initialisePromises = new WeakMap();
885
912
  }
886
913
  /**
887
914
  * Register a controller instance in this store.
@@ -917,6 +944,22 @@ class ObjectStore {
917
944
  controller.initialise();
918
945
  });
919
946
  }
947
+ /**
948
+ * Initialise a controller once per store, even if multiple callers request it.
949
+ */
950
+ initialiseOnce(name) {
951
+ const controller = this.get(name);
952
+ const existing = this.initialisePromises.get(controller);
953
+ if (existing)
954
+ return existing;
955
+ const promise = controller.initialise().finally(() => {
956
+ if (this.initialisePromises.get(controller) === promise) {
957
+ this.initialisePromises.delete(controller);
958
+ }
959
+ });
960
+ this.initialisePromises.set(controller, promise);
961
+ return promise;
962
+ }
920
963
  }
921
964
  const _objectStore = new ObjectStore();
922
965
  /**
@@ -934,6 +977,84 @@ function createObjectStore() {
934
977
  return new ObjectStore();
935
978
  }
936
979
 
980
+ class TransactionsInstance {
981
+ constructor(storageManager) {
982
+ this.storageManager = storageManager;
983
+ }
984
+ add(collection) {
985
+ return __awaiter(this, void 0, void 0, function* () {
986
+ const transaction_name = `transaction::${collection.name}::${Date.now()}`;
987
+ yield this.storageManager.set(transaction_name, collection.serialize());
988
+ return transaction_name;
989
+ });
990
+ }
991
+ rollback(transaction_name, name) {
992
+ return __awaiter(this, void 0, void 0, function* () {
993
+ const collection = yield this.get(transaction_name, name);
994
+ yield this.storageManager.delete(transaction_name);
995
+ return collection;
996
+ });
997
+ }
998
+ finish(name) {
999
+ return __awaiter(this, void 0, void 0, function* () {
1000
+ const params = yield this.storageManager.getParams();
1001
+ const _txn_name = `transaction::${name}::`;
1002
+ for (const param of params) {
1003
+ if (param.startsWith(_txn_name)) {
1004
+ yield this.storageManager.delete(param);
1005
+ }
1006
+ }
1007
+ });
1008
+ }
1009
+ get(transaction_name, name) {
1010
+ return __awaiter(this, void 0, void 0, function* () {
1011
+ const serialized = yield this.storageManager.get(transaction_name);
1012
+ if (!serialized) {
1013
+ throw new Error("Transaction not found");
1014
+ }
1015
+ return Collection.deserialize(name, serialized);
1016
+ });
1017
+ }
1018
+ }
1019
+ class Transactions {
1020
+ static createInstance(storageManager) {
1021
+ if (Transactions.instance) {
1022
+ throw new Error("Transactions instance already initialized");
1023
+ }
1024
+ Transactions.instance = new TransactionsInstance(storageManager);
1025
+ }
1026
+ static getInstance() {
1027
+ if (!Transactions.instance) {
1028
+ throw new Error("Transactions instance not initialized");
1029
+ }
1030
+ return Transactions.instance;
1031
+ }
1032
+ static add(collection) {
1033
+ return __awaiter(this, void 0, void 0, function* () {
1034
+ const instance = Transactions.getInstance();
1035
+ return instance.add(collection);
1036
+ });
1037
+ }
1038
+ static rollback(transaction_name, name) {
1039
+ return __awaiter(this, void 0, void 0, function* () {
1040
+ const instance = Transactions.getInstance();
1041
+ return instance.rollback(transaction_name, name);
1042
+ });
1043
+ }
1044
+ static finish(name) {
1045
+ return __awaiter(this, void 0, void 0, function* () {
1046
+ const instance = Transactions.getInstance();
1047
+ return instance.finish(name);
1048
+ });
1049
+ }
1050
+ static get(transaction_name, name) {
1051
+ return __awaiter(this, void 0, void 0, function* () {
1052
+ const instance = Transactions.getInstance();
1053
+ return instance.get(transaction_name, name);
1054
+ });
1055
+ }
1056
+ }
1057
+
937
1058
  /**
938
1059
  * IndexedDB-backed StorageManager.
939
1060
  *
@@ -943,12 +1064,12 @@ function createObjectStore() {
943
1064
  */
944
1065
  class IndexDbStorageManager extends StorageManager {
945
1066
  constructor(options = {}) {
946
- var _a, _b, _c;
947
- super();
1067
+ var _a, _b, _c, _d;
1068
+ super((_a = options.prefix) !== null && _a !== void 0 ? _a : "live-cache:");
948
1069
  this.dbPromise = null;
949
- this.dbName = (_a = options.dbName) !== null && _a !== void 0 ? _a : "live-cache";
950
- this.storeName = (_b = options.storeName) !== null && _b !== void 0 ? _b : "collections";
951
- this.prefix = (_c = options.prefix) !== null && _c !== void 0 ? _c : "live-cache:";
1070
+ this.dbName = (_b = options.dbName) !== null && _b !== void 0 ? _b : "live-cache";
1071
+ this.storeName = (_c = options.storeName) !== null && _c !== void 0 ? _c : "collections";
1072
+ this.prefix = (_d = options.prefix) !== null && _d !== void 0 ? _d : "live-cache:";
952
1073
  }
953
1074
  key(name) {
954
1075
  return `${this.prefix}${name}`;
@@ -1051,6 +1172,17 @@ class IndexDbStorageManager extends StorageManager {
1051
1172
  }
1052
1173
  });
1053
1174
  }
1175
+ getParams() {
1176
+ return __awaiter(this, void 0, void 0, function* () {
1177
+ const db = yield this.openDb();
1178
+ return new Promise((resolve, reject) => {
1179
+ const req = db.transaction(this.storeName, "readonly").objectStore(this.storeName).getAllKeys();
1180
+ const keys = req.result.map(x => x.toString().replace(this.prefix, ""));
1181
+ req.onsuccess = () => resolve(keys);
1182
+ req.onerror = () => { var _a; return reject((_a = req.error) !== null && _a !== void 0 ? _a : new Error("IndexedDB get params failed")); };
1183
+ });
1184
+ });
1185
+ }
1054
1186
  }
1055
1187
 
1056
1188
  /**
@@ -1061,8 +1193,7 @@ class IndexDbStorageManager extends StorageManager {
1061
1193
  */
1062
1194
  class LocalStorageStorageManager extends StorageManager {
1063
1195
  constructor(prefix = "live-cache:") {
1064
- super();
1065
- this.prefix = prefix;
1196
+ super(prefix);
1066
1197
  }
1067
1198
  key(name) {
1068
1199
  return `${this.prefix}${name}`;
@@ -1101,6 +1232,11 @@ class LocalStorageStorageManager extends StorageManager {
1101
1232
  }
1102
1233
  });
1103
1234
  }
1235
+ getParams() {
1236
+ return __awaiter(this, void 0, void 0, function* () {
1237
+ return Object.keys(localStorage).filter(key => key.startsWith(this.prefix)).map(key => key.replace(this.prefix, ""));
1238
+ });
1239
+ }
1104
1240
  }
1105
1241
 
1106
1242
  const context = React.createContext(null);
@@ -1150,12 +1286,15 @@ function useRegister(controller, store = getDefaultObjectStore()) {
1150
1286
  * @param where - optional `Collection.find()` filter (string `_id` or partial)
1151
1287
  * @param options - store selection, initialise behavior, abort-on-unmount, and invalidation wiring
1152
1288
  *
1153
- * When `options.withInvalidation` is true, this hook calls `controller.invalidate()` once on mount
1154
- * and calls the returned cleanup function on unmount.
1289
+ * When `options.withInvalidation` is true, this hook calls
1290
+ * `controller.invalidator.registerInvalidation()` on mount and
1291
+ * `controller.invalidator.unregisterInvalidation()` on unmount.
1155
1292
  *
1156
1293
  * @example
1157
1294
  * ```tsx
1158
- * const { data, controller } = useController<User, "users">("users");
1295
+ * const { data, controller } = useController<User, "users">("users", undefined, {
1296
+ * withInvalidation: true,
1297
+ * });
1159
1298
  * return (
1160
1299
  * <button onClick={() => void controller.invalidate()}>Refresh</button>
1161
1300
  * );
@@ -1177,9 +1316,6 @@ function useController(name, where, options) {
1177
1316
  }
1178
1317
  const controller = React.useMemo(() => store.get(name), [store, name]);
1179
1318
  React.useEffect(() => {
1180
- if (initialise) {
1181
- controller.initialise();
1182
- }
1183
1319
  const callback = () => {
1184
1320
  var _a;
1185
1321
  setLoading(controller.loading);
@@ -1189,16 +1325,18 @@ function useController(name, where, options) {
1189
1325
  // Prime state immediately.
1190
1326
  callback();
1191
1327
  const cleanup = controller.publish(callback);
1192
- let cleanupInvalidation = () => { };
1193
1328
  if (withInvalidation) {
1194
- cleanupInvalidation = controller.invalidate();
1329
+ controller.invalidator.registerInvalidation();
1330
+ }
1331
+ if (initialise) {
1332
+ void store.initialiseOnce(name);
1195
1333
  }
1196
1334
  return () => {
1197
1335
  if (abortOnUnmount) {
1198
1336
  controller.abort();
1199
1337
  }
1200
1338
  cleanup();
1201
- cleanupInvalidation();
1339
+ controller.invalidator.unregisterInvalidation();
1202
1340
  };
1203
1341
  }, [controller, where, initialise, abortOnUnmount, withInvalidation]);
1204
1342
  return { controller, data, loading, error };
@@ -1231,6 +1369,34 @@ function useJoinController({ from, where, select }) {
1231
1369
  return data;
1232
1370
  }
1233
1371
 
1372
+ class TimeoutInvalidator extends Invalidator {
1373
+ constructor(timeoutMs = 0, options = {}) {
1374
+ var _a;
1375
+ super();
1376
+ this.intervalId = null;
1377
+ this.timeoutMs = timeoutMs;
1378
+ this.immediate = (_a = options.immediate) !== null && _a !== void 0 ? _a : true;
1379
+ }
1380
+ registerInvalidation() {
1381
+ if (this.intervalId)
1382
+ return;
1383
+ if (this.immediate) {
1384
+ this.invalidator();
1385
+ }
1386
+ if (this.timeoutMs > 0) {
1387
+ this.intervalId = setInterval(() => {
1388
+ this.invalidator();
1389
+ }, this.timeoutMs);
1390
+ }
1391
+ }
1392
+ unregisterInvalidation() {
1393
+ if (!this.intervalId)
1394
+ return;
1395
+ clearInterval(this.intervalId);
1396
+ this.intervalId = null;
1397
+ }
1398
+ }
1399
+
1234
1400
  // Main library exports
1235
1401
  // Default export for UMD/browser usage
1236
1402
  var index = {
@@ -1241,6 +1407,7 @@ var index = {
1241
1407
  ObjectStore,
1242
1408
  createObjectStore,
1243
1409
  getDefaultObjectStore,
1410
+ Transactions,
1244
1411
  StorageManager,
1245
1412
  DefaultStorageManager,
1246
1413
  IndexDbStorageManager,
@@ -1249,17 +1416,24 @@ var index = {
1249
1416
  useRegister,
1250
1417
  useController,
1251
1418
  useJoinController,
1419
+ DefaultInvalidator,
1420
+ Invalidator,
1421
+ TimeoutInvalidator,
1252
1422
  };
1253
1423
 
1254
1424
  exports.Collection = Collection;
1255
1425
  exports.ContextProvider = ContextProvider;
1256
1426
  exports.Controller = Controller;
1427
+ exports.DefaultInvalidator = DefaultInvalidator;
1257
1428
  exports.DefaultStorageManager = DefaultStorageManager;
1258
1429
  exports.Document = Document;
1259
1430
  exports.IndexDbStorageManager = IndexDbStorageManager;
1431
+ exports.Invalidator = Invalidator;
1260
1432
  exports.LocalStorageStorageManager = LocalStorageStorageManager;
1261
1433
  exports.ObjectStore = ObjectStore;
1262
1434
  exports.StorageManager = StorageManager;
1435
+ exports.TimeoutInvalidator = TimeoutInvalidator;
1436
+ exports.Transactions = Transactions;
1263
1437
  exports.createObjectStore = createObjectStore;
1264
1438
  exports.default = index;
1265
1439
  exports.getDefaultObjectStore = getDefaultObjectStore;