@yaasl/core 0.13.1 → 0.14.0-alpha.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.
@@ -12,3 +12,4 @@ export { expiration } from "./expiration";
12
12
  export type { ExpirationOptions } from "./expiration";
13
13
  export { migration, createMigrationStep } from "./migration";
14
14
  export type { MigrationOptions, MigrationStep } from "./migration";
15
+ export { sync } from "./sync";
@@ -1,14 +1,14 @@
1
1
  export interface IndexedDbOptions {
2
2
  /** Use your own store key. Will be `atom.name` by default. */
3
3
  key?: string;
4
- /** Disable the synchronization of values over browser tabs */
5
- noTabSync?: boolean;
6
4
  }
7
5
  /** Middleware to save and load atom values to an indexedDb.
8
6
  *
9
7
  * Will use one database and store for all atoms with your `CONFIG.name`
10
8
  * as name or `yaasl` if not set.
11
9
  *
10
+ * Should be used in combination with the `sync` effect, to ensure value integrity.
11
+ *
12
12
  * @param {IndexedDbOptions | undefined} options
13
13
  * @param options.key Use your own store key. Will be `atom.name` by default.
14
14
  *
@@ -15,7 +15,6 @@ export interface SessionStorageOptions {
15
15
  * @param {SessionStorageOptions | undefined} options
16
16
  * @param options.key Use your own key for the session storage.
17
17
  * Will be "{config-name}/{atom-name}" by default.
18
- * @param options.noTabSync Disable the synchronization of values over browser tabs.
19
18
  * @param options.parser Custom functions to stringify and parse values. Defaults to JSON.stringify and JSON.parse. Use this when handling complex datatypes like Maps or Sets.
20
19
  *
21
20
  * @returns The effect to be used on atoms.
@@ -0,0 +1,5 @@
1
+ /** Effect to synchronize the atoms value over tabs.
2
+ *
3
+ * @returns The effect to be used on atoms.
4
+ **/
5
+ export declare const sync: (...[optionsArg]: [] | [undefined]) => import("./create-effect").EffectAtomCallback<undefined, any>;
@@ -46,6 +46,11 @@ class Atom extends stateful_1.Stateful {
46
46
  * new value based off the previous value.
47
47
  */
48
48
  set(next) {
49
+ if (this.didInit !== true) {
50
+ throw new Error((0, utils_1.consoleMessage)("Tried to set a value during initialization. " +
51
+ "You are probably using an async effect. " +
52
+ "Use `await atom.didInit` to wait for the initialization process to be finished.", { scope: this.name }));
53
+ }
49
54
  const oldState = this.get();
50
55
  const newState = (0, utils_1.updater)(next, oldState);
51
56
  if (oldState === newState)
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.createMigrationStep = exports.migration = exports.expiration = exports.indexedDb = exports.sessionStorage = exports.localStorage = exports.autoSort = exports.createEffect = void 0;
3
+ exports.sync = exports.createMigrationStep = exports.migration = exports.expiration = exports.indexedDb = exports.sessionStorage = exports.localStorage = exports.autoSort = exports.createEffect = void 0;
4
4
  var create_effect_1 = require("./create-effect");
5
5
  Object.defineProperty(exports, "createEffect", { enumerable: true, get: function () { return create_effect_1.createEffect; } });
6
6
  var auto_sort_1 = require("./auto-sort");
@@ -16,3 +16,5 @@ Object.defineProperty(exports, "expiration", { enumerable: true, get: function (
16
16
  var migration_1 = require("./migration");
17
17
  Object.defineProperty(exports, "migration", { enumerable: true, get: function () { return migration_1.migration; } });
18
18
  Object.defineProperty(exports, "createMigrationStep", { enumerable: true, get: function () { return migration_1.createMigrationStep; } });
19
+ var sync_1 = require("./sync");
20
+ Object.defineProperty(exports, "sync", { enumerable: true, get: function () { return sync_1.sync; } });
@@ -12,37 +12,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.indexedDb = void 0;
13
13
  const create_effect_1 = require("./create-effect");
14
14
  const base_1 = require("../base");
15
- const get_scoped_key_1 = require("../utils/get-scoped-key");
16
15
  const idb_store_1 = require("../utils/idb-store");
17
16
  let atomDb = null;
18
- const createSync = (storeKey, onTabSync) => {
19
- const key = (0, get_scoped_key_1.getScopedKey)(storeKey);
20
- const channel = new BroadcastChannel(key);
21
- let changeTrigger = null;
22
- channel.onmessage = () => {
23
- if (changeTrigger === "self") {
24
- changeTrigger = null;
25
- return;
26
- }
27
- changeTrigger = "sync";
28
- onTabSync();
29
- };
30
- return {
31
- pushSync: () => {
32
- if (changeTrigger === "sync") {
33
- changeTrigger = null;
34
- return;
35
- }
36
- changeTrigger = "self";
37
- channel.postMessage("sync");
38
- },
39
- };
40
- };
41
17
  /** Middleware to save and load atom values to an indexedDb.
42
18
  *
43
19
  * Will use one database and store for all atoms with your `CONFIG.name`
44
20
  * as name or `yaasl` if not set.
45
21
  *
22
+ * Should be used in combination with the `sync` effect, to ensure value integrity.
23
+ *
46
24
  * @param {IndexedDbOptions | undefined} options
47
25
  * @param options.key Use your own store key. Will be `atom.name` by default.
48
26
  *
@@ -51,15 +29,6 @@ const createSync = (storeKey, onTabSync) => {
51
29
  exports.indexedDb = (0, create_effect_1.createEffect)(({ atom, options }) => {
52
30
  var _a;
53
31
  const key = (_a = options === null || options === void 0 ? void 0 : options.key) !== null && _a !== void 0 ? _a : atom.name;
54
- const { pushSync } = (options === null || options === void 0 ? void 0 : options.noTabSync)
55
- ? {}
56
- : createSync(key, () => {
57
- void (atomDb === null || atomDb === void 0 ? void 0 : atomDb.get(key).then(value => {
58
- if (!value)
59
- return;
60
- atom.set(value);
61
- }));
62
- });
63
32
  return {
64
33
  sort: "pre",
65
34
  init: (_a) => __awaiter(void 0, [_a], void 0, function* ({ atom, set }) {
@@ -76,11 +45,13 @@ exports.indexedDb = (0, create_effect_1.createEffect)(({ atom, options }) => {
76
45
  }
77
46
  }),
78
47
  set: ({ value, atom }) => {
79
- const action = value === atom.defaultValue
80
- ? atomDb === null || atomDb === void 0 ? void 0 : atomDb.delete(key)
81
- : atomDb === null || atomDb === void 0 ? void 0 : atomDb.set(key, value);
82
- // don't wait to set the atom value, directly pass it into the atom
83
- void (action === null || action === void 0 ? void 0 : action.then(pushSync));
48
+ // Don't wait for promises since this would cause lag
49
+ if (value === atom.defaultValue) {
50
+ void (atomDb === null || atomDb === void 0 ? void 0 : atomDb.delete(key));
51
+ }
52
+ else {
53
+ void (atomDb === null || atomDb === void 0 ? void 0 : atomDb.set(key, value));
54
+ }
84
55
  },
85
56
  };
86
57
  });
@@ -10,7 +10,6 @@ const string_storage_1 = require("../utils/string-storage");
10
10
  * @param {SessionStorageOptions | undefined} options
11
11
  * @param options.key Use your own key for the session storage.
12
12
  * Will be "{config-name}/{atom-name}" by default.
13
- * @param options.noTabSync Disable the synchronization of values over browser tabs.
14
13
  * @param options.parser Custom functions to stringify and parse values. Defaults to JSON.stringify and JSON.parse. Use this when handling complex datatypes like Maps or Sets.
15
14
  *
16
15
  * @returns The effect to be used on atoms.
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.sync = void 0;
4
+ const utils_1 = require("@yaasl/utils");
5
+ const create_effect_1 = require("./create-effect");
6
+ const base_1 = require("../base");
7
+ const getId = () => { var _a, _b; return (_b = (_a = (0, utils_1.getWindow)()) === null || _a === void 0 ? void 0 : _a.crypto.randomUUID()) !== null && _b !== void 0 ? _b : Math.random().toString(36).slice(2, 10); };
8
+ const getChannelName = (key) => ["yaasl", "sync-channel", base_1.CONFIG.name, key].filter(Boolean).join("/");
9
+ class SyncChannel {
10
+ constructor(key) {
11
+ this.listeners = new Set();
12
+ this.channel = new BroadcastChannel(getChannelName(key));
13
+ this.channel.onmessage = event => {
14
+ const { id, data } = event.data;
15
+ if (id === SyncChannel.id)
16
+ return;
17
+ this.listeners.forEach(listener => listener(data));
18
+ };
19
+ }
20
+ push(data) {
21
+ this.channel.postMessage({
22
+ id: SyncChannel.id,
23
+ data,
24
+ });
25
+ }
26
+ subscribe(listener) {
27
+ this.listeners.add(listener);
28
+ return () => this.listeners.delete(listener);
29
+ }
30
+ }
31
+ SyncChannel.id = getId();
32
+ /** Effect to synchronize the atoms value over tabs.
33
+ *
34
+ * @returns The effect to be used on atoms.
35
+ **/
36
+ exports.sync = (0, create_effect_1.createEffect)(({ atom }) => {
37
+ const channel = new SyncChannel(atom.name);
38
+ let skip = false;
39
+ return {
40
+ didInit: () => {
41
+ channel.subscribe(data => {
42
+ atom.set(data);
43
+ skip = true;
44
+ });
45
+ },
46
+ set: ({ value }) => {
47
+ if (skip) {
48
+ skip = false;
49
+ return;
50
+ }
51
+ channel.push(value);
52
+ },
53
+ };
54
+ });
@@ -15,7 +15,8 @@ class Queue {
15
15
  const init = this.last ? this.last.then(() => prev) : new utils_1.Thenable(prev);
16
16
  const result = this.queue.reduce((result, next) => result.then(prev => next(prev)), init);
17
17
  this.queue = [];
18
- this.last = result.then(value => {
18
+ this.last = result;
19
+ result.then(value => {
19
20
  if (this.last === result) {
20
21
  this.last = null;
21
22
  }
@@ -1,4 +1,4 @@
1
- import { updater, Thenable, toVoid } from "@yaasl/utils";
1
+ import { updater, Thenable, toVoid, consoleMessage, } from "@yaasl/utils";
2
2
  import { CONFIG } from "./config";
3
3
  import { Stateful } from "./stateful";
4
4
  import { EffectDispatcher } from "../effects/effect-dispatcher";
@@ -48,6 +48,11 @@ export class Atom extends Stateful {
48
48
  * new value based off the previous value.
49
49
  */
50
50
  set(next) {
51
+ if (this.didInit !== true) {
52
+ throw new Error(consoleMessage("Tried to set a value during initialization. " +
53
+ "You are probably using an async effect. " +
54
+ "Use `await atom.didInit` to wait for the initialization process to be finished.", { scope: this.name }));
55
+ }
51
56
  const oldState = this.get();
52
57
  const newState = updater(next, oldState);
53
58
  if (oldState === newState)
@@ -5,3 +5,4 @@ export { sessionStorage } from "./session-storage";
5
5
  export { indexedDb } from "./indexed-db";
6
6
  export { expiration } from "./expiration";
7
7
  export { migration, createMigrationStep } from "./migration";
8
+ export { sync } from "./sync";
@@ -1,36 +1,14 @@
1
1
  import { createEffect } from "./create-effect";
2
2
  import { CONFIG } from "../base";
3
- import { getScopedKey } from "../utils/get-scoped-key";
4
3
  import { IdbStore } from "../utils/idb-store";
5
4
  let atomDb = null;
6
- const createSync = (storeKey, onTabSync) => {
7
- const key = getScopedKey(storeKey);
8
- const channel = new BroadcastChannel(key);
9
- let changeTrigger = null;
10
- channel.onmessage = () => {
11
- if (changeTrigger === "self") {
12
- changeTrigger = null;
13
- return;
14
- }
15
- changeTrigger = "sync";
16
- onTabSync();
17
- };
18
- return {
19
- pushSync: () => {
20
- if (changeTrigger === "sync") {
21
- changeTrigger = null;
22
- return;
23
- }
24
- changeTrigger = "self";
25
- channel.postMessage("sync");
26
- },
27
- };
28
- };
29
5
  /** Middleware to save and load atom values to an indexedDb.
30
6
  *
31
7
  * Will use one database and store for all atoms with your `CONFIG.name`
32
8
  * as name or `yaasl` if not set.
33
9
  *
10
+ * Should be used in combination with the `sync` effect, to ensure value integrity.
11
+ *
34
12
  * @param {IndexedDbOptions | undefined} options
35
13
  * @param options.key Use your own store key. Will be `atom.name` by default.
36
14
  *
@@ -38,15 +16,6 @@ const createSync = (storeKey, onTabSync) => {
38
16
  **/
39
17
  export const indexedDb = createEffect(({ atom, options }) => {
40
18
  const key = options?.key ?? atom.name;
41
- const { pushSync } = options?.noTabSync
42
- ? {}
43
- : createSync(key, () => {
44
- void atomDb?.get(key).then(value => {
45
- if (!value)
46
- return;
47
- atom.set(value);
48
- });
49
- });
50
19
  return {
51
20
  sort: "pre",
52
21
  init: async ({ atom, set }) => {
@@ -62,11 +31,13 @@ export const indexedDb = createEffect(({ atom, options }) => {
62
31
  }
63
32
  },
64
33
  set: ({ value, atom }) => {
65
- const action = value === atom.defaultValue
66
- ? atomDb?.delete(key)
67
- : atomDb?.set(key, value);
68
- // don't wait to set the atom value, directly pass it into the atom
69
- void action?.then(pushSync);
34
+ // Don't wait for promises since this would cause lag
35
+ if (value === atom.defaultValue) {
36
+ void atomDb?.delete(key);
37
+ }
38
+ else {
39
+ void atomDb?.set(key, value);
40
+ }
70
41
  },
71
42
  };
72
43
  });
@@ -7,7 +7,6 @@ import { StringStorage } from "../utils/string-storage";
7
7
  * @param {SessionStorageOptions | undefined} options
8
8
  * @param options.key Use your own key for the session storage.
9
9
  * Will be "{config-name}/{atom-name}" by default.
10
- * @param options.noTabSync Disable the synchronization of values over browser tabs.
11
10
  * @param options.parser Custom functions to stringify and parse values. Defaults to JSON.stringify and JSON.parse. Use this when handling complex datatypes like Maps or Sets.
12
11
  *
13
12
  * @returns The effect to be used on atoms.
@@ -0,0 +1,52 @@
1
+ import { getWindow } from "@yaasl/utils";
2
+ import { createEffect } from "./create-effect";
3
+ import { CONFIG } from "../base";
4
+ const getId = () => getWindow()?.crypto.randomUUID() ?? Math.random().toString(36).slice(2, 10);
5
+ const getChannelName = (key) => ["yaasl", "sync-channel", CONFIG.name, key].filter(Boolean).join("/");
6
+ class SyncChannel {
7
+ static id = getId();
8
+ channel;
9
+ listeners = new Set();
10
+ constructor(key) {
11
+ this.channel = new BroadcastChannel(getChannelName(key));
12
+ this.channel.onmessage = event => {
13
+ const { id, data } = event.data;
14
+ if (id === SyncChannel.id)
15
+ return;
16
+ this.listeners.forEach(listener => listener(data));
17
+ };
18
+ }
19
+ push(data) {
20
+ this.channel.postMessage({
21
+ id: SyncChannel.id,
22
+ data,
23
+ });
24
+ }
25
+ subscribe(listener) {
26
+ this.listeners.add(listener);
27
+ return () => this.listeners.delete(listener);
28
+ }
29
+ }
30
+ /** Effect to synchronize the atoms value over tabs.
31
+ *
32
+ * @returns The effect to be used on atoms.
33
+ **/
34
+ export const sync = createEffect(({ atom }) => {
35
+ const channel = new SyncChannel(atom.name);
36
+ let skip = false;
37
+ return {
38
+ didInit: () => {
39
+ channel.subscribe(data => {
40
+ atom.set(data);
41
+ skip = true;
42
+ });
43
+ },
44
+ set: ({ value }) => {
45
+ if (skip) {
46
+ skip = false;
47
+ return;
48
+ }
49
+ channel.push(value);
50
+ },
51
+ };
52
+ });
@@ -10,7 +10,8 @@ export class Queue {
10
10
  const init = this.last ? this.last.then(() => prev) : new Thenable(prev);
11
11
  const result = this.queue.reduce((result, next) => result.then(prev => next(prev)), init);
12
12
  this.queue = [];
13
- this.last = result.then(value => {
13
+ this.last = result;
14
+ result.then(value => {
14
15
  if (this.last === result) {
15
16
  this.last = null;
16
17
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yaasl/core",
3
- "version": "0.13.1",
3
+ "version": "0.14.0-alpha.0",
4
4
  "description": "yet another atomic store library (vanilla-js)",
5
5
  "author": "PrettyCoffee",
6
6
  "license": "MIT",