@gradial/aci 0.1.7 → 0.1.9

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.
@@ -0,0 +1,25 @@
1
+ import type { ContentProvider, CompiledManifest } from './provider.js';
2
+ export declare function getCacheStats(): {
3
+ manifest: {
4
+ hits: number;
5
+ misses: number;
6
+ };
7
+ siteConfig: {
8
+ hits: number;
9
+ misses: number;
10
+ };
11
+ };
12
+ export declare function resetCacheStats(): void;
13
+ /**
14
+ * Get manifest with caching. Use this instead of calling provider.manifest() directly.
15
+ */
16
+ export declare function getCachedManifest(provider: ContentProvider): Promise<CompiledManifest>;
17
+ /**
18
+ * Get site config with caching. Use this instead of calling provider.getSiteConfig() directly.
19
+ */
20
+ export declare function getCachedSiteConfig<T>(provider: ContentProvider): Promise<T>;
21
+ /**
22
+ * Clear cached data for a specific provider.
23
+ * Useful for testing or manual cache invalidation.
24
+ */
25
+ export declare function clearProviderCache(provider: ContentProvider): void;
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Framework-agnostic content caching layer.
3
+ *
4
+ * Caches shared resources (manifest, siteConfig) across requests
5
+ * to reduce S3 fetches on warm Lambda/serverless containers.
6
+ *
7
+ * Keyed by provider instance for automatic invalidation when
8
+ * provider config changes (e.g., new releaseId).
9
+ */
10
+ // Cross-request cache - survives serverless warm starts
11
+ const manifestCache = new WeakMap();
12
+ const siteConfigCache = new WeakMap();
13
+ // Cache hit/miss stats for observability
14
+ let manifestHits = 0;
15
+ let manifestMisses = 0;
16
+ let siteConfigHits = 0;
17
+ let siteConfigMisses = 0;
18
+ export function getCacheStats() {
19
+ return {
20
+ manifest: { hits: manifestHits, misses: manifestMisses },
21
+ siteConfig: { hits: siteConfigHits, misses: siteConfigMisses },
22
+ };
23
+ }
24
+ export function resetCacheStats() {
25
+ manifestHits = 0;
26
+ manifestMisses = 0;
27
+ siteConfigHits = 0;
28
+ siteConfigMisses = 0;
29
+ }
30
+ /**
31
+ * Get manifest with caching. Use this instead of calling provider.manifest() directly.
32
+ */
33
+ export function getCachedManifest(provider) {
34
+ const cached = manifestCache.get(provider);
35
+ if (cached) {
36
+ manifestHits++;
37
+ console.log('[ACI Cache] manifest HIT - serving from cache');
38
+ return cached;
39
+ }
40
+ manifestMisses++;
41
+ console.log('[ACI Cache] manifest MISS - fetching from provider');
42
+ // Only S3ContentProvider has manifest(), FileContentProvider doesn't
43
+ if (!('manifest' in provider) || typeof provider.manifest !== 'function') {
44
+ throw new Error('Provider does not support manifest()');
45
+ }
46
+ const promise = provider.manifest().catch((error) => {
47
+ console.error('[ACI Cache] manifest fetch FAILED:', error.message);
48
+ // Clear cache on error to allow retry on next request
49
+ manifestCache.delete(provider);
50
+ throw error;
51
+ });
52
+ manifestCache.set(provider, promise);
53
+ return promise;
54
+ }
55
+ /**
56
+ * Get site config with caching. Use this instead of calling provider.getSiteConfig() directly.
57
+ */
58
+ export function getCachedSiteConfig(provider) {
59
+ const cached = siteConfigCache.get(provider);
60
+ if (cached) {
61
+ siteConfigHits++;
62
+ console.log('[ACI Cache] siteConfig HIT - serving from cache');
63
+ return cached;
64
+ }
65
+ siteConfigMisses++;
66
+ console.log('[ACI Cache] siteConfig MISS - fetching from provider');
67
+ const promise = provider.getSiteConfig().catch((error) => {
68
+ console.error('[ACI Cache] siteConfig fetch FAILED:', error.message);
69
+ // Clear cache on error to allow retry on next request
70
+ siteConfigCache.delete(provider);
71
+ throw error;
72
+ });
73
+ siteConfigCache.set(provider, promise);
74
+ return promise;
75
+ }
76
+ /**
77
+ * Clear cached data for a specific provider.
78
+ * Useful for testing or manual cache invalidation.
79
+ */
80
+ export function clearProviderCache(provider) {
81
+ manifestCache.delete(provider);
82
+ siteConfigCache.delete(provider);
83
+ }
@@ -1,3 +1,4 @@
1
1
  export * from './provider.js';
2
2
  export * from './routes.js';
3
3
  export * from './types.js';
4
+ export * from './cache.js';
@@ -1,3 +1,4 @@
1
1
  export * from './provider.js';
2
2
  export * from './routes.js';
3
3
  export * from './types.js';
4
+ export * from './cache.js';
@@ -16,10 +16,11 @@ export class FragmentNotFoundError extends Error {
16
16
  this.cause = cause;
17
17
  }
18
18
  }
19
+ import { getCachedSiteConfig } from './cache.js';
19
20
  export async function loadRenderInput(provider, route = '/', options = {}) {
20
21
  const normalized = normalizeRoute(route);
21
22
  const [siteConfig, page] = await Promise.all([
22
- provider.getSiteConfig(),
23
+ getCachedSiteConfig(provider),
23
24
  provider.getPage(normalized).catch((error) => {
24
25
  if (error instanceof PageNotFoundError)
25
26
  return null;
@@ -1,5 +1,6 @@
1
1
  import { headers } from 'next/headers.js';
2
2
  import { FragmentNotFoundError, PageNotFoundError, } from '../content/provider.js';
3
+ import { getCachedManifest, getCachedSiteConfig } from '../content/cache.js';
3
4
  import { S3ContentProvider } from '../providers/s3.js';
4
5
  import { GRADIAL_RENDER_INPUT_HEADER, getPendingRenderInput } from '../runtime/page.js';
5
6
  import { getUncachedEdgeConfigValue } from './edge-config.js';
@@ -25,7 +26,6 @@ export async function getFragment(name, config = {}) {
25
26
  const fragments = runtimeInput.fragments || {};
26
27
  if (name in fragments)
27
28
  return fragments[name];
28
- throw new FragmentNotFoundError(name);
29
29
  }
30
30
  const provider = await resolveProviderWithFallback(config);
31
31
  return await provider.getFragment(name);
@@ -36,15 +36,23 @@ export async function getRoutes(config = {}) {
36
36
  return routes.map((route) => route.path).sort();
37
37
  }
38
38
  export async function getRenderInput(route = '/', config = {}) {
39
+ const startTime = Date.now();
39
40
  const runtimeInput = await getPageRuntimeRenderInput();
40
41
  if (runtimeInput) {
41
42
  return runtimeInput;
42
43
  }
44
+ const providerStart = Date.now();
43
45
  const provider = await resolveProviderWithFallback(config);
46
+ console.log(`[ACI SSR] Provider resolved in ${Date.now() - providerStart}ms`);
47
+ // Cache shared resources (manifest, siteConfig) but not per-page content
48
+ // Page-level caching is handled by CDN
49
+ const fetchStart = Date.now();
44
50
  const [page, siteConfig] = await Promise.all([
45
51
  provider.getPage(route),
46
- provider.getSiteConfig(),
52
+ getCachedSiteConfig(provider),
47
53
  ]);
54
+ console.log(`[ACI SSR] Content fetched in ${Date.now() - fetchStart}ms`);
55
+ console.log(`[ACI SSR] Total getRenderInput for ${route}: ${Date.now() - startTime}ms`);
48
56
  return {
49
57
  route,
50
58
  domain: siteConfig.domain || 'www.aci.local',
@@ -146,7 +154,7 @@ export async function resolveReleaseId(config = {}) {
146
154
  }
147
155
  async function resolveProvider(config) {
148
156
  const releaseId = await resolveReleaseId(config);
149
- return new S3ContentProvider({
157
+ const provider = new S3ContentProvider({
150
158
  bucket: config.bucket,
151
159
  keyPrefix: releaseKey(config, releaseId),
152
160
  region: config.region,
@@ -155,6 +163,9 @@ async function resolveProvider(config) {
155
163
  sessionToken: config.sessionToken,
156
164
  endpoint: config.endpoint,
157
165
  });
166
+ // Pre-warm the manifest with cached version
167
+ await getCachedManifest(provider);
168
+ return provider;
158
169
  }
159
170
  async function resolveProviderWithFallback(config) {
160
171
  if (process.env.ACI_CONTENT_PROVIDER === 'file') {
@@ -253,3 +264,5 @@ function joinKey(...parts) {
253
264
  .filter(Boolean)
254
265
  .join('/');
255
266
  }
267
+ // Note: React cache() would be redundant here since getCachedSiteConfig already
268
+ // returns the same Promise for the same provider instance within a request
@@ -36,23 +36,14 @@ export class FileContentProvider {
36
36
  async getFragment(id) {
37
37
  const manifest = await this.manifest();
38
38
  const entry = manifest.fragments[id];
39
- if (entry) {
40
- try {
41
- return await this.readJSON(entry.payloadRef);
42
- }
43
- catch (error) {
44
- throw new FragmentNotFoundError(id, error);
45
- }
39
+ if (!entry) {
40
+ throw new FragmentNotFoundError(id);
46
41
  }
47
- // Raw content directory stores fragments as flat files
48
- const contentRoot = process.env.ACI_CONTENT_DIR || path.join(path.resolve(process.cwd()), '.content');
49
- const rawPath = path.join(contentRoot, 'fragments', `${id}.json`);
50
42
  try {
51
- const raw = await fs.readFile(rawPath, 'utf8');
52
- return JSON.parse(raw);
43
+ return await this.readJSON(entry.payloadRef);
53
44
  }
54
- catch {
55
- throw new FragmentNotFoundError(id);
45
+ catch (error) {
46
+ throw new FragmentNotFoundError(id, error);
56
47
  }
57
48
  }
58
49
  async manifest() {
@@ -60,6 +60,7 @@ export class S3ContentProvider {
60
60
  // S3 fetch with AWS SigV4 signing
61
61
  // ---------------------------------------------------------------------------
62
62
  export async function getS3JSON(key, config) {
63
+ const startTime = Date.now();
63
64
  const url = s3ObjectURL(config, key);
64
65
  const signedHeaders = signedS3Headers('GET', url, config);
65
66
  const res = await fetch(url, {
@@ -67,6 +68,8 @@ export async function getS3JSON(key, config) {
67
68
  headers: signedHeaders,
68
69
  cache: 'no-store'
69
70
  });
71
+ const duration = Date.now() - startTime;
72
+ console.log(`[ACI S3] GET ${key} - ${res.status} in ${duration}ms`);
70
73
  if (!res.ok) {
71
74
  throw new Error(`S3 GET ${key} failed with status ${res.status}: ${await res.text()}`);
72
75
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gradial/aci",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",