@uniweb/core 0.6.0 → 0.6.2
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 +1 -1
- package/src/block.js +11 -24
- package/src/datastore.js +102 -101
- package/src/entity-store.js +181 -193
- package/src/fetcher-dispatcher.js +333 -0
- package/src/index.js +19 -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/package.json
CHANGED
package/src/block.js
CHANGED
|
@@ -197,17 +197,19 @@ export default class Block {
|
|
|
197
197
|
}
|
|
198
198
|
|
|
199
199
|
/**
|
|
200
|
-
* Parse content into
|
|
201
|
-
*
|
|
202
|
-
*
|
|
200
|
+
* Parse content into a flat semantic structure using @uniweb/semantic-parser.
|
|
201
|
+
*
|
|
202
|
+
* Supports multiple input shapes:
|
|
203
|
+
* 1. Pre-parsed groups structure (from the editor)
|
|
203
204
|
* 2. ProseMirror document (from markdown collection)
|
|
204
|
-
* 3.
|
|
205
|
+
* 3. Wrapped ProseMirror document (content-API format)
|
|
206
|
+
* 4. Plain object (passed through directly)
|
|
205
207
|
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
208
|
+
* Pure and idempotent — safe to call more than once on the same block.
|
|
209
|
+
* The constructor calls it once to populate `this.parsedContent`; the
|
|
210
|
+
* runtime's `prepareProps` may call it again after a foundation content
|
|
211
|
+
* handler transforms `rawContent`, to produce fresh semantic content
|
|
212
|
+
* from the instantiated tree.
|
|
211
213
|
*/
|
|
212
214
|
parseContent(content) {
|
|
213
215
|
// If content is already parsed with groups structure
|
|
@@ -215,21 +217,6 @@ export default class Block {
|
|
|
215
217
|
return content.groups
|
|
216
218
|
}
|
|
217
219
|
|
|
218
|
-
// Foundation content handler hook
|
|
219
|
-
// Runs BEFORE semantic parsing. Foundations can declare a handler
|
|
220
|
-
// in foundation.js `handlers.content` to transform raw ProseMirror
|
|
221
|
-
// content — typically for template engine instantiation of
|
|
222
|
-
// {placeholder} expressions against profile/report data.
|
|
223
|
-
const contentHandler = globalThis.uniweb?.foundationConfig?.handlers?.content
|
|
224
|
-
if (contentHandler && typeof contentHandler === 'function') {
|
|
225
|
-
try {
|
|
226
|
-
const transformed = contentHandler(content, this)
|
|
227
|
-
if (transformed !== undefined) content = transformed
|
|
228
|
-
} catch (err) {
|
|
229
|
-
console.error('Foundation content handler failed:', err)
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
220
|
// ProseMirror document - use semantic-parser
|
|
234
221
|
if (content?.type === 'doc') {
|
|
235
222
|
return this.extractFromProseMirror(content)
|
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
|
}
|