@uniweb/runtime 0.16.0 → 0.17.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/ssr.js CHANGED
@@ -11,6 +11,7 @@ import { resolveFetchConfigs } from "@uniweb/core/fetch-config";
11
11
  import { deriveCacheKey as deriveCacheKey$1 } from "@uniweb/core/datastore";
12
12
  import { buildDetailConfig } from "@uniweb/core/detail-url";
13
13
  import { resolveDefaultLocale as resolveDefaultLocale$1 } from "@uniweb/core/locale-config";
14
+ import { resolveRecordsService } from "@uniweb/core/records-service";
14
15
  function guaranteeItemStructure(item) {
15
16
  return {
16
17
  title: item.title || "",
@@ -842,23 +843,23 @@ function generate404Html({ baseHtml, website, siteContent }) {
842
843
  function createDefaultFetcher({ basePath = "", dev = false, fetch: fetchImpl = null } = {}) {
843
844
  const doFetch = (input, init) => (fetchImpl || globalThis.fetch)(input, init);
844
845
  const pathPrefix = basePath && basePath !== "/" ? basePath.replace(/\/$/, "") : "";
845
- const doorQueues = /* @__PURE__ */ new Map();
846
- const askDoor = (request, ctx) => {
846
+ const askQueues = /* @__PURE__ */ new Map();
847
+ const askRecords = (request, ctx) => {
847
848
  if (typeof request.schema !== "string" || !request.schema) {
848
849
  return Promise.resolve({
849
850
  data: null,
850
- error: `the payload stamps a records door but carries no Model ref for query "${request.query ?? request.as}" (config.queries) — the door cannot be asked`
851
+ error: `the payload offers the records service but carries no Model ref for query "${request.query ?? request.as}" (config.queries) — it cannot be asked`
851
852
  });
852
853
  }
853
854
  return new Promise((resolve) => {
854
- const url = resolveServiceUrl(request.door, pathPrefix);
855
- let queue = doorQueues.get(url);
855
+ const url = resolveServiceUrl(request.ask, pathPrefix);
856
+ let queue = askQueues.get(url);
856
857
  if (!queue) {
857
858
  queue = [];
858
- doorQueues.set(url, queue);
859
+ askQueues.set(url, queue);
859
860
  queueMicrotask(() => {
860
- doorQueues.delete(url);
861
- flushDoor(url, queue, doFetch);
861
+ askQueues.delete(url);
862
+ flushAsked(url, queue, doFetch);
862
863
  });
863
864
  }
864
865
  queue.push({ request, ctx, resolve });
@@ -866,8 +867,8 @@ function createDefaultFetcher({ basePath = "", dev = false, fetch: fetchImpl = n
866
867
  };
867
868
  return {
868
869
  /**
869
- * The cache identity is the request's ADDRESS — or, on a question door,
870
- * the QUESTION (`deriveCacheKey` hashes every operator of an address-less
870
+ * The cache identity is the request's ADDRESS — or, when asked of the
871
+ * records service, the QUESTION (`deriveCacheKey` hashes every operator of an address-less
871
872
  * request). Operators evaluated here run over a shared cached value and
872
873
  * must NOT split the cache: two pages declaring different `where:` clauses
873
874
  * against the same path share one entry — the file is fetched once and
@@ -878,7 +879,7 @@ function createDefaultFetcher({ basePath = "", dev = false, fetch: fetchImpl = n
878
879
  },
879
880
  async resolve(request, ctx = {}) {
880
881
  if (!request) return { data: null };
881
- if (request.door) return askDoor(request, ctx);
882
+ if (request.ask) return askRecords(request, ctx);
882
883
  const { path, url, transform, body: rawBody } = request;
883
884
  let method = (request.method || "GET").toUpperCase();
884
885
  if (method !== "GET" && method !== "POST") {
@@ -891,7 +892,7 @@ function createDefaultFetcher({ basePath = "", dev = false, fetch: fetchImpl = n
891
892
  } else if (url) {
892
893
  target = url;
893
894
  } else {
894
- return { data: [], error: "No path, url or door specified" };
895
+ return { data: [], error: "No path, url or ask specified" };
895
896
  }
896
897
  const init = { signal: ctx.signal, method };
897
898
  if (method === "POST") {
@@ -954,7 +955,7 @@ function createDefaultFetcher({ basePath = "", dev = false, fetch: fetchImpl = n
954
955
  }
955
956
  };
956
957
  }
957
- function doorQuestion(request) {
958
+ function toQuestion(request) {
958
959
  const q = { schema: request.schema };
959
960
  let where = request.where && typeof request.where === "object" ? request.where : null;
960
961
  let scope = typeof request.scope === "string" && request.scope ? request.scope : null;
@@ -969,19 +970,22 @@ function doorQuestion(request) {
969
970
  if (sort) q.sort = sort;
970
971
  if (typeof request.limit === "number" && request.limit > 0) q.limit = request.limit;
971
972
  if (request.depth === "brief" || request.depth === "full") q.depth = request.depth;
973
+ if (typeof request.cursor === "string" && request.cursor) q.cursor = request.cursor;
972
974
  return q;
973
975
  }
974
- const DOOR_OPERATOR = { nin: "not_in" };
976
+ const OPERATOR_ALIAS = { nin: "not_in" };
975
977
  function renameOperators(where) {
976
978
  if (Array.isArray(where)) return where.map(renameOperators);
977
979
  if (!where || typeof where !== "object") return where;
978
980
  const out = {};
979
981
  for (const [key, value] of Object.entries(where)) {
980
- out[DOOR_OPERATOR[key] ?? key] = value && typeof value === "object" ? renameOperators(value) : value;
982
+ out[OPERATOR_ALIAS[key] ?? key] = value && typeof value === "object" ? renameOperators(value) : value;
981
983
  }
982
984
  return out;
983
985
  }
984
- async function flushDoor(url, queue, doFetch) {
986
+ const MAX_PAGES = 50;
987
+ async function flushAsked(url, queue, doFetch) {
988
+ const pending = /* @__PURE__ */ new Map();
985
989
  const body = {};
986
990
  const keys = [];
987
991
  for (const entry of queue) {
@@ -989,7 +993,7 @@ async function flushDoor(url, queue, doFetch) {
989
993
  let key = base;
990
994
  for (let n = 2; key in body; n += 1) key = `${base}#${n}`;
991
995
  keys.push(key);
992
- body[key] = doorQuestion(entry.request);
996
+ body[key] = toQuestion(entry.request);
993
997
  }
994
998
  let parsed;
995
999
  try {
@@ -1018,6 +1022,8 @@ async function flushDoor(url, queue, doFetch) {
1018
1022
  const data = parsed && typeof parsed.data === "object" && parsed.data ? parsed.data : {};
1019
1023
  const errors = parsed && typeof parsed.errors === "object" && parsed.errors ? parsed.errors : {};
1020
1024
  const depths = parsed && typeof parsed.depths === "object" && parsed.depths ? parsed.depths : {};
1025
+ const cursors = parsed && typeof parsed.cursors === "object" && parsed.cursors ? parsed.cursors : {};
1026
+ const limits = parsed && typeof parsed.limits === "object" && parsed.limits ? parsed.limits : {};
1021
1027
  queue.forEach((entry, i) => {
1022
1028
  const key = keys[i];
1023
1029
  if (key in errors) {
@@ -1029,12 +1035,50 @@ async function flushDoor(url, queue, doFetch) {
1029
1035
  return;
1030
1036
  }
1031
1037
  if (!(key in data)) {
1032
- entry.resolve({ data: null, error: `the records door answered without the key "${key}"` });
1038
+ entry.resolve({ data: null, error: `the records service answered without the key "${key}"` });
1033
1039
  return;
1034
1040
  }
1035
1041
  const depth = depths[key] === "brief" || depths[key] === "full" ? depths[key] : entry.request.depth === "brief" || entry.request.depth === "full" ? entry.request.depth : void 0;
1036
- entry.resolve(depth ? { data: data[key], meta: { depth } } : { data: data[key] });
1042
+ const cursor = typeof cursors[key] === "string" && cursors[key] ? cursors[key] : null;
1043
+ const bound = typeof limits[key] === "number" ? limits[key] : void 0;
1044
+ const rows = Array.isArray(data[key]) ? data[key] : data[key];
1045
+ if (cursor && entry.request.exhaustive && Array.isArray(rows)) {
1046
+ const acc = entry.collected ? entry.collected.concat(rows) : rows.slice();
1047
+ const page = (entry.page || 1) + 1;
1048
+ if (page <= MAX_PAGES) {
1049
+ pending.set(entry, { cursor, collected: acc, page, depth, bound });
1050
+ return;
1051
+ }
1052
+ entry.resolve({ data: acc, meta: withMeta({ depth, bound, truncated: true, pages: MAX_PAGES }) });
1053
+ return;
1054
+ }
1055
+ const collected = entry.collected ? entry.collected.concat(Array.isArray(rows) ? rows : []) : rows;
1056
+ const meta = withMeta({
1057
+ depth,
1058
+ bound,
1059
+ // ⭐ A cursor IS the truncation signal, and it is the only one for a query
1060
+ // that declared no `limit`: `limits` is reported only when an author's own
1061
+ // limit was clamped (the records contract §5).
1062
+ truncated: cursor ? true : void 0,
1063
+ pages: entry.page && entry.page > 1 ? entry.page : void 0
1064
+ });
1065
+ entry.resolve(meta ? { data: collected, meta } : { data: collected });
1037
1066
  });
1067
+ if (pending.size === 0) return;
1068
+ await Promise.all([...pending].map(([entry, state]) => {
1069
+ const next = {
1070
+ ...entry,
1071
+ request: { ...entry.request, cursor: state.cursor },
1072
+ collected: state.collected,
1073
+ page: state.page
1074
+ };
1075
+ return flushAsked(url, [next], doFetch);
1076
+ }));
1077
+ }
1078
+ function withMeta(fields) {
1079
+ const out = {};
1080
+ for (const [k, v] of Object.entries(fields)) if (v !== void 0) out[k] = v;
1081
+ return Object.keys(out).length ? out : void 0;
1038
1082
  }
1039
1083
  function applyOperators(data, request, { dev = false } = {}) {
1040
1084
  if (!Array.isArray(data)) return data;
@@ -1114,7 +1158,7 @@ function resolvePageFetchConfigs(content, route, { locale = null } = {}) {
1114
1158
  locale,
1115
1159
  defaultLocale: resolveDefaultLocale$1(content?.config) ?? null,
1116
1160
  queries: content?.config?.queries ?? null,
1117
- records: content?.config?.records ?? null,
1161
+ services: content?.config?.services ?? null,
1118
1162
  variables: binding?.variables ?? null
1119
1163
  };
1120
1164
  const out = /* @__PURE__ */ new Map();
@@ -1199,10 +1243,42 @@ async function prefetchAndHydrate({ website, content, route, locale = null, fetc
1199
1243
  hydrateDataStore(website, fetched);
1200
1244
  return fetched;
1201
1245
  }
1246
+ async function collectSiteRecords(content, { locale, fetch: fetch2, signal, only = null } = {}) {
1247
+ const config = content?.config;
1248
+ const services = config?.services ?? null;
1249
+ const queries = config?.queries ?? null;
1250
+ if (!queries || typeof queries !== "object") return empty();
1251
+ if (!resolveRecordsService(services, locale)) return empty();
1252
+ const names = Object.keys(queries).filter((n) => only ? only.includes(n) : true);
1253
+ if (names.length === 0) return empty();
1254
+ const configs = resolveFetchConfigs(
1255
+ names.map((name) => ({ query: name, as: name })),
1256
+ { services, queries, locale, defaultLocale: config?.defaultLanguage ?? locale }
1257
+ );
1258
+ const fetcher = createDefaultFetcher({ fetch: fetch2, basePath: config?.base ?? "" });
1259
+ const records = {};
1260
+ const errors = {};
1261
+ const meta = {};
1262
+ await Promise.all([...configs].map(async ([name, cfg]) => {
1263
+ if (!cfg.ask) return;
1264
+ const result = await fetcher.resolve({ ...cfg, depth: "brief", exhaustive: true }, { signal });
1265
+ if (result?.error) {
1266
+ errors[name] = result.error;
1267
+ return;
1268
+ }
1269
+ records[name] = Array.isArray(result?.data) ? result.data : [];
1270
+ if (result?.meta) meta[name] = result.meta;
1271
+ }));
1272
+ return { records, errors: Object.keys(errors).length ? errors : null, meta };
1273
+ }
1274
+ function empty() {
1275
+ return { records: {}, errors: null, meta: {} };
1276
+ }
1202
1277
  export {
1203
1278
  applyDefaults,
1204
1279
  applySchemas,
1205
1280
  classifyRenderError,
1281
+ collectSiteRecords,
1206
1282
  createPageRenderer,
1207
1283
  escapeHtml,
1208
1284
  executeFetchConfigs,