@abide/abide 0.112.0 → 0.114.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.
@@ -22,6 +22,9 @@ import { IDLE_TIMEOUT_SECONDS } from './IDLE_TIMEOUT_SECONDS.ts'
22
22
  /** The three framings a stream is answered in. 17.6. */
23
23
  export type Framing = Framed | 'bytes'
24
24
 
25
+ /** A principal cookie's claims, `exp` and `iat` aside, and when it lapses. 26.11. */
26
+ export type Sealed = { claims: Record<string, unknown>; expires: number }
27
+
25
28
  /** What one request, or one `App.run`, carries across every `await` its work makes. */
26
29
  export type RequestScope = Scope & {
27
30
  /**
@@ -38,6 +41,8 @@ export type RequestScope = Scope & {
38
41
  caller: string | undefined
39
42
  /** This request's one resolve of the principal. 26.14. */
40
43
  principal: Promise<Principal> | undefined
44
+ /** What the principal cookie opened to once read, or `null` where it held no seal. */
45
+ opened: Sealed | null | undefined
41
46
  /** 16.23 — whether the received request's idle timeout is off until the answer is sent. */
42
47
  unreaped: boolean
43
48
  /** This response's nonce, minted by the `csp` rung or on the first ask. 29.3. */
@@ -168,6 +173,7 @@ export function scopeFor(
168
173
  unreaped: false,
169
174
  caller: undefined,
170
175
  principal: undefined,
176
+ opened: undefined,
171
177
  applied: false,
172
178
  request: given,
173
179
  route: undefined,
@@ -0,0 +1,24 @@
1
+ const XLINK = 'http://www.w3.org/1999/xlink'
2
+ const XML = 'http://www.w3.org/XML/1998/namespace'
3
+ const XMLNS = 'http://www.w3.org/2000/xmlns/'
4
+
5
+ /**
6
+ * The attributes the HTML parser puts in a namespace on an SVG or MathML element — its "adjust
7
+ * foreign attributes" table — each to that namespace. Every other attribute it writes has none.
8
+ *
9
+ * A leaf, because the compiler reads it to know which built attribute needs the client's
10
+ * `addAttribute`, and the client reads it to write one.
11
+ */
12
+ export const FOREIGN_ATTRIBUTES: ReadonlyMap<string, string> = new Map([
13
+ ['xlink:actuate', XLINK],
14
+ ['xlink:arcrole', XLINK],
15
+ ['xlink:href', XLINK],
16
+ ['xlink:role', XLINK],
17
+ ['xlink:show', XLINK],
18
+ ['xlink:title', XLINK],
19
+ ['xlink:type', XLINK],
20
+ ['xml:lang', XML],
21
+ ['xml:space', XML],
22
+ ['xmlns', XMLNS],
23
+ ['xmlns:xlink', XMLNS],
24
+ ])
@@ -0,0 +1,20 @@
1
+ /**
2
+ * The SVG and MathML elements whose children the HTML parser reads as HTML again — the
3
+ * integration points — less `annotation-xml`, which is one only by the encoding it declares.
4
+ *
5
+ * A leaf, because the client's parse and build read it (`namespaceUnder`) and the compiler reads
6
+ * it to know a built element below one is HTML's, whatever its parent's namespace.
7
+ */
8
+ export const INTEGRATION_POINTS: ReadonlySet<string> = new Set([
9
+ 'foreignObject',
10
+ 'desc',
11
+ 'title',
12
+ 'mi',
13
+ 'mo',
14
+ 'mn',
15
+ 'ms',
16
+ 'mtext',
17
+ ])
18
+
19
+ /** HTML's namespace: what the parser gives an element outside SVG and MathML, and below an integration point. */
20
+ export const HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml'
@@ -0,0 +1,5 @@
1
+ // The origin a request made in process is addressed to, where no listener names a real one: an
2
+ // rpc's `raw` answered by the app this process holds, a CLI command asking the app it booted, and
3
+ // an `App.run` given a principal and no request (42.19). A leaf, because the server and the shared
4
+ // rpc both build one.
5
+ export const IN_PROCESS_ORIGIN = 'http://app.internal'
@@ -34,6 +34,7 @@ import {
34
34
  refuseBothCaps,
35
35
  signalMoved,
36
36
  startDeferred,
37
+ unwatchStore,
37
38
  } from './Reactive.ts'
38
39
  import {
39
40
  detach,
@@ -46,6 +47,7 @@ import {
46
47
  type Tracked,
47
48
  trackProbe,
48
49
  } from './tracking.ts'
50
+ import { owned, owning } from './watch.ts'
49
51
 
50
52
  /**
51
53
  * The keyed face: one `Reactive` per `Args` key, plus the probes and triggers over its entries.
@@ -262,27 +264,22 @@ class StartWindow {
262
264
  }
263
265
 
264
266
  /**
265
- * `body` behind the `args` gate: validate, then run the body with what the gate accepted.
266
- *
267
- * 11.27's LAST TWO STEPS as one body. The gate runs on every run of the entry — a refresh
268
- * re-validates — and throws the refusal, which is what a body failing already does: the entry's
269
- * `error` holds it and its read throws it (2.2), and the body the gate stands in front of is never
270
- * called. What it accepts is what the body is handed (11.68).
267
+ * `body` handed what the `args` gate accepted for one entry (11.68), rather than the args it was
268
+ * reached with.
271
269
  *
272
270
  * A STREAM STAYS A STREAM FROM CONSTRUCTION. 9.6 reads the body's KIND, so the wrapper around an
273
271
  * async generator is an async generator, or a gated keyed stream would be one only from its first
274
272
  * run.
275
273
  */
276
- function gated<Args>(
274
+ function handed<Args>(
277
275
  body: (args: Args) => unknown,
278
- gate: Schema<Args>,
276
+ accepted: Args,
279
277
  ): (args: Args) => unknown {
280
- const check = (args: Args): Args => checkArgs(gate, args)
281
278
  if (isStreamBody(body))
282
- return async function* (args: Args) {
283
- yield* (body as (args: Args) => AsyncIterable<unknown>)(check(args))
279
+ return async function* () {
280
+ yield* (body as (args: Args) => AsyncIterable<unknown>)(accepted)
284
281
  }
285
- return (args: Args): unknown => body(check(args))
282
+ return (): unknown => body(accepted)
286
283
  }
287
284
 
288
285
  /**
@@ -348,7 +345,7 @@ export function createKeyed<
348
345
  (throttle ?? debounce) as number,
349
346
  )
350
347
  const lifetime = ttl === undefined ? null : new Lifetime(ttl)
351
- const entryBody = heldOnce(gate === undefined ? body : gated(body, gate))
348
+ const entryBody = heldOnce(body)
352
349
  // 11.27, 11.29 — the defaults are applied before the key is taken, so the entry is one for a
353
350
  // call that wrote them and one that left them to the parameter. Read once, here: a body with
354
351
  // none pays a `null` test per reach.
@@ -358,6 +355,12 @@ export function createKeyed<
358
355
  // there were, so the key that starts loading next — search-as-you-type — never woke it. One
359
356
  // signal per `Keyed`, moved where an entry is reached and where one is released.
360
357
  const membership = newTracked()
358
+ // 8.21 — A `Keyed` BUILT IN A SETUP OR A REQUEST ends with it, and every store its entries
359
+ // watch is stopped then: the entries are the cache's, so no owner of their own stops them.
360
+ if (options?.store !== undefined && owning())
361
+ owned(() => {
362
+ for (const entry of entries.values()) unwatchStore(entry)
363
+ })
361
364
 
362
365
  function askMembership(): void {
363
366
  const reader = TRACKING.reader
@@ -372,6 +375,7 @@ export function createKeyed<
372
375
  ): void {
373
376
  entries.delete(key)
374
377
  leaveSelections(created)
378
+ unwatchStore(created)
375
379
  signalMoved(membership)
376
380
  }
377
381
 
@@ -385,9 +389,32 @@ export function createKeyed<
385
389
  if (failedTransiently(standing)) standing.refresh()
386
390
  return standing
387
391
  }
392
+ // 11.27's VALIDATE, ONCE PER ENTRY AND BEFORE IT IS FILED: a refused `Args` is answered by a
393
+ // value holding the refusal (2.2) that no map keeps, so args nobody should have sent never
394
+ // take a place among the entries — on a `global` value, a place nothing but `invalidate`
395
+ // would take back (11.35). Built with none of the options: a `store` would restore a value
396
+ // over the refusal and watch for an entry nothing releases.
397
+ let run: (args: Args) => unknown = entryBody
398
+ if (gate !== undefined) {
399
+ let accepted: Args
400
+ try {
401
+ accepted = checkArgs(gate, args)
402
+ } catch (refusal) {
403
+ return createReactive<Stored, Accepted, Failures>(
404
+ `${label}(${key})`,
405
+ () => {
406
+ throw refusal
407
+ },
408
+ args,
409
+ undefined,
410
+ undefined,
411
+ )
412
+ }
413
+ run = handed(entryBody, accepted)
414
+ }
388
415
  const created = createReactive<Stored, Accepted, Failures>(
389
416
  `${label}(${key})`,
390
- entryBody as (args: unknown) => unknown,
417
+ run as (args: unknown) => unknown,
391
418
  args,
392
419
  undefined,
393
420
  entryOptions as ReactiveOptions<Accepted, Stored, Failures>,
@@ -75,6 +75,7 @@ export type Transformer<Accepted, Stored, Failures = never> = (
75
75
  export type Store<Stored> = {
76
76
  get: () => Stored | undefined | Promise<Stored | undefined>
77
77
  set: (value: Stored, retention: { ttl: number }) => void | Promise<void>
78
+ watch?: (changed: () => void) => () => void
78
79
  }
79
80
 
80
81
  /**
@@ -366,6 +367,8 @@ type Persisted = {
366
367
  * what `pending` sends next, and what 8.18 serves a re-read in place of calling `get`.
367
368
  */
368
369
  unconfirmed: unknown
370
+ /** What stops the store's `watch`, where it has one (8.21). */
371
+ unwatch: (() => void) | null
369
372
  }
370
373
 
371
374
  /**
@@ -891,6 +894,7 @@ class Cell {
891
894
  writing: false,
892
895
  pending: false,
893
896
  unconfirmed: undefined,
897
+ unwatch: null,
894
898
  }
895
899
  this.by = (options?.by ?? null) as
896
900
  | string
@@ -3801,6 +3805,32 @@ function rereadStore(cell: Cell, persisted: Persisted): void {
3801
3805
  )
3802
3806
  }
3803
3807
 
3808
+ /**
3809
+ * Follows what the store says changed outside this process, re-reading it each time.
3810
+ *
3811
+ * 8.20 — A RE-READ, NEVER THE BODY: an edit made where the value lives is the store's news, so it
3812
+ * is read back as a refresh of a value whose only producer is its store would read it, and 8.18
3813
+ * still has a write the app made stand over it. 8.21 — stopped with the value: by its owner here,
3814
+ * and by its `Keyed` through `unwatchStore` where an entry is evicted.
3815
+ */
3816
+ function watchStore(cell: Cell, persisted: Persisted, cached: boolean): void {
3817
+ const watch = persisted.store.watch
3818
+ if (watch === undefined) return
3819
+ persisted.unwatch = watch(() => {
3820
+ rereadStore(cell, persisted)
3821
+ })
3822
+ if (!cached && owning()) owned(() => unwatchStore(cell.callable))
3823
+ }
3824
+
3825
+ /** Stops the store's `watch` on `value`, once (8.21). */
3826
+ export function unwatchStore(value: unknown): void {
3827
+ const persisted = (value as Callable)[CELL].store
3828
+ if (persisted === null || persisted.unwatch === null) return
3829
+ const unwatch = persisted.unwatch
3830
+ persisted.unwatch = null
3831
+ unwatch()
3832
+ }
3833
+
3804
3834
  /**
3805
3835
  * Lands a store re-read, letting an unconfirmed write win.
3806
3836
  *
@@ -4693,8 +4723,10 @@ export function createReactive<
4693
4723
  // (2.2, 4.14) rather than serving an `undefined` its own type says is a `Stored`.
4694
4724
  // 8.3 AND 8.14: a store answers first, where the value is constructed, and whatever it misses
4695
4725
  // falls back to what the value would have done without one.
4696
- if (cell.store !== null) restoreFirst(cell, cell.store, initial)
4697
- else if (body === null) {
4726
+ if (cell.store !== null) {
4727
+ restoreFirst(cell, cell.store, initial)
4728
+ watchStore(cell, cell.store, cached)
4729
+ } else if (body === null) {
4698
4730
  if (initial === undefined) landOpening(cell)
4699
4731
  else if (initial !== NO_INITIAL) open(cell, initial)
4700
4732
  } else if (deferred) cell.loading = true
package/src/shared/rpc.ts CHANGED
@@ -16,6 +16,7 @@
16
16
  import { type Failed, isFailedNamed } from './Failed.ts'
17
17
  import { GLOBAL_GETS } from './GLOBAL_GETS.ts'
18
18
  import { HandlerRoute } from './HandlerRoute.ts'
19
+ import { IN_PROCESS_ORIGIN } from './IN_PROCESS_ORIGIN.ts'
19
20
  import { compose, type Middleware } from './Middleware.ts'
20
21
  import { principalRotated } from './principalRotated.ts'
21
22
  import { RPC_SERVER } from './RPC_SERVER.ts'
@@ -442,7 +443,7 @@ export function declare(method: Method): Declare {
442
443
  if (RPC_SERVER.raw !== null)
443
444
  return RPC_SERVER.raw(
444
445
  new Request(
445
- new URL(asked.url, 'http://app.internal'),
446
+ new URL(asked.url, IN_PROCESS_ORIGIN),
446
447
  given,
447
448
  ),
448
449
  )
@@ -4,6 +4,7 @@
4
4
 
5
5
  import { spreadEntries } from '#shared/spread.ts'
6
6
  import type { Scope } from './Scope.ts'
7
+ import { addAttribute } from './setAttribute.ts'
7
8
 
8
9
  /** Keeps an element's spread attributes and `on<event>` listeners written as the value changes. */
9
10
  export function bindSpread(
@@ -19,8 +20,9 @@ export function bindSpread(
19
20
  const next = new Set<string>()
20
21
  for (const [name, text] of spreadEntries(read(), explicit, wanted)) {
21
22
  next.add(name)
22
- if (element.getAttribute(name) !== text)
23
- element.setAttribute(name, text)
23
+ const standing = element.getAttribute(name)
24
+ if (standing === null) addAttribute(element, name, text)
25
+ else if (standing !== text) element.setAttribute(name, text)
24
26
  }
25
27
  for (const name of written)
26
28
  if (!next.has(name)) element.removeAttribute(name)
package/src/ui/index.ts CHANGED
@@ -48,7 +48,7 @@ export { escapeless } from './escapeless.ts'
48
48
  export { contributeHead } from './Level.ts'
49
49
  export { listAt, updateList } from './List.ts'
50
50
  export { warnMismatch } from './Mismatch.ts'
51
- export { markupNodes } from './parseMarkup.ts'
51
+ export { markupNodes, namespaceUnder } from './parseMarkup.ts'
52
52
  export {
53
53
  fillEmpty,
54
54
  fillRegion,
@@ -58,7 +58,7 @@ export {
58
58
  export { rowKeyOf } from './rowKeyOf.ts'
59
59
  export { itemOf, noUpdate, replaceRow, rowCell } from './rows.ts'
60
60
  export { EMPTY_SCOPE, Scope } from './Scope.ts'
61
- export { setAttribute } from './setAttribute.ts'
61
+ export { addAttribute, setAttribute } from './setAttribute.ts'
62
62
  export { setClass } from './setClass.ts'
63
63
  export { setClassGroup } from './setClassGroup.ts'
64
64
  export { setMarkup } from './setMarkup.ts'
@@ -1,3 +1,8 @@
1
+ import {
2
+ HTML_NAMESPACE,
3
+ INTEGRATION_POINTS,
4
+ } from '#shared/INTEGRATION_POINTS.ts'
5
+
1
6
  // MARKUP PARSED IN THE DOCUMENT `parent` BELONGS TO, not the global one. A page mounted into a second
2
7
  // document (an example `<iframe>`) parsed its `{#if}` arms in the right one and its `{#for}` rows in
3
8
  // the page's while the two were separate copies of this; one copy is one answer.
@@ -9,26 +14,22 @@ const TEMPLATES = new WeakMap<Document, HTMLTemplateElement>()
9
14
  const SVG = 'http://www.w3.org/2000/svg'
10
15
  const MATHML = 'http://www.w3.org/1998/Math/MathML'
11
16
 
12
- // The foreign elements whose children the parser reads as HTML again — the integration points —
13
- // so markup going into one parses the way it would at the template's own top level.
14
- const INTEGRATION_POINTS = new Set([
15
- 'foreignObject',
16
- 'desc',
17
- 'title',
18
- 'mi',
19
- 'mo',
20
- 'mn',
21
- 'ms',
22
- 'mtext',
23
- ])
24
-
25
- /** Whether the parser reads `element`'s children as HTML again, though it is SVG or MathML. */
26
- function isIntegrationPoint(element: Element): boolean {
27
- if (INTEGRATION_POINTS.has(element.localName)) return true
17
+ /**
18
+ * The namespace an element the parser makes inside `parent` takes: SVG's or MathML's under an
19
+ * element of either, and HTML's under anything else — an HTML element, a document, a fragment, and
20
+ * an integration point (`INTEGRATION_POINTS`), whose children the parser reads as HTML again.
21
+ */
22
+ export function namespaceUnder(parent: Node): string {
23
+ const element = parent as Element
24
+ const space = element.namespaceURI
25
+ if (space !== SVG && space !== MATHML) return HTML_NAMESPACE
26
+ if (INTEGRATION_POINTS.has(element.localName)) return HTML_NAMESPACE
28
27
  // The HTML standard makes an `<annotation-xml>` one only by the encoding it declares.
29
- if (element.localName !== 'annotation-xml') return false
28
+ if (element.localName !== 'annotation-xml') return space
30
29
  const encoding = element.getAttribute('encoding')?.toLowerCase()
31
30
  return encoding === 'text/html' || encoding === 'application/xhtml+xml'
31
+ ? HTML_NAMESPACE
32
+ : space
32
33
  }
33
34
 
34
35
  /**
@@ -48,14 +49,8 @@ export function parseMarkup(parent: Node, markup: string): DocumentFragment {
48
49
  template = document.createElement('template')
49
50
  TEMPLATES.set(document, template)
50
51
  }
51
- const space = (parent as Element).namespaceURI
52
- const wrapper =
53
- (space !== SVG && space !== MATHML) ||
54
- isIntegrationPoint(parent as Element)
55
- ? undefined
56
- : space === SVG
57
- ? 'svg'
58
- : 'math'
52
+ const space = namespaceUnder(parent)
53
+ const wrapper = space === HTML_NAMESPACE ? undefined : space === SVG ? 'svg' : 'math'
59
54
  if (wrapper === undefined) {
60
55
  template.innerHTML = markup
61
56
  return template.content
@@ -1,4 +1,6 @@
1
1
  import { attributeText } from '#shared/attributeText.ts'
2
+ import { FOREIGN_ATTRIBUTES } from '#shared/FOREIGN_ATTRIBUTES.ts'
3
+ import { HTML_NAMESPACE } from '#shared/INTEGRATION_POINTS.ts'
2
4
  import { WORK } from '#shared/WORK.ts'
3
5
 
4
6
  /**
@@ -33,5 +35,27 @@ export function setAttribute(
33
35
  if (element.hasAttribute(name)) element.removeAttribute(name)
34
36
  return
35
37
  }
36
- if (element.getAttribute(name) !== next) element.setAttribute(name, next)
38
+ const standing = element.getAttribute(name)
39
+ if (standing === next) return
40
+ if (standing === null) addAttribute(element, name, next)
41
+ else element.setAttribute(name, next)
42
+ }
43
+
44
+ /**
45
+ * Writes an attribute `element` does not hold yet, in the namespace the parser gives it there.
46
+ *
47
+ * 32.70 — an `xlink:href` on an SVG element is XLink's (`FOREIGN_ATTRIBUTES`), and one
48
+ * `setAttribute` makes has none, so a `<use>` it names draws nothing. Only an attribute's CREATION
49
+ * decides its namespace: `setAttribute` and `getAttribute` find a standing one by its qualified
50
+ * name, whatever namespace it is in.
51
+ */
52
+ export function addAttribute(
53
+ element: Element,
54
+ name: string,
55
+ text: string,
56
+ ): void {
57
+ const space = FOREIGN_ATTRIBUTES.get(name)
58
+ if (space === undefined || element.namespaceURI === HTML_NAMESPACE)
59
+ element.setAttribute(name, text)
60
+ else element.setAttributeNS(space, name, text)
37
61
  }