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