@uniweb/core 0.6.1 → 0.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.
- package/package.json +3 -3
- package/src/datastore.js +102 -101
- package/src/entity-store.js +214 -194
- package/src/fetcher-dispatcher.js +333 -0
- package/src/index.js +20 -5
- package/src/observable-state.js +103 -0
- package/src/page.js +18 -0
- package/src/substitute-placeholders.js +63 -0
- package/src/uniweb.js +70 -79
- package/src/website.js +155 -46
- package/src/where.js +223 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Core classes for the Uniweb platform - Uniweb, Website, Page, Block",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -30,8 +30,8 @@
|
|
|
30
30
|
"jest": "^29.7.0"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@uniweb/
|
|
34
|
-
"@uniweb/
|
|
33
|
+
"@uniweb/theming": "0.1.3",
|
|
34
|
+
"@uniweb/semantic-parser": "1.1.9"
|
|
35
35
|
},
|
|
36
36
|
"scripts": {
|
|
37
37
|
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
|
package/src/datastore.js
CHANGED
|
@@ -1,152 +1,153 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* DataStore
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* Deduplicates in-flight fetches so concurrent callers share a single request.
|
|
4
|
+
* Pure keyed cache with in-flight deduplication. Persists across SPA navigation.
|
|
6
5
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* Owned by the Website; accessed only by the FetcherDispatcher (which computes
|
|
7
|
+
* cache keys and runs fetchers) and by build-time / startup preload paths
|
|
8
|
+
* (which write entries keyed by the default cache key so runtime cache probes
|
|
9
|
+
* find them).
|
|
10
|
+
*
|
|
11
|
+
* No knowledge of fetchers, transports, or cascades. Keys are opaque strings.
|
|
9
12
|
*/
|
|
10
13
|
|
|
11
14
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
15
|
+
* Default cache-key derivation for a request or fetch config.
|
|
16
|
+
*
|
|
17
|
+
* The framework's default URL fetcher and the build-time preload path both
|
|
18
|
+
* use this key shape. Fetchers with state-dependent requests (e.g., a query
|
|
19
|
+
* slug read from `page.state`) must declare their own `cacheKey(request)`
|
|
20
|
+
* on the fetcher so reactive changes miss the cache and re-fetch.
|
|
21
|
+
*
|
|
22
|
+
* Fields that contribute to the key:
|
|
23
|
+
* - path, url — what resource is being fetched
|
|
24
|
+
* - schema — which entity type the response will be stored under
|
|
25
|
+
* - transform — any per-fetch response unwrap; different transforms
|
|
26
|
+
* of the same endpoint produce different cached data
|
|
27
|
+
* - method (POST) — POST requests may share a URL with GET; don't collide
|
|
28
|
+
* - body (POST) — two POSTs to the same URL with different bodies are
|
|
29
|
+
* different queries; must cache distinctly
|
|
14
30
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
31
|
+
* Post-processing fields like `limit`, `sort`, `filter` are applied after
|
|
32
|
+
* fetch and must not split the cache.
|
|
33
|
+
*
|
|
34
|
+
* @param {Object} request - Normalized request (or fetch config)
|
|
35
|
+
* @returns {string} A stable JSON string usable as a cache-Map key
|
|
17
36
|
*/
|
|
18
|
-
function
|
|
19
|
-
const { path, url, schema, transform } =
|
|
20
|
-
|
|
37
|
+
export function deriveCacheKey(request) {
|
|
38
|
+
const { path, url, schema, transform } = request || {}
|
|
39
|
+
const method = request?.method && request.method.toUpperCase() !== 'GET'
|
|
40
|
+
? request.method.toUpperCase()
|
|
41
|
+
: undefined
|
|
42
|
+
const body = method === 'POST' ? request?.body : undefined
|
|
43
|
+
return JSON.stringify({ path, url, schema, transform, method, body })
|
|
21
44
|
}
|
|
22
45
|
|
|
23
46
|
export default class DataStore {
|
|
24
47
|
constructor() {
|
|
48
|
+
// key → { data, meta? }
|
|
25
49
|
this._cache = new Map()
|
|
50
|
+
// key → { promise, signals: Set<AbortSignal> }
|
|
26
51
|
this._inflight = new Map()
|
|
27
|
-
|
|
28
|
-
this._transforms = new Map()
|
|
52
|
+
// Notified on every successful `set()`.
|
|
29
53
|
this._listeners = new Set()
|
|
54
|
+
// Key-scoped listeners: key → Set<Function>
|
|
55
|
+
this._keyedListeners = new Map()
|
|
30
56
|
|
|
31
57
|
Object.seal(this)
|
|
32
58
|
}
|
|
33
59
|
|
|
34
60
|
/**
|
|
35
|
-
* Subscribe to
|
|
36
|
-
*
|
|
37
|
-
*
|
|
61
|
+
* Subscribe to cache updates.
|
|
62
|
+
*
|
|
63
|
+
* Two forms:
|
|
64
|
+
* - `subscribe(fn)` — fires after every successful `set()` (all keys).
|
|
65
|
+
* - `subscribe(key, fn)` — fires only when `set(key, ...)` is called.
|
|
66
|
+
*
|
|
67
|
+
* The global form is useful for debugging / blanket observers. The keyed
|
|
68
|
+
* form is what Layer-3 kit hooks (`useFetched`, `useCacheEntry`) use so
|
|
69
|
+
* a cache write for one request doesn't wake up every subscriber.
|
|
70
|
+
*
|
|
71
|
+
* @param {string|Function} keyOrFn
|
|
72
|
+
* @param {Function} [maybeFn]
|
|
38
73
|
* @returns {Function} unsubscribe
|
|
39
74
|
*/
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
75
|
+
subscribe(keyOrFn, maybeFn) {
|
|
76
|
+
if (typeof keyOrFn === 'string' && typeof maybeFn === 'function') {
|
|
77
|
+
const key = keyOrFn
|
|
78
|
+
let set = this._keyedListeners.get(key)
|
|
79
|
+
if (!set) {
|
|
80
|
+
set = new Set()
|
|
81
|
+
this._keyedListeners.set(key, set)
|
|
82
|
+
}
|
|
83
|
+
set.add(maybeFn)
|
|
84
|
+
return () => {
|
|
85
|
+
const s = this._keyedListeners.get(key)
|
|
86
|
+
if (!s) return
|
|
87
|
+
s.delete(maybeFn)
|
|
88
|
+
if (s.size === 0) this._keyedListeners.delete(key)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (typeof keyOrFn === 'function') {
|
|
92
|
+
this._listeners.add(keyOrFn)
|
|
93
|
+
return () => this._listeners.delete(keyOrFn)
|
|
94
|
+
}
|
|
95
|
+
throw new TypeError('DataStore.subscribe: expected (fn) or (key, fn)')
|
|
61
96
|
}
|
|
62
97
|
|
|
63
98
|
/**
|
|
64
|
-
*
|
|
65
|
-
*
|
|
99
|
+
* Cache presence check.
|
|
100
|
+
*
|
|
101
|
+
* @param {string} key
|
|
66
102
|
* @returns {boolean}
|
|
67
103
|
*/
|
|
68
|
-
has(
|
|
69
|
-
return this._cache.has(
|
|
104
|
+
has(key) {
|
|
105
|
+
return this._cache.has(key)
|
|
70
106
|
}
|
|
71
107
|
|
|
72
108
|
/**
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
* @
|
|
109
|
+
* Cache lookup.
|
|
110
|
+
*
|
|
111
|
+
* @param {string} key
|
|
112
|
+
* @returns {{ data: any, meta?: Object } | null}
|
|
76
113
|
*/
|
|
77
|
-
get(
|
|
78
|
-
const key = cacheKey(config)
|
|
114
|
+
get(key) {
|
|
79
115
|
return this._cache.has(key) ? this._cache.get(key) : null
|
|
80
116
|
}
|
|
81
117
|
|
|
82
118
|
/**
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
119
|
+
* Cache store. Fires listeners: first the global ones (all-writes), then
|
|
120
|
+
* any subscribers registered for this specific key.
|
|
121
|
+
*
|
|
122
|
+
* @param {string} key
|
|
123
|
+
* @param {{ data: any, meta?: Object }} entry
|
|
86
124
|
*/
|
|
87
|
-
set(
|
|
88
|
-
this._cache.set(
|
|
89
|
-
this._listeners
|
|
125
|
+
set(key, entry) {
|
|
126
|
+
this._cache.set(key, entry)
|
|
127
|
+
for (const fn of this._listeners) fn()
|
|
128
|
+
const keyed = this._keyedListeners.get(key)
|
|
129
|
+
if (keyed) {
|
|
130
|
+
for (const fn of keyed) fn()
|
|
131
|
+
}
|
|
90
132
|
}
|
|
91
133
|
|
|
92
134
|
/**
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
* - In-flight: returns existing promise (no duplicate request).
|
|
97
|
-
* - Miss: calls the registered fetcher, caches the result.
|
|
135
|
+
* In-flight fetch registry — used by the dispatcher to dedup concurrent
|
|
136
|
+
* requests and collect abort signals so the underlying fetch is cancelled
|
|
137
|
+
* only when every attached block aborts.
|
|
98
138
|
*
|
|
99
|
-
* @
|
|
100
|
-
* @returns {Promise<{ data: any, error?: string }>}
|
|
139
|
+
* @returns {Map<string, { promise: Promise, signals: Set<AbortSignal> }>}
|
|
101
140
|
*/
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
throw new Error('DataStore: no fetcher registered. Call registerFetcher() first.')
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const key = cacheKey(config)
|
|
108
|
-
|
|
109
|
-
// Cache hit
|
|
110
|
-
if (this._cache.has(key)) {
|
|
111
|
-
return { data: this._cache.get(key) }
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// In-flight dedup
|
|
115
|
-
if (this._inflight.has(key)) {
|
|
116
|
-
return this._inflight.get(key)
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Miss — execute fetch
|
|
120
|
-
const promise = this._fetcher(config).then((result) => {
|
|
121
|
-
this._inflight.delete(key)
|
|
122
|
-
let data = result.data
|
|
123
|
-
// Apply named transform if registered (dot-path transforms
|
|
124
|
-
// are handled by the fetcher itself via getNestedValue)
|
|
125
|
-
if (
|
|
126
|
-
data !== undefined &&
|
|
127
|
-
data !== null &&
|
|
128
|
-
config.transform &&
|
|
129
|
-
this._transforms.has(config.transform)
|
|
130
|
-
) {
|
|
131
|
-
data = this._transforms.get(config.transform)(data, config)
|
|
132
|
-
}
|
|
133
|
-
if (data !== undefined && data !== null) {
|
|
134
|
-
this._cache.set(key, data)
|
|
135
|
-
this._listeners.forEach((fn) => fn())
|
|
136
|
-
}
|
|
137
|
-
return { ...result, data }
|
|
138
|
-
})
|
|
139
|
-
|
|
140
|
-
this._inflight.set(key, promise)
|
|
141
|
-
return promise
|
|
141
|
+
get inflight() {
|
|
142
|
+
return this._inflight
|
|
142
143
|
}
|
|
143
144
|
|
|
144
145
|
/**
|
|
145
|
-
* Flush cache and in-flight map.
|
|
146
|
+
* Flush cache and in-flight map. Listeners are preserved so subscribers
|
|
147
|
+
* that outlive the cache (kit hooks waiting on a key) aren't orphaned.
|
|
146
148
|
*/
|
|
147
149
|
clear() {
|
|
148
150
|
this._cache.clear()
|
|
149
151
|
this._inflight.clear()
|
|
150
|
-
this._transforms.clear()
|
|
151
152
|
}
|
|
152
153
|
}
|