@gradial/aci 0.1.51-ssr.4 → 0.1.51-ssr.5
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/README.md +4 -4
- package/dist/next/page.js +7 -2
- package/dist/next/server.d.ts +3 -1
- package/dist/next/server.js +1 -0
- package/dist/providers/object.d.ts +4 -3
- package/dist/providers/object.js +129 -42
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Agentic Content Infrastructure SDK
|
|
2
2
|
|
|
3
|
-
`@gradial/aci` `0.1.51-ssr.
|
|
3
|
+
`@gradial/aci` `0.1.51-ssr.5` is a pre-1.0 public-surface reset for the ACI
|
|
4
4
|
client SDK. Breaking changes from the old `0.1.x` accidental surface are
|
|
5
5
|
intentional: the root import is framework-neutral, framework runtime behavior is
|
|
6
6
|
behind explicit subpaths, and the first required customer path is React/Next.
|
|
@@ -8,7 +8,7 @@ behind explicit subpaths, and the first required customer path is React/Next.
|
|
|
8
8
|
## Install
|
|
9
9
|
|
|
10
10
|
```bash
|
|
11
|
-
npm install @gradial/aci@0.1.51-ssr.
|
|
11
|
+
npm install @gradial/aci@0.1.51-ssr.5 zod@^4
|
|
12
12
|
```
|
|
13
13
|
|
|
14
14
|
## Root Import
|
|
@@ -78,13 +78,13 @@ host bundles select and serve immutable release artifacts.
|
|
|
78
78
|
The first RC scaffold path is Next:
|
|
79
79
|
|
|
80
80
|
```bash
|
|
81
|
-
npx @gradial/aci@0.1.51-ssr.
|
|
81
|
+
npx @gradial/aci@0.1.51-ssr.5 init my-site --template next-starter
|
|
82
82
|
```
|
|
83
83
|
|
|
84
84
|
List available templates:
|
|
85
85
|
|
|
86
86
|
```bash
|
|
87
|
-
npx @gradial/aci@0.1.51-ssr.
|
|
87
|
+
npx @gradial/aci@0.1.51-ssr.5 init --list
|
|
88
88
|
```
|
|
89
89
|
|
|
90
90
|
## Personalization: The One-Tag Install
|
package/dist/next/page.js
CHANGED
|
@@ -69,8 +69,13 @@ export function createPage(registry, options) {
|
|
|
69
69
|
const input = await getRenderInput(resolved.route, context);
|
|
70
70
|
if (!input.page)
|
|
71
71
|
notFound();
|
|
72
|
-
const
|
|
73
|
-
const
|
|
72
|
+
const provider = input.contentProvider;
|
|
73
|
+
const layout = provider
|
|
74
|
+
? await provider.getLayout(input.page.layout)
|
|
75
|
+
: await getLayout(input.page.layout, context);
|
|
76
|
+
const resolvedSlots = await resolveSlots(layout, input.page.regions || {}, (id) => provider
|
|
77
|
+
? provider.getFragment(id)
|
|
78
|
+
: getFragment(id, context));
|
|
74
79
|
const renderedSlots = await Promise.all(layout.slots.map(async (slot) => {
|
|
75
80
|
const blocks = resolvedSlots[slot.name];
|
|
76
81
|
if (!blocks?.length)
|
package/dist/next/server.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { FragmentNotFoundError, LayoutNotFoundError, PageNotFoundError } from '../content/provider.js';
|
|
1
|
+
import { FragmentNotFoundError, LayoutNotFoundError, PageNotFoundError, type ContentProvider } from '../content/provider.js';
|
|
2
2
|
import { type ReleaseObjectReader } from '../providers/object.js';
|
|
3
3
|
export { FragmentNotFoundError, PageNotFoundError, LayoutNotFoundError };
|
|
4
4
|
export interface ServerRenderInput<TPage, TSite extends SiteConfig = SiteConfig> {
|
|
@@ -8,6 +8,8 @@ export interface ServerRenderInput<TPage, TSite extends SiteConfig = SiteConfig>
|
|
|
8
8
|
siteConfig: TSite;
|
|
9
9
|
page: TPage;
|
|
10
10
|
fragments?: Record<string, unknown>;
|
|
11
|
+
/** Internal request-local provider reused by createPage for chrome reads. */
|
|
12
|
+
contentProvider?: ContentProvider;
|
|
11
13
|
}
|
|
12
14
|
export interface SiteConfig {
|
|
13
15
|
domain?: string;
|
package/dist/next/server.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type ContentProvider, type RouteEntry } from '../content/provider.js';
|
|
2
2
|
export interface ReleaseObjectReader {
|
|
3
3
|
readReleaseObject(releaseId: string, path: string): Promise<Uint8Array>;
|
|
4
4
|
}
|
|
@@ -7,7 +7,7 @@ export interface ObjectContentProviderOptions {
|
|
|
7
7
|
pageRef: string;
|
|
8
8
|
reader: ReleaseObjectReader;
|
|
9
9
|
}
|
|
10
|
-
/** Reads one immutable
|
|
10
|
+
/** Reads one exact immutable SSR page release through a host-injected reader. */
|
|
11
11
|
export declare class ObjectContentProvider implements ContentProvider {
|
|
12
12
|
#private;
|
|
13
13
|
readonly releaseId: string;
|
|
@@ -19,6 +19,7 @@ export declare class ObjectContentProvider implements ContentProvider {
|
|
|
19
19
|
getPage<T = unknown>(route: string): Promise<T>;
|
|
20
20
|
getFragment<T = unknown>(id: string): Promise<T>;
|
|
21
21
|
getLayout<T = unknown>(name: string): Promise<T>;
|
|
22
|
-
|
|
22
|
+
private envelope;
|
|
23
|
+
private readSharedJSON;
|
|
23
24
|
private readJSON;
|
|
24
25
|
}
|
package/dist/providers/object.js
CHANGED
|
@@ -1,11 +1,58 @@
|
|
|
1
1
|
import { FragmentNotFoundError, LayoutNotFoundError, PageNotFoundError, } from '../content/provider.js';
|
|
2
2
|
import { normalizeRoute } from '../content/routes.js';
|
|
3
|
-
|
|
3
|
+
const SSR_PAGE_ENVELOPE_SCHEMA = 'gradial.ssr-page.v1';
|
|
4
|
+
const SHARED_CACHE_MAX_ENTRIES = 128;
|
|
5
|
+
const SHARED_CACHE_MAX_BYTES = 16 * 1024 * 1024;
|
|
6
|
+
// Release objects are immutable. This bounded process-local cache needs no
|
|
7
|
+
// invalidation or TTL: an entry is either the exact verified bytes named by a
|
|
8
|
+
// release envelope or it is evicted. Pages and manifests intentionally bypass
|
|
9
|
+
// it so high-cardinality request objects cannot consume the cache.
|
|
10
|
+
class ImmutableSharedObjectCache {
|
|
11
|
+
#entries = new Map();
|
|
12
|
+
#pending = new Map();
|
|
13
|
+
#bytes = 0;
|
|
14
|
+
async read(key, load) {
|
|
15
|
+
const cached = this.#entries.get(key);
|
|
16
|
+
if (cached) {
|
|
17
|
+
this.#entries.delete(key);
|
|
18
|
+
this.#entries.set(key, cached);
|
|
19
|
+
return cached.bytes;
|
|
20
|
+
}
|
|
21
|
+
let pending = this.#pending.get(key);
|
|
22
|
+
if (!pending) {
|
|
23
|
+
pending = load();
|
|
24
|
+
this.#pending.set(key, pending);
|
|
25
|
+
pending.then((bytes) => this.#store(key, bytes), () => undefined)
|
|
26
|
+
.finally(() => this.#pending.delete(key));
|
|
27
|
+
}
|
|
28
|
+
return await pending;
|
|
29
|
+
}
|
|
30
|
+
#store(key, bytes) {
|
|
31
|
+
if (bytes.byteLength > SHARED_CACHE_MAX_BYTES)
|
|
32
|
+
return;
|
|
33
|
+
const existing = this.#entries.get(key);
|
|
34
|
+
if (existing)
|
|
35
|
+
this.#bytes -= existing.size;
|
|
36
|
+
while (this.#entries.size >= SHARED_CACHE_MAX_ENTRIES || this.#bytes + bytes.byteLength > SHARED_CACHE_MAX_BYTES) {
|
|
37
|
+
const oldest = this.#entries.keys().next().value;
|
|
38
|
+
if (oldest === undefined)
|
|
39
|
+
break;
|
|
40
|
+
const evicted = this.#entries.get(oldest);
|
|
41
|
+
this.#entries.delete(oldest);
|
|
42
|
+
if (evicted)
|
|
43
|
+
this.#bytes -= evicted.size;
|
|
44
|
+
}
|
|
45
|
+
this.#entries.set(key, { bytes, size: bytes.byteLength });
|
|
46
|
+
this.#bytes += bytes.byteLength;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const sharedObjectCache = new ImmutableSharedObjectCache();
|
|
50
|
+
/** Reads one exact immutable SSR page release through a host-injected reader. */
|
|
4
51
|
export class ObjectContentProvider {
|
|
5
52
|
releaseId;
|
|
6
53
|
pageRef;
|
|
7
54
|
reader;
|
|
8
|
-
#
|
|
55
|
+
#envelope;
|
|
9
56
|
constructor(options) {
|
|
10
57
|
if (!portableReleaseId(options.releaseId))
|
|
11
58
|
throw new Error('ACI release ID is invalid');
|
|
@@ -15,73 +62,113 @@ export class ObjectContentProvider {
|
|
|
15
62
|
this.pageRef = options.pageRef;
|
|
16
63
|
this.reader = options.reader;
|
|
17
64
|
}
|
|
65
|
+
// Hosted SSR receives an exact page ref from edge routing. Enumerating a
|
|
66
|
+
// whole release would require manifest.json and is intentionally unavailable
|
|
67
|
+
// on this provider; FileContentProvider remains the manifest consumer.
|
|
18
68
|
async listRoutes() {
|
|
19
|
-
|
|
20
|
-
.sort((left, right) => left.path.localeCompare(right.path));
|
|
69
|
+
throw new Error('ACI hosted SSR does not support route enumeration');
|
|
21
70
|
}
|
|
22
71
|
async getSiteConfig() {
|
|
23
|
-
const
|
|
24
|
-
return await this.
|
|
72
|
+
const envelope = await this.envelope();
|
|
73
|
+
return await this.readSharedJSON(envelope.render.site);
|
|
25
74
|
}
|
|
26
75
|
async getPage(route) {
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if (entries.length !== 1)
|
|
31
|
-
throw new PageNotFoundError(route);
|
|
32
|
-
const entry = entries[0];
|
|
33
|
-
const candidates = [
|
|
34
|
-
{ ref: entry.payloadRef, digest: entry.digest },
|
|
35
|
-
...Object.values(entry.variants ?? {}).map((variant) => ({
|
|
36
|
-
ref: variant.payloadRef,
|
|
37
|
-
digest: variant.digest,
|
|
38
|
-
})),
|
|
39
|
-
];
|
|
40
|
-
const exact = candidates.find((candidate) => candidate.ref === this.pageRef);
|
|
41
|
-
if (!exact)
|
|
42
|
-
throw new PageNotFoundError(route, new Error(`selected page ref is not declared: ${this.pageRef}`));
|
|
43
|
-
try {
|
|
44
|
-
return await this.readJSON(exact.ref, exact.digest);
|
|
45
|
-
}
|
|
46
|
-
catch (error) {
|
|
47
|
-
throw new PageNotFoundError(route, error);
|
|
76
|
+
const envelope = await this.envelope();
|
|
77
|
+
if (normalizeRoute(route) !== normalizeRoute(envelope.route)) {
|
|
78
|
+
throw new PageNotFoundError(route, new Error(`selected page route is ${envelope.route}`));
|
|
48
79
|
}
|
|
80
|
+
return envelope.page;
|
|
49
81
|
}
|
|
50
82
|
async getFragment(id) {
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
83
|
+
const envelope = await this.envelope();
|
|
84
|
+
const ref = envelope.render.fragments?.[id];
|
|
85
|
+
if (!ref)
|
|
86
|
+
throw new FragmentNotFoundError(id, new Error('fragment is not bound by the selected page'));
|
|
54
87
|
try {
|
|
55
|
-
return await this.
|
|
88
|
+
return await this.readSharedJSON(ref);
|
|
56
89
|
}
|
|
57
90
|
catch (error) {
|
|
58
91
|
throw new FragmentNotFoundError(id, error);
|
|
59
92
|
}
|
|
60
93
|
}
|
|
61
94
|
async getLayout(name) {
|
|
62
|
-
const
|
|
63
|
-
if (
|
|
64
|
-
throw new LayoutNotFoundError(name);
|
|
95
|
+
const envelope = await this.envelope();
|
|
96
|
+
if (name !== envelope.render.layout.name) {
|
|
97
|
+
throw new LayoutNotFoundError(name, new Error(`selected page layout is ${envelope.render.layout.name}`));
|
|
98
|
+
}
|
|
65
99
|
try {
|
|
66
|
-
return await this.
|
|
100
|
+
return await this.readSharedJSON(envelope.render.layout);
|
|
67
101
|
}
|
|
68
102
|
catch (error) {
|
|
69
103
|
throw new LayoutNotFoundError(name, error);
|
|
70
104
|
}
|
|
71
105
|
}
|
|
72
|
-
async
|
|
73
|
-
this.#
|
|
74
|
-
return await this.#
|
|
106
|
+
async envelope() {
|
|
107
|
+
this.#envelope ??= this.readJSON(this.pageRef).then((value) => validateEnvelope(value, this.pageRef));
|
|
108
|
+
return await this.#envelope;
|
|
109
|
+
}
|
|
110
|
+
async readSharedJSON(ref) {
|
|
111
|
+
const path = cleanObjectPath(ref.payloadRef);
|
|
112
|
+
if (!path || path !== ref.payloadRef)
|
|
113
|
+
throw new Error(`ACI shared object path is invalid: ${ref.payloadRef}`);
|
|
114
|
+
const key = JSON.stringify([this.releaseId, path, ref.digest]);
|
|
115
|
+
const bytes = await sharedObjectCache.read(key, async () => {
|
|
116
|
+
const value = await this.reader.readReleaseObject(this.releaseId, path);
|
|
117
|
+
await verifyDigest(value, ref.digest, path);
|
|
118
|
+
return value;
|
|
119
|
+
});
|
|
120
|
+
return parseJSON(bytes, path);
|
|
75
121
|
}
|
|
76
|
-
async readJSON(path
|
|
122
|
+
async readJSON(path) {
|
|
77
123
|
const clean = cleanObjectPath(path);
|
|
78
124
|
if (!clean || clean !== path)
|
|
79
125
|
throw new Error(`ACI release object path is invalid: ${path}`);
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
126
|
+
return parseJSON(await this.reader.readReleaseObject(this.releaseId, clean), clean);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function validateEnvelope(value, pageRef) {
|
|
130
|
+
if (!isObject(value) || value.schemaVersion !== SSR_PAGE_ENVELOPE_SCHEMA || typeof value.route !== 'string' || !isObject(value.render)) {
|
|
131
|
+
throw new Error(`ACI SSR page envelope is invalid: ${pageRef}`);
|
|
132
|
+
}
|
|
133
|
+
const render = value.render;
|
|
134
|
+
if (!validRef(render.site) || !isObject(render.layout) || typeof render.layout.name !== 'string' || !validRef(render.layout)) {
|
|
135
|
+
throw new Error(`ACI SSR page envelope bindings are invalid: ${pageRef}`);
|
|
136
|
+
}
|
|
137
|
+
const fragments = {};
|
|
138
|
+
if (render.fragments !== undefined) {
|
|
139
|
+
if (!isObject(render.fragments))
|
|
140
|
+
throw new Error(`ACI SSR page fragments are invalid: ${pageRef}`);
|
|
141
|
+
for (const [id, ref] of Object.entries(render.fragments)) {
|
|
142
|
+
if (!id || !validRef(ref))
|
|
143
|
+
throw new Error(`ACI SSR page fragment binding is invalid: ${pageRef}`);
|
|
144
|
+
fragments[id] = ref;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
schemaVersion: SSR_PAGE_ENVELOPE_SCHEMA,
|
|
149
|
+
route: value.route,
|
|
150
|
+
variantKey: typeof value.variantKey === 'string' ? value.variantKey : '',
|
|
151
|
+
page: value.page,
|
|
152
|
+
render: {
|
|
153
|
+
site: render.site,
|
|
154
|
+
layout: { name: render.layout.name, payloadRef: render.layout.payloadRef, digest: render.layout.digest },
|
|
155
|
+
fragments,
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
function validRef(value) {
|
|
160
|
+
return isObject(value) && typeof value.payloadRef === 'string' && cleanObjectPath(value.payloadRef) === value.payloadRef && /^sha256:[0-9a-f]{64}$/.test(String(value.digest));
|
|
161
|
+
}
|
|
162
|
+
function isObject(value) {
|
|
163
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
164
|
+
}
|
|
165
|
+
function parseJSON(bytes, path) {
|
|
166
|
+
try {
|
|
83
167
|
return JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
|
|
84
168
|
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
throw new Error(`ACI release object is not valid JSON: ${path}`, { cause: error });
|
|
171
|
+
}
|
|
85
172
|
}
|
|
86
173
|
async function verifyDigest(bytes, expected, path) {
|
|
87
174
|
const match = /^sha256:([0-9a-f]{64})$/.exec(expected);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gradial/aci",
|
|
3
|
-
"version": "0.1.51-ssr.
|
|
3
|
+
"version": "0.1.51-ssr.5",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/gradial/bare-metal-go.git",
|
|
@@ -146,11 +146,11 @@
|
|
|
146
146
|
"zod": "^4.0.0"
|
|
147
147
|
},
|
|
148
148
|
"optionalDependencies": {
|
|
149
|
-
"@gradial/aci-cli-darwin-arm64": "0.1.51-ssr.
|
|
150
|
-
"@gradial/aci-cli-darwin-x64": "0.1.51-ssr.
|
|
151
|
-
"@gradial/aci-cli-linux-arm64": "0.1.51-ssr.
|
|
152
|
-
"@gradial/aci-cli-linux-x64": "0.1.51-ssr.
|
|
153
|
-
"@gradial/aci-cli-win32-arm64": "0.1.51-ssr.
|
|
154
|
-
"@gradial/aci-cli-win32-x64": "0.1.51-ssr.
|
|
149
|
+
"@gradial/aci-cli-darwin-arm64": "0.1.51-ssr.5",
|
|
150
|
+
"@gradial/aci-cli-darwin-x64": "0.1.51-ssr.5",
|
|
151
|
+
"@gradial/aci-cli-linux-arm64": "0.1.51-ssr.5",
|
|
152
|
+
"@gradial/aci-cli-linux-x64": "0.1.51-ssr.5",
|
|
153
|
+
"@gradial/aci-cli-win32-arm64": "0.1.51-ssr.5",
|
|
154
|
+
"@gradial/aci-cli-win32-x64": "0.1.51-ssr.5"
|
|
155
155
|
}
|
|
156
156
|
}
|