valyrian.js 7.2.12 → 8.0.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.
Files changed (168) hide show
  1. package/README.md +6 -6
  2. package/dist/flux-store/index.d.ts +32 -0
  3. package/dist/flux-store/index.d.ts.map +1 -0
  4. package/dist/flux-store/index.js +267 -0
  5. package/dist/flux-store/index.js.map +7 -0
  6. package/dist/flux-store/index.min.js +1 -0
  7. package/dist/flux-store/index.min.js.map +1 -0
  8. package/dist/flux-store/index.mjs +246 -0
  9. package/dist/flux-store/index.mjs.map +7 -0
  10. package/dist/hooks/index.d.ts.map +1 -1
  11. package/dist/hooks/index.js +35 -51
  12. package/dist/hooks/index.js.map +3 -3
  13. package/dist/hooks/index.min.js +1 -0
  14. package/dist/hooks/index.min.js.map +1 -0
  15. package/dist/hooks/index.mjs +36 -52
  16. package/dist/hooks/index.mjs.map +3 -3
  17. package/dist/index.d.ts +18 -14
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +107 -88
  20. package/dist/index.js.map +3 -3
  21. package/dist/index.min.js +1 -1
  22. package/dist/index.min.js.map +1 -1
  23. package/dist/index.mjs +107 -88
  24. package/dist/index.mjs.map +3 -3
  25. package/dist/native-store/index.d.ts +14 -0
  26. package/dist/native-store/index.d.ts.map +1 -0
  27. package/dist/native-store/index.js +103 -0
  28. package/dist/native-store/index.js.map +7 -0
  29. package/dist/native-store/index.min.js +1 -0
  30. package/dist/native-store/index.min.js.map +1 -0
  31. package/dist/native-store/index.mjs +82 -0
  32. package/dist/native-store/index.mjs.map +7 -0
  33. package/dist/node/index.d.ts +1 -1
  34. package/dist/node/index.d.ts.map +1 -1
  35. package/dist/node/index.js +125 -10
  36. package/dist/node/index.js.map +4 -4
  37. package/dist/node/index.mjs +125 -10
  38. package/dist/node/index.mjs.map +4 -4
  39. package/dist/node/node.sw.js +152 -0
  40. package/dist/node/utils/icons.d.ts +4 -5
  41. package/dist/node/utils/icons.d.ts.map +1 -1
  42. package/dist/node/utils/inline.d.ts +1 -1
  43. package/dist/node/utils/inline.d.ts.map +1 -1
  44. package/dist/node/utils/node.sw.js +152 -0
  45. package/dist/node/utils/session-storage.d.ts +22 -0
  46. package/dist/node/utils/session-storage.d.ts.map +1 -0
  47. package/dist/node/utils/sw.d.ts.map +1 -1
  48. package/dist/node/utils/tree-adapter.d.ts +5 -1
  49. package/dist/node/utils/tree-adapter.d.ts.map +1 -1
  50. package/dist/pulse-store/index.d.ts +16 -0
  51. package/dist/pulse-store/index.d.ts.map +1 -0
  52. package/dist/pulse-store/index.js +143 -0
  53. package/dist/pulse-store/index.js.map +7 -0
  54. package/dist/pulse-store/index.min.js +1 -0
  55. package/dist/pulse-store/index.min.js.map +1 -0
  56. package/dist/pulse-store/index.mjs +122 -0
  57. package/dist/pulse-store/index.mjs.map +7 -0
  58. package/dist/request/index.d.ts +11 -11
  59. package/dist/request/index.d.ts.map +1 -1
  60. package/dist/request/index.js +63 -84
  61. package/dist/request/index.js.map +2 -2
  62. package/dist/request/index.min.js +1 -0
  63. package/dist/request/index.min.js.map +1 -0
  64. package/dist/request/index.mjs +63 -84
  65. package/dist/request/index.mjs.map +2 -2
  66. package/dist/router/index.d.ts +36 -33
  67. package/dist/router/index.d.ts.map +1 -1
  68. package/dist/router/index.js +247 -96
  69. package/dist/router/index.js.map +3 -3
  70. package/dist/router/index.min.js +1 -0
  71. package/dist/router/index.min.js.map +1 -0
  72. package/dist/router/index.mjs +247 -96
  73. package/dist/router/index.mjs.map +3 -3
  74. package/dist/signals/index.d.ts +6 -0
  75. package/dist/signals/index.d.ts.map +1 -0
  76. package/dist/signals/index.js +92 -0
  77. package/dist/signals/index.js.map +7 -0
  78. package/dist/signals/index.min.js +1 -0
  79. package/dist/signals/index.min.js.map +1 -0
  80. package/dist/signals/index.mjs +71 -0
  81. package/dist/signals/index.mjs.map +7 -0
  82. package/dist/suspense/index.d.ts +6 -0
  83. package/dist/suspense/index.d.ts.map +1 -0
  84. package/dist/suspense/index.js +67 -0
  85. package/dist/suspense/index.js.map +7 -0
  86. package/dist/suspense/index.min.js +1 -0
  87. package/dist/suspense/index.min.js.map +1 -0
  88. package/dist/suspense/index.mjs +46 -0
  89. package/dist/suspense/index.mjs.map +7 -0
  90. package/dist/sw/index.min.js +1 -0
  91. package/dist/sw/index.min.js.map +1 -0
  92. package/dist/translate/index.d.ts +19 -0
  93. package/dist/translate/index.d.ts.map +1 -0
  94. package/dist/translate/index.js +150 -0
  95. package/dist/translate/index.js.map +7 -0
  96. package/dist/translate/index.min.js +1 -0
  97. package/dist/translate/index.min.js.map +1 -0
  98. package/dist/translate/index.mjs +129 -0
  99. package/dist/translate/index.mjs.map +7 -0
  100. package/dist/tsconfig.tsbuildinfo +1 -1
  101. package/dist/utils/deep-freeze.d.ts +3 -0
  102. package/dist/utils/deep-freeze.d.ts.map +1 -0
  103. package/dist/utils/getter-setter.d.ts +3 -0
  104. package/dist/utils/getter-setter.d.ts.map +1 -0
  105. package/dist/utils/has-changed.d.ts +2 -0
  106. package/dist/utils/has-changed.d.ts.map +1 -0
  107. package/dist/utils/index.d.ts +4 -0
  108. package/dist/utils/index.d.ts.map +1 -0
  109. package/dist/utils/index.js +138 -0
  110. package/dist/utils/index.js.map +7 -0
  111. package/dist/utils/index.min.js +1 -0
  112. package/dist/utils/index.min.js.map +1 -0
  113. package/dist/utils/index.mjs +115 -0
  114. package/dist/utils/index.mjs.map +7 -0
  115. package/lib/flux-store/index.ts +312 -0
  116. package/lib/hooks/index.ts +39 -57
  117. package/lib/index.ts +135 -118
  118. package/lib/native-store/index.ts +106 -0
  119. package/lib/node/index.ts +3 -1
  120. package/lib/node/utils/icons.ts +4 -4
  121. package/lib/node/utils/inline.ts +2 -0
  122. package/lib/node/utils/node.sw.js +152 -0
  123. package/lib/node/utils/session-storage.ts +117 -0
  124. package/lib/node/utils/sw.ts +34 -10
  125. package/lib/node/utils/tree-adapter.ts +19 -1
  126. package/lib/pulse-store/index.ts +188 -0
  127. package/lib/request/index.ts +92 -122
  128. package/lib/router/index.ts +353 -164
  129. package/lib/signals/index.ts +98 -0
  130. package/lib/suspense/index.ts +57 -0
  131. package/lib/translate/index.ts +156 -0
  132. package/lib/utils/deep-freeze.ts +54 -0
  133. package/lib/utils/getter-setter.ts +40 -0
  134. package/lib/utils/has-changed.ts +43 -0
  135. package/lib/utils/index.ts +3 -0
  136. package/package.json +40 -57
  137. package/tsconfig.json +5 -4
  138. package/dist/dataset/index.d.ts +0 -24
  139. package/dist/dataset/index.d.ts.map +0 -1
  140. package/dist/dataset/index.js +0 -178
  141. package/dist/dataset/index.js.map +0 -7
  142. package/dist/dataset/index.mjs +0 -157
  143. package/dist/dataset/index.mjs.map +0 -7
  144. package/dist/node/node.sw.tpl +0 -133
  145. package/dist/node/utils/node.sw.tpl +0 -133
  146. package/dist/proxy-signal/index.d.ts +0 -23
  147. package/dist/proxy-signal/index.d.ts.map +0 -1
  148. package/dist/proxy-signal/index.js +0 -138
  149. package/dist/proxy-signal/index.js.map +0 -7
  150. package/dist/proxy-signal/index.mjs +0 -117
  151. package/dist/proxy-signal/index.mjs.map +0 -7
  152. package/dist/signal/index.d.ts +0 -9
  153. package/dist/signal/index.d.ts.map +0 -1
  154. package/dist/signal/index.js +0 -76
  155. package/dist/signal/index.js.map +0 -7
  156. package/dist/signal/index.mjs +0 -55
  157. package/dist/signal/index.mjs.map +0 -7
  158. package/dist/store/index.d.ts +0 -16
  159. package/dist/store/index.d.ts.map +0 -1
  160. package/dist/store/index.js +0 -93
  161. package/dist/store/index.js.map +0 -7
  162. package/dist/store/index.mjs +0 -72
  163. package/dist/store/index.mjs.map +0 -7
  164. package/lib/dataset/index.ts +0 -193
  165. package/lib/node/utils/node.sw.tpl +0 -133
  166. package/lib/proxy-signal/index.ts +0 -187
  167. package/lib/signal/index.ts +0 -86
  168. package/lib/store/index.ts +0 -101
@@ -0,0 +1,152 @@
1
+ // eslint-disable-next-line no-console
2
+ const Log = console.log;
3
+
4
+ const config = {
5
+ version: "v1.2::",
6
+ name: "Valyrian.js",
7
+ // Critical resources and offline page
8
+ urls: ["/"]
9
+ };
10
+
11
+ const cacheName = config.version + config.name;
12
+ const MAX_CACHE_SIZE = 50; // Max cache size
13
+
14
+ // Send messages to clients (controlled pages)
15
+ function sendMessageToClients(message) {
16
+ self.clients.matchAll().then((clients) => {
17
+ clients.forEach((client) => {
18
+ client.postMessage(message);
19
+ });
20
+ });
21
+ }
22
+
23
+ // Limit the cache size by deleting the oldest entries
24
+ function limitCacheSize(name, size) {
25
+ caches.open(name).then((cache) => {
26
+ cache.keys().then((keys) => {
27
+ if (keys.length > size) {
28
+ cache.delete(keys[0]).then(() => {
29
+ limitCacheSize(name, size);
30
+ });
31
+ }
32
+ });
33
+ });
34
+ }
35
+
36
+ async function fetchRequest(event) {
37
+ Log("WORKER: fetch event for " + event.request.url);
38
+ try {
39
+ // Clone the request to store it in the cache
40
+ const fetchRequest = event.request.clone();
41
+ const response = await fetch(fetchRequest);
42
+
43
+ // Verify if the response is valid
44
+ if (response && response.status === 200 && response.type === "basic") {
45
+ // Clone the response to store it in the cache
46
+ const responseToCache = response.clone();
47
+ const cache = await caches.open(cacheName);
48
+ cache.put(event.request, responseToCache);
49
+
50
+ // Limit the cache size
51
+ limitCacheSize(cacheName, MAX_CACHE_SIZE);
52
+
53
+ Log("WORKER: fetch response stored in cache.", event.request.url);
54
+ }
55
+ return response;
56
+ } catch (error) {
57
+ Log("WORKER: fetch request failed.", error);
58
+
59
+ // Try to serve the cached response if available
60
+ const cachedResponse = await caches.match(event.request);
61
+ if (cachedResponse) {
62
+ Log("WORKER: fetch request failed, responding with cache.");
63
+ return cachedResponse;
64
+ }
65
+
66
+ // Send the offline page if no cache is available
67
+ const cache = await caches.open(cacheName);
68
+ const offlineResponse = await cache.match("/offline.html");
69
+ if (offlineResponse) {
70
+ return offlineResponse;
71
+ }
72
+
73
+ // Generic offline page
74
+ return new Response("<h1>Offline</h1>", {
75
+ status: 503,
76
+ statusText: "Service Unavailable",
77
+ headers: new Headers({
78
+ "Content-Type": "text/html"
79
+ })
80
+ });
81
+ }
82
+ }
83
+
84
+ self.addEventListener("fetch", (event) => {
85
+ // Ignore requests with the "only-if-cached" cache mode
86
+ if (event.request.cache === "only-if-cached" && event.request.mode !== "same-origin") {
87
+ return;
88
+ }
89
+
90
+ Log("WORKER: fetch event in progress.", event.request.url);
91
+
92
+ // Ignore requests that are not GET
93
+ if (event.request.method !== "GET") {
94
+ return;
95
+ }
96
+
97
+ // If the request is for the API, use the network first
98
+ if (event.request.url.includes("/api/")) {
99
+ event.respondWith(fetchRequest(event));
100
+ } else {
101
+ // Use the cache first for other requests
102
+ event.respondWith(
103
+ caches.match(event.request).then((response) => {
104
+ if (response) {
105
+ Log("WORKER: returning from cache.", event.request.url);
106
+ return response;
107
+ }
108
+ return fetchRequest(event);
109
+ })
110
+ );
111
+ }
112
+ });
113
+
114
+ self.addEventListener("install", (event) => {
115
+ Log("WORKER: installing version", cacheName);
116
+ event.waitUntil(
117
+ caches
118
+ .open(cacheName)
119
+ .then((cache) => {
120
+ return cache.addAll(config.urls);
121
+ })
122
+ .then(() => {
123
+ Log("WORKER: install completed.");
124
+ })
125
+ );
126
+ });
127
+
128
+ self.addEventListener("activate", (event) => {
129
+ Log("WORKER: activating new version", cacheName);
130
+
131
+ event.waitUntil(
132
+ caches.keys().then((keys) =>
133
+ Promise.all(
134
+ keys
135
+ // Filter the caches that belong to this app version
136
+ .filter((key) => key !== cacheName)
137
+ .map((key) => caches.delete(key))
138
+ ).then(() => {
139
+ Log("WORKER: old caches cleared.");
140
+ // Notify clients about the new version
141
+ sendMessageToClients({ type: "NEW_VERSION" });
142
+ })
143
+ )
144
+ );
145
+ });
146
+
147
+ // Listen for messages from clients
148
+ self.addEventListener("message", (event) => {
149
+ if (event.data && event.data.type === "SKIP_WAITING") {
150
+ self.skipWaiting();
151
+ }
152
+ });
@@ -0,0 +1,117 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+
4
+ export class SessionStorage {
5
+ private storage: { [key: string]: string };
6
+ private limit: number;
7
+ private persist: boolean;
8
+ private filePath: string;
9
+ private directory: string = ".session-storage";
10
+
11
+ constructor({ persist = false, filePath = "./sessionData.json" }: { persist?: boolean; filePath?: string } = {}) {
12
+ this.storage = {};
13
+ this.limit = 5 * 1024 * 1024; // 5MB storage limit
14
+ this.persist = persist;
15
+ this.filePath = path.resolve(this.directory, filePath);
16
+
17
+ if (!fs.existsSync(this.directory)) {
18
+ fs.mkdirSync(this.directory, { recursive: true });
19
+ }
20
+
21
+ // Load data from file if persistence is enabled
22
+ if (this.persist) {
23
+ this.loadFromFile();
24
+ }
25
+ }
26
+
27
+ // Calculate total size in bytes of stored data
28
+ private getStorageSize(): number {
29
+ return new TextEncoder().encode(JSON.stringify(this.storage)).length;
30
+ }
31
+
32
+ // Check if storage limit is exceeded
33
+ private checkSizeLimit(): void {
34
+ const size = this.getStorageSize();
35
+ if (size > this.limit) {
36
+ throw new DOMException("Storage limit exceeded", "QuotaExceededError");
37
+ }
38
+ }
39
+
40
+ // Store value under the specified key
41
+ setItem(key: string | null | undefined, value: string | null | undefined): void {
42
+ if (key === null || key === undefined) {
43
+ throw new TypeError("Failed to execute 'setItem' on 'Storage': 1 argument required, but only 0 present.");
44
+ }
45
+
46
+ if (value === null) {
47
+ value = "null"; // Convert null to "null"
48
+ } else if (value === undefined) {
49
+ value = "undefined"; // Convert undefined to "undefined"
50
+ }
51
+
52
+ this.storage[key] = String(value); // Store as string
53
+ this.checkSizeLimit(); // Check storage limit
54
+ if (this.persist) {
55
+ this.saveToFile(); // Save to file if persistence is enabled
56
+ }
57
+ }
58
+
59
+ // Retrieve value stored under the specified key
60
+ getItem(key: string | null | undefined): string | null {
61
+ if (key === null || key === undefined) {
62
+ throw new TypeError("Failed to execute 'getItem' on 'Storage': 1 argument required, but only 0 present.");
63
+ }
64
+ return this.storage[key] || null; // Return null if key doesn't exist
65
+ }
66
+
67
+ // Remove the value under the specified key
68
+ removeItem(key: string | null | undefined): void {
69
+ if (key === null || key === undefined) {
70
+ throw new TypeError("Failed to execute 'removeItem' on 'Storage': 1 argument required, but only 0 present.");
71
+ }
72
+ delete this.storage[key];
73
+ if (this.persist) {
74
+ this.saveToFile(); // Save to file if persistence is enabled
75
+ }
76
+ }
77
+
78
+ // Clear all stored values
79
+ clear(): void {
80
+ this.storage = {};
81
+ if (this.persist) {
82
+ this.saveToFile(); // Save to file if persistence is enabled
83
+ }
84
+ }
85
+
86
+ // Return the number of stored items
87
+ get length(): number {
88
+ return Object.keys(this.storage).length;
89
+ }
90
+
91
+ // Return the key at the specified index
92
+ key(index: number): string | null {
93
+ const keys = Object.keys(this.storage);
94
+ return keys[index] || null;
95
+ }
96
+
97
+ // Save data to a file (only if persistence is enabled)
98
+ private saveToFile(): void {
99
+ try {
100
+ fs.writeFileSync(this.filePath, JSON.stringify(this.storage), "utf-8");
101
+ } catch (error) {
102
+ throw new Error(`Error saving data to file: ${(error as any).message}`);
103
+ }
104
+ }
105
+
106
+ // Load data from a file (only if persistence is enabled)
107
+ private loadFromFile(): void {
108
+ try {
109
+ if (fs.existsSync(this.filePath)) {
110
+ const data = fs.readFileSync(this.filePath, "utf-8");
111
+ this.storage = JSON.parse(data || "{}");
112
+ }
113
+ } catch (error) {
114
+ throw new Error(`Error loading data from file: ${(error as any).message}`);
115
+ }
116
+ }
117
+ }
@@ -2,17 +2,15 @@ import fs from "fs";
2
2
  import path from "path";
3
3
 
4
4
  export function sw(file: string, options = {}) {
5
- const swfiletemplate = path.resolve(__dirname, "./node.sw.tpl");
5
+ const swfiletemplate = path.resolve(__dirname, "./node.sw.js");
6
6
  const swTpl = fs.readFileSync(swfiletemplate, "utf8");
7
- const opt = Object.assign(
8
- {
9
- version: "v1::",
10
- name: "Valyrian.js",
11
- urls: ["/"],
12
- debug: false
13
- },
14
- options
15
- );
7
+ const opt = {
8
+ version: "v1::",
9
+ name: "Valyrian.js",
10
+ urls: ["/"],
11
+ debug: false,
12
+ ...options
13
+ };
16
14
  let contents = swTpl
17
15
  .replace("v1::", "v" + opt.version + "::")
18
16
  .replace("Valyrian.js", opt.name)
@@ -24,3 +22,29 @@ export function sw(file: string, options = {}) {
24
22
 
25
23
  fs.writeFileSync(file, contents, "utf8");
26
24
  }
25
+
26
+ /*
27
+ sw("sw.js", {
28
+ version: "1",
29
+ name: "Valyrian.js",
30
+ urls: ["/", "/index.html"],
31
+ debug: false
32
+ });
33
+
34
+
35
+ // On the client side
36
+ if ("serviceWorker" in navigator) {
37
+ navigator.serviceWorker.register("/service-worker.js");
38
+
39
+ navigator.serviceWorker.addEventListener("message", (event) => {
40
+ if (event.data && event.data.type === "NEW_VERSION") {
41
+ // Notify the user about the new version and ask if they want to update
42
+ if (confirm("Hay una nueva versión disponible. ¿Deseas actualizar?")) {
43
+ // Send a message to the service worker to skip the waiting
44
+ navigator.serviceWorker.controller.postMessage({ type: "SKIP_WAITING" });
45
+ window.location.reload();
46
+ }
47
+ }
48
+ });
49
+ }
50
+ */
@@ -53,6 +53,16 @@ export class Node implements Node {
53
53
  this.parent_node = node;
54
54
  }
55
55
 
56
+ #dataset: Record<string | number, any> = {};
57
+
58
+ get dataset() {
59
+ return this.#dataset;
60
+ }
61
+
62
+ set dataset(value) {
63
+ this.#dataset = value;
64
+ }
65
+
56
66
  constructor() {}
57
67
 
58
68
  appendChild<T extends Node>(node: T): T {
@@ -112,6 +122,11 @@ export class Node implements Node {
112
122
  node.setAttribute(this.attributes[i].nodeName, this.attributes[i].nodeValue);
113
123
  }
114
124
  }
125
+
126
+ for (const key in this.dataset) {
127
+ node.dataset[key] = this.dataset[key];
128
+ }
129
+
115
130
  if (deep) {
116
131
  for (let i = 0, l = this.childNodes.length; i < l; i++) {
117
132
  node.appendChild(this.childNodes[i].cloneNode(deep));
@@ -431,8 +446,11 @@ export class Document extends Element {
431
446
  super();
432
447
  this.nodeType = 9;
433
448
  this.nodeName = "#document";
449
+ this.body = this.createElement("body");
434
450
  }
435
451
 
452
+ body: Element;
453
+
436
454
  createDocumentFragment(): DocumentFragment {
437
455
  return new DocumentFragment();
438
456
  }
@@ -472,7 +490,7 @@ const selfClosingTags = [
472
490
  "!doctype"
473
491
  ];
474
492
 
475
- export function domToHtml(dom: Element): string {
493
+ export function domToHtml(dom: Element | Text | DocumentFragment): string {
476
494
  if (dom.nodeType === 3) {
477
495
  return dom.textContent;
478
496
  }
@@ -0,0 +1,188 @@
1
+ /* eslint-disable no-console */
2
+ import { updateVnode, Vnode, VnodeWithDom, current } from "valyrian.js";
3
+ import { deepCloneUnfreeze, deepFreeze, hasChanged } from "valyrian.js/utils";
4
+
5
+ type State = Record<string, any>;
6
+
7
+ // An action or pulse type definition
8
+ // eslint-disable-next-line no-unused-vars
9
+ export type Pulse<StateType> = (state: StateType, ...args: any[]) => void | Promise<void>;
10
+
11
+ // A collection of pulses
12
+ export type Pulses<StateType> = {
13
+ [key: string]: Pulse<StateType>;
14
+ };
15
+
16
+ // A proxy state
17
+ // This is a state that is proxied to automatically subscribe to changes
18
+ // And is used internally to update the vnode when the state changes
19
+ type ProxyState<StateType> = StateType & {
20
+ [key: string]: any;
21
+ };
22
+
23
+ // The effect stack
24
+ const effectStack: Function[] = [];
25
+
26
+ type StorePulses<PulsesType> = {
27
+ [K in keyof PulsesType]: PulsesType[K] extends (state: any, ...args: infer Args) => infer R
28
+ ? (...args: Args) => R
29
+ : never;
30
+ };
31
+
32
+ // Creates the store
33
+ // eslint-disable-next-line sonarjs/cognitive-complexity
34
+ function createStore<StateType extends State, PulsesType extends Pulses<StateType>>(
35
+ initialState: StateType | (() => StateType) | null,
36
+ pulses: PulsesType,
37
+ immutable = false
38
+ ): () => [ProxyState<StateType>, StorePulses<PulsesType>] {
39
+ const subscribers = new Set<Function>();
40
+ const vnodesToUpdate = new WeakSet<Vnode>();
41
+
42
+ // Initialize the localState for this store
43
+ const localState: StateType =
44
+ (typeof initialState === "function" ? initialState() : initialState) || ({} as StateType);
45
+
46
+ function isMutable() {
47
+ if (immutable) {
48
+ throw new Error("You need to call a pulse to modify the state");
49
+ }
50
+ }
51
+
52
+ // We create a proxy for the state
53
+ const proxyState = new Proxy(localState, {
54
+ get: (state, prop: string) => {
55
+ const currentEffect = effectStack[effectStack.length - 1];
56
+ if (currentEffect && !subscribers.has(currentEffect)) {
57
+ subscribers.add(currentEffect);
58
+ }
59
+
60
+ const currentVnode = current.vnode as VnodeWithDom;
61
+ if (currentVnode && !vnodesToUpdate.has(currentVnode)) {
62
+ const subscription = () => {
63
+ if (!currentVnode.dom) {
64
+ subscribers.delete(subscription);
65
+ vnodesToUpdate.delete(currentVnode);
66
+ return;
67
+ }
68
+
69
+ updateVnode(currentVnode);
70
+ };
71
+
72
+ subscribers.add(subscription);
73
+ vnodesToUpdate.add(currentVnode);
74
+ }
75
+
76
+ return state[prop];
77
+ },
78
+ // If the user tries to set directly it will throw an error
79
+ set: (state, prop: string, value: any) => {
80
+ isMutable();
81
+ Reflect.set(state, prop, value);
82
+ return true;
83
+ },
84
+ // If the user tries to delete directly it will throw an error
85
+ deleteProperty: (state, prop: string) => {
86
+ isMutable();
87
+ Reflect.deleteProperty(state, prop);
88
+ return true;
89
+ }
90
+ });
91
+
92
+ function syncState(newState: StateType) {
93
+ for (const key in newState) {
94
+ localState[key] = immutable ? deepFreeze(newState[key]) : newState[key];
95
+ }
96
+
97
+ for (const key in localState) {
98
+ if (!(key in newState)) {
99
+ Reflect.deleteProperty(localState, key);
100
+ }
101
+ }
102
+ }
103
+
104
+ function setState(newState: StateType) {
105
+ if (!hasChanged(localState, newState)) {
106
+ return;
107
+ }
108
+
109
+ syncState(newState);
110
+
111
+ subscribers.forEach((subscriber) => subscriber());
112
+ }
113
+
114
+ function getPulseMethod(key: string) {
115
+ return (...args: any[]) => {
116
+ const currentState = deepCloneUnfreeze(localState);
117
+ const pulse = pulses[key](currentState, ...args);
118
+
119
+ if (pulse instanceof Promise) {
120
+ return pulse
121
+ .then(() => setState(currentState))
122
+ .catch((error) => {
123
+ console.error("Error in pulse:", error);
124
+ });
125
+ }
126
+
127
+ setState(currentState);
128
+ };
129
+ }
130
+
131
+ const boundPulses: Record<string, Pulse<StateType>> = {};
132
+ for (const key in pulses) {
133
+ if (typeof pulses[key] !== "function") {
134
+ throw new Error(`Pulse '${key}' must be a function`);
135
+ }
136
+ boundPulses[key] = getPulseMethod(key);
137
+ }
138
+
139
+ const pulsesProxy = new Proxy(boundPulses, {
140
+ get: (pulses, prop: string) => {
141
+ if (!(prop in pulses)) {
142
+ throw new Error(`Pulse '${prop}' does not exist`);
143
+ }
144
+ return pulses[prop];
145
+ }
146
+ });
147
+
148
+ function usePulseStore(): [ProxyState<StateType>, StorePulses<PulsesType>] {
149
+ return [proxyState, pulsesProxy as StorePulses<PulsesType>];
150
+ }
151
+
152
+ syncState(localState);
153
+
154
+ return usePulseStore;
155
+ }
156
+
157
+ // Creates a pulse store with an immutable state by default
158
+ export function createPulseStore<StateType extends State, PulsesType extends Pulses<StateType>>(
159
+ initialState: StateType,
160
+ pulses: PulsesType
161
+ ): () => [ProxyState<StateType>, StorePulses<PulsesType>] {
162
+ return createStore(initialState, pulses, true);
163
+ }
164
+
165
+ // Creates a mutable store, useful for performance, but not recommended
166
+ export function createMutableStore<StateType extends State, PulsesType extends Pulses<StateType>>(
167
+ initialState: StateType,
168
+ pulses: PulsesType
169
+ ): () => [ProxyState<StateType>, StorePulses<PulsesType>] {
170
+ console.warn(
171
+ "Warning: You are working with a mutable state. This can lead to unpredictable behavior. All state changes made outside of a pulse will not trigger a re-render."
172
+ );
173
+ return createStore(initialState, pulses, false);
174
+ }
175
+
176
+ // Creates an effect
177
+ export function createEffect(effect: Function) {
178
+ const runEffect = () => {
179
+ try {
180
+ effectStack.push(runEffect);
181
+ effect();
182
+ } finally {
183
+ effectStack.pop();
184
+ }
185
+ };
186
+
187
+ runEffect();
188
+ }