@vobs/resource 0.1.0 → 0.3.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.js +65 -24
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -5,12 +5,30 @@
|
|
|
5
5
|
import { signal } from '@vobs/reactivity';
|
|
6
6
|
import { currentOwner, createRuntimeError } from '@vobs/runtime-core';
|
|
7
7
|
const defaultCacheStore = new Map();
|
|
8
|
-
|
|
9
|
-
// (an SSR per-request cache gets per-request deduplication without cross-contamination; the default cache shares one table, matching historical behavior).
|
|
8
|
+
const DEFAULT_CACHE_LIMIT = 100;
|
|
10
9
|
const cacheInFlightTables = new WeakMap();
|
|
11
10
|
let defaultKeyCounter = 0;
|
|
12
11
|
/** Default key pattern (generated by a process-level counter, unstable across processes — SSR serialization must use explicit keys). */
|
|
13
12
|
const DEFAULT_KEY_PATTERN = /^resource:\d+$/u;
|
|
13
|
+
function readCache(cache, key) {
|
|
14
|
+
const entry = cache.get(key);
|
|
15
|
+
if (cache === defaultCacheStore && entry !== undefined && !isPromiseLike(entry)) {
|
|
16
|
+
defaultCacheStore.delete(key);
|
|
17
|
+
defaultCacheStore.set(key, entry);
|
|
18
|
+
}
|
|
19
|
+
return entry;
|
|
20
|
+
}
|
|
21
|
+
function writeCache(cache, key, entry) {
|
|
22
|
+
cache.set(key, entry);
|
|
23
|
+
if (cache !== defaultCacheStore)
|
|
24
|
+
return;
|
|
25
|
+
while (defaultCacheStore.size > DEFAULT_CACHE_LIMIT) {
|
|
26
|
+
const oldest = defaultCacheStore.keys().next().value;
|
|
27
|
+
if (oldest === undefined)
|
|
28
|
+
return;
|
|
29
|
+
defaultCacheStore.delete(oldest);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
14
32
|
function inFlightTable(cache) {
|
|
15
33
|
let table = cacheInFlightTables.get(cache);
|
|
16
34
|
if (table === undefined) {
|
|
@@ -38,7 +56,7 @@ export function createResource(source, options = {}) {
|
|
|
38
56
|
const data = signal(undefined);
|
|
39
57
|
const error = signal(undefined);
|
|
40
58
|
const loading = signal(false);
|
|
41
|
-
let
|
|
59
|
+
let activeRequest;
|
|
42
60
|
let fetchToken = 0;
|
|
43
61
|
let disposed = false;
|
|
44
62
|
let readyResolved = false;
|
|
@@ -55,7 +73,7 @@ export function createResource(source, options = {}) {
|
|
|
55
73
|
const applySuccess = (value, token) => {
|
|
56
74
|
if (disposed || token !== fetchToken)
|
|
57
75
|
return;
|
|
58
|
-
cache
|
|
76
|
+
writeCache(cache, key, { data: value });
|
|
59
77
|
data.value = value;
|
|
60
78
|
error.value = undefined;
|
|
61
79
|
loading.value = false;
|
|
@@ -75,11 +93,10 @@ export function createResource(source, options = {}) {
|
|
|
75
93
|
markReady();
|
|
76
94
|
};
|
|
77
95
|
const run = async (token) => {
|
|
78
|
-
|
|
79
|
-
controller = new AbortController();
|
|
96
|
+
releaseActiveRequest();
|
|
80
97
|
loading.value = true;
|
|
81
98
|
// Sync hit (hydrated/pre-warmed cache): data is readable on the first frame of template rendering (contract §8 sync readiness).
|
|
82
|
-
const maybeCached = cache
|
|
99
|
+
const maybeCached = readCache(cache, key);
|
|
83
100
|
if (maybeCached !== undefined && !isPromiseLike(maybeCached)) {
|
|
84
101
|
if (disposed || token !== fetchToken)
|
|
85
102
|
return;
|
|
@@ -91,7 +108,14 @@ export function createResource(source, options = {}) {
|
|
|
91
108
|
}
|
|
92
109
|
// Miss: a single await yields uniformly (a Promise awaits resolve / a sync value is wrapped in a resolved promise) —
|
|
93
110
|
// if dispose or a new refetch runs during the yield, this run terminates (cancelled runs never call source, contract semantics).
|
|
94
|
-
|
|
111
|
+
let cached;
|
|
112
|
+
try {
|
|
113
|
+
cached = await Promise.resolve(maybeCached);
|
|
114
|
+
}
|
|
115
|
+
catch (failure) {
|
|
116
|
+
applyFailure(failure, token);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
95
119
|
if (disposed || token !== fetchToken)
|
|
96
120
|
return;
|
|
97
121
|
if (cached !== undefined) {
|
|
@@ -101,20 +125,24 @@ export function createResource(source, options = {}) {
|
|
|
101
125
|
markReady();
|
|
102
126
|
return;
|
|
103
127
|
}
|
|
104
|
-
let
|
|
105
|
-
if (
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
inFlight.set(key,
|
|
110
|
-
promise.then(() => {
|
|
111
|
-
inFlight.
|
|
128
|
+
let request = inFlight.get(key);
|
|
129
|
+
if (request === undefined) {
|
|
130
|
+
const controller = new AbortController();
|
|
131
|
+
const promise = Promise.resolve().then(() => source({ signal: controller.signal, key }));
|
|
132
|
+
request = { controller, promise, consumers: 0 };
|
|
133
|
+
inFlight.set(key, request);
|
|
134
|
+
void promise.then(() => {
|
|
135
|
+
if (inFlight.get(key) === request)
|
|
136
|
+
inFlight.delete(key);
|
|
112
137
|
}, () => {
|
|
113
|
-
inFlight.
|
|
138
|
+
if (inFlight.get(key) === request)
|
|
139
|
+
inFlight.delete(key);
|
|
114
140
|
});
|
|
115
141
|
}
|
|
142
|
+
request.consumers += 1;
|
|
143
|
+
activeRequest = request;
|
|
116
144
|
try {
|
|
117
|
-
const value = await promise;
|
|
145
|
+
const value = (await request.promise);
|
|
118
146
|
if (disposed || token !== fetchToken)
|
|
119
147
|
return;
|
|
120
148
|
applySuccess(value, token);
|
|
@@ -124,6 +152,23 @@ export function createResource(source, options = {}) {
|
|
|
124
152
|
return;
|
|
125
153
|
applyFailure(failure, token);
|
|
126
154
|
}
|
|
155
|
+
finally {
|
|
156
|
+
if (activeRequest === request)
|
|
157
|
+
releaseActiveRequest();
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
const releaseActiveRequest = () => {
|
|
161
|
+
const request = activeRequest;
|
|
162
|
+
if (request === undefined)
|
|
163
|
+
return;
|
|
164
|
+
activeRequest = undefined;
|
|
165
|
+
request.consumers -= 1;
|
|
166
|
+
if (request.consumers > 0)
|
|
167
|
+
return;
|
|
168
|
+
if (inFlight.get(key) === request) {
|
|
169
|
+
inFlight.delete(key);
|
|
170
|
+
request.controller.abort();
|
|
171
|
+
}
|
|
127
172
|
};
|
|
128
173
|
const refetch = () => {
|
|
129
174
|
if (disposed) {
|
|
@@ -150,7 +195,7 @@ export function createResource(source, options = {}) {
|
|
|
150
195
|
if (disposed)
|
|
151
196
|
return;
|
|
152
197
|
data.value = next;
|
|
153
|
-
cache
|
|
198
|
+
writeCache(cache, key, { data: next });
|
|
154
199
|
error.value = undefined;
|
|
155
200
|
}
|
|
156
201
|
catch (failure) {
|
|
@@ -165,13 +210,9 @@ export function createResource(source, options = {}) {
|
|
|
165
210
|
if (disposed)
|
|
166
211
|
return;
|
|
167
212
|
disposed = true;
|
|
168
|
-
|
|
169
|
-
controller = undefined;
|
|
213
|
+
releaseActiveRequest();
|
|
170
214
|
fetchToken += 1;
|
|
171
215
|
loading.value = false;
|
|
172
|
-
// Clean the in-flight entry on cancellation: a pending promise that was aborted before source responded
|
|
173
|
-
// must not poison later requests for the same key (a new request would hang on a dead promise).
|
|
174
|
-
inFlight.delete(key);
|
|
175
216
|
// Disposal counts as settle (SSR await on whenReady does not hang).
|
|
176
217
|
markReady();
|
|
177
218
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vobs/resource",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Declarative async data resources for vobs: fetch, cancel, cache, invalidate, refetch and mutation.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
},
|
|
19
19
|
"homepage": "https://github.com/vobsjs/vobs#readme",
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@vobs/reactivity": "0.
|
|
22
|
-
"@vobs/runtime-core": "0.
|
|
21
|
+
"@vobs/reactivity": "0.3.0",
|
|
22
|
+
"@vobs/runtime-core": "0.3.0"
|
|
23
23
|
},
|
|
24
24
|
"files": [
|
|
25
25
|
"dist"
|
|
@@ -36,6 +36,6 @@
|
|
|
36
36
|
"main": "./dist/index.js",
|
|
37
37
|
"sideEffects": false,
|
|
38
38
|
"engines": {
|
|
39
|
-
"node": ">=
|
|
39
|
+
"node": ">=22.12.0"
|
|
40
40
|
}
|
|
41
41
|
}
|