@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 +96 -20
- package/dist/ssr.js.map +1 -1
- package/package.json +3 -3
- package/src/collect-records.js +99 -0
- package/src/default-fetcher.js +124 -52
- package/src/isolate-api.js +48 -1
- package/src/prefetch.js +7 -7
- package/src/ssr.js +8 -1
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
|
|
846
|
-
const
|
|
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
|
|
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.
|
|
855
|
-
let queue =
|
|
855
|
+
const url = resolveServiceUrl(request.ask, pathPrefix);
|
|
856
|
+
let queue = askQueues.get(url);
|
|
856
857
|
if (!queue) {
|
|
857
858
|
queue = [];
|
|
858
|
-
|
|
859
|
+
askQueues.set(url, queue);
|
|
859
860
|
queueMicrotask(() => {
|
|
860
|
-
|
|
861
|
-
|
|
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,
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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[
|
|
982
|
+
out[OPERATOR_ALIAS[key] ?? key] = value && typeof value === "object" ? renameOperators(value) : value;
|
|
981
983
|
}
|
|
982
984
|
return out;
|
|
983
985
|
}
|
|
984
|
-
|
|
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] =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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,
|