@tyndall/dynamic-manifest 0.0.1

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 ADDED
@@ -0,0 +1,27 @@
1
+ # @tyndall/dynamic-manifest
2
+
3
+ ## Overview
4
+ Dynamic manifest resolver package for remote page resolution, caching, and policy-aware retrieval.
5
+
6
+ ## Responsibilities
7
+ - Resolve dynamic page definitions from HTTP providers
8
+ - Cache responses with etag and ttl semantics
9
+ - Expose resolver interfaces shared by runtime, dev, and client paths
10
+
11
+ ## Public API Highlights
12
+ - createHttpResolver
13
+ - createDynamicManifestResolver
14
+
15
+ ## Development
16
+ - Build: bun run --filter @tyndall/dynamic-manifest build
17
+ - Test (from workspace root): bun test
18
+
19
+ ## Documentation
20
+ - Package specification: [spec.md](./spec.md)
21
+ - Package architecture: [architecture.md](./architecture.md)
22
+ - Package changes: [CHANGELOG.md](./CHANGELOG.md)
23
+
24
+ ## Maintenance Rules
25
+ - Keep this document aligned with implemented package behavior.
26
+ - Update spec.md and architecture.md whenever package contracts or design boundaries change.
27
+ - Record user-visible package changes in CHANGELOG.md.
@@ -0,0 +1,19 @@
1
+ import type { DynamicPageEnvelope } from "@tyndall/core";
2
+ export interface DynamicManifestCacheOptions {
3
+ ttlMs?: number;
4
+ maxEntries?: number;
5
+ now?: () => number;
6
+ }
7
+ export interface DynamicManifestCacheEntry {
8
+ envelope: DynamicPageEnvelope;
9
+ etag?: string;
10
+ expiresAt: number;
11
+ }
12
+ export interface DynamicManifestCache {
13
+ get: (key: string) => DynamicManifestCacheEntry | null;
14
+ set: (key: string, envelope: DynamicPageEnvelope, etag?: string) => void;
15
+ delete: (key: string) => void;
16
+ clear: () => void;
17
+ }
18
+ export declare const createDynamicManifestCache: (options?: DynamicManifestCacheOptions) => DynamicManifestCache;
19
+ //# sourceMappingURL=cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAEzD,MAAM,WAAW,2BAA2B;IAC1C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,yBAAyB;IACxC,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,oBAAoB;IACnC,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,yBAAyB,GAAG,IAAI,CAAC;IACvD,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,mBAAmB,EAAE,IAAI,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IACzE,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9B,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;AAED,eAAO,MAAM,0BAA0B,GACrC,UAAS,2BAAgC,KACxC,oBA6CF,CAAC"}
package/dist/cache.js ADDED
@@ -0,0 +1,44 @@
1
+ export const createDynamicManifestCache = (options = {}) => {
2
+ const ttlMs = options.ttlMs ?? 5 * 60000;
3
+ const maxEntries = options.maxEntries ?? 100;
4
+ const now = options.now ?? (() => Date.now());
5
+ const entries = new Map();
6
+ const touch = (key, entry) => {
7
+ entries.delete(key);
8
+ entries.set(key, entry);
9
+ };
10
+ return {
11
+ get: (key) => {
12
+ const entry = entries.get(key);
13
+ if (!entry) {
14
+ return null;
15
+ }
16
+ if (entry.expiresAt <= now()) {
17
+ entries.delete(key);
18
+ return null;
19
+ }
20
+ touch(key, entry);
21
+ return entry;
22
+ },
23
+ set: (key, envelope, etag) => {
24
+ const entry = {
25
+ envelope,
26
+ etag,
27
+ expiresAt: now() + ttlMs,
28
+ };
29
+ entries.set(key, entry);
30
+ if (entries.size > maxEntries) {
31
+ const oldestKey = entries.keys().next().value;
32
+ if (oldestKey) {
33
+ entries.delete(oldestKey);
34
+ }
35
+ }
36
+ },
37
+ delete: (key) => {
38
+ entries.delete(key);
39
+ },
40
+ clear: () => {
41
+ entries.clear();
42
+ },
43
+ };
44
+ };
@@ -0,0 +1,18 @@
1
+ import type { DynamicPageComponentRegistry, DynamicPageEnvelope, DynamicPageResolveInput } from "@tyndall/core";
2
+ import type { DynamicManifestCache } from "./cache.js";
3
+ export interface DynamicManifestHandler {
4
+ resolveByPath: (input: DynamicPageResolveInput) => Promise<DynamicPageEnvelope | null>;
5
+ resolveById: (pageId: string) => Promise<DynamicPageEnvelope | null>;
6
+ listComponents?: (filters?: Record<string, string>) => Promise<DynamicPageComponentRegistry | null>;
7
+ }
8
+ export type DynamicManifestResolver = (path: string) => Promise<DynamicPageEnvelope | null>;
9
+ export interface DynamicManifestResolverOptions {
10
+ baseUrl?: string;
11
+ cache?: DynamicManifestCache;
12
+ fetcher?: typeof fetch;
13
+ ttlMs?: number;
14
+ maxEntries?: number;
15
+ }
16
+ export declare const createHttpResolver: (options?: DynamicManifestResolverOptions) => DynamicManifestHandler;
17
+ export declare const createDynamicManifestResolver: (options?: DynamicManifestResolverOptions) => DynamicManifestResolver;
18
+ //# sourceMappingURL=http-resolver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http-resolver.d.ts","sourceRoot":"","sources":["../src/http-resolver.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,4BAA4B,EAC5B,mBAAmB,EACnB,uBAAuB,EACxB,MAAM,eAAe,CAAC;AAEvB,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAGvD,MAAM,WAAW,sBAAsB;IACrC,aAAa,EAAE,CAAC,KAAK,EAAE,uBAAuB,KAAK,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC,CAAC;IACvF,WAAW,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC,CAAC;IACrE,cAAc,CAAC,EAAE,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,OAAO,CAAC,4BAA4B,GAAG,IAAI,CAAC,CAAC;CACrG;AAED,MAAM,MAAM,uBAAuB,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC,CAAC;AAE5F,MAAM,WAAW,8BAA8B;IAC7C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,oBAAoB,CAAC;IAC7B,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AA0CD,eAAO,MAAM,kBAAkB,GAC7B,UAAS,8BAAmC,KAC3C,sBA8CF,CAAC;AAEF,eAAO,MAAM,6BAA6B,GACxC,UAAS,8BAAmC,KAC3C,uBAGF,CAAC"}
@@ -0,0 +1,82 @@
1
+ import { paramsKey } from "@tyndall/shared";
2
+ import { createDynamicManifestCache } from "./cache.js";
3
+ const resolveBaseUrl = (baseUrl) => {
4
+ if (baseUrl) {
5
+ return baseUrl;
6
+ }
7
+ if (typeof globalThis !== "undefined" && "location" in globalThis) {
8
+ const location = globalThis.location;
9
+ if (location?.origin) {
10
+ return location.origin;
11
+ }
12
+ }
13
+ return "http://localhost";
14
+ };
15
+ const fetchEnvelope = async (key, url, cache, fetcher) => {
16
+ const cached = cache.get(key);
17
+ const headers = {};
18
+ if (cached?.etag) {
19
+ headers["If-None-Match"] = cached.etag;
20
+ }
21
+ const response = await fetcher(url.toString(), { method: "GET", headers });
22
+ if (response.status === 304 && cached) {
23
+ cache.set(key, cached.envelope, cached.etag);
24
+ return cached.envelope;
25
+ }
26
+ if (!response.ok) {
27
+ return null;
28
+ }
29
+ const envelope = (await response.json());
30
+ const etag = response.headers.get("ETag") ?? response.headers.get("etag") ?? undefined;
31
+ cache.set(key, envelope, etag);
32
+ return envelope;
33
+ };
34
+ export const createHttpResolver = (options = {}) => {
35
+ const cache = options.cache ??
36
+ createDynamicManifestCache({
37
+ ttlMs: options.ttlMs,
38
+ maxEntries: options.maxEntries,
39
+ });
40
+ const fetcher = options.fetcher ?? fetch;
41
+ const baseUrl = resolveBaseUrl(options.baseUrl);
42
+ return {
43
+ resolveByPath: async (input) => {
44
+ const key = `path:${paramsKey(input)}`;
45
+ const url = new URL("/api/pages/resolve", baseUrl);
46
+ url.searchParams.set("path", input.path);
47
+ if (input.locale) {
48
+ url.searchParams.set("locale", input.locale);
49
+ }
50
+ if (input.variant) {
51
+ url.searchParams.set("variant", input.variant);
52
+ }
53
+ if (input.device) {
54
+ url.searchParams.set("device", input.device);
55
+ }
56
+ if (input.ctxHash) {
57
+ url.searchParams.set("ctxHash", input.ctxHash);
58
+ }
59
+ return fetchEnvelope(key, url, cache, fetcher);
60
+ },
61
+ resolveById: async (pageId) => {
62
+ const key = `id:${pageId}`;
63
+ const url = new URL(`/api/pages/${encodeURIComponent(pageId)}`, baseUrl);
64
+ return fetchEnvelope(key, url, cache, fetcher);
65
+ },
66
+ listComponents: async (filters = {}) => {
67
+ const url = new URL("/api/components/registry", baseUrl);
68
+ for (const [key, value] of Object.entries(filters)) {
69
+ url.searchParams.set(key, value);
70
+ }
71
+ const response = await fetcher(url.toString(), { method: "GET" });
72
+ if (!response.ok) {
73
+ return null;
74
+ }
75
+ return (await response.json());
76
+ },
77
+ };
78
+ };
79
+ export const createDynamicManifestResolver = (options = {}) => {
80
+ const handler = createHttpResolver(options);
81
+ return async (path) => handler.resolveByPath({ path });
82
+ };
@@ -0,0 +1,9 @@
1
+ import type { HyperPlugin } from "@tyndall/core";
2
+ export { createDynamicManifestCache } from "./cache.js";
3
+ export type { DynamicManifestCache, DynamicManifestCacheEntry, DynamicManifestCacheOptions, } from "./cache.js";
4
+ export { createHttpResolver, createDynamicManifestResolver } from "./http-resolver.js";
5
+ export type { DynamicManifestHandler, DynamicManifestResolver, DynamicManifestResolverOptions, } from "./http-resolver.js";
6
+ export { DEFAULT_PROGRAM_SANDBOX_POLICY, buildProgramSandboxContext, dynamicManifestSchema, resolveProgramSandboxPolicy, validateDynamicManifest, validateDynamicManifest as validateManifest, validateProgramIntegrity, } from "@tyndall/core";
7
+ export type { DynamicPageCache, DynamicPageDefinition, DynamicPageEnvelope, DynamicPageMode, DynamicPagePolicy, MetaDefinition, ProgramDefinition, } from "@tyndall/core";
8
+ export declare const registerDynamicManifestModule: () => HyperPlugin;
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAEjD,OAAO,EAAE,0BAA0B,EAAE,MAAM,YAAY,CAAC;AACxD,YAAY,EACV,oBAAoB,EACpB,yBAAyB,EACzB,2BAA2B,GAC5B,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,kBAAkB,EAAE,6BAA6B,EAAE,MAAM,oBAAoB,CAAC;AACvF,YAAY,EACV,sBAAsB,EACtB,uBAAuB,EACvB,8BAA8B,GAC/B,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EACL,8BAA8B,EAC9B,0BAA0B,EAC1B,qBAAqB,EACrB,2BAA2B,EAC3B,uBAAuB,EACvB,uBAAuB,IAAI,gBAAgB,EAC3C,wBAAwB,GACzB,MAAM,eAAe,CAAC;AACvB,YAAY,EACV,gBAAgB,EAChB,qBAAqB,EACrB,mBAAmB,EACnB,eAAe,EACf,iBAAiB,EACjB,cAAc,EACd,iBAAiB,GAClB,MAAM,eAAe,CAAC;AAEvB,eAAO,MAAM,6BAA6B,QAAO,WAiB/C,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,21 @@
1
+ export { createDynamicManifestCache } from "./cache.js";
2
+ export { createHttpResolver, createDynamicManifestResolver } from "./http-resolver.js";
3
+ export { DEFAULT_PROGRAM_SANDBOX_POLICY, buildProgramSandboxContext, dynamicManifestSchema, resolveProgramSandboxPolicy, validateDynamicManifest, validateDynamicManifest as validateManifest, validateProgramIntegrity, } from "@tyndall/core";
4
+ export const registerDynamicManifestModule = () => ({
5
+ name: "@tyndall/dynamic-manifest",
6
+ configResolved: (config) => {
7
+ if (config.modules.dynamicManifest.enabled) {
8
+ return;
9
+ }
10
+ return {
11
+ ...config,
12
+ modules: {
13
+ ...config.modules,
14
+ dynamicManifest: {
15
+ ...config.modules.dynamicManifest,
16
+ enabled: true,
17
+ },
18
+ },
19
+ };
20
+ },
21
+ });
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@tyndall/dynamic-manifest",
3
+ "version": "0.0.1",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
7
+ "type": "module",
8
+ "main": "dist/index.js",
9
+ "types": "dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "bun": "./src/index.ts",
14
+ "default": "./dist/index.js"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist"
19
+ ],
20
+ "scripts": {
21
+ "build": "tsc -p tsconfig.json"
22
+ },
23
+ "dependencies": {
24
+ "@tyndall/core": "workspace:*",
25
+ "@tyndall/shared": "workspace:*"
26
+ }
27
+ }