@yaasl/core 0.7.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.
Files changed (54) hide show
  1. package/dist/@types/base/Stateful.d.ts +19 -0
  2. package/dist/@types/base/atom.d.ts +38 -0
  3. package/dist/@types/base/config.d.ts +16 -0
  4. package/dist/@types/base/derive.d.ts +19 -0
  5. package/dist/@types/base/index.d.ts +4 -0
  6. package/dist/@types/index.d.ts +2 -0
  7. package/dist/@types/middleware/MiddlewareDispatcher.d.ts +14 -0
  8. package/dist/@types/middleware/expiration.d.ts +17 -0
  9. package/dist/@types/middleware/index.d.ts +10 -0
  10. package/dist/@types/middleware/indexedDb.d.ts +15 -0
  11. package/dist/@types/middleware/localStorage.d.ts +28 -0
  12. package/dist/@types/middleware/middleware.d.ts +31 -0
  13. package/dist/@types/middleware/migration.d.ts +19 -0
  14. package/dist/@types/utils/Expiration.d.ts +18 -0
  15. package/dist/@types/utils/LocalStorage.d.ts +17 -0
  16. package/dist/@types/utils/Store.d.ts +9 -0
  17. package/dist/@types/utils/Thenable.d.ts +7 -0
  18. package/dist/cjs/base/Stateful.js +36 -0
  19. package/dist/cjs/base/atom.js +67 -0
  20. package/dist/cjs/base/config.js +11 -0
  21. package/dist/cjs/base/derive.js +32 -0
  22. package/dist/cjs/base/index.js +20 -0
  23. package/dist/cjs/index.js +18 -0
  24. package/dist/cjs/middleware/MiddlewareDispatcher.js +34 -0
  25. package/dist/cjs/middleware/expiration.js +48 -0
  26. package/dist/cjs/middleware/index.js +14 -0
  27. package/dist/cjs/middleware/indexedDb.js +49 -0
  28. package/dist/cjs/middleware/localStorage.js +38 -0
  29. package/dist/cjs/middleware/middleware.js +19 -0
  30. package/dist/cjs/middleware/migration.js +91 -0
  31. package/dist/cjs/utils/Expiration.js +64 -0
  32. package/dist/cjs/utils/LocalStorage.js +60 -0
  33. package/dist/cjs/utils/Store.js +50 -0
  34. package/dist/cjs/utils/Thenable.js +25 -0
  35. package/dist/mjs/base/Stateful.js +32 -0
  36. package/dist/mjs/base/atom.js +51 -0
  37. package/dist/mjs/base/config.js +8 -0
  38. package/dist/mjs/base/derive.js +27 -0
  39. package/dist/mjs/base/index.js +4 -0
  40. package/dist/mjs/index.js +2 -0
  41. package/dist/mjs/middleware/MiddlewareDispatcher.js +30 -0
  42. package/dist/mjs/middleware/expiration.js +47 -0
  43. package/dist/mjs/middleware/index.js +5 -0
  44. package/dist/mjs/middleware/indexedDb.js +35 -0
  45. package/dist/mjs/middleware/localStorage.js +35 -0
  46. package/dist/mjs/middleware/middleware.js +15 -0
  47. package/dist/mjs/middleware/migration.js +86 -0
  48. package/dist/mjs/utils/Expiration.js +60 -0
  49. package/dist/mjs/utils/LocalStorage.js +54 -0
  50. package/dist/mjs/utils/Store.js +33 -0
  51. package/dist/mjs/utils/Thenable.js +21 -0
  52. package/package.json +57 -0
  53. package/tsconfig.json +3 -0
  54. package/tsconfig.node.json +9 -0
@@ -0,0 +1,86 @@
1
+ import { log } from "@yaasl/utils";
2
+ import { middleware } from "./middleware";
3
+ import { CONFIG } from "../base";
4
+ const sortMigrations = (migrations) => {
5
+ const first = migrations.find(migration => migration.previous === null);
6
+ if (!first) {
7
+ log.error("No migration with previous version null found");
8
+ return [];
9
+ }
10
+ return migrations.reduce(sorted => {
11
+ const last = sorted.at(-1);
12
+ const next = migrations.find(migration => migration.previous === last?.version);
13
+ if (next) {
14
+ sorted.push(next);
15
+ }
16
+ return sorted;
17
+ }, [first]);
18
+ };
19
+ const getVersion = (atom) => {
20
+ const key = CONFIG.name ? `${CONFIG.name}/${atom.name}` : atom.name;
21
+ return localStorage.getItem(`${key}-version`);
22
+ };
23
+ const setVersion = (atom, version) => {
24
+ const key = CONFIG.name ? `${CONFIG.name}/${atom.name}` : atom.name;
25
+ localStorage.setItem(`${key}-version`, version);
26
+ };
27
+ const migrateVersion = (atom, data, migration) => {
28
+ if (migration.validate && !migration.validate(data)) {
29
+ return {
30
+ error: `The data of the "${atom.name}" atom does not match its version.`,
31
+ };
32
+ }
33
+ try {
34
+ return {
35
+ data: migration.migrate(data),
36
+ version: migration.version,
37
+ };
38
+ }
39
+ catch {
40
+ return {
41
+ error: `Something went wrong while migrating the data of the "${atom.name}" atom.`,
42
+ };
43
+ }
44
+ };
45
+ const performMigration = (atom, version, migrations) => {
46
+ const currentState = {
47
+ version,
48
+ data: atom.get(),
49
+ };
50
+ return migrations.reduce((result, migration) => {
51
+ const { data, version, error } = result;
52
+ if (error || version !== migration.previous) {
53
+ return result;
54
+ }
55
+ return migrateVersion(atom, data, migration);
56
+ }, currentState);
57
+ };
58
+ export const migration = middleware({
59
+ didInit: ({ atom, options }) => {
60
+ const steps = sortMigrations(options.steps);
61
+ const currentVersion = getVersion(atom);
62
+ const isLatestVersion = currentVersion === steps.at(-1)?.version;
63
+ if (steps.length < 1 || isLatestVersion) {
64
+ return;
65
+ }
66
+ if (atom.get() === atom.defaultValue) {
67
+ const version = steps.at(-1)?.version;
68
+ if (version)
69
+ setVersion(atom, version);
70
+ return;
71
+ }
72
+ const { data, version, error } = performMigration(atom, currentVersion, steps);
73
+ if (error) {
74
+ log.error(error, {
75
+ version: currentVersion,
76
+ data: atom.get(),
77
+ });
78
+ return;
79
+ }
80
+ if (version == null)
81
+ return;
82
+ setVersion(atom, version);
83
+ atom.set(data);
84
+ },
85
+ });
86
+ export const createMigrationStep = (migration) => migration;
@@ -0,0 +1,60 @@
1
+ const STORAGE = window.localStorage;
2
+ export class Expiration {
3
+ constructor({ key, expiresAt, expiresIn }) {
4
+ this.timeout = null;
5
+ this.dispatchExpiration = (expiresIn, onExpire) => {
6
+ this.timeout = setTimeout(() => {
7
+ onExpire();
8
+ this.remove();
9
+ }, expiresIn);
10
+ };
11
+ this.timeout = null;
12
+ this.key = key;
13
+ if (expiresAt) {
14
+ this.getExpiration = () => expiresAt instanceof Function ? expiresAt() : expiresAt;
15
+ }
16
+ else if (expiresIn) {
17
+ this.getExpiration = () => {
18
+ const afterMs = expiresIn instanceof Function ? expiresIn() : expiresIn;
19
+ return new Date(Date.now() + afterMs);
20
+ };
21
+ }
22
+ else {
23
+ this.getExpiration = null;
24
+ }
25
+ }
26
+ remove() {
27
+ STORAGE.removeItem(this.key);
28
+ if (this.timeout != null) {
29
+ clearTimeout(this.timeout);
30
+ this.timeout = null;
31
+ }
32
+ }
33
+ init(onExpire) {
34
+ if (!this.getExpiration)
35
+ return;
36
+ const existing = STORAGE.getItem(this.key);
37
+ if (!existing) {
38
+ this.remove();
39
+ return;
40
+ }
41
+ const expiresIn = Number(existing) - Date.now();
42
+ const isExpired = Number.isNaN(expiresIn) || expiresIn <= 0;
43
+ if (isExpired) {
44
+ onExpire();
45
+ this.remove();
46
+ }
47
+ else {
48
+ this.dispatchExpiration(expiresIn, onExpire);
49
+ }
50
+ }
51
+ set(onExpire) {
52
+ this.remove();
53
+ if (!this.getExpiration)
54
+ return;
55
+ const expiration = this.getExpiration();
56
+ const expiresIn = Number(expiration) - Date.now();
57
+ this.dispatchExpiration(expiresIn, onExpire);
58
+ STORAGE.setItem(this.key, String(expiration.valueOf()));
59
+ }
60
+ }
@@ -0,0 +1,54 @@
1
+ import { consoleMessage, log } from "@yaasl/utils";
2
+ const STORAGE = window.localStorage;
3
+ const defaultParser = {
4
+ parse: JSON.parse,
5
+ stringify: JSON.stringify,
6
+ };
7
+ const syncOverBrowserTabs = (observingKey, onTabSync) => window.addEventListener("storage", ({ key, newValue }) => {
8
+ if (observingKey !== key)
9
+ return;
10
+ onTabSync(newValue);
11
+ });
12
+ export class LocalStorage {
13
+ constructor(key, options = {}) {
14
+ this.key = key;
15
+ this.parser = options.parser ?? defaultParser;
16
+ if (!options.onTabSync)
17
+ return;
18
+ syncOverBrowserTabs(key, value => {
19
+ const newValue = value === null ? null : this.parser.parse(value);
20
+ if (newValue === null) {
21
+ this.remove();
22
+ }
23
+ else {
24
+ this.set(newValue);
25
+ }
26
+ options.onTabSync?.(newValue);
27
+ });
28
+ }
29
+ get() {
30
+ const value = STORAGE.getItem(this.key);
31
+ try {
32
+ return typeof value !== "string" ? null : this.parser.parse(value);
33
+ }
34
+ catch {
35
+ throw new Error(consoleMessage(`Value of local storage key "${this.key}" could not be parsed.`));
36
+ }
37
+ }
38
+ set(value) {
39
+ try {
40
+ if (value === null) {
41
+ STORAGE.removeItem(this.key);
42
+ }
43
+ else {
44
+ STORAGE.setItem(this.key, this.parser.stringify(value));
45
+ }
46
+ }
47
+ catch {
48
+ log.error(`Value of atom with local storage key "${this.key}" could not be set.`, { value });
49
+ }
50
+ }
51
+ remove() {
52
+ STORAGE.removeItem(this.key);
53
+ }
54
+ }
@@ -0,0 +1,33 @@
1
+ const promisifyRequest = (request) => new Promise((resolve, reject) => {
2
+ request.onsuccess = () => resolve(request.result);
3
+ request.onerror = () => reject(request.error);
4
+ });
5
+ export class Store {
6
+ constructor(name) {
7
+ this.name = name;
8
+ const openRequest = indexedDB.open(`${name}-database`);
9
+ this.database = promisifyRequest(openRequest);
10
+ openRequest.onupgradeneeded = () => {
11
+ const database = openRequest.result;
12
+ const exists = database.objectStoreNames.contains(name);
13
+ if (!exists) {
14
+ database.createObjectStore(name);
15
+ }
16
+ };
17
+ }
18
+ async getStore(mode) {
19
+ const database = await this.database;
20
+ return database.transaction(this.name, mode).objectStore(this.name);
21
+ }
22
+ async get(key) {
23
+ return this.getStore("readonly").then(store => promisifyRequest(store.get(key)));
24
+ }
25
+ set(key, value) {
26
+ return this.getStore("readwrite").then(async (store) => {
27
+ await promisifyRequest(store.put(value, key));
28
+ });
29
+ }
30
+ delete(key) {
31
+ return this.getStore("readwrite").then(store => promisifyRequest(store.delete(key)));
32
+ }
33
+ }
@@ -0,0 +1,21 @@
1
+ const isPromiselike = (value) => typeof value === "object" &&
2
+ value !== null &&
3
+ "then" in value &&
4
+ typeof value.then === "function";
5
+ export class Thenable {
6
+ constructor(value) {
7
+ this.value = value;
8
+ }
9
+ then(onfulfilled, onrejected) {
10
+ try {
11
+ const result = !onfulfilled ? this.value : onfulfilled(this.value);
12
+ return isPromiselike(result) ? result : new Thenable(result);
13
+ }
14
+ catch (error) {
15
+ if (!onrejected)
16
+ throw error;
17
+ const result = onrejected(error);
18
+ return isPromiselike(result) ? result : new Thenable(result);
19
+ }
20
+ }
21
+ }
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@yaasl/core",
3
+ "version": "0.7.0-alpha.0",
4
+ "description": "yet another atomic store library (vanilla-js)",
5
+ "author": "PrettyCoffee",
6
+ "license": "MIT",
7
+ "homepage": "https://github.com/PrettyCoffee/yaasl#readme",
8
+ "keywords": [
9
+ "atom",
10
+ "store",
11
+ "vanilla-js"
12
+ ],
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/PrettyCoffee/yaasl.git"
16
+ },
17
+ "bugs": {
18
+ "url": "https://github.com/PrettyCoffee/yaasl/issues"
19
+ },
20
+ "main": "./dist/cjs/index.js",
21
+ "types": "./dist/@types/index.d.ts",
22
+ "exports": {
23
+ "types": "./dist/@types/index.d.ts",
24
+ "import": "./dist/mjs/index.js",
25
+ "require": "./dist/cjs/index.js"
26
+ },
27
+ "scripts": {
28
+ "build": "run-s clean compile",
29
+ "clean": "rimraf ./dist",
30
+ "compile": "run-p compile:mjs compile:cjs compile:types",
31
+ "compile:mjs": "tsc -p ./tsconfig.node.json --outDir ./dist/mjs -m es2020 -t es2020",
32
+ "compile:cjs": "tsc -p ./tsconfig.node.json --outDir ./dist/cjs -m commonjs -t es2015",
33
+ "compile:types": "tsc -p ./tsconfig.node.json --outDir ./dist/@types --declaration --emitDeclarationOnly",
34
+ "test": "jest --testMatch ./**/*.test.*",
35
+ "test:watch": "npm run test -- --watchAll",
36
+ "lint": "eslint ./src",
37
+ "lint:fix": "eslint ./src --fix",
38
+ "validate": "run-s lint test build"
39
+ },
40
+ "dependencies": {
41
+ "@yaasl/utils": "0.7.0-alpha.0"
42
+ },
43
+ "jest": {
44
+ "preset": "ts-jest",
45
+ "testEnvironment": "jsdom"
46
+ },
47
+ "eslintConfig": {
48
+ "extends": [
49
+ "../../.eslintrc"
50
+ ]
51
+ },
52
+ "lint-staged": {
53
+ "*.{ts,tsx}": [
54
+ "npm run lint:fix"
55
+ ]
56
+ }
57
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,3 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "include": ["./src"],
4
+ "exclude": ["./**/*.test.ts"],
5
+ "compilerOptions": {
6
+ "declaration": false,
7
+ "noEmit": false,
8
+ }
9
+ }