@nifrajs/content 5.1.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 +150 -0
  2. package/package.json +3 -2
  3. package/src/fs.ts +31 -12
package/CHANGELOG.md ADDED
@@ -0,0 +1,150 @@
1
+ # @nifrajs/content
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
+
13
+ ## 5.2.0
14
+
15
+ ### Patch Changes
16
+
17
+ - 9498cf8: The package ships its `CHANGELOG.md`, so the installed package has the notes for every version, as the GitHub release does. Before, the tarball held only the code, `README.md`, `LICENSE` and `package.json`.
18
+
19
+ ## 5.1.0
20
+
21
+ No changes in this release.
22
+
23
+ ## 5.0.0
24
+
25
+ ### Patch Changes
26
+
27
+ - 1f81be1: `defineCollection` reads the directory, stats and files with async file IO. It used the synchronous calls inside `all()` and `get()`, which blocked a long-lived server's event loop for the whole read.
28
+ - 5f867e3: `collection.get(slug)` now throws for an entry whose frontmatter fails to parse or validate, as `all()` does, and returns `null` only when no file exists for the slug. A broken entry used to look like a missing one, so a page answered 404 instead of surfacing the error.
29
+
30
+ ## 4.0.2
31
+
32
+ ## 4.0.1
33
+
34
+ ## 4.0.0
35
+
36
+ ## 3.5.0
37
+
38
+ ## 3.4.0
39
+
40
+ ## 3.3.0
41
+
42
+ ## 3.2.0
43
+
44
+ ### Patch Changes
45
+
46
+ - f34a050: Record the pending source changes for these packages in their release notes so fixed-version publishing does not omit their changelog entries.
47
+
48
+ ## 3.1.0
49
+
50
+ ## 3.0.0
51
+
52
+ ### Patch Changes
53
+
54
+ - 6e43c15: A failed boundary load no longer publishes the thrown error's own message. Boundary states are serialized into the document, so a driver or fetch failure was putting hosts, credentials, and query text on the page for every visitor who loaded it while the dependency was down. The client slot is now always `Boundary failed`, an `Error` subclass name is withheld for the same reason it names the failing internal library, and the real error is reported to the server console - the split already used for a rejected deferred value. A boundary that wants to show the user something specific catches its own failure inside `load` and returns that as data.
55
+
56
+ Opt-in outbound WebSocket validation (`validateSend`) contains a rejected diagnostic instead of recursing. An `error()` handler that answers a dropped frame by sending one of its own re-entered the reporter through the two paths that bypassed its guard - schema rejection and an async validator - and unwound only when the stack was exhausted, silently. Every failure path now reports through the guard.
57
+
58
+ A content index cursor is checked for the query that issued it separately from running off the end: a cursor from a different sort or filter is rejected at any position, and a matching cursor left past the last row by an index that shrank between pages returns an exhausted page rather than throwing.
59
+
60
+ - 86a555b: The roadmap contract surfaces are now shipped across the public packages: shared island triggers,
61
+ typed content indexes and joins, client loader/action hooks, and unified static, dynamic, and
62
+ intercepting boundary modes. WebSocket routes also support opt-in synchronous outbound validation
63
+ through `sendSchema` + `validateSend`; invalid or asynchronous outbound frames fail closed while the
64
+ default remains type-level only.
65
+
66
+ ## 2.14.1
67
+
68
+ ## 2.14.0
69
+
70
+ ## 2.13.0
71
+
72
+ ## 2.12.1
73
+
74
+ ## 2.12.0
75
+
76
+ ## 2.11.0
77
+
78
+ ## 2.10.0
79
+
80
+ ## 2.9.1
81
+
82
+ ## 2.9.0
83
+
84
+ ## 2.8.2
85
+
86
+ ## 2.8.1
87
+
88
+ ## 2.8.0
89
+
90
+ ## 2.7.1
91
+
92
+ ## 2.7.0
93
+
94
+ ## 2.6.1
95
+
96
+ ## 2.6.0
97
+
98
+ ## 2.5.0
99
+
100
+ ## 2.4.0
101
+
102
+ ## 2.3.0
103
+
104
+ ## 2.2.0
105
+
106
+ ## 2.1.0
107
+
108
+ ## 2.0.0
109
+
110
+ ## 1.13.0
111
+
112
+ ## 1.12.0
113
+
114
+ ## 1.11.0
115
+
116
+ ## 1.10.0
117
+
118
+ ## 1.9.1
119
+
120
+ ## 1.9.0
121
+
122
+ ## 1.8.0
123
+
124
+ ## 1.7.0
125
+
126
+ ## 1.6.0
127
+
128
+ ## 1.5.0
129
+
130
+ ## 1.4.0
131
+
132
+ ## 1.3.1
133
+
134
+ ## 1.3.0
135
+
136
+ ## 1.2.2
137
+
138
+ ## 1.2.1
139
+
140
+ ## 1.2.0
141
+
142
+ ## 1.1.0
143
+
144
+ ## 1.0.0
145
+
146
+ ## 1.0.0-beta.4
147
+
148
+ ## 1.0.0-beta.3
149
+
150
+ ## 0.1.0-beta.2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nifrajs/content",
3
- "version": "5.1.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",
@@ -36,7 +36,8 @@
36
36
  }
37
37
  },
38
38
  "files": [
39
- "src"
39
+ "src",
40
+ "CHANGELOG.md"
40
41
  ],
41
42
  "sideEffects": false,
42
43
  "license": "MIT",
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