@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniweb/core",
3
- "version": "0.23.0",
3
+ "version": "0.24.0",
4
4
  "description": "Core classes for the Uniweb platform - Uniweb, Website, Page, Block",
5
5
  "type": "module",
6
6
  "exports": {
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, depth } = request || {}
64
- return JSON.stringify({ query, schema, scope, where, sort, limit, depth, as, transform, locale })
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
- /** Which of two depths holds MORE of a record. */
81
- const DEPTH_RANK = { brief: 1, full: 2 }
82
-
83
- function indexableDepth(entry) {
84
- const depth = entry?.meta?.depth
85
- return depth === 'brief' || depth === 'full' ? depth : null
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 → { depth, record } — the record index
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, depth) {
241
+ upsertRecord(record, whole) {
233
242
  const id = recordIdentity(record)
234
- if (!id || !DEPTH_RANK[depth]) return record
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, { depth, record })
246
+ this._records.set(id, { whole, record })
238
247
  this._recordsVersion += 1
239
248
  return record
240
249
  }
241
- if (DEPTH_RANK[depth] < DEPTH_RANK[held.depth]) return held.record // R2
242
- const next = DEPTH_RANK[depth] > DEPTH_RANK[held.depth]
243
- ? { ...held.record, ...record } // R3 merge on upgrade
244
- : record // same depth: the fresher copy
245
- this._records.set(id, { depth, record: next })
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 depth = indexableDepth(entry)
252
- if (!depth || !entry || entry.ids) return entry
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, depth)
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, depth)
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
- depth: 'full',
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
- depth: 'full',
145
+ whole: true,
146
146
  dynamicContext: { paramName, paramValue },
147
147
  }
148
148
  if (typeof queryConfig.query === 'string') common.query = queryConfig.query
@@ -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 = heldInFull(dispatcher, match)
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 ? heldInFull(dispatcher, match) : null
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 heldInFull(dispatcher, match) {
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?.depth === 'full' ? held.record : null
523
+ return held?.whole === true ? held.record : null
524
524
  }
525
525
 
526
526
  /**
@@ -424,8 +424,8 @@ function foldScope(cfg) {
424
424
  */
425
425
  function stampDepthAndLocale(cfg, locale, defaultLocale) {
426
426
  let out = cfg
427
- if (out.depth !== 'brief' && out.depth !== 'full') {
428
- out = { ...out, depth: out.detail ? 'brief' : 'full' }
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.