@uniweb/core 0.23.0 → 0.24.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 +1 -1
- package/src/datastore.js +30 -21
- package/src/detail-url.js +2 -2
- package/src/entity-store.js +4 -4
- package/src/fetch-config.js +2 -2
package/package.json
CHANGED
package/src/datastore.js
CHANGED
|
@@ -60,8 +60,8 @@ export function deriveCacheKey(request) {
|
|
|
60
60
|
// the rename inside the one function whose job is to be canonical.
|
|
61
61
|
return JSON.stringify({ path, url, as, transform, method, body, locale })
|
|
62
62
|
}
|
|
63
|
-
const { query, schema, scope, where, sort, limit,
|
|
64
|
-
return JSON.stringify({ query, schema, scope, where, sort, limit,
|
|
63
|
+
const { query, schema, scope, where, sort, limit, whole } = request || {}
|
|
64
|
+
return JSON.stringify({ query, schema, scope, where, sort, limit, whole, as, transform, locale })
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
/**
|
|
@@ -77,12 +77,21 @@ export function recordIdentity(record) {
|
|
|
77
77
|
return typeof id === 'string' && id.length > 0 ? id : null
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
/**
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
80
|
+
/**
|
|
81
|
+
* Did this entry deliver WHOLE records, or briefs?
|
|
82
|
+
*
|
|
83
|
+
* ⛔ **This was `DEPTH_RANK = { brief: 1, full: 2 }` and a `'brief'|'full'` string
|
|
84
|
+
* until 2026-09-06.** A rank over exactly two values is a boolean with ceremony,
|
|
85
|
+
* and the only consumer outside this file already tested `=== 'full'`. The word
|
|
86
|
+
* went with it: the brief is a *Section*, so nothing here is nested and `depth`
|
|
87
|
+
* implied a continuum that never existed. The wire says `whole` now, and so does
|
|
88
|
+
* this — one vocabulary from the door to the record index.
|
|
89
|
+
*
|
|
90
|
+
* `null` means the entry says nothing about it and is not indexable by identity.
|
|
91
|
+
*/
|
|
92
|
+
function indexableWhole(entry) {
|
|
93
|
+
const whole = entry?.meta?.whole
|
|
94
|
+
return typeof whole === 'boolean' ? whole : null
|
|
86
95
|
}
|
|
87
96
|
|
|
88
97
|
/**
|
|
@@ -116,7 +125,7 @@ export default class DataStore {
|
|
|
116
125
|
this._listeners = new Set()
|
|
117
126
|
// Key-scoped listeners: key → Set<Function>
|
|
118
127
|
this._keyedListeners = new Map()
|
|
119
|
-
// $uuid → {
|
|
128
|
+
// $uuid → { whole, record } — the record index
|
|
120
129
|
this._records = new Map()
|
|
121
130
|
// bumps on every index write, so a materialized list knows it is stale
|
|
122
131
|
this._recordsVersion = 0
|
|
@@ -229,36 +238,36 @@ export default class DataStore {
|
|
|
229
238
|
* @param {'brief'|'full'} depth
|
|
230
239
|
* @returns {Object}
|
|
231
240
|
*/
|
|
232
|
-
upsertRecord(record,
|
|
241
|
+
upsertRecord(record, whole) {
|
|
233
242
|
const id = recordIdentity(record)
|
|
234
|
-
if (!id ||
|
|
243
|
+
if (!id || typeof whole !== 'boolean') return record
|
|
235
244
|
const held = this._records.get(id)
|
|
236
245
|
if (!held) {
|
|
237
|
-
this._records.set(id, {
|
|
246
|
+
this._records.set(id, { whole, record })
|
|
238
247
|
this._recordsVersion += 1
|
|
239
248
|
return record
|
|
240
249
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
this._records.set(id, {
|
|
250
|
+
// R2 — a brief never displaces a whole record.
|
|
251
|
+
if (!whole && held.whole) return held.record
|
|
252
|
+
// R3 — a whole record merges over a brief; same kind takes the fresher copy.
|
|
253
|
+
const next = whole && !held.whole ? { ...held.record, ...record } : record
|
|
254
|
+
this._records.set(id, { whole, record: next })
|
|
246
255
|
this._recordsVersion += 1
|
|
247
256
|
return next
|
|
248
257
|
}
|
|
249
258
|
|
|
250
259
|
_index(entry) {
|
|
251
|
-
const
|
|
252
|
-
if (
|
|
260
|
+
const whole = indexableWhole(entry)
|
|
261
|
+
if (whole === null || !entry || entry.ids) return entry
|
|
253
262
|
const { data } = entry
|
|
254
263
|
if (Array.isArray(data)) {
|
|
255
264
|
if (data.length === 0 || !data.every((r) => recordIdentity(r))) return entry
|
|
256
265
|
const ids = data.map((r) => recordIdentity(r))
|
|
257
|
-
for (const r of data) this.upsertRecord(r,
|
|
266
|
+
for (const r of data) this.upsertRecord(r, whole)
|
|
258
267
|
return { ids, meta: entry.meta, _at: -1, _data: null }
|
|
259
268
|
}
|
|
260
269
|
if (recordIdentity(data)) {
|
|
261
|
-
this.upsertRecord(data,
|
|
270
|
+
this.upsertRecord(data, whole)
|
|
262
271
|
return { ids: [recordIdentity(data)], single: true, meta: entry.meta, _at: -1, _data: null }
|
|
263
272
|
}
|
|
264
273
|
return entry
|
package/src/detail-url.js
CHANGED
|
@@ -117,7 +117,7 @@ export function buildDetailConfig(queryConfig, dynamicContext) {
|
|
|
117
117
|
return {
|
|
118
118
|
...rest,
|
|
119
119
|
where: { ...(queryConfig.where && typeof queryConfig.where === 'object' ? queryConfig.where : {}), [ROUTE_HANDLE_KEY]: String(paramValue) },
|
|
120
|
-
|
|
120
|
+
whole: true,
|
|
121
121
|
dynamicContext: { paramName, paramValue },
|
|
122
122
|
}
|
|
123
123
|
}
|
|
@@ -142,7 +142,7 @@ export function buildDetailConfig(queryConfig, dynamicContext) {
|
|
|
142
142
|
const common = {
|
|
143
143
|
as: queryConfig.as,
|
|
144
144
|
transform: queryConfig.transform,
|
|
145
|
-
|
|
145
|
+
whole: true,
|
|
146
146
|
dynamicContext: { paramName, paramValue },
|
|
147
147
|
}
|
|
148
148
|
if (typeof queryConfig.query === 'string') common.query = queryConfig.query
|
package/src/entity-store.js
CHANGED
|
@@ -287,7 +287,7 @@ export default class EntityStore {
|
|
|
287
287
|
// ⭐ Held in full already? Then it IS the record — no detail probe.
|
|
288
288
|
// The list is materialized from the record index, so `match` is the
|
|
289
289
|
// record at its latest depth; the index says which depth that is.
|
|
290
|
-
const held =
|
|
290
|
+
const held = heldWhole(dispatcher, match)
|
|
291
291
|
const detailCfg = held ? null : this._buildDetailConfig(cfg, { ...dynamicContext, record: match })
|
|
292
292
|
const detailCached = detailCfg ? dispatcher?.peek(detailCfg, ctx) : null
|
|
293
293
|
if (held) {
|
|
@@ -433,7 +433,7 @@ export default class EntityStore {
|
|
|
433
433
|
continue
|
|
434
434
|
}
|
|
435
435
|
|
|
436
|
-
const held = cfg.detail ?
|
|
436
|
+
const held = cfg.detail ? heldWhole(dispatcher, match) : null
|
|
437
437
|
if (held) {
|
|
438
438
|
// R1: the record index holds it in full — a detail fetch would only
|
|
439
439
|
// re-fetch what the page already has.
|
|
@@ -516,11 +516,11 @@ function reportFetchFailure(dev, block, key, cfg, message) {
|
|
|
516
516
|
* A record with no identity (`$uuid`) is never indexed, so the answer for it is
|
|
517
517
|
* null and the detail fetch proceeds as before.
|
|
518
518
|
*/
|
|
519
|
-
function
|
|
519
|
+
function heldWhole(dispatcher, match) {
|
|
520
520
|
const id = match?.$uuid
|
|
521
521
|
if (typeof id !== 'string' || !id || typeof dispatcher?.peekRecord !== 'function') return null
|
|
522
522
|
const held = dispatcher.peekRecord(id)
|
|
523
|
-
return held?.
|
|
523
|
+
return held?.whole === true ? held.record : null
|
|
524
524
|
}
|
|
525
525
|
|
|
526
526
|
/**
|
package/src/fetch-config.js
CHANGED
|
@@ -424,8 +424,8 @@ function foldScope(cfg) {
|
|
|
424
424
|
*/
|
|
425
425
|
function stampDepthAndLocale(cfg, locale, defaultLocale) {
|
|
426
426
|
let out = cfg
|
|
427
|
-
if (
|
|
428
|
-
out = { ...out,
|
|
427
|
+
if (typeof out.whole !== 'boolean') {
|
|
428
|
+
out = { ...out, whole: !out.detail }
|
|
429
429
|
}
|
|
430
430
|
// The service is asked in exactly one locale — it is in the route — so the config
|
|
431
431
|
// carries it whatever the locale is; two locales' answers never share an entry.
|