@nifrajs/content 5.2.0 → 5.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 (3) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/package.json +1 -1
  3. package/src/fs.ts +31 -12
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # @nifrajs/content
2
2
 
3
+ ## 5.3.0
4
+
5
+ ### Patch Changes
6
+
7
+ - 7d7bbe4: `defineCollection` takes `watch: false` to list and parse its directory once and return that snapshot from every later `all()`. The default still lists the directory and stats every file per call, which costs 3.5 ms per `all()` at 2,000 files; with `watch: false` it costs 0.005 ms. A file deleted and recreated with the same size and modification time is also read again, where a parse cached before the deletion used to be returned.
8
+
9
+ ## 5.2.1
10
+
11
+ No changes in this release.
12
+
3
13
  ## 5.2.0
4
14
 
5
15
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nifrajs/content",
3
- "version": "5.2.0",
3
+ "version": "5.3.0",
4
4
  "description": "Typed, schema-validated content collections for nifra - Markdown + frontmatter, framework-agnostic.",
5
5
  "keywords": [
6
6
  "bun",
package/src/fs.ts CHANGED
@@ -15,6 +15,11 @@ export interface CollectionConfig<S extends StandardSchemaV1> {
15
15
  readonly schema: S
16
16
  /** File extensions to include (default `[".md", ".markdown"]`). */
17
17
  readonly extensions?: readonly string[]
18
+ /** Look for changes on every `all()` (default `true`): a directory listing and a `stat` per file,
19
+ * so an edit shows without a restart. `false` lists and parses the directory once and returns that
20
+ * snapshot from then on, for a server whose content ships with its build. `get` checks its one
21
+ * file either way. */
22
+ readonly watch?: boolean
18
23
  }
19
24
 
20
25
  /** A typed collection over a content directory. */
@@ -80,6 +85,7 @@ export function defineCollection<S extends StandardSchemaV1>(
80
85
  const matches = (file: string): boolean => extensions.some((ext) => file.endsWith(ext))
81
86
  const fileCache = new Map<string, CachedEntry<InferSchema<S>>>()
82
87
  let collectionCache: CollectionSnapshot<InferSchema<S>> | undefined
88
+ let frozen: Promise<Array<Entry<InferSchema<S>>>> | undefined
83
89
 
84
90
  const readEntry = async (
85
91
  path: string,
@@ -109,20 +115,32 @@ export function defineCollection<S extends StandardSchemaV1>(
109
115
  )
110
116
  }
111
117
 
118
+ const load = async (): Promise<Array<Entry<InferSchema<S>>>> => {
119
+ const files = await listFiles()
120
+ const fingerprint = files.map((file) => `${file.file}\0${file.stamp}`).join("\0")
121
+ if (collectionCache?.fingerprint === fingerprint) return collectionCache.entries
122
+
123
+ const listed = new Set(files.map((file) => file.path))
124
+ for (const path of fileCache.keys()) if (!listed.has(path)) fileCache.delete(path)
125
+ const entries = await Promise.all(
126
+ files.map((file) => readEntry(file.path, slugOf(file.file), file.stamp)),
127
+ )
128
+ collectionCache = {
129
+ fingerprint,
130
+ entries,
131
+ }
132
+ return entries
133
+ }
134
+
112
135
  return {
113
136
  async all() {
114
- const files = await listFiles()
115
- const fingerprint = files.map((file) => `${file.file}\0${file.stamp}`).join("\0")
116
- if (collectionCache?.fingerprint === fingerprint) return [...collectionCache.entries]
117
-
118
- const entries = await Promise.all(
119
- files.map((file) => readEntry(file.path, slugOf(file.file), file.stamp)),
120
- )
121
- collectionCache = {
122
- fingerprint,
123
- entries,
124
- }
125
- return [...entries]
137
+ if (config.watch !== false) return [...(await load())]
138
+ // A failed first read is not kept, so the next call retries it.
139
+ frozen ??= load().catch((error: unknown) => {
140
+ frozen = undefined
141
+ throw error
142
+ })
143
+ return [...(await frozen)]
126
144
  },
127
145
  async get(slug) {
128
146
  if (!isSafeSlug(slug)) return null
@@ -134,6 +152,7 @@ export function defineCollection<S extends StandardSchemaV1>(
134
152
  try {
135
153
  stat = await statPath(file)
136
154
  } catch {
155
+ fileCache.delete(file)
137
156
  continue // Not this extension; try the next configured one.
138
157
  }
139
158
  if (!stat.isFile()) continue