jq79 0.5.5 → 0.5.6

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jq79",
3
- "version": "0.5.5",
3
+ "version": "0.5.6",
4
4
  "description": "Mini reactive component library: single-file components, Svelte-style setup scripts, fine-grained proxy reactivity. Single-file build, zero dependencies.",
5
5
  "keywords": [
6
6
  "reactive",
package/src/jq79.ts CHANGED
@@ -127,6 +127,36 @@ const reportedExprErrors = new Set<string>()
127
127
  let pendingScripts = 0
128
128
  let flushScheduled = false
129
129
 
130
+ // everything else an expression can throw - overwhelmingly a member access on
131
+ // an undefined value, `{{ game.is.loaded }}` over a game with no `is`. There is
132
+ // no pure question to ask about it the way there is for a missing name: `in`
133
+ // answers "declared anywhere?" without evaluating, and nothing answers "would
134
+ // this still throw?" without running the expression again, which is exactly
135
+ // what mustn't happen (`@click="count++ + missing"` increments twice).
136
+ //
137
+ // What is pure is watching the evaluations that were going to happen anyway. An
138
+ // expression whose value is merely late is re-evaluated when the value arrives -
139
+ // the reads that did land registered as dependencies, so the write wakes the
140
+ // effect - and it succeeds then. So transient means "succeeds later", and the
141
+ // re-check is to clear the entry on the next successful evaluation and report
142
+ // only what is still queued once the page has gone quiet.
143
+ //
144
+ // Quiet is a timer rather than the microtask the other queue flushes on,
145
+ // because a value from fetch() arrives many tasks later and clearing on success
146
+ // exists to let it. A second covers that and still lands the warning inside the
147
+ // page load. It is a guess, and the cost is a slower fetch warning about correct
148
+ // code with nothing to take it back - accepted because the silence it replaces
149
+ // hid a render that was actively wrong
150
+ const FAILED_EXPR_DELAY = 1000
151
+
152
+ // keyed by expression text alone, not `message|expr`: the expression is what the
153
+ // author edits, and one broken path throwing two messages is still one bug. The
154
+ // entries hold no scope, only strings, so this queue cannot retain a live
155
+ // component the way pendingReports can
156
+ const failedExprs = new Map<string, string>()
157
+ const reportedFailedExprs = new Set<string>()
158
+ let failedFlushTimer: ReturnType<typeof setTimeout> | undefined
159
+
130
160
  const flushExprReports = () => {
131
161
  flushScheduled = false
132
162
  if (pendingScripts > 0) return // a script started meanwhile; its release re-schedules
@@ -149,6 +179,25 @@ const scheduleExprReportFlush = () => {
149
179
  queueMicrotask(flushExprReports)
150
180
  }
151
181
 
182
+ const flushFailedExprs = () => {
183
+ failedFlushTimer = undefined
184
+ if (pendingScripts > 0) return // a script started meanwhile; its release re-schedules
185
+ failedExprs.forEach((message, expr) => {
186
+ reportedFailedExprs.add(expr)
187
+ console.warn(
188
+ `jq79: ${message} - evaluating "${expr}". It rendered as nothing and was ` +
189
+ `still failing a moment later, so the value is probably never coming: check ` +
190
+ `the path. If it does arrive asynchronously, guard it - "a?.b", or :if on the element.`
191
+ )
192
+ })
193
+ failedExprs.clear()
194
+ }
195
+
196
+ const scheduleFailedExprFlush = () => {
197
+ if (failedFlushTimer !== undefined || pendingScripts > 0 || !failedExprs.size) return
198
+ failedFlushTimer = setTimeout(flushFailedExprs, FAILED_EXPR_DELAY)
199
+ }
200
+
152
201
  // scripts run before the template renders, so the counter is already up when
153
202
  // the first evaluation fails. Both script modes settle through a promise;
154
203
  // the factory's has to cover the merge, not just the module body
@@ -157,12 +206,20 @@ const trackScript = (settled: Promise<unknown>) => {
157
206
  const release = () => {
158
207
  pendingScripts--
159
208
  scheduleExprReportFlush()
209
+ scheduleFailedExprFlush()
160
210
  }
161
211
  settled.then(release, release)
162
212
  }
163
213
 
214
+ const reportFailedExpr = (expr: string, error: unknown) => {
215
+ if (reportedFailedExprs.has(expr) || failedExprs.has(expr)) return
216
+ if (failedExprs.size >= MAX_PENDING_REPORTS) return
217
+ failedExprs.set(expr, (error as Error)?.message || String(error))
218
+ scheduleFailedExprFlush()
219
+ }
220
+
164
221
  const reportExprError = (expr: string, scope: Record<string, any>, error: unknown) => {
165
- if (!(error instanceof ReferenceError)) return
222
+ if (!(error instanceof ReferenceError)) return reportFailedExpr(expr, error)
166
223
  const match = MISSING_NAME_RE.exec(error.message)
167
224
  const name = match?.[1] ?? match?.[2]
168
225
  if (!name) return // an engine whose wording we don't know: stay quiet, as before
@@ -184,7 +241,12 @@ const runExpr = (expr: string, scope: Record<string, any>, extras?: Record<strin
184
241
 
185
242
  const evalExpr = (expr: string, scope: Record<string, any>, extras?: Record<string, any>): any => {
186
243
  try {
187
- return runExpr(expr, scope, extras)
244
+ const value = runExpr(expr, scope, extras)
245
+ // the re-check for the failed-expression queue: this run is the proof that
246
+ // the earlier throw was a value arriving late. Guarded on size so the
247
+ // normal page - where nothing has ever thrown - pays one comparison
248
+ if (failedExprs.size) failedExprs.delete(expr)
249
+ return value
188
250
  } catch (error) {
189
251
  reportExprError(expr, scope, error)
190
252
  return undefined
@@ -2276,6 +2338,8 @@ export class Component79 {
2276
2338
  // a different key
2277
2339
  reportedExprErrors.clear()
2278
2340
  pendingReports.clear()
2341
+ reportedFailedExprs.clear()
2342
+ failedExprs.clear()
2279
2343
  const marker = this.startMarker
2280
2344
  const rendered = !!(marker && this.content)
2281
2345