@stati/core 1.1.0 → 1.3.0

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.
Files changed (39) hide show
  1. package/README.md +19 -7
  2. package/dist/config/loader.d.ts.map +1 -1
  3. package/dist/config/loader.js +24 -2
  4. package/dist/core/build.d.ts +21 -15
  5. package/dist/core/build.d.ts.map +1 -1
  6. package/dist/core/build.js +141 -42
  7. package/dist/core/dev.d.ts.map +1 -1
  8. package/dist/core/dev.js +84 -18
  9. package/dist/core/invalidate.d.ts +67 -1
  10. package/dist/core/invalidate.d.ts.map +1 -1
  11. package/dist/core/invalidate.js +321 -4
  12. package/dist/core/isg/build-lock.d.ts +116 -0
  13. package/dist/core/isg/build-lock.d.ts.map +1 -0
  14. package/dist/core/isg/build-lock.js +245 -0
  15. package/dist/core/isg/builder.d.ts +51 -0
  16. package/dist/core/isg/builder.d.ts.map +1 -0
  17. package/dist/core/isg/builder.js +321 -0
  18. package/dist/core/isg/deps.d.ts +63 -0
  19. package/dist/core/isg/deps.d.ts.map +1 -0
  20. package/dist/core/isg/deps.js +332 -0
  21. package/dist/core/isg/hash.d.ts +48 -0
  22. package/dist/core/isg/hash.d.ts.map +1 -0
  23. package/dist/core/isg/hash.js +82 -0
  24. package/dist/core/isg/manifest.d.ts +47 -0
  25. package/dist/core/isg/manifest.d.ts.map +1 -0
  26. package/dist/core/isg/manifest.js +233 -0
  27. package/dist/core/isg/ttl.d.ts +101 -0
  28. package/dist/core/isg/ttl.d.ts.map +1 -0
  29. package/dist/core/isg/ttl.js +222 -0
  30. package/dist/core/isg/validation.d.ts +71 -0
  31. package/dist/core/isg/validation.d.ts.map +1 -0
  32. package/dist/core/isg/validation.js +226 -0
  33. package/dist/core/templates.d.ts.map +1 -1
  34. package/dist/core/templates.js +3 -2
  35. package/dist/index.d.ts +1 -0
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/types.d.ts +110 -20
  38. package/dist/types.d.ts.map +1 -1
  39. package/package.json +1 -1
@@ -1,2 +1,68 @@
1
- export declare function invalidate(query?: string): Promise<void>;
1
+ import type { CacheEntry } from '../types.js';
2
+ /**
3
+ * Invalidation result containing affected cache entries.
4
+ */
5
+ export interface InvalidationResult {
6
+ /** Number of cache entries invalidated */
7
+ invalidatedCount: number;
8
+ /** Paths of invalidated pages */
9
+ invalidatedPaths: string[];
10
+ /** Whether the entire cache was cleared */
11
+ clearedAll: boolean;
12
+ }
13
+ /**
14
+ * Parses an invalidation query string into individual query terms.
15
+ * Supports space-separated values and quoted strings.
16
+ *
17
+ * @param query - The query string to parse
18
+ * @returns Array of parsed query terms
19
+ *
20
+ * @example
21
+ * ```typescript
22
+ * parseInvalidationQuery('tag:blog path:/posts') // ['tag:blog', 'path:/posts']
23
+ * parseInvalidationQuery('"tag:my tag" path:"/my path"') // ['tag:my tag', 'path:/my path']
24
+ * ```
25
+ */
26
+ export declare function parseInvalidationQuery(query: string): string[];
27
+ /**
28
+ * Checks if a cache entry matches a specific invalidation term.
29
+ *
30
+ * @param entry - Cache entry to check
31
+ * @param path - The page path for this entry
32
+ * @param term - Invalidation term to match against
33
+ * @returns True if the entry matches the term
34
+ *
35
+ * @example
36
+ * ```typescript
37
+ * matchesInvalidationTerm(entry, '/blog/post-1', 'tag:blog') // true if entry has 'blog' tag
38
+ * matchesInvalidationTerm(entry, '/blog/post-1', 'path:/blog') // true (path prefix match)
39
+ * ```
40
+ */
41
+ export declare function matchesInvalidationTerm(entry: CacheEntry, path: string, term: string): boolean;
42
+ /**
43
+ * Invalidates cache entries based on a query string.
44
+ * Supports tag-based, path-based, pattern-based, and time-based invalidation.
45
+ *
46
+ * @param query - Invalidation query string, or undefined to clear all cache
47
+ * @returns Promise resolving to invalidation result
48
+ *
49
+ * @example
50
+ * ```typescript
51
+ * // Invalidate all pages with 'blog' tag
52
+ * await invalidate('tag:blog');
53
+ *
54
+ * // Invalidate specific path
55
+ * await invalidate('path:/about');
56
+ *
57
+ * // Invalidate content younger than 3 months
58
+ * await invalidate('age:3months');
59
+ *
60
+ * // Invalidate multiple criteria
61
+ * await invalidate('tag:blog age:1week');
62
+ *
63
+ * // Clear entire cache
64
+ * await invalidate();
65
+ * ```
66
+ */
67
+ export declare function invalidate(query?: string): Promise<InvalidationResult>;
2
68
  //# sourceMappingURL=invalidate.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"invalidate.d.ts","sourceRoot":"","sources":["../../src/core/invalidate.ts"],"names":[],"mappings":"AAAA,wBAAsB,UAAU,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAM9D"}
1
+ {"version":3,"file":"invalidate.d.ts","sourceRoot":"","sources":["../../src/core/invalidate.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,0CAA0C;IAC1C,gBAAgB,EAAE,MAAM,CAAC;IACzB,iCAAiC;IACjC,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,2CAA2C;IAC3C,UAAU,EAAE,OAAO,CAAC;CACrB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAkC9D;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAkC9F;AAiLD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAsB,UAAU,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAoD5E"}
@@ -1,7 +1,324 @@
1
+ import { join } from 'path';
2
+ import { loadCacheManifest, saveCacheManifest } from './isg/manifest.js';
3
+ /**
4
+ * Parses an invalidation query string into individual query terms.
5
+ * Supports space-separated values and quoted strings.
6
+ *
7
+ * @param query - The query string to parse
8
+ * @returns Array of parsed query terms
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * parseInvalidationQuery('tag:blog path:/posts') // ['tag:blog', 'path:/posts']
13
+ * parseInvalidationQuery('"tag:my tag" path:"/my path"') // ['tag:my tag', 'path:/my path']
14
+ * ```
15
+ */
16
+ export function parseInvalidationQuery(query) {
17
+ const terms = [];
18
+ let current = '';
19
+ let inQuotes = false;
20
+ let quoteChar = '';
21
+ for (let i = 0; i < query.length; i++) {
22
+ const char = query[i];
23
+ if (char === '"' || char === "'") {
24
+ if (!inQuotes) {
25
+ inQuotes = true;
26
+ quoteChar = char;
27
+ }
28
+ else if (char === quoteChar) {
29
+ inQuotes = false;
30
+ quoteChar = '';
31
+ }
32
+ else {
33
+ current += char;
34
+ }
35
+ }
36
+ else if (char === ' ' && !inQuotes) {
37
+ if (current.trim()) {
38
+ terms.push(current.trim());
39
+ current = '';
40
+ }
41
+ }
42
+ else {
43
+ current += char;
44
+ }
45
+ }
46
+ if (current.trim()) {
47
+ terms.push(current.trim());
48
+ }
49
+ return terms;
50
+ }
51
+ /**
52
+ * Checks if a cache entry matches a specific invalidation term.
53
+ *
54
+ * @param entry - Cache entry to check
55
+ * @param path - The page path for this entry
56
+ * @param term - Invalidation term to match against
57
+ * @returns True if the entry matches the term
58
+ *
59
+ * @example
60
+ * ```typescript
61
+ * matchesInvalidationTerm(entry, '/blog/post-1', 'tag:blog') // true if entry has 'blog' tag
62
+ * matchesInvalidationTerm(entry, '/blog/post-1', 'path:/blog') // true (path prefix match)
63
+ * ```
64
+ */
65
+ export function matchesInvalidationTerm(entry, path, term) {
66
+ // Parse term into type and value
67
+ if (term.includes(':')) {
68
+ const [type, value] = term.split(':', 2);
69
+ // Ensure both type and value exist
70
+ if (!type || !value) {
71
+ return false;
72
+ }
73
+ switch (type.toLowerCase()) {
74
+ case 'tag':
75
+ return entry.tags.includes(value);
76
+ case 'path':
77
+ // Support both exact path match and prefix match
78
+ return path === value || path.startsWith(value);
79
+ case 'glob':
80
+ // Simple glob pattern matching for paths
81
+ return matchesGlob(path, value);
82
+ case 'age':
83
+ // Time-based invalidation: age:3months, age:1week, age:30days
84
+ return matchesAge(entry, value);
85
+ default:
86
+ console.warn(`Unknown invalidation term type: ${type}`);
87
+ return false;
88
+ }
89
+ }
90
+ else {
91
+ // Plain term - search in tags and path
92
+ return entry.tags.some((tag) => tag.includes(term)) || path.includes(term);
93
+ }
94
+ }
95
+ /**
96
+ * Simple glob pattern matching for paths.
97
+ * Supports * and ** wildcards.
98
+ *
99
+ * @param path - Path to test
100
+ * @param pattern - Glob pattern
101
+ * @returns True if path matches pattern
102
+ */
103
+ function matchesGlob(path, pattern) {
104
+ try {
105
+ // Convert glob pattern to regex by processing character by character
106
+ // This avoids the magic string replacement issue
107
+ const regex = globToRegex(pattern);
108
+ return regex.test(path);
109
+ }
110
+ catch {
111
+ console.warn(`Invalid glob pattern: ${pattern}`);
112
+ return false;
113
+ }
114
+ }
115
+ /**
116
+ * Converts a glob pattern to a regular expression.
117
+ * Processes the pattern character by character to avoid placeholder conflicts.
118
+ *
119
+ * @param pattern - Glob pattern to convert
120
+ * @returns Regular expression that matches the glob pattern
121
+ */
122
+ function globToRegex(pattern) {
123
+ let regexStr = '^';
124
+ let i = 0;
125
+ while (i < pattern.length) {
126
+ const char = pattern[i];
127
+ switch (char) {
128
+ case '*':
129
+ if (i + 1 < pattern.length && pattern[i + 1] === '*') {
130
+ // Handle ** (matches any path including subdirectories)
131
+ if (i + 2 < pattern.length && pattern[i + 2] === '/') {
132
+ // **/ pattern - matches zero or more directories
133
+ regexStr += '(?:.*/)?';
134
+ i += 3;
135
+ }
136
+ else if (i + 2 === pattern.length) {
137
+ // ** at end - matches everything
138
+ regexStr += '.*';
139
+ i += 2;
140
+ }
141
+ else {
142
+ // ** not followed by / or end - treat as single *
143
+ regexStr += '[^/]*';
144
+ i += 1;
145
+ }
146
+ }
147
+ else {
148
+ // Single * matches any characters except path separator
149
+ regexStr += '[^/]*';
150
+ i += 1;
151
+ }
152
+ break;
153
+ case '?':
154
+ // ? matches any single character except path separator
155
+ regexStr += '[^/]';
156
+ i += 1;
157
+ break;
158
+ case '[': {
159
+ // Handle character classes - find the closing bracket
160
+ let closeIndex = i + 1;
161
+ while (closeIndex < pattern.length && pattern[closeIndex] !== ']') {
162
+ closeIndex++;
163
+ }
164
+ if (closeIndex >= pattern.length) {
165
+ // No closing bracket found - this creates an invalid regex
166
+ // Just add the character and let the regex constructor throw an error
167
+ regexStr += char;
168
+ i += 1;
169
+ }
170
+ else {
171
+ // Valid character class - copy it as-is
172
+ regexStr += pattern.slice(i, closeIndex + 1);
173
+ i = closeIndex + 1;
174
+ }
175
+ break;
176
+ }
177
+ case '.':
178
+ case '+':
179
+ case '^':
180
+ case '$':
181
+ case '(':
182
+ case ')':
183
+ case ']':
184
+ case '{':
185
+ case '}':
186
+ case '|':
187
+ case '\\':
188
+ // Escape regex special characters
189
+ regexStr += '\\' + char;
190
+ i += 1;
191
+ break;
192
+ default:
193
+ // Regular character
194
+ regexStr += char;
195
+ i += 1;
196
+ break;
197
+ }
198
+ }
199
+ regexStr += '$';
200
+ return new RegExp(regexStr);
201
+ }
202
+ /**
203
+ * Checks if a cache entry matches an age-based invalidation term.
204
+ * Supports various time units: days, weeks, months, years.
205
+ *
206
+ * @param entry - Cache entry to check
207
+ * @param ageValue - Age specification (e.g., "3months", "1week", "30days")
208
+ * @returns True if the entry is younger than the specified age
209
+ *
210
+ * @example
211
+ * ```typescript
212
+ * matchesAge(entry, "3months") // true if rendered within the last 3 months
213
+ * matchesAge(entry, "1week") // true if rendered within the last 1 week
214
+ * ```
215
+ */
216
+ function matchesAge(entry, ageValue) {
217
+ const now = new Date();
218
+ const renderedAt = new Date(entry.renderedAt);
219
+ // Parse age value (e.g., "3months", "1week", "30days")
220
+ const match = ageValue.match(/^(\d+)(days?|weeks?|months?|years?)$/i);
221
+ if (!match || !match[1] || !match[2]) {
222
+ console.warn(`Invalid age format: ${ageValue}. Use format like "3months", "1week", "30days"`);
223
+ return false;
224
+ }
225
+ const numStr = match[1];
226
+ const unit = match[2];
227
+ const num = parseInt(numStr, 10);
228
+ if (isNaN(num) || num <= 0) {
229
+ console.warn(`Invalid age number: ${numStr}`);
230
+ return false;
231
+ }
232
+ // Calculate cutoff date
233
+ const cutoffDate = new Date(now);
234
+ switch (unit.toLowerCase()) {
235
+ case 'day':
236
+ case 'days':
237
+ cutoffDate.setDate(cutoffDate.getDate() - num);
238
+ break;
239
+ case 'week':
240
+ case 'weeks':
241
+ cutoffDate.setDate(cutoffDate.getDate() - num * 7);
242
+ break;
243
+ case 'month':
244
+ case 'months':
245
+ cutoffDate.setMonth(cutoffDate.getMonth() - num);
246
+ break;
247
+ case 'year':
248
+ case 'years':
249
+ cutoffDate.setFullYear(cutoffDate.getFullYear() - num);
250
+ break;
251
+ default:
252
+ console.warn(`Unknown time unit: ${unit}`);
253
+ return false;
254
+ }
255
+ // Entry matches if it was rendered after the cutoff date (i.e., younger than specified age)
256
+ return renderedAt > cutoffDate;
257
+ }
258
+ /**
259
+ * Invalidates cache entries based on a query string.
260
+ * Supports tag-based, path-based, pattern-based, and time-based invalidation.
261
+ *
262
+ * @param query - Invalidation query string, or undefined to clear all cache
263
+ * @returns Promise resolving to invalidation result
264
+ *
265
+ * @example
266
+ * ```typescript
267
+ * // Invalidate all pages with 'blog' tag
268
+ * await invalidate('tag:blog');
269
+ *
270
+ * // Invalidate specific path
271
+ * await invalidate('path:/about');
272
+ *
273
+ * // Invalidate content younger than 3 months
274
+ * await invalidate('age:3months');
275
+ *
276
+ * // Invalidate multiple criteria
277
+ * await invalidate('tag:blog age:1week');
278
+ *
279
+ * // Clear entire cache
280
+ * await invalidate();
281
+ * ```
282
+ */
1
283
  export async function invalidate(query) {
2
- // TODO This will be implemented in Milestone 4 (ISG)
3
- console.log('Invalidate functionality will be available in a future release');
4
- if (query) {
5
- console.log(`Query: ${query}`);
284
+ const cacheDir = join(process.cwd(), '.stati');
285
+ // Load existing cache manifest
286
+ let cacheManifest = await loadCacheManifest(cacheDir);
287
+ if (!cacheManifest) {
288
+ // No cache to invalidate
289
+ return {
290
+ invalidatedCount: 0,
291
+ invalidatedPaths: [],
292
+ clearedAll: false,
293
+ };
294
+ }
295
+ const invalidatedPaths = [];
296
+ if (!query || query.trim() === '') {
297
+ // Clear entire cache
298
+ invalidatedPaths.push(...Object.keys(cacheManifest.entries));
299
+ cacheManifest.entries = {};
300
+ await saveCacheManifest(cacheDir, cacheManifest);
301
+ return {
302
+ invalidatedCount: invalidatedPaths.length,
303
+ invalidatedPaths,
304
+ clearedAll: true,
305
+ };
306
+ }
307
+ // Parse query terms
308
+ const terms = parseInvalidationQuery(query.trim());
309
+ // Find entries that match any of the terms
310
+ for (const [path, entry] of Object.entries(cacheManifest.entries)) {
311
+ const shouldInvalidate = terms.some((term) => matchesInvalidationTerm(entry, path, term));
312
+ if (shouldInvalidate) {
313
+ delete cacheManifest.entries[path];
314
+ invalidatedPaths.push(path);
315
+ }
6
316
  }
317
+ // Save updated cache manifest
318
+ await saveCacheManifest(cacheDir, cacheManifest);
319
+ return {
320
+ invalidatedCount: invalidatedPaths.length,
321
+ invalidatedPaths,
322
+ clearedAll: false,
323
+ };
7
324
  }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Build lock information stored in the lock file.
3
+ */
4
+ interface BuildLock {
5
+ pid: number;
6
+ timestamp: string;
7
+ hostname?: string;
8
+ }
9
+ /**
10
+ * Manages build process locking to prevent concurrent Stati builds from corrupting cache.
11
+ * Uses a simple file-based locking mechanism with process ID tracking.
12
+ */
13
+ export declare class BuildLockManager {
14
+ private lockPath;
15
+ private isLocked;
16
+ constructor(cacheDir: string);
17
+ /**
18
+ * Attempts to acquire a build lock.
19
+ * Throws an error if another build process is already running.
20
+ *
21
+ * @param options - Lock acquisition options
22
+ * @param options.force - Force acquire lock even if another process holds it
23
+ * @param options.timeout - Maximum time to wait for lock in milliseconds
24
+ * @throws {Error} When lock cannot be acquired
25
+ *
26
+ * @example
27
+ * ```typescript
28
+ * const lockManager = new BuildLockManager('.stati');
29
+ * try {
30
+ * await lockManager.acquireLock();
31
+ * // Proceed with build
32
+ * } finally {
33
+ * await lockManager.releaseLock();
34
+ * }
35
+ * ```
36
+ */
37
+ acquireLock(options?: {
38
+ force?: boolean;
39
+ timeout?: number;
40
+ }): Promise<void>;
41
+ /**
42
+ * Releases the build lock if this process owns it.
43
+ *
44
+ * @example
45
+ * ```typescript
46
+ * await lockManager.releaseLock();
47
+ * ```
48
+ */
49
+ releaseLock(): Promise<void>;
50
+ /**
51
+ * Checks if a build lock is currently held by any process.
52
+ *
53
+ * @returns True if a lock exists and the owning process is running
54
+ *
55
+ * @example
56
+ * ```typescript
57
+ * if (await lockManager.isLocked()) {
58
+ * console.log('Another build is in progress');
59
+ * }
60
+ * ```
61
+ */
62
+ isLockHeld(): Promise<boolean>;
63
+ /**
64
+ * Gets information about the current lock holder.
65
+ *
66
+ * @returns Lock information or null if no lock exists
67
+ */
68
+ getLockInfo(): Promise<BuildLock | null>;
69
+ /**
70
+ * Force removes the lock file without checking ownership.
71
+ * Should only be used in error recovery scenarios.
72
+ */
73
+ private forceRemoveLock;
74
+ /**
75
+ * Creates a new lock file with current process information.
76
+ */
77
+ private createLockFile;
78
+ /**
79
+ * Reads and parses the lock file.
80
+ */
81
+ private readLockFile;
82
+ /**
83
+ * Checks if a process with the given PID is currently running.
84
+ */
85
+ private isProcessRunning;
86
+ /**
87
+ * Gets the hostname for lock identification.
88
+ */
89
+ private getHostname;
90
+ /**
91
+ * Simple sleep utility for polling delays.
92
+ */
93
+ private sleep;
94
+ }
95
+ /**
96
+ * Convenience function to safely execute a build with automatic lock management.
97
+ *
98
+ * @param cacheDir - Path to the cache directory
99
+ * @param buildFn - Function to execute while holding the lock
100
+ * @param options - Lock options
101
+ * @returns Result of the build function
102
+ *
103
+ * @example
104
+ * ```typescript
105
+ * const result = await withBuildLock('.stati', async () => {
106
+ * // Your build logic here
107
+ * return await performBuild();
108
+ * });
109
+ * ```
110
+ */
111
+ export declare function withBuildLock<T>(cacheDir: string, buildFn: () => Promise<T>, options?: {
112
+ force?: boolean;
113
+ timeout?: number;
114
+ }): Promise<T>;
115
+ export {};
116
+ //# sourceMappingURL=build-lock.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build-lock.d.ts","sourceRoot":"","sources":["../../../src/core/isg/build-lock.ts"],"names":[],"mappings":"AAYA;;GAEG;AACH,UAAU,SAAS;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;GAGG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,QAAQ,CAAS;gBAEb,QAAQ,EAAE,MAAM;IAI5B;;;;;;;;;;;;;;;;;;;OAmBG;IACG,WAAW,CAAC,OAAO,GAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAoDrF;;;;;;;OAOG;IACG,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC;IAqBlC;;;;;;;;;;;OAWG;IACG,UAAU,IAAI,OAAO,CAAC,OAAO,CAAC;IAiBpC;;;;OAIG;IACG,WAAW,IAAI,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC;IAY9C;;;OAGG;YACW,eAAe;IAQ7B;;OAEG;YACW,cAAc;IAc5B;;OAEG;YACW,YAAY;IAS1B;;OAEG;YACW,gBAAgB;IAY9B;;OAEG;IACH,OAAO,CAAC,WAAW;IAQnB;;OAEG;IACH,OAAO,CAAC,KAAK;CAGd;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,aAAa,CAAC,CAAC,EACnC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACzB,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAO,GAClD,OAAO,CAAC,CAAC,CAAC,CASZ"}