@batterai/host 0.1.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/CHANGELOG.md +6 -0
- package/LICENSE +21 -0
- package/README.md +23 -0
- package/dist/cache/dependency-graph.d.ts +18 -0
- package/dist/cache/dependency-graph.d.ts.map +1 -0
- package/dist/cache/dependency-graph.js +30 -0
- package/dist/cache/dependency-graph.js.map +1 -0
- package/dist/cache/render-cache.d.ts +39 -0
- package/dist/cache/render-cache.d.ts.map +1 -0
- package/dist/cache/render-cache.js +60 -0
- package/dist/cache/render-cache.js.map +1 -0
- package/dist/client/mount.d.ts +9 -0
- package/dist/client/mount.d.ts.map +1 -0
- package/dist/client/mount.js +69 -0
- package/dist/client/mount.js.map +1 -0
- package/dist/client.d.ts +2 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +2 -0
- package/dist/client.js.map +1 -0
- package/dist/collab/in-memory-page-draft-store.d.ts +44 -0
- package/dist/collab/in-memory-page-draft-store.d.ts.map +1 -0
- package/dist/collab/in-memory-page-draft-store.js +217 -0
- package/dist/collab/in-memory-page-draft-store.js.map +1 -0
- package/dist/collab/page-doc-page-draft-store.d.ts +41 -0
- package/dist/collab/page-doc-page-draft-store.d.ts.map +1 -0
- package/dist/collab/page-doc-page-draft-store.js +328 -0
- package/dist/collab/page-doc-page-draft-store.js.map +1 -0
- package/dist/collab/page-draft-store.d.ts +152 -0
- package/dist/collab/page-draft-store.d.ts.map +1 -0
- package/dist/collab/page-draft-store.js +61 -0
- package/dist/collab/page-draft-store.js.map +1 -0
- package/dist/content-source/conformance/cases-delegation.d.ts +3 -0
- package/dist/content-source/conformance/cases-delegation.d.ts.map +1 -0
- package/dist/content-source/conformance/cases-delegation.js +55 -0
- package/dist/content-source/conformance/cases-delegation.js.map +1 -0
- package/dist/content-source/conformance/cases-fidelity.d.ts +3 -0
- package/dist/content-source/conformance/cases-fidelity.d.ts.map +1 -0
- package/dist/content-source/conformance/cases-fidelity.js +67 -0
- package/dist/content-source/conformance/cases-fidelity.js.map +1 -0
- package/dist/content-source/conformance/cases-isolation.d.ts +3 -0
- package/dist/content-source/conformance/cases-isolation.d.ts.map +1 -0
- package/dist/content-source/conformance/cases-isolation.js +57 -0
- package/dist/content-source/conformance/cases-isolation.js.map +1 -0
- package/dist/content-source/conformance/cases-permissions.d.ts +3 -0
- package/dist/content-source/conformance/cases-permissions.d.ts.map +1 -0
- package/dist/content-source/conformance/cases-permissions.js +50 -0
- package/dist/content-source/conformance/cases-permissions.js.map +1 -0
- package/dist/content-source/conformance/cases-reads.d.ts +3 -0
- package/dist/content-source/conformance/cases-reads.d.ts.map +1 -0
- package/dist/content-source/conformance/cases-reads.js +216 -0
- package/dist/content-source/conformance/cases-reads.js.map +1 -0
- package/dist/content-source/conformance/cases-writes.d.ts +11 -0
- package/dist/content-source/conformance/cases-writes.d.ts.map +1 -0
- package/dist/content-source/conformance/cases-writes.js +73 -0
- package/dist/content-source/conformance/cases-writes.js.map +1 -0
- package/dist/content-source/conformance/session-required.d.ts +10 -0
- package/dist/content-source/conformance/session-required.d.ts.map +1 -0
- package/dist/content-source/conformance/session-required.js +11 -0
- package/dist/content-source/conformance/session-required.js.map +1 -0
- package/dist/content-source/conformance/sessionless-viewer.d.ts +12 -0
- package/dist/content-source/conformance/sessionless-viewer.d.ts.map +1 -0
- package/dist/content-source/conformance/sessionless-viewer.js +5 -0
- package/dist/content-source/conformance/sessionless-viewer.js.map +1 -0
- package/dist/content-source/conformance/target.d.ts +111 -0
- package/dist/content-source/conformance/target.d.ts.map +1 -0
- package/dist/content-source/conformance/target.js +2 -0
- package/dist/content-source/conformance/target.js.map +1 -0
- package/dist/content-source/conformance-kit.d.ts +4 -0
- package/dist/content-source/conformance-kit.d.ts.map +1 -0
- package/dist/content-source/conformance-kit.js +41 -0
- package/dist/content-source/conformance-kit.js.map +1 -0
- package/dist/content-source/content-source.d.ts +153 -0
- package/dist/content-source/content-source.d.ts.map +1 -0
- package/dist/content-source/content-source.js +12 -0
- package/dist/content-source/content-source.js.map +1 -0
- package/dist/content-source/permissions.d.ts +46 -0
- package/dist/content-source/permissions.d.ts.map +1 -0
- package/dist/content-source/permissions.js +33 -0
- package/dist/content-source/permissions.js.map +1 -0
- package/dist/content-source/viewer.d.ts +48 -0
- package/dist/content-source/viewer.d.ts.map +1 -0
- package/dist/content-source/viewer.js +24 -0
- package/dist/content-source/viewer.js.map +1 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +30 -0
- package/dist/index.js.map +1 -0
- package/dist/lifecycle/cms-lifecycle-store.d.ts +72 -0
- package/dist/lifecycle/cms-lifecycle-store.d.ts.map +1 -0
- package/dist/lifecycle/cms-lifecycle-store.js +150 -0
- package/dist/lifecycle/cms-lifecycle-store.js.map +1 -0
- package/dist/lifecycle/lifecycle-store.d.ts +35 -0
- package/dist/lifecycle/lifecycle-store.d.ts.map +1 -0
- package/dist/lifecycle/lifecycle-store.js +69 -0
- package/dist/lifecycle/lifecycle-store.js.map +1 -0
- package/dist/lifecycle/version-token.d.ts +30 -0
- package/dist/lifecycle/version-token.d.ts.map +1 -0
- package/dist/lifecycle/version-token.js +22 -0
- package/dist/lifecycle/version-token.js.map +1 -0
- package/dist/messages.d.ts +26 -0
- package/dist/messages.d.ts.map +1 -0
- package/dist/messages.js +31 -0
- package/dist/messages.js.map +1 -0
- package/dist/render/image-usage-preview.d.ts +19 -0
- package/dist/render/image-usage-preview.d.ts.map +1 -0
- package/dist/render/image-usage-preview.js +25 -0
- package/dist/render/image-usage-preview.js.map +1 -0
- package/dist/render/image-usage.d.ts +32 -0
- package/dist/render/image-usage.d.ts.map +1 -0
- package/dist/render/image-usage.js +116 -0
- package/dist/render/image-usage.js.map +1 -0
- package/dist/render/page-renderer.d.ts +44 -0
- package/dist/render/page-renderer.d.ts.map +1 -0
- package/dist/render/page-renderer.js +115 -0
- package/dist/render/page-renderer.js.map +1 -0
- package/dist/resolver/data-loader.d.ts +49 -0
- package/dist/resolver/data-loader.d.ts.map +1 -0
- package/dist/resolver/data-loader.js +121 -0
- package/dist/resolver/data-loader.js.map +1 -0
- package/dist/resolver/resolution-service.d.ts +49 -0
- package/dist/resolver/resolution-service.d.ts.map +1 -0
- package/dist/resolver/resolution-service.js +193 -0
- package/dist/resolver/resolution-service.js.map +1 -0
- package/dist/routing/route-resolver.d.ts +73 -0
- package/dist/routing/route-resolver.d.ts.map +1 -0
- package/dist/routing/route-resolver.js +114 -0
- package/dist/routing/route-resolver.js.map +1 -0
- package/dist/scope-engine/build-stack.d.ts +49 -0
- package/dist/scope-engine/build-stack.d.ts.map +1 -0
- package/dist/scope-engine/build-stack.js +77 -0
- package/dist/scope-engine/build-stack.js.map +1 -0
- package/dist/scope-engine/repeater-query.d.ts +9 -0
- package/dist/scope-engine/repeater-query.d.ts.map +1 -0
- package/dist/scope-engine/repeater-query.js +39 -0
- package/dist/scope-engine/repeater-query.js.map +1 -0
- package/dist/seo/metadata.d.ts +30 -0
- package/dist/seo/metadata.d.ts.map +1 -0
- package/dist/seo/metadata.js +118 -0
- package/dist/seo/metadata.js.map +1 -0
- package/dist/seo/sitemap.d.ts +11 -0
- package/dist/seo/sitemap.d.ts.map +1 -0
- package/dist/seo/sitemap.js +20 -0
- package/dist/seo/sitemap.js.map +1 -0
- package/dist/write/write-path.d.ts +63 -0
- package/dist/write/write-path.d.ts.map +1 -0
- package/dist/write/write-path.js +139 -0
- package/dist/write/write-path.js.map +1 -0
- package/package.json +44 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BindingResolutionService (decision 004 §2.2 + §3.2): the one facade every
|
|
3
|
+
* render path calls. Wave 1 = page-level needs + repeater row sets; wave N =
|
|
4
|
+
* per-item template needs, batched across ALL items of ALL repeaters at that
|
|
5
|
+
* depth — depth+1 loader calls for nesting depth d, never per-row I/O.
|
|
6
|
+
* Records binding->page dependencies as a by-product (invariant F).
|
|
7
|
+
*/
|
|
8
|
+
import { type ContentType, type ContentTypeId, type DataView, type Instance, type Locale, type Page, type RepeaterNode, type ScopeStack, type TimeZone } from "@batterai/bind-core";
|
|
9
|
+
import type { ContentSource } from "../content-source/content-source";
|
|
10
|
+
import type { Viewer } from "../content-source/viewer";
|
|
11
|
+
import type { BindingDependencyGraph } from "../cache/dependency-graph";
|
|
12
|
+
export interface RenderRequestCtx {
|
|
13
|
+
readonly locale: Locale;
|
|
14
|
+
readonly fallbackLocale?: Locale;
|
|
15
|
+
readonly timeZone: TimeZone;
|
|
16
|
+
readonly viewer: Viewer;
|
|
17
|
+
}
|
|
18
|
+
export interface RepeaterRender {
|
|
19
|
+
readonly node: RepeaterNode;
|
|
20
|
+
readonly items: readonly Instance[];
|
|
21
|
+
readonly itemStacks: readonly ScopeStack[];
|
|
22
|
+
readonly cursor?: string;
|
|
23
|
+
readonly hasMore: boolean;
|
|
24
|
+
}
|
|
25
|
+
export interface ResolvedPage {
|
|
26
|
+
readonly view: DataView;
|
|
27
|
+
readonly pageStack: ScopeStack;
|
|
28
|
+
readonly repeaters: ReadonlyMap<string, RepeaterRender>;
|
|
29
|
+
readonly loaderCalls: readonly string[];
|
|
30
|
+
}
|
|
31
|
+
export declare class BindingResolutionService {
|
|
32
|
+
private readonly source;
|
|
33
|
+
private readonly contentTypes;
|
|
34
|
+
private readonly depGraph?;
|
|
35
|
+
constructor(source: ContentSource, contentTypes: ReadonlyMap<ContentTypeId, ContentType>, depGraph?: BindingDependencyGraph | undefined);
|
|
36
|
+
private ctxOf;
|
|
37
|
+
resolvePage(page: Page, ctx: RenderRequestCtx, detail?: {
|
|
38
|
+
contentTypeId: ContentTypeId;
|
|
39
|
+
instanceId: Instance["id"];
|
|
40
|
+
}): Promise<ResolvedPage>;
|
|
41
|
+
/** Infinite-scroll append (Phase 7): resolve ONE top-level repeater's next
|
|
42
|
+
* page through the SAME loader/query machinery as the initial render —
|
|
43
|
+
* never a bespoke lighter path that could drift (invariant A append). */
|
|
44
|
+
resolveNextPage(page: Page, repeaterId: string, cursor: string, ctx: RenderRequestCtx): Promise<{
|
|
45
|
+
render: RepeaterRender;
|
|
46
|
+
view: DataView;
|
|
47
|
+
} | undefined>;
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=resolution-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolution-service.d.ts","sourceRoot":"","sources":["../../src/resolver/resolution-service.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAEL,KAAK,WAAW,EAChB,KAAK,aAAa,EAElB,KAAK,QAAQ,EACb,KAAK,QAAQ,EACb,KAAK,MAAM,EACX,KAAK,IAAI,EAGT,KAAK,YAAY,EAEjB,KAAK,UAAU,EACf,KAAK,QAAQ,EACd,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kCAAkC,CAAC;AACtE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,0BAA0B,CAAC;AACvD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,2BAA2B,CAAC;AAWxE,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,SAAS,QAAQ,EAAE,CAAC;IACpC,QAAQ,CAAC,UAAU,EAAE,SAAS,UAAU,EAAE,CAAC;IAC3C,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC3B;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,QAAQ,CAAC,SAAS,EAAE,UAAU,CAAC;IAC/B,QAAQ,CAAC,SAAS,EAAE,WAAW,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;IACxD,QAAQ,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;CACzC;AAcD,qBAAa,wBAAwB;IAEjC,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,YAAY;IAC7B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC;gBAFT,MAAM,EAAE,aAAa,EACrB,YAAY,EAAE,WAAW,CAAC,aAAa,EAAE,WAAW,CAAC,EACrD,QAAQ,CAAC,EAAE,sBAAsB,YAAA;IAGpD,OAAO,CAAC,KAAK;IAUP,WAAW,CACf,IAAI,EAAE,IAAI,EACV,GAAG,EAAE,gBAAgB,EACrB,MAAM,CAAC,EAAE;QAAE,aAAa,EAAE,aAAa,CAAC;QAAC,UAAU,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAA;KAAE,GACpE,OAAO,CAAC,YAAY,CAAC;IAqHxB;;6EAEyE;IACnE,eAAe,CACnB,IAAI,EAAE,IAAI,EACV,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,gBAAgB,GACpB,OAAO,CAAC;QAAE,MAAM,EAAE,cAAc,CAAC;QAAC,IAAI,EAAE,QAAQ,CAAA;KAAE,GAAG,SAAS,CAAC;CA2BnE"}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BindingResolutionService (decision 004 §2.2 + §3.2): the one facade every
|
|
3
|
+
* render path calls. Wave 1 = page-level needs + repeater row sets; wave N =
|
|
4
|
+
* per-item template needs, batched across ALL items of ALL repeaters at that
|
|
5
|
+
* depth — depth+1 loader calls for nesting depth d, never per-row I/O.
|
|
6
|
+
* Records binding->page dependencies as a by-product (invariant F).
|
|
7
|
+
*/
|
|
8
|
+
import { planDataNeeds, } from "@batterai/bind-core";
|
|
9
|
+
import { buildDetailStack, buildPageStack, fillStructural, itemStack, } from "../scope-engine/build-stack.js";
|
|
10
|
+
import { applyRepeaterQuery } from "../scope-engine/repeater-query.js";
|
|
11
|
+
import { HostDataLoader } from "./data-loader.js";
|
|
12
|
+
function collectRepeaters(nodes, out = []) {
|
|
13
|
+
for (const n of nodes) {
|
|
14
|
+
if (n.kind === "repeater") {
|
|
15
|
+
out.push(n);
|
|
16
|
+
collectRepeaters([n.itemTemplate], out);
|
|
17
|
+
}
|
|
18
|
+
else if (n.children) {
|
|
19
|
+
collectRepeaters(n.children, out);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
export class BindingResolutionService {
|
|
25
|
+
source;
|
|
26
|
+
contentTypes;
|
|
27
|
+
depGraph;
|
|
28
|
+
constructor(source, contentTypes, depGraph) {
|
|
29
|
+
this.source = source;
|
|
30
|
+
this.contentTypes = contentTypes;
|
|
31
|
+
this.depGraph = depGraph;
|
|
32
|
+
}
|
|
33
|
+
ctxOf(ctx) {
|
|
34
|
+
// No viewer/scope cross-check (decision 012, invariant D4): data scoping is
|
|
35
|
+
// delegated to the CMS and the resolve pipeline carries no owner brand.
|
|
36
|
+
return {
|
|
37
|
+
locale: ctx.locale,
|
|
38
|
+
fallbackLocale: ctx.fallbackLocale,
|
|
39
|
+
timeZone: ctx.timeZone,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
async resolvePage(page, ctx, detail) {
|
|
43
|
+
const planCtx = this.ctxOf(ctx);
|
|
44
|
+
const structural = detail
|
|
45
|
+
? buildDetailStack(page, detail.contentTypeId)
|
|
46
|
+
: buildPageStack(page);
|
|
47
|
+
const loader = new HostDataLoader(this.source, ctx.viewer, this.contentTypes);
|
|
48
|
+
const needKeyOf = (node) => HostDataLoader.needKey({ kind: "instances", contentTypeId: node.boundType, query: node.query });
|
|
49
|
+
// ---- wave 1: page-level needs + the detail instance ------------------
|
|
50
|
+
const waveOneNeeds = [];
|
|
51
|
+
if (detail) {
|
|
52
|
+
waveOneNeeds.push({
|
|
53
|
+
kind: "instance",
|
|
54
|
+
contentTypeId: detail.contentTypeId,
|
|
55
|
+
instanceId: detail.instanceId,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
const topRepeaters = collectRepeaters(page.root).filter((r) => !isNested(r, page.root));
|
|
59
|
+
for (const rep of topRepeaters) {
|
|
60
|
+
waveOneNeeds.push({ kind: "instances", contentTypeId: rep.boundType, query: rep.query });
|
|
61
|
+
}
|
|
62
|
+
// Page-level (non-repeater) block needs against the (partially) filled stack.
|
|
63
|
+
const filledHoles = new Map();
|
|
64
|
+
let view = await loader.load(waveOneNeeds, planCtx);
|
|
65
|
+
if (detail) {
|
|
66
|
+
const inst = view.getInstance(detail.contentTypeId, detail.instanceId);
|
|
67
|
+
if (!inst) {
|
|
68
|
+
// Loader contract already threw UnsatisfiableNeed if missing; this is
|
|
69
|
+
// a type-level guard only.
|
|
70
|
+
throw new Error("detail instance missing after load");
|
|
71
|
+
}
|
|
72
|
+
const ct = this.contentTypes.get(detail.contentTypeId);
|
|
73
|
+
const holeId = structural.find((f) => f.kind === "instance-hole").scopeId;
|
|
74
|
+
filledHoles.set(holeId, { instance: inst, contentType: ct });
|
|
75
|
+
this.depGraph?.recordDependency(page.id, {
|
|
76
|
+
kind: "instance",
|
|
77
|
+
contentTypeId: detail.contentTypeId,
|
|
78
|
+
instanceId: detail.instanceId,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
const pageStack = fillStructural(structural, filledHoles);
|
|
82
|
+
// Plan the non-repeater needs of the page tree now that the root stack is real.
|
|
83
|
+
const pageNeeds = [];
|
|
84
|
+
for (const node of page.root) {
|
|
85
|
+
pageNeeds.push(...planDataNeeds(node, pageStack, planCtx));
|
|
86
|
+
}
|
|
87
|
+
if (pageNeeds.length > 0)
|
|
88
|
+
view = await loader.load(pageNeeds, planCtx);
|
|
89
|
+
// ---- wave 2..depth+1: repeater items, batched per depth --------------
|
|
90
|
+
const repeaters = new Map();
|
|
91
|
+
let frontier = topRepeaters.map((node) => ({
|
|
92
|
+
node,
|
|
93
|
+
parent: structural,
|
|
94
|
+
}));
|
|
95
|
+
while (frontier.length > 0) {
|
|
96
|
+
const next = [];
|
|
97
|
+
const waveNeeds = [];
|
|
98
|
+
const perRepeater = [];
|
|
99
|
+
for (const entry of frontier) {
|
|
100
|
+
const { node } = entry;
|
|
101
|
+
const ct = this.contentTypes.get(node.boundType);
|
|
102
|
+
if (!ct)
|
|
103
|
+
throw new Error(`unknown content type ${node.boundType}`);
|
|
104
|
+
const raw = view.getInstancePage(node.boundType, needKeyOf(node)) ?? [];
|
|
105
|
+
// Invariant I: filter -> sort -> pageSize against candidates.
|
|
106
|
+
const items = applyRepeaterQuery(raw, node.query, (inst, i) => itemStack(entry.parent, node, i, inst, ct), view);
|
|
107
|
+
const stacks = items.map((inst, i) => itemStack(entry.parent, node, i, inst, ct));
|
|
108
|
+
perRepeater.push({ entry, items, stacks });
|
|
109
|
+
this.depGraph?.recordDependency(page.id, {
|
|
110
|
+
kind: "instance",
|
|
111
|
+
contentTypeId: node.boundType,
|
|
112
|
+
instanceId: "*", // content-type-granular (conservative)
|
|
113
|
+
});
|
|
114
|
+
// Union the item templates' needs across ALL items (batched wave).
|
|
115
|
+
items.forEach((_inst, i) => {
|
|
116
|
+
waveNeeds.push(...planDataNeeds(node.itemTemplate, stacks[i], planCtx));
|
|
117
|
+
});
|
|
118
|
+
// Discover nested repeaters for the next depth.
|
|
119
|
+
for (const nested of collectRepeaters([node.itemTemplate]).filter((r) => r !== node)) {
|
|
120
|
+
waveNeeds.push({ kind: "instances", contentTypeId: nested.boundType, query: nested.query });
|
|
121
|
+
next.push({ node: nested, parent: entry.parent });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (waveNeeds.length > 0)
|
|
125
|
+
view = await loader.load(waveNeeds, planCtx);
|
|
126
|
+
for (const { entry, items, stacks } of perRepeater) {
|
|
127
|
+
const meta = loader.pageMeta(needKeyOf(entry.node));
|
|
128
|
+
repeaters.set(entry.node.id, {
|
|
129
|
+
node: entry.node,
|
|
130
|
+
items,
|
|
131
|
+
itemStacks: stacks,
|
|
132
|
+
cursor: meta?.cursor,
|
|
133
|
+
hasMore: meta?.hasMore ?? false,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
frontier = next;
|
|
137
|
+
}
|
|
138
|
+
return { view, pageStack, repeaters, loaderCalls: loader.callLog };
|
|
139
|
+
}
|
|
140
|
+
/** Infinite-scroll append (Phase 7): resolve ONE top-level repeater's next
|
|
141
|
+
* page through the SAME loader/query machinery as the initial render —
|
|
142
|
+
* never a bespoke lighter path that could drift (invariant A append). */
|
|
143
|
+
async resolveNextPage(page, repeaterId, cursor, ctx) {
|
|
144
|
+
const planCtx = this.ctxOf(ctx);
|
|
145
|
+
const node = collectRepeaters(page.root).find((r) => r.id === repeaterId);
|
|
146
|
+
if (!node)
|
|
147
|
+
return undefined;
|
|
148
|
+
const structural = buildPageStack(page);
|
|
149
|
+
const loader = new HostDataLoader(this.source, ctx.viewer, this.contentTypes);
|
|
150
|
+
const need = {
|
|
151
|
+
kind: "instances",
|
|
152
|
+
contentTypeId: node.boundType,
|
|
153
|
+
query: node.query,
|
|
154
|
+
cursor,
|
|
155
|
+
};
|
|
156
|
+
let view = await loader.load([need], planCtx);
|
|
157
|
+
const ct = this.contentTypes.get(node.boundType);
|
|
158
|
+
if (!ct)
|
|
159
|
+
throw new Error(`unknown content type ${node.boundType}`);
|
|
160
|
+
const raw = view.getInstancePage(node.boundType, HostDataLoader.needKey(need)) ?? [];
|
|
161
|
+
const items = applyRepeaterQuery(raw, node.query, (inst, i) => itemStack(structural, node, i, inst, ct), view);
|
|
162
|
+
const stacks = items.map((inst, i) => itemStack(structural, node, i, inst, ct));
|
|
163
|
+
const waveNeeds = [];
|
|
164
|
+
items.forEach((_inst, i) => waveNeeds.push(...planDataNeeds(node.itemTemplate, stacks[i], planCtx)));
|
|
165
|
+
if (waveNeeds.length > 0)
|
|
166
|
+
view = await loader.load(waveNeeds, planCtx);
|
|
167
|
+
const meta = loader.pageMeta(HostDataLoader.needKey(need));
|
|
168
|
+
return {
|
|
169
|
+
render: { node, items, itemStacks: stacks, cursor: meta?.cursor, hasMore: meta?.hasMore ?? false },
|
|
170
|
+
view,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function isNested(rep, roots) {
|
|
175
|
+
// A repeater is nested iff it appears inside another repeater's itemTemplate.
|
|
176
|
+
const walk = (nodes, insideRepeater) => {
|
|
177
|
+
for (const n of nodes) {
|
|
178
|
+
if (n.kind === "repeater") {
|
|
179
|
+
if (n === rep)
|
|
180
|
+
return insideRepeater;
|
|
181
|
+
if (walk([n.itemTemplate], true))
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
else if (n.children) {
|
|
185
|
+
if (walk(n.children, insideRepeater))
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return false;
|
|
190
|
+
};
|
|
191
|
+
return walk(roots, false);
|
|
192
|
+
}
|
|
193
|
+
//# sourceMappingURL=resolution-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolution-service.js","sourceRoot":"","sources":["../../src/resolver/resolution-service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RouteResolver (decision 004 §6): path -> page | detail+instance,
|
|
3
|
+
* preview-vs-public modes, real 404 semantics. Public mode reads ONLY
|
|
4
|
+
* published snapshots. Tenancy is delegated to the CMS (decision 012): the
|
|
5
|
+
* resolver never gates on identity — the CMS authorizes each delegated read.
|
|
6
|
+
*/
|
|
7
|
+
import type { ContentTypeId, InstanceId, Page, PageId } from "@batterai/bind-core";
|
|
8
|
+
import type { Viewer } from "../content-source/viewer";
|
|
9
|
+
import type { ContentSource } from "../content-source/content-source";
|
|
10
|
+
import type { LifecycleStore } from "../lifecycle/lifecycle-store";
|
|
11
|
+
import type { PageMetadataModel } from "../seo/metadata";
|
|
12
|
+
import type { CanvasPage } from "@batterai/contract";
|
|
13
|
+
export type RequestMode = "public" | "preview";
|
|
14
|
+
export type RouteMatch = {
|
|
15
|
+
readonly kind: "page";
|
|
16
|
+
readonly page: Page;
|
|
17
|
+
readonly metadata?: PageMetadataModel;
|
|
18
|
+
} | {
|
|
19
|
+
readonly kind: "detail";
|
|
20
|
+
readonly page: Page;
|
|
21
|
+
readonly metadata?: PageMetadataModel;
|
|
22
|
+
readonly contentTypeId: ContentTypeId;
|
|
23
|
+
readonly instanceId: InstanceId;
|
|
24
|
+
} | {
|
|
25
|
+
readonly kind: "not-found";
|
|
26
|
+
} | {
|
|
27
|
+
readonly kind: "forbidden";
|
|
28
|
+
readonly reason: string;
|
|
29
|
+
};
|
|
30
|
+
/** The lifecycle unit: page tree + its metadata versioned TOGETHER (decision
|
|
31
|
+
* 004 §7.1 — a rollback can't leave stale bound metadata behind). */
|
|
32
|
+
export interface PageRecord {
|
|
33
|
+
readonly page: Page;
|
|
34
|
+
readonly metadata?: PageMetadataModel;
|
|
35
|
+
/** Canvas-owned page fields versioned in the same durable collaboration
|
|
36
|
+
* record as the ordered block tree. CMS authority fields never belong here. */
|
|
37
|
+
readonly canvasSettings?: Omit<CanvasPage, "id" | "root">;
|
|
38
|
+
}
|
|
39
|
+
export interface DetailRoutePattern {
|
|
40
|
+
/** e.g. "/nieuws/" — a path prefix; the remainder is the instance slug/id. */
|
|
41
|
+
readonly prefix: string;
|
|
42
|
+
readonly contentTypeId: ContentTypeId;
|
|
43
|
+
readonly detailPageId: PageId;
|
|
44
|
+
/** Which field carries the slug; "id" means the raw instance id. */
|
|
45
|
+
readonly slugField: string;
|
|
46
|
+
}
|
|
47
|
+
export interface RouteRegistry {
|
|
48
|
+
/** Static page routes: path -> pageId. */
|
|
49
|
+
readonly pages: ReadonlyMap<string, PageId>;
|
|
50
|
+
readonly detailPatterns: readonly DetailRoutePattern[];
|
|
51
|
+
}
|
|
52
|
+
export interface RouteResolverDeps {
|
|
53
|
+
readonly registry: RouteRegistry | undefined;
|
|
54
|
+
readonly lifecycle: LifecycleStore<PageRecord>;
|
|
55
|
+
readonly source: ContentSource;
|
|
56
|
+
/** Resolve a slug to an instance id (adapter-backed; "id" slugField skips). */
|
|
57
|
+
readonly findBySlug: (contentTypeId: ContentTypeId, slugField: string, slug: string, viewer: Viewer) => Promise<InstanceId | undefined>;
|
|
58
|
+
}
|
|
59
|
+
export declare function createRouteResolver(deps: RouteResolverDeps): {
|
|
60
|
+
resolve(path: string, mode: RequestMode, viewer: Viewer): Promise<RouteMatch>;
|
|
61
|
+
/** Public-mode routable URLs — the sitemap's data source. Draft-only and
|
|
62
|
+
* unpublished content never appears. */
|
|
63
|
+
listPublicRoutes(viewer: Viewer): AsyncIterable<{
|
|
64
|
+
path: string;
|
|
65
|
+
lastModified: string;
|
|
66
|
+
}>;
|
|
67
|
+
/** page-id -> public href lookup for portable internal links. Detail
|
|
68
|
+
* routes remain instance-driven; this helper only maps static page ids
|
|
69
|
+
* present in the route registry. */
|
|
70
|
+
hrefForPage(pageId: PageId): string | undefined;
|
|
71
|
+
};
|
|
72
|
+
export type RouteResolver = ReturnType<typeof createRouteResolver>;
|
|
73
|
+
//# sourceMappingURL=route-resolver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"route-resolver.d.ts","sourceRoot":"","sources":["../../src/routing/route-resolver.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
function instanceLastModified(inst, fallback) {
|
|
2
|
+
for (const field of ["updatedAt", "publishedAt", "date"]) {
|
|
3
|
+
const value = inst.values[field];
|
|
4
|
+
if (typeof value === "string" && !Number.isNaN(Date.parse(value)))
|
|
5
|
+
return value;
|
|
6
|
+
}
|
|
7
|
+
return fallback;
|
|
8
|
+
}
|
|
9
|
+
export function createRouteResolver(deps) {
|
|
10
|
+
async function loadPage(pageId, mode, viewer) {
|
|
11
|
+
if (mode === "preview") {
|
|
12
|
+
// Preview requires an authenticated viewer — checked by resolve().
|
|
13
|
+
return ((await deps.lifecycle.getDraft({ kind: "page", pageId }, viewer)) ??
|
|
14
|
+
(await deps.lifecycle.getPublished({ kind: "page", pageId }))?.data);
|
|
15
|
+
}
|
|
16
|
+
// Public: ONLY published snapshots; never a draft fallback.
|
|
17
|
+
return (await deps.lifecycle.getPublished({ kind: "page", pageId }))?.data;
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
async resolve(path, mode, viewer) {
|
|
21
|
+
// No identity gate here (decision 012, invariant D4): every Viewer carries
|
|
22
|
+
// a delegated CMS session, and the CMS authorizes each draft/published
|
|
23
|
+
// read (getDraft/getPublished delegate the session). battery-editor is not
|
|
24
|
+
// a second authority — it never refuses on its own.
|
|
25
|
+
const registry = deps.registry;
|
|
26
|
+
if (!registry)
|
|
27
|
+
return { kind: "not-found" };
|
|
28
|
+
const normalized = path !== "/" && path.endsWith("/") ? path.slice(0, -1) : path;
|
|
29
|
+
const pageId = registry.pages.get(normalized);
|
|
30
|
+
if (pageId) {
|
|
31
|
+
const record = await loadPage(pageId, mode, viewer);
|
|
32
|
+
if (!record)
|
|
33
|
+
return { kind: "not-found" }; // never published => 404, not an empty shell
|
|
34
|
+
return { kind: "page", page: record.page, metadata: record.metadata };
|
|
35
|
+
}
|
|
36
|
+
for (const pattern of registry.detailPatterns) {
|
|
37
|
+
if (!normalized.startsWith(pattern.prefix))
|
|
38
|
+
continue;
|
|
39
|
+
const slug = normalized.slice(pattern.prefix.length);
|
|
40
|
+
if (!slug || slug.includes("/"))
|
|
41
|
+
continue;
|
|
42
|
+
const instanceId = pattern.slugField === "id"
|
|
43
|
+
? slug
|
|
44
|
+
: await deps.findBySlug(pattern.contentTypeId, pattern.slugField, slug, viewer);
|
|
45
|
+
if (!instanceId)
|
|
46
|
+
return { kind: "not-found" };
|
|
47
|
+
const exists = await deps.source.getInstance(pattern.contentTypeId, instanceId, viewer);
|
|
48
|
+
if (!exists)
|
|
49
|
+
return { kind: "not-found" };
|
|
50
|
+
const record = await loadPage(pattern.detailPageId, mode, viewer);
|
|
51
|
+
if (!record)
|
|
52
|
+
return { kind: "not-found" };
|
|
53
|
+
return {
|
|
54
|
+
kind: "detail",
|
|
55
|
+
page: record.page,
|
|
56
|
+
metadata: record.metadata,
|
|
57
|
+
contentTypeId: pattern.contentTypeId,
|
|
58
|
+
instanceId,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
return { kind: "not-found" };
|
|
62
|
+
},
|
|
63
|
+
/** Public-mode routable URLs — the sitemap's data source. Draft-only and
|
|
64
|
+
* unpublished content never appears. */
|
|
65
|
+
async *listPublicRoutes(viewer) {
|
|
66
|
+
const registry = deps.registry;
|
|
67
|
+
if (!registry)
|
|
68
|
+
return;
|
|
69
|
+
for (const [path, pageId] of registry.pages) {
|
|
70
|
+
const snap = await deps.lifecycle.getPublished({ kind: "page", pageId });
|
|
71
|
+
if (snap)
|
|
72
|
+
yield { path, lastModified: snap.publishedAt };
|
|
73
|
+
}
|
|
74
|
+
for (const pattern of registry.detailPatterns) {
|
|
75
|
+
const detailPublished = await deps.lifecycle.getPublished({
|
|
76
|
+
kind: "page",
|
|
77
|
+
pageId: pattern.detailPageId,
|
|
78
|
+
});
|
|
79
|
+
if (!detailPublished)
|
|
80
|
+
continue;
|
|
81
|
+
let cursor;
|
|
82
|
+
do {
|
|
83
|
+
const page = await deps.source.listInstances(pattern.contentTypeId, { pageSize: 100 }, cursor, viewer);
|
|
84
|
+
for (const inst of page.items) {
|
|
85
|
+
const slug = pattern.slugField === "id"
|
|
86
|
+
? inst.id
|
|
87
|
+
: String(inst.values[pattern.slugField] ?? "");
|
|
88
|
+
if (slug) {
|
|
89
|
+
yield {
|
|
90
|
+
path: `${pattern.prefix}${slug}`,
|
|
91
|
+
lastModified: instanceLastModified(inst, detailPublished.publishedAt),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
cursor = page.hasMore ? page.cursor : undefined;
|
|
96
|
+
} while (cursor);
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
/** page-id -> public href lookup for portable internal links. Detail
|
|
100
|
+
* routes remain instance-driven; this helper only maps static page ids
|
|
101
|
+
* present in the route registry. */
|
|
102
|
+
hrefForPage(pageId) {
|
|
103
|
+
const registry = deps.registry;
|
|
104
|
+
if (!registry)
|
|
105
|
+
return undefined;
|
|
106
|
+
for (const [path, candidate] of registry.pages) {
|
|
107
|
+
if (candidate === pageId)
|
|
108
|
+
return path;
|
|
109
|
+
}
|
|
110
|
+
return undefined;
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=route-resolver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"route-resolver.js","sourceRoot":"","sources":["../../src/routing/route-resolver.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structural scope-stack construction (decision 004 §2.1). Pure sync tree
|
|
3
|
+
* shaping; instances attach in the planning waves. StructuralStack is a
|
|
4
|
+
* distinct type so the compiler forces callers through the fill functions
|
|
5
|
+
* before treating a stack as resolve()-ready.
|
|
6
|
+
*/
|
|
7
|
+
import { type ContentType, type ContentTypeId, type Instance, type Page, type RepeaterNode, type ScopeId, type ScopeStack } from "@batterai/bind-core";
|
|
8
|
+
export type StructuralFrame = {
|
|
9
|
+
readonly kind: "page";
|
|
10
|
+
readonly scopeId: ScopeId;
|
|
11
|
+
readonly page: Page;
|
|
12
|
+
} | {
|
|
13
|
+
readonly kind: "repeater-hole";
|
|
14
|
+
readonly scopeId: ScopeId;
|
|
15
|
+
readonly node: RepeaterNode;
|
|
16
|
+
} | {
|
|
17
|
+
readonly kind: "instance-hole";
|
|
18
|
+
readonly scopeId: ScopeId;
|
|
19
|
+
readonly contentTypeId: ContentTypeId;
|
|
20
|
+
};
|
|
21
|
+
export type StructuralStack = readonly StructuralFrame[];
|
|
22
|
+
/**
|
|
23
|
+
* STABLE TEMPLATE SCOPE IDS (host convention, surfaced during the Phase-5
|
|
24
|
+
* build — a gap decision 004 didn't name): templates are authored ONCE but
|
|
25
|
+
* render for MANY instances, so the frame scopeId an authored chip matches
|
|
26
|
+
* must be stable across instances. Sentinels:
|
|
27
|
+
* detail template's instance frame : instance:<ct>:@detail
|
|
28
|
+
* repeater item's instance frame : <repeaterScope>>>>instance:<ct>:@item
|
|
29
|
+
* The frame's DATA varies per item; its identity does not. bind-core's exact
|
|
30
|
+
* findFrame match is untouched — this is purely how the host names frames.
|
|
31
|
+
*/
|
|
32
|
+
export declare function detailScopeId(contentTypeId: ContentTypeId): ScopeId;
|
|
33
|
+
export declare function itemScopeId(repeaterScope: ScopeId, contentTypeId: ContentTypeId): ScopeId;
|
|
34
|
+
export declare function buildPageStack(page: Page): StructuralStack;
|
|
35
|
+
/** Detail-template root: page frame + one instance hole under the STABLE
|
|
36
|
+
* detail scope id (authored chips match every routed instance). */
|
|
37
|
+
export declare function buildDetailStack(page: Page, contentTypeId: ContentTypeId): StructuralStack;
|
|
38
|
+
/** Per-item descent for a repeater: repeater frame (index) + instance frame
|
|
39
|
+
* under the STABLE item scope id. Nested repeaters chain via the parent's
|
|
40
|
+
* item scope so ids never collide across nesting levels. */
|
|
41
|
+
export declare function itemStack(parent: StructuralStack, node: RepeaterNode, index: number, instance: Instance, contentType: ContentType): ScopeStack;
|
|
42
|
+
/** Fill a structural stack whose holes have known data into a real ScopeStack.
|
|
43
|
+
* Holes without data are DROPPED — resolve() throwing MissingData on a ref
|
|
44
|
+
* into a dropped frame is the honest failure, never a fabricated frame. */
|
|
45
|
+
export declare function fillStructural(stack: StructuralStack, instances?: ReadonlyMap<ScopeId, {
|
|
46
|
+
instance: Instance;
|
|
47
|
+
contentType: ContentType;
|
|
48
|
+
}>): ScopeStack;
|
|
49
|
+
//# sourceMappingURL=build-stack.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build-stack.d.ts","sourceRoot":"","sources":["../../src/scope-engine/build-stack.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAIL,KAAK,WAAW,EAChB,KAAK,aAAa,EAClB,KAAK,QAAQ,EACb,KAAK,IAAI,EACT,KAAK,YAAY,EAEjB,KAAK,OAAO,EACZ,KAAK,UAAU,EAChB,MAAM,qBAAqB,CAAC;AAE7B,MAAM,MAAM,eAAe,GACvB;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAA;CAAE,GACzE;IAAE,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAA;CAAE,GAC1F;IAAE,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,aAAa,EAAE,aAAa,CAAA;CAAE,CAAC;AAEzG,MAAM,MAAM,eAAe,GAAG,SAAS,eAAe,EAAE,CAAC;AAEzD;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,aAAa,EAAE,aAAa,GAAG,OAAO,CAEnE;AAED,wBAAgB,WAAW,CAAC,aAAa,EAAE,OAAO,EAAE,aAAa,EAAE,aAAa,GAAG,OAAO,CAEzF;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,IAAI,GAAG,eAAe,CAE1D;AAED;mEACmE;AACnE,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,IAAI,EAAE,aAAa,EAAE,aAAa,GAAG,eAAe,CAK1F;AAED;;4DAE4D;AAC5D,wBAAgB,SAAS,CACvB,MAAM,EAAE,eAAe,EACvB,IAAI,EAAE,YAAY,EAClB,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,QAAQ,EAClB,WAAW,EAAE,WAAW,GACvB,UAAU,CAcZ;AAED;;2EAE2E;AAC3E,wBAAgB,cAAc,CAC5B,KAAK,EAAE,eAAe,EACtB,SAAS,CAAC,EAAE,WAAW,CAAC,OAAO,EAAE;IAAE,QAAQ,EAAE,QAAQ,CAAC;IAAC,WAAW,EAAE,WAAW,CAAA;CAAE,CAAC,GACjF,UAAU,CAmBZ"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structural scope-stack construction (decision 004 §2.1). Pure sync tree
|
|
3
|
+
* shaping; instances attach in the planning waves. StructuralStack is a
|
|
4
|
+
* distinct type so the compiler forces callers through the fill functions
|
|
5
|
+
* before treating a stack as resolve()-ready.
|
|
6
|
+
*/
|
|
7
|
+
import { instanceScopeId, nestedScopeId, pageScopeId, } from "@batterai/bind-core";
|
|
8
|
+
/**
|
|
9
|
+
* STABLE TEMPLATE SCOPE IDS (host convention, surfaced during the Phase-5
|
|
10
|
+
* build — a gap decision 004 didn't name): templates are authored ONCE but
|
|
11
|
+
* render for MANY instances, so the frame scopeId an authored chip matches
|
|
12
|
+
* must be stable across instances. Sentinels:
|
|
13
|
+
* detail template's instance frame : instance:<ct>:@detail
|
|
14
|
+
* repeater item's instance frame : <repeaterScope>>>>instance:<ct>:@item
|
|
15
|
+
* The frame's DATA varies per item; its identity does not. bind-core's exact
|
|
16
|
+
* findFrame match is untouched — this is purely how the host names frames.
|
|
17
|
+
*/
|
|
18
|
+
export function detailScopeId(contentTypeId) {
|
|
19
|
+
return instanceScopeId(contentTypeId, "@detail");
|
|
20
|
+
}
|
|
21
|
+
export function itemScopeId(repeaterScope, contentTypeId) {
|
|
22
|
+
return nestedScopeId(repeaterScope, instanceScopeId(contentTypeId, "@item"));
|
|
23
|
+
}
|
|
24
|
+
export function buildPageStack(page) {
|
|
25
|
+
return [{ kind: "page", scopeId: pageScopeId(page.id), page }];
|
|
26
|
+
}
|
|
27
|
+
/** Detail-template root: page frame + one instance hole under the STABLE
|
|
28
|
+
* detail scope id (authored chips match every routed instance). */
|
|
29
|
+
export function buildDetailStack(page, contentTypeId) {
|
|
30
|
+
return [
|
|
31
|
+
{ kind: "page", scopeId: pageScopeId(page.id), page },
|
|
32
|
+
{ kind: "instance-hole", scopeId: detailScopeId(contentTypeId), contentTypeId },
|
|
33
|
+
];
|
|
34
|
+
}
|
|
35
|
+
/** Per-item descent for a repeater: repeater frame (index) + instance frame
|
|
36
|
+
* under the STABLE item scope id. Nested repeaters chain via the parent's
|
|
37
|
+
* item scope so ids never collide across nesting levels. */
|
|
38
|
+
export function itemStack(parent, node, index, instance, contentType) {
|
|
39
|
+
const parentInstance = [...parent].reverse().find((f) => f.kind === "instance-hole");
|
|
40
|
+
const repScope = parentInstance ? nestedScopeId(parentInstance.scopeId, node.scopeId) : node.scopeId;
|
|
41
|
+
const filled = fillStructural(parent);
|
|
42
|
+
return [
|
|
43
|
+
...filled,
|
|
44
|
+
{ kind: "repeater", scopeId: repScope, index },
|
|
45
|
+
{
|
|
46
|
+
kind: "instance",
|
|
47
|
+
scopeId: itemScopeId(repScope, instance.contentTypeId),
|
|
48
|
+
instance,
|
|
49
|
+
contentType,
|
|
50
|
+
},
|
|
51
|
+
];
|
|
52
|
+
}
|
|
53
|
+
/** Fill a structural stack whose holes have known data into a real ScopeStack.
|
|
54
|
+
* Holes without data are DROPPED — resolve() throwing MissingData on a ref
|
|
55
|
+
* into a dropped frame is the honest failure, never a fabricated frame. */
|
|
56
|
+
export function fillStructural(stack, instances) {
|
|
57
|
+
const out = [];
|
|
58
|
+
for (const f of stack) {
|
|
59
|
+
if (f.kind === "page")
|
|
60
|
+
out.push({ kind: "page", scopeId: f.scopeId, page: f.page });
|
|
61
|
+
else if (f.kind === "instance-hole") {
|
|
62
|
+
const hit = instances?.get(f.scopeId);
|
|
63
|
+
if (hit) {
|
|
64
|
+
out.push({
|
|
65
|
+
kind: "instance",
|
|
66
|
+
scopeId: f.scopeId,
|
|
67
|
+
instance: hit.instance,
|
|
68
|
+
contentType: hit.contentType,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// repeater-holes never appear in a resolve-ready stack; itemStack builds
|
|
73
|
+
// real repeater frames per item.
|
|
74
|
+
}
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=build-stack.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build-stack.js","sourceRoot":"","sources":["../../src/scope-engine/build-stack.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EACL,eAAe,EACf,aAAa,EACb,WAAW,GASZ,MAAM,qBAAqB,CAAC;AAS7B;;;;;;;;;GASG;AACH,MAAM,UAAU,aAAa,CAAC,aAA4B;IACxD,OAAO,eAAe,CAAC,aAAa,EAAE,SAA2B,CAAC,CAAC;AACrE,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,aAAsB,EAAE,aAA4B;IAC9E,OAAO,aAAa,CAAC,aAAa,EAAE,eAAe,CAAC,aAAa,EAAE,OAAyB,CAAC,CAAC,CAAC;AACjG,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,IAAU;IACvC,OAAO,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC;AACjE,CAAC;AAED;mEACmE;AACnE,MAAM,UAAU,gBAAgB,CAAC,IAAU,EAAE,aAA4B;IACvE,OAAO;QACL,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,IAAI,EAAE;QACrD,EAAE,IAAI,EAAE,eAAe,EAAE,OAAO,EAAE,aAAa,CAAC,aAAa,CAAC,EAAE,aAAa,EAAE;KAChF,CAAC;AACJ,CAAC;AAED;;4DAE4D;AAC5D,MAAM,UAAU,SAAS,CACvB,MAAuB,EACvB,IAAkB,EAClB,KAAa,EACb,QAAkB,EAClB,WAAwB;IAExB,MAAM,cAAc,GAAG,CAAC,GAAG,MAAM,CAAC,CAAC,OAAO,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,eAAe,CAAC,CAAC;IACrF,MAAM,QAAQ,GAAG,cAAc,CAAC,CAAC,CAAC,aAAa,CAAC,cAAc,CAAC,OAAO,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC;IACrG,MAAM,MAAM,GAAG,cAAc,CAAC,MAAM,CAAC,CAAC;IACtC,OAAO;QACL,GAAG,MAAM;QACT,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE;QAC9C;YACE,IAAI,EAAE,UAAU;YAChB,OAAO,EAAE,WAAW,CAAC,QAAQ,EAAE,QAAQ,CAAC,aAAa,CAAC;YACtD,QAAQ;YACR,WAAW;SACZ;KACF,CAAC;AACJ,CAAC;AAED;;2EAE2E;AAC3E,MAAM,UAAU,cAAc,CAC5B,KAAsB,EACtB,SAAkF;IAElF,MAAM,GAAG,GAAiB,EAAE,CAAC;IAC7B,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;QACtB,IAAI,CAAC,CAAC,IAAI,KAAK,MAAM;YAAE,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;aAC/E,IAAI,CAAC,CAAC,IAAI,KAAK,eAAe,EAAE,CAAC;YACpC,MAAM,GAAG,GAAG,SAAS,EAAE,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;YACtC,IAAI,GAAG,EAAE,CAAC;gBACR,GAAG,CAAC,IAAI,CAAC;oBACP,IAAI,EAAE,UAAU;oBAChB,OAAO,EAAE,CAAC,CAAC,OAAO;oBAClB,QAAQ,EAAE,GAAG,CAAC,QAAQ;oBACtB,WAAW,EAAE,GAAG,CAAC,WAAW;iBAC7B,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QACD,yEAAyE;QACzE,iCAAiC;IACnC,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Invariant-I filter/sort/limit (decision 004 §2.3): the authoritative,
|
|
3
|
+
* adapter-independent truth pass using bind-core's evaluators — one engine,
|
|
4
|
+
* never N adapter dialects. Order of operations is contract: filter -> sort
|
|
5
|
+
* -> slice(pageSize) — pagination never truncates a filtered set early.
|
|
6
|
+
*/
|
|
7
|
+
import { type DataView, type Instance, type RepeaterQuery, type ScopeStack } from "@batterai/bind-core";
|
|
8
|
+
export declare function applyRepeaterQuery(candidates: readonly Instance[], query: RepeaterQuery, itemStackFor: (instance: Instance, index: number) => ScopeStack, data: DataView): readonly Instance[];
|
|
9
|
+
//# sourceMappingURL=repeater-query.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"repeater-query.d.ts","sourceRoot":"","sources":["../../src/scope-engine/repeater-query.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAGL,KAAK,QAAQ,EACb,KAAK,QAAQ,EACb,KAAK,aAAa,EAClB,KAAK,UAAU,EAChB,MAAM,qBAAqB,CAAC;AAE7B,wBAAgB,kBAAkB,CAChC,UAAU,EAAE,SAAS,QAAQ,EAAE,EAC/B,KAAK,EAAE,aAAa,EACpB,YAAY,EAAE,CAAC,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,KAAK,UAAU,EAC/D,IAAI,EAAE,QAAQ,GACb,SAAS,QAAQ,EAAE,CAgCrB"}
|