@abide/abide 0.117.1 → 0.117.2

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/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # @abide/abide
2
2
 
3
+ ## 0.117.2
4
+
5
+ ### Patch Changes
6
+
7
+ - ea4e27f: a value opening at a member of a name read by name — `reactive(route.url.searchParams.get('book') ?? '')`, `reactive(name.toUpperCase())` — is now a `Reactive` the file reads and writes by name, where before `abide check` refused every write as a string assigned to the `Reactive` and the build rebound the name instead of calling `set`
8
+ - ea4e27f: a value a `refresh({ tags })` marked while nothing read it loads when the next page adopts it — `reactive(() => getMeetings())` on a page visited after the write sends the request and serves the new answer, where it served the answer held before the write and asked nothing (RULEBOOK 13.7)
9
+
3
10
  ## 0.117.1
4
11
 
5
12
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abide/abide",
3
- "version": "0.117.1",
3
+ "version": "0.117.2",
4
4
  "type": "module",
5
5
  "description": "Isomorphic type-safe framework for reactive async interfaces for humans and machines",
6
6
  "license": "MIT",
@@ -167,7 +167,7 @@ function copiedFrom(
167
167
  * markup, or a read the lowering rewrote. Runs are ascending by where they stand, not by where they
168
168
  * came from, so this is a scan.
169
169
  */
170
- function mapForward(
170
+ export function mapForward(
171
171
  origins: readonly Origin[],
172
172
  position: number,
173
173
  ): number | undefined {
@@ -38,10 +38,15 @@ import { CHECKED_VALUES } from './Lowering.ts'
38
38
  import { diagnosisAt, lineOf } from './lineOf.ts'
39
39
  import type { AbideFile } from './parseAbide.ts'
40
40
  import { projectShadow, type Span } from './projectShadow.ts'
41
- import { abideIn } from './reactiveTypes.ts'
41
+ import { type Abide, abideIn, isReactiveType } from './reactiveTypes.ts'
42
42
  import type { ParsedSource, ReadSource } from './readSources.ts'
43
43
  import { readsIn, type ShadowSpans } from './readsIn.ts'
44
- import { CHECKED_SUFFIX, SHADOW_SUFFIX, ShadowMap } from './ShadowMap.ts'
44
+ import {
45
+ CHECKED_SUFFIX,
46
+ mapForward,
47
+ SHADOW_SUFFIX,
48
+ ShadowMap,
49
+ } from './ShadowMap.ts'
45
50
  import { SOURCE_SUFFIX } from './splitImports.ts'
46
51
  import { type TemplateContext, templateContext } from './templateContext.ts'
47
52
  import { templateWarnings } from './templateWarnings.ts'
@@ -711,56 +716,144 @@ export class TypeSession {
711
716
  if (queries.every((query) => query === undefined))
712
717
  return { answers, warnings }
713
718
 
714
- const project = await this.currentProject(created, changed)
719
+ let project = await this.currentProject(created, changed)
715
720
  if (project === undefined) return { answers, warnings }
716
- const checker = project.checker
717
- const abide = await abideIn(project)
718
-
719
- await Promise.all(
720
- queries.map(async (query, at) => {
721
- if (query === undefined) return
722
- const source = sources[at] as (typeof sources)[number]
723
- const sourceFile = await project.program.getSourceFile(
721
+ // Per source, its compiler warnings, the bindings the projection could not type
722
+ // (`bindingsOfReads`), and those the lowered program proved a `Reactive`.
723
+ const warned: Diagnosis[][] = sources.map(() => [])
724
+ const suspects: number[][] = sources.map(() => [])
725
+ const held: Set<number>[] = sources.map(() => new Set())
726
+ const answer = async (
727
+ asked: Project,
728
+ abide: Abide,
729
+ at: number,
730
+ ): Promise<void> => {
731
+ const query = queries[at]
732
+ if (query === undefined) return
733
+ const source = sources[at] as (typeof sources)[number]
734
+ const sourceFile = await asked.program.getSourceFile(
735
+ query.map.shadowPath,
736
+ )
737
+ // The shadow has to be INSIDE the opened project or the checker never reads it.
738
+ // Refusing loudly rather than answering nothing: no reads is a file whose every
739
+ // name lowers as written, which renders `[object Object]` where a value was due —
740
+ // a wrong page, with nothing about it saying so.
741
+ if (sourceFile === undefined)
742
+ throw new Error(
743
+ `abide: ${source.path} is not inside the project ${this.tsconfig}, so no name in it can be typed (RULEBOOK 31.3). Give the app a tsconfig whose include reaches it.`,
744
+ )
745
+ const read = await readsIn(
746
+ asked.checker,
747
+ abide,
748
+ sourceFile,
749
+ query.map.shadowPath,
750
+ query.spans,
751
+ source.file,
752
+ held[at] as Set<number>,
753
+ )
754
+ answers[at] = read.answer
755
+ suspects[at] = read.suspects
756
+ const placed: Diagnosis[] = []
757
+ for (const warning of read.warnings) {
758
+ const offset = query.map.toSource(
724
759
  query.map.shadowPath,
760
+ warning.at,
761
+ )
762
+ if (
763
+ source.text === undefined ||
764
+ !isIgnored(source.text, offset, warning.message)
725
765
  )
726
- // The shadow has to be INSIDE the opened project or the checker never reads it.
727
- // Refusing loudly rather than answering nothing: no reads is a file whose every
728
- // name lowers as written, which renders `[object Object]` where a value was due —
729
- // a wrong page, with nothing about it saying so.
730
- if (sourceFile === undefined)
731
- throw new Error(
732
- `abide: ${source.path} is not inside the project ${this.tsconfig}, so no name in it can be typed (RULEBOOK 31.3). Give the app a tsconfig whose include reaches it.`,
766
+ placed.push(
767
+ diagnosisAt(
768
+ source.path,
769
+ source.text,
770
+ offset,
771
+ 'warning',
772
+ warning.message,
773
+ ),
733
774
  )
734
- const read = await readsIn(
735
- checker,
736
- abide,
737
- sourceFile,
738
- query.map.shadowPath,
739
- query.spans,
775
+ }
776
+ warned[at] = placed
777
+ }
778
+ const every = queries.map((_, at) => at)
779
+ const first = project
780
+ const firstAbide = await abideIn(first)
781
+ await Promise.all(every.map((at) => answer(first, firstAbide, at)))
782
+ // 31.1 — A BINDING THE PROJECTION TYPED FROM A `Reactive` IS ASKED OF THE PROGRAM IT RUNS
783
+ // AS: each file holding one is lowered with what it has answered so far, and a binding that
784
+ // is a `Reactive` there is one in the projection too. A binding typed from one that just
785
+ // turned out to be a `Reactive` is asked again, until a round turns none. Nothing is asked,
786
+ // and no second snapshot taken, where no file holds one (`bindingsOfReads`).
787
+ for (;;) {
788
+ const asking = every.filter(
789
+ (at) => (suspects[at] as number[]).length > 0,
790
+ )
791
+ if (asking.length === 0) break
792
+ const loweredCreated: string[] = []
793
+ const loweredChanged: string[] = []
794
+ const lowered = asking.map((at) => {
795
+ const source = sources[at] as (typeof sources)[number]
796
+ const query = queries[at] as { map: ShadowMap }
797
+ const shadow = checkedShadow(
740
798
  source.file,
799
+ answers[at] as CheckerAnswer,
741
800
  )
742
- answers[at] = read.answer
743
- for (const warning of read.warnings) {
744
- const offset = query.map.toSource(
745
- query.map.shadowPath,
746
- warning.at,
747
- )
748
- if (
749
- source.text === undefined ||
750
- !isIgnored(source.text, offset, warning.message)
751
- )
752
- warnings.push(
753
- diagnosisAt(
754
- source.path,
755
- source.text,
756
- offset,
757
- 'warning',
758
- warning.message,
759
- ),
760
- )
801
+ this.register(
802
+ query.map.checkedPath,
803
+ shadow.text,
804
+ loweredCreated,
805
+ loweredChanged,
806
+ )
807
+ this.openOutside(
808
+ source.path,
809
+ query.map.checkedPath,
810
+ loweredChanged,
811
+ )
812
+ return {
813
+ at,
814
+ path: query.map.checkedPath,
815
+ origins: shadow.origins,
761
816
  }
762
- }),
763
- )
817
+ })
818
+ project = await this.currentProject(loweredCreated, loweredChanged)
819
+ if (project === undefined) return { answers, warnings }
820
+ const asked = project
821
+ const abide = await abideIn(asked)
822
+ const turned = new Set<number>()
823
+ await Promise.all(
824
+ lowered.map(async ({ at, path, origins }) => {
825
+ const sourceFile = await asked.program.getSourceFile(path)
826
+ if (sourceFile === undefined) return
827
+ for (const start of suspects[at] as number[]) {
828
+ const position = mapForward(origins, start)
829
+ if (position === undefined) continue
830
+ const symbol = await asked.checker.getSymbolAtLocation(
831
+ getTouchingPropertyName(sourceFile, position),
832
+ )
833
+ const type =
834
+ symbol === undefined
835
+ ? undefined
836
+ : await asked.checker.getTypeOfSymbol(symbol)
837
+ if (
838
+ type !== undefined &&
839
+ (await isReactiveType(
840
+ asked.checker,
841
+ abide,
842
+ type,
843
+ false,
844
+ ))
845
+ ) {
846
+ ;(held[at] as Set<number>).add(start)
847
+ turned.add(at)
848
+ }
849
+ }
850
+ }),
851
+ )
852
+ if (turned.size === 0) break
853
+ await Promise.all([...turned].map((at) => answer(asked, abide, at)))
854
+ for (const at of asking) if (!turned.has(at)) suspects[at] = []
855
+ }
856
+ for (const placed of warned) warnings.push(...placed)
764
857
  return { answers, warnings }
765
858
  }
766
859
 
@@ -28,6 +28,7 @@ import {
28
28
  import {
29
29
  type Symbol as CheckedSymbol,
30
30
  type Checker,
31
+ SignatureKind,
31
32
  SymbolFlags,
32
33
  type Type,
33
34
  TypeFlags,
@@ -129,7 +130,9 @@ export async function readsIn(
129
130
  shadowPath: string,
130
131
  spans: ShadowSpans,
131
132
  file: AbideFile,
132
- ): Promise<{ answer: CheckerAnswer; warnings: Finding[] }> {
133
+ /** Where each name the lowered program proved a `Reactive` starts (`bindingsOfReads`). */
134
+ held: ReadonlySet<number>,
135
+ ): Promise<{ answer: CheckerAnswer; warnings: Finding[]; suspects: number[] }> {
133
136
  // A destructuring pattern binds names this does not see, and that is 31.12's case.
134
137
  const names: Node[] = []
135
138
  const spelled = new Set<string>()
@@ -210,6 +213,12 @@ export async function readsIn(
210
213
  ),
211
214
  )
212
215
  await dropValueHolders(checker, names, reactive)
216
+ for (let at = 0; at < names.length; at += 1)
217
+ if (
218
+ reactive[at] === undefined &&
219
+ held.has((names[at] as Node).getStart())
220
+ )
221
+ reactive[at] = symbols[at]
213
222
  // 32.32 — A VALUE WHOSE `reactive(…)` DECLARES `by` keys a `{#for}` over it,
214
223
  // wherever the value is declared.
215
224
  const byValue = await Promise.all(
@@ -296,7 +305,86 @@ export async function readsIn(
296
305
  spans.scripts,
297
306
  ))
298
307
  warnings.push(warning)
299
- return { answer: new CheckerAnswer(reads, statics), warnings }
308
+ const answer = new CheckerAnswer(reads, statics)
309
+ return {
310
+ answer,
311
+ warnings,
312
+ suspects: await bindingsOfReads(
313
+ checker,
314
+ abide,
315
+ names,
316
+ reactive,
317
+ answer,
318
+ ),
319
+ }
320
+ }
321
+
322
+ /**
323
+ * Where each binding starts whose type the projection cannot vouch for, for the lowered program to
324
+ * answer (`TypeSession.classify`).
325
+ *
326
+ * 31.1 — A BINDING TYPED FROM A READ IS TYPED FROM THE `Reactive` IN THE PROJECTION, and the read is
327
+ * of its value. `reactive(route.url.pathname)` asks a `Reactive<URL>` for a `pathname` it does not
328
+ * declare; the argument is the error type, which `reactive`'s overloads resolve to a `Keyed`, and
329
+ * the binding then lowered as no `Reactive` at all — `bookId = 'y'` rebinding the name and nothing
330
+ * waking. Lowered, the argument is `route.url().pathname` and the binding a `Reactive<string>`.
331
+ *
332
+ * ONLY A CALL THAT CAN HAND BACK A `Reactive` IS ASKED: an un-annotated binding, not one already,
333
+ * initialized by a call holding a read, to a callee one of whose signatures returns a `Reactive`.
334
+ * Anything else is a value however its reads are typed, and asking would cost every page that
335
+ * computes from one a second snapshot.
336
+ */
337
+ async function bindingsOfReads(
338
+ checker: Checker,
339
+ abide: Abide,
340
+ names: readonly Node[],
341
+ reactive: readonly (CheckedSymbol | undefined)[],
342
+ answer: CheckerAnswer,
343
+ ): Promise<number[]> {
344
+ const suspects: number[] = []
345
+ const asked: Promise<void>[] = []
346
+ for (let at = 0; at < names.length; at += 1) {
347
+ const name = names[at] as Node
348
+ const declaration = name.parent
349
+ if (
350
+ reactive[at] !== undefined ||
351
+ !isVariableDeclaration(declaration) ||
352
+ declaration.type !== undefined ||
353
+ declaration.initializer === undefined
354
+ )
355
+ continue
356
+ let call: Node = declaration.initializer
357
+ while (TRANSPARENT.has(call.kind))
358
+ call = (call as unknown as { expression: Node }).expression
359
+ if (
360
+ !isCallExpression(call) ||
361
+ !answer.holdsRead({ start: call.getStart(), end: call.end })
362
+ )
363
+ continue
364
+ const callee = call.expression
365
+ asked.push(
366
+ (async () => {
367
+ const type = await checker.getTypeAtLocation(callee)
368
+ if (type === undefined) return
369
+ for (const signature of await checker.getSignaturesOfType(
370
+ type,
371
+ SignatureKind.Call,
372
+ )) {
373
+ const returned =
374
+ await checker.getReturnTypeOfSignature(signature)
375
+ if (
376
+ returned !== undefined &&
377
+ (await isReactiveType(checker, abide, returned, false))
378
+ ) {
379
+ suspects.push(name.getStart())
380
+ return
381
+ }
382
+ }
383
+ })(),
384
+ )
385
+ }
386
+ await Promise.all(asked)
387
+ return suspects
300
388
  }
301
389
 
302
390
  /**
@@ -2219,6 +2219,10 @@ function mirror(cell: Cell, inner: Cell): void {
2219
2219
  // 7.13 through the adoption: the re-settle a rerun of the adopter makes reaches a reload the
2220
2220
  // adopted value owes, rather than finding it unservable and giving up.
2221
2221
  if (inner.owes !== OWES_NOTHING) settleOwed(inner)
2222
+ // 13.7 THROUGH THE ADOPTION TOO: a sweep's mark on a value nothing read is `DIRTY`, and an
2223
+ // adoption is that value's next read. A mirror reached through a rerun finds it resolved
2224
+ // already; the first one `adopt` runs does not, and copied the held value over the mark.
2225
+ if (inner.state !== CLEAN) resolve(inner)
2222
2226
  const adopted = inner.stream
2223
2227
  if (adopted !== null) {
2224
2228
  forwardStream(cell, inner, adopted)