1 | import { NotFoundError } from "./errors";
|
2 | import { type Awaitable } from "./types";
|
3 |
|
4 | import { createDebugger } from "./lib/createDebugger";
|
5 | import { pathToFilePath } from "./lib/pathToFilePath";
|
6 |
|
7 |
|
8 |
|
9 | import type { AkteApp } from "./AkteApp";
|
10 |
|
11 |
|
12 |
|
13 | type Path<
|
14 | TParams extends string[],
|
15 | TPrefix extends string = string,
|
16 | > = TParams extends []
|
17 | ? ""
|
18 | : TParams extends [string]
|
19 | ? `${TPrefix}:${TParams[0]}${string}`
|
20 | : TParams extends readonly [string, ...infer Rest extends string[]]
|
21 | ? Path<Rest, `${TPrefix}:${TParams[0]}${string}`>
|
22 | : string;
|
23 |
|
24 |
|
25 |
|
26 |
|
27 |
|
28 |
|
29 | export type FilesDataFn<
|
30 | TGlobalData,
|
31 | TParams extends string[],
|
32 | TData,
|
33 | > = (context: {
|
34 |
|
35 | path: string;
|
36 |
|
37 |
|
38 | params: Record<TParams[number], string>;
|
39 |
|
40 |
|
41 | globalData: TGlobalData;
|
42 | }) => Awaitable<TData>;
|
43 |
|
44 |
|
45 | export type FilesBulkDataFn<TGlobalData, TData> = (context: {
|
46 |
|
47 | globalData: TGlobalData;
|
48 | }) => Awaitable<Record<string, TData>>;
|
49 |
|
50 | export type FilesDefinition<TGlobalData, TParams extends string[], TData> = {
|
51 | |
52 |
|
53 |
|
54 |
|
55 |
|
56 |
|
57 |
|
58 |
|
59 |
|
60 |
|
61 |
|
62 |
|
63 | path: Path<TParams>;
|
64 |
|
65 | |
66 |
|
67 |
|
68 |
|
69 |
|
70 |
|
71 |
|
72 | data?: FilesDataFn<TGlobalData, TParams, TData>;
|
73 |
|
74 |
|
75 | bulkData?: FilesBulkDataFn<TGlobalData, TData>;
|
76 |
|
77 | |
78 |
|
79 |
|
80 |
|
81 |
|
82 |
|
83 |
|
84 | render: (context: {
|
85 |
|
86 | path: string;
|
87 |
|
88 |
|
89 | globalData: TGlobalData;
|
90 |
|
91 |
|
92 | data: TData;
|
93 | }) => Awaitable<string>;
|
94 | };
|
95 |
|
96 | const debug = createDebugger("akte:files");
|
97 | const debugRender = createDebugger("akte:files:render");
|
98 | const debugCache = createDebugger("akte:files:cache");
|
99 |
|
100 |
|
101 | export class AkteFiles<
|
102 | TGlobalData = unknown,
|
103 | TParams extends string[] = string[],
|
104 |
|
105 | TData = any,
|
106 | > {
|
107 | protected definition: FilesDefinition<TGlobalData, TParams, TData>;
|
108 |
|
109 |
|
110 | get path(): string {
|
111 | return this.definition.path;
|
112 | }
|
113 |
|
114 | constructor(definition: FilesDefinition<TGlobalData, TParams, TData>) {
|
115 | this.definition = definition;
|
116 |
|
117 | debug("created %o", this.path);
|
118 | }
|
119 |
|
120 |
|
121 | async render(args: {
|
122 | path: string;
|
123 | params: Record<TParams[number], string>;
|
124 | globalData: TGlobalData;
|
125 | }): Promise<string> {
|
126 | const data = await this.getDataPromise(args);
|
127 |
|
128 | return this.definition.render({
|
129 | path: args.path,
|
130 | globalData: args.globalData,
|
131 | data,
|
132 | });
|
133 | }
|
134 |
|
135 |
|
136 | async renderAll(args: {
|
137 | globalData: TGlobalData;
|
138 | }): Promise<Record<string, string>> {
|
139 | if (!this.definition.bulkData) {
|
140 | debugRender("no files to render %o", this.path);
|
141 |
|
142 | return {};
|
143 | }
|
144 |
|
145 | debugRender("rendering files... %o", this.path);
|
146 |
|
147 | const bulkData = await this.getBulkDataPromise(args);
|
148 |
|
149 | const render = async (
|
150 | path: string,
|
151 | data: TData,
|
152 | ): Promise<[string, string]> => {
|
153 | const content = await this.definition.render({
|
154 | path,
|
155 | globalData: args.globalData,
|
156 | data,
|
157 | });
|
158 |
|
159 | debugRender("rendered %o", path);
|
160 |
|
161 | return [pathToFilePath(path), content];
|
162 | };
|
163 |
|
164 | const promises: Awaitable<[string, string]>[] = [];
|
165 | for (const path in bulkData) {
|
166 | const data = bulkData[path];
|
167 |
|
168 | promises.push(render(path, data));
|
169 | }
|
170 |
|
171 | const fileEntries = await Promise.all(Object.values(promises));
|
172 |
|
173 | debugRender(
|
174 | `rendered %o ${fileEntries.length > 1 ? "files" : "file"} %o`,
|
175 | fileEntries.length,
|
176 | this.path,
|
177 | );
|
178 |
|
179 | return Object.fromEntries(fileEntries);
|
180 | }
|
181 |
|
182 |
|
183 | clearCache(): void {
|
184 | this._dataPromiseMap = new Map();
|
185 | this._bulkDataPromise = undefined;
|
186 | }
|
187 |
|
188 | private _dataPromiseMap: Map<string, Awaitable<TData>> = new Map();
|
189 |
|
190 | protected getDataPromise: FilesDataFn<TGlobalData, TParams, TData> = (
|
191 | context,
|
192 | ) => {
|
193 | const maybePromise = this._dataPromiseMap.get(context.path);
|
194 | if (maybePromise) {
|
195 | debugCache("using cached data %o", context.path);
|
196 |
|
197 | return maybePromise;
|
198 | }
|
199 |
|
200 | debugCache("retrieving data... %o", context.path);
|
201 |
|
202 | let promise: Awaitable<TData>;
|
203 | if (this.definition.data) {
|
204 | promise = this.definition.data(context);
|
205 | } else if (this.definition.bulkData) {
|
206 | const dataFromBulkData = async (path: string): Promise<TData> => {
|
207 |
|
208 | const bulkData = await this.definition.bulkData!({
|
209 | globalData: context.globalData,
|
210 | });
|
211 |
|
212 | if (path in bulkData) {
|
213 | return bulkData[path];
|
214 | }
|
215 |
|
216 | throw new NotFoundError(path);
|
217 | };
|
218 |
|
219 | promise = dataFromBulkData(context.path);
|
220 | } else {
|
221 | throw new Error(
|
222 | `Cannot render file for path \`${context.path}\`, no \`data\` or \`bulkData\` function available`,
|
223 | );
|
224 | }
|
225 |
|
226 | if (promise instanceof Promise) {
|
227 | promise
|
228 | .then(() => {
|
229 | debugCache("retrieved data %o", context.path);
|
230 | })
|
231 | .catch(() => {});
|
232 | } else {
|
233 | debugCache("retrieved data %o", context.path);
|
234 | }
|
235 |
|
236 | this._dataPromiseMap.set(context.path, promise);
|
237 |
|
238 | return promise;
|
239 | };
|
240 |
|
241 | private _bulkDataPromise: Awaitable<Record<string, TData>> | undefined;
|
242 |
|
243 | protected getBulkDataPromise: FilesBulkDataFn<TGlobalData, TData> = (
|
244 | context,
|
245 | ) => {
|
246 | if (!this._bulkDataPromise) {
|
247 | debugCache("retrieving bulk data... %o", this.path);
|
248 |
|
249 | const bulkDataPromise =
|
250 | this.definition.bulkData?.(context) || ({} as Record<string, TData>);
|
251 |
|
252 | if (bulkDataPromise instanceof Promise) {
|
253 | bulkDataPromise.then(() => {
|
254 | debugCache("retrieved bulk data %o", this.path);
|
255 | });
|
256 | } else {
|
257 | debugCache("retrieved bulk data %o", this.path);
|
258 | }
|
259 |
|
260 | this._bulkDataPromise = bulkDataPromise;
|
261 | } else {
|
262 | debugCache("using cached bulk data %o", this.path);
|
263 | }
|
264 |
|
265 | return this._bulkDataPromise;
|
266 | };
|
267 | }
|