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