@praxisjs/core 1.6.0 → 1.7.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 (35) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/__tests__/computed.test.js +23 -0
  3. package/dist/__tests__/computed.test.js.map +1 -1
  4. package/dist/__tests__/resource-focus.test.d.ts +2 -0
  5. package/dist/__tests__/resource-focus.test.d.ts.map +1 -0
  6. package/dist/__tests__/resource-focus.test.js +50 -0
  7. package/dist/__tests__/resource-focus.test.js.map +1 -0
  8. package/dist/__tests__/resource.test.js +137 -1
  9. package/dist/__tests__/resource.test.js.map +1 -1
  10. package/dist/__tests__/signal.test.js +50 -1
  11. package/dist/__tests__/signal.test.js.map +1 -1
  12. package/dist/async/resource-cache.d.ts +21 -0
  13. package/dist/async/resource-cache.d.ts.map +1 -0
  14. package/dist/async/resource-cache.js +48 -0
  15. package/dist/async/resource-cache.js.map +1 -0
  16. package/dist/async/resource.d.ts +4 -0
  17. package/dist/async/resource.d.ts.map +1 -1
  18. package/dist/async/resource.js +74 -9
  19. package/dist/async/resource.js.map +1 -1
  20. package/dist/internal.d.ts +1 -0
  21. package/dist/internal.d.ts.map +1 -1
  22. package/dist/internal.js +1 -0
  23. package/dist/internal.js.map +1 -1
  24. package/dist/reactive/reactive.d.ts.map +1 -1
  25. package/dist/reactive/reactive.js +0 -2
  26. package/dist/reactive/reactive.js.map +1 -1
  27. package/package.json +1 -1
  28. package/src/__tests__/computed.test.ts +29 -0
  29. package/src/__tests__/resource-focus.test.ts +61 -0
  30. package/src/__tests__/resource.test.ts +178 -1
  31. package/src/__tests__/signal.test.ts +55 -1
  32. package/src/async/resource-cache.ts +62 -0
  33. package/src/async/resource.ts +92 -10
  34. package/src/internal.ts +1 -0
  35. package/src/reactive/reactive.ts +0 -1
@@ -1,7 +1,16 @@
1
1
  import type { Computed, Signal } from "@praxisjs/shared";
2
2
 
3
3
  import { computed, signal } from "../signal";
4
- import { effect } from "../signal/effect";
4
+ import {
5
+ getCacheEntry,
6
+ setCacheEntry,
7
+ deleteInflight,
8
+ getInflight,
9
+ setInflight,
10
+ registerResource,
11
+ unregisterResource,
12
+ } from "./resource-cache";
13
+ import { effect, untrack } from "../signal/effect";
5
14
 
6
15
  export type ResourceStatus = "idle" | "pending" | "success" | "error";
7
16
 
@@ -13,12 +22,16 @@ export interface Resource<T> {
13
22
  refetch(): void;
14
23
  cancel(): void;
15
24
  mutate(data: T): void;
25
+ destroy(): void;
16
26
  }
17
27
 
18
28
  export interface ResourceOptions<T> {
19
29
  initialData?: T;
20
30
  immediate?: boolean;
21
31
  keepPreviousData?: boolean;
32
+ key?: string;
33
+ staleTime?: number;
34
+ refetchOnFocus?: boolean;
22
35
  }
23
36
 
24
37
  export function resource<T>(
@@ -29,18 +42,27 @@ export function resource<T>(
29
42
  initialData = null,
30
43
  immediate = true,
31
44
  keepPreviousData = false,
45
+ key,
46
+ staleTime = 0,
47
+ refetchOnFocus = false,
32
48
  } = options;
33
49
 
34
- const _data = signal(initialData);
50
+ const cached = key ? getCacheEntry(key) : undefined;
51
+ const isCacheFresh =
52
+ cached !== undefined && Date.now() - cached.timestamp < staleTime;
53
+
54
+ const _data = signal(cached ? (cached.data as T) : initialData);
35
55
  const _error = signal<Error | null>(null);
36
- const _status = signal<ResourceStatus>("idle");
56
+ const _status = signal<ResourceStatus>(cached ? "success" : "idle");
37
57
 
38
58
  let _runId = 0;
39
59
 
40
- function _execute(fn: Promise<T>) {
60
+ function _execute(fn: Promise<T>): void {
41
61
  const currentRunId = ++_runId;
42
62
 
43
- if (!keepPreviousData) {
63
+ // untrack: _data.set(result) in the resolve handler must not re-trigger the enclosing effect.
64
+ const hasCachedData = key !== undefined && untrack(() => _data()) !== null;
65
+ if (!keepPreviousData && !hasCachedData) {
44
66
  _data.set(null);
45
67
  }
46
68
  _error.set(null);
@@ -51,16 +73,36 @@ export function resource<T>(
51
73
  _data.set(result);
52
74
  _error.set(null);
53
75
  _status.set("success");
76
+ if (key) {
77
+ setCacheEntry(key, result);
78
+ deleteInflight(key);
79
+ }
54
80
  }).catch((err: unknown) => {
55
81
  if (currentRunId !== _runId) return;
56
82
  _error.set(err instanceof Error ? err : new Error(String(err)));
57
83
  _status.set("error");
84
+ if (key) deleteInflight(key);
58
85
  });
59
86
  }
60
87
 
61
- function execute() {
62
- try {
88
+ function _startFetch(): void {
89
+ if (key) {
90
+ const existing = getInflight(key);
91
+ if (existing) {
92
+ _execute(existing as Promise<T>);
93
+ return;
94
+ }
95
+ const promise = fetcher();
96
+ setInflight(key, promise as Promise<unknown>);
97
+ _execute(promise);
98
+ } else {
63
99
  _execute(fetcher());
100
+ }
101
+ }
102
+
103
+ function execute(): void {
104
+ try {
105
+ _startFetch();
64
106
  } catch (err: unknown) {
65
107
  _runId++;
66
108
  _error.set(err instanceof Error ? err : new Error(String(err)));
@@ -68,17 +110,56 @@ export function resource<T>(
68
110
  }
69
111
  }
70
112
 
71
- if (immediate) {
72
- effect(() => {
113
+ if (key) {
114
+ registerResource(key, execute);
115
+ }
116
+
117
+ let stopEffect: (() => void) | undefined;
118
+
119
+ if (immediate && !isCacheFresh) {
120
+ stopEffect = effect(() => {
73
121
  try {
74
- _execute(fetcher());
122
+ if (key) {
123
+ const existing = getInflight(key);
124
+ if (existing) {
125
+ _execute(existing as Promise<T>);
126
+ } else {
127
+ const promise = fetcher();
128
+ setInflight(key, promise as Promise<unknown>);
129
+ _execute(promise);
130
+ }
131
+ } else {
132
+ _execute(fetcher());
133
+ }
75
134
  } catch (err: unknown) {
135
+ _runId++;
76
136
  _error.set(err instanceof Error ? err : new Error(String(err)));
77
137
  _status.set("error");
78
138
  }
79
139
  });
80
140
  }
81
141
 
142
+ let _removeFocusListener: (() => void) | undefined;
143
+ if (refetchOnFocus && typeof document !== "undefined") {
144
+ const onVisibility = (): void => {
145
+ if (document.visibilityState === "visible") {
146
+ execute();
147
+ }
148
+ };
149
+ document.addEventListener("visibilitychange", onVisibility);
150
+ _removeFocusListener = (): void => {
151
+ document.removeEventListener("visibilitychange", onVisibility);
152
+ };
153
+ }
154
+
155
+ function destroy(): void {
156
+ stopEffect?.();
157
+ stopEffect = undefined;
158
+ _removeFocusListener?.();
159
+ _removeFocusListener = undefined;
160
+ if (key) unregisterResource(key, execute);
161
+ }
162
+
82
163
  return {
83
164
  data: computed(() => _data()),
84
165
  pending: computed(() => _status() === "pending"),
@@ -97,6 +178,7 @@ export function resource<T>(
97
178
  _error.set(null);
98
179
  _status.set("success");
99
180
  },
181
+ destroy,
100
182
  };
101
183
  }
102
184
 
package/src/internal.ts CHANGED
@@ -25,3 +25,4 @@ export {
25
25
  type Resource,
26
26
  type ResourceOptions,
27
27
  } from "./async/resource";
28
+ export { invalidateResource, _clearCache } from "./async/resource-cache";
@@ -45,7 +45,6 @@ export function debounced<T>(source: Signal<T> | Computed<T>, ms: number) {
45
45
 
46
46
  const stop = effect(() => {
47
47
  const value = source();
48
- if (timeout) clearTimeout(timeout);
49
48
  timeout = setTimeout(() => {
50
49
  current.set(value);
51
50
  timeout = undefined;