@abide/abide 0.123.0 → 0.123.1
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 +9 -0
- package/docs/REGISTRY.md +1 -1
- package/docs/RULEBOOK.md +3 -0
- package/docs/abide.md +1 -1
- package/package.json +1 -1
- package/src/compiler/projectShadow.ts +3 -1
- package/src/server/derivedArgs.ts +71 -2
- package/src/shared/rpc.ts +9 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# @abide/abide
|
|
2
2
|
|
|
3
|
+
## 0.123.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- the args-as-sent walk returns the args it does not copy, walks a cycle once, and keeps a parsed __proto__ a member (RULEBOOK 19.25) ([`ee02cac`](https://github.com/briancray/abide/commit/ee02cacbb64f00374ae98154e788c59c5a4ec49b))
|
|
8
|
+
- bf77c28: `abide check` no longer fails when an inline component is passed to a component that sits inside another component's body: the outer close tag's read in the check's projection now ends with a `;`, so the braces that scope the passed component are not parsed as an arrow function.
|
|
9
|
+
- 7c24094: A no-argument mutation behind `middleware` no longer starts its body before its rungs run. Dropping the previous call's answer built the request's instance of the handler, which started the body ahead of every rung, so a refusing rung (`signedIn`) answered after the body had already run. Calling the same mutation twice in one request now writes twice behind rungs as well (RULEBOOK 16.7, 16.11, 16.16).
|
|
10
|
+
- 94fbea7: A handler's derived args schema reads a member left `undefined` as absent, so an in-process call like `getResearch({ home, topic: undefined })` is answered as the wire answers it rather than refused 422 (RULEBOOK 19.25, 15.8).
|
|
11
|
+
|
|
3
12
|
## 0.123.0
|
|
4
13
|
|
|
5
14
|
### Minor Changes
|
package/docs/REGISTRY.md
CHANGED
|
@@ -273,7 +273,7 @@ type Gated<Gate, Accepted> = [Schema<Accepted>] extends [Gate] ? never : Failed<
|
|
|
273
273
|
|
|
274
274
|
| Name | Signature | Meaning | Rules |
|
|
275
275
|
| --- | --- | --- | --- |
|
|
276
|
-
| `Schema<T>` | `((value: unknown) => T) \| StandardSchemaV1<T>` | The two forms a shape may be declared in. | 19.2, 19.3, 19.4, 19.6, 19.8, 19.9, 19.10, 19.18, 19.11, 19.12 |
|
|
276
|
+
| `Schema<T>` | `((value: unknown) => T) \| StandardSchemaV1<T>` | The two forms a shape may be declared in. | 19.2, 19.3, 19.4, 19.6, 19.8, 19.9, 19.10, 19.18, 19.25, 19.11, 19.12 |
|
|
277
277
|
| `JsonValue` | `null \| boolean \| number \| string \| JsonValue[] \| { [k: string]: JsonValue }` | What an `Args` may hold. | — |
|
|
278
278
|
| `Issues<T>` | `T extends object ? Partial<Record<Paths<T> \| '', string[]>> : string[]` | What was wrong, keyed by where. | 19.4, 19.19 |
|
|
279
279
|
| `Paths<T>` | `string` | The dot-joined leaf paths of a type. | 19.5 |
|
package/docs/RULEBOOK.md
CHANGED
|
@@ -1335,6 +1335,9 @@ to, and MUST NOT change the type of the value that slot gates.
|
|
|
1335
1335
|
19.24 The value a body produces MUST be typed from the body alone, and a `schema` declared beside it
|
|
1336
1336
|
MUST NOT change that type.
|
|
1337
1337
|
|
|
1338
|
+
19.25 A `Schema` derived per 19.9 MUST read a member left `undefined`, at any depth of a record, as
|
|
1339
|
+
absent — in process as the wire carries it (30.28).
|
|
1340
|
+
|
|
1338
1341
|
# 20. Generated surfaces
|
|
1339
1342
|
|
|
1340
1343
|
20.1 Every surface `Clients` names MUST be derived from the route table, and MUST NOT be authored
|
package/docs/abide.md
CHANGED
|
@@ -8067,7 +8067,7 @@ Transports: what is written in place rather than imported.
|
|
|
8067
8067
|
|
|
8068
8068
|
| Name | Signature | Meaning | Rules |
|
|
8069
8069
|
| --- | --- | --- | --- |
|
|
8070
|
-
| `Schema<T>` | `((value: unknown) => T) \| StandardSchemaV1<T>` | The two forms a shape may be declared in. | [19.2](/rulebook#19.2), [19.3](/rulebook#19.3), [19.4](/rulebook#19.4), [19.6](/rulebook#19.6), [19.8](/rulebook#19.8), [19.9](/rulebook#19.9), [19.10](/rulebook#19.10), [19.18](/rulebook#19.18), [19.11](/rulebook#19.11), [19.12](/rulebook#19.12) |
|
|
8070
|
+
| `Schema<T>` | `((value: unknown) => T) \| StandardSchemaV1<T>` | The two forms a shape may be declared in. | [19.2](/rulebook#19.2), [19.3](/rulebook#19.3), [19.4](/rulebook#19.4), [19.6](/rulebook#19.6), [19.8](/rulebook#19.8), [19.9](/rulebook#19.9), [19.10](/rulebook#19.10), [19.18](/rulebook#19.18), [19.25](/rulebook#19.25), [19.11](/rulebook#19.11), [19.12](/rulebook#19.12) |
|
|
8071
8071
|
| `JsonSchema` | `JsonValue` | A JSON Schema document, the native form. | [19.7](/rulebook#19.7), [19.14](/rulebook#19.14) |
|
|
8072
8072
|
|
|
8073
8073
|
## Taught in
|
package/package.json
CHANGED
|
@@ -391,11 +391,13 @@ function project(file: AbideFile): Projection {
|
|
|
391
391
|
}
|
|
392
392
|
text += '}))\n'
|
|
393
393
|
// 38.116 — a close tag of its own is a second read of the name, so it is renamed with it.
|
|
394
|
+
// Ended by its `;`: the braces a passed component is scoped in (32.51) can follow, and
|
|
395
|
+
// `(Name)\n{` parses as an arrow function's head.
|
|
394
396
|
const closed = file.origins.closes.get(node)
|
|
395
397
|
if (closed !== undefined) {
|
|
396
398
|
text += ';('
|
|
397
399
|
copied(node.tag, closed)
|
|
398
|
-
text += ')
|
|
400
|
+
text += ');\n'
|
|
399
401
|
}
|
|
400
402
|
for (const attribute of refused)
|
|
401
403
|
slots.set(
|
|
@@ -15,12 +15,81 @@ import { argsDocumentOf } from './coerceArgs.ts'
|
|
|
15
15
|
* called, whether or not an app mounted it. Not where its `args` gate publishes a document of its
|
|
16
16
|
* own, which holds the args itself (19.7). Args that did not come are held as an empty record: a
|
|
17
17
|
* schema is only derived where the handler's args are a required record, so a required member
|
|
18
|
-
* missing is refused, not read off nothing.
|
|
18
|
+
* missing is refused, not read off nothing. The args are held as SENT (`asSent`): a call in process
|
|
19
|
+
* passing a member `undefined` is the call the wire carries without it.
|
|
19
20
|
*/
|
|
20
21
|
export function derivedArgs(
|
|
21
22
|
handler: HeldToAnnotation & Gated<unknown>,
|
|
22
23
|
schema: JsonValue,
|
|
23
24
|
): void {
|
|
24
25
|
if (argsDocumentOf(handler) !== undefined) return
|
|
25
|
-
handler[DERIVED_ARGS] = (args) => validateJson(schema, args ?? {})
|
|
26
|
+
handler[DERIVED_ARGS] = (args) => validateJson(schema, asSent(args ?? {}))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The records `asSent` is inside of, so a cyclic value is walked once rather than forever. */
|
|
30
|
+
const WALKING: object[] = []
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* `value` as the wire carries it, which drops a member left `undefined` (19.25): a URL omits it
|
|
34
|
+
* (30.28) and JSON leaves it out, at any depth of a body. `value` itself wherever it holds none, so
|
|
35
|
+
* the walk allocates nothing for the call that writes no `undefined` member.
|
|
36
|
+
*
|
|
37
|
+
* AN `undefined` IN A LIST IS KEPT. The two encodings part there — JSON writes `null`, a URL the
|
|
38
|
+
* text `undefined` — so neither is the wire's, and the schema answers the value as given.
|
|
39
|
+
*/
|
|
40
|
+
export function asSent(value: unknown): unknown {
|
|
41
|
+
if (value === null || typeof value !== 'object' || WALKING.includes(value))
|
|
42
|
+
return value
|
|
43
|
+
WALKING.push(value)
|
|
44
|
+
try {
|
|
45
|
+
return Array.isArray(value)
|
|
46
|
+
? listAsSent(value)
|
|
47
|
+
: recordAsSent(value as Record<string, unknown>)
|
|
48
|
+
} finally {
|
|
49
|
+
WALKING.pop()
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function listAsSent(list: unknown[]): unknown[] {
|
|
54
|
+
let sent: unknown[] | null = null
|
|
55
|
+
for (let index = 0; index < list.length; index += 1) {
|
|
56
|
+
const member = list[index]
|
|
57
|
+
const held = asSent(member)
|
|
58
|
+
if (sent === null && held !== member) sent = list.slice(0, index)
|
|
59
|
+
sent?.push(held)
|
|
60
|
+
}
|
|
61
|
+
return sent ?? list
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function recordAsSent(
|
|
65
|
+
record: Record<string, unknown>,
|
|
66
|
+
): Record<string, unknown> {
|
|
67
|
+
let sent: Record<string, unknown> | null = null
|
|
68
|
+
for (const key in record) {
|
|
69
|
+
if (!Object.hasOwn(record, key)) continue
|
|
70
|
+
const member = record[key]
|
|
71
|
+
const held = member === undefined ? undefined : asSent(member)
|
|
72
|
+
if (sent === null && (member === undefined || held !== member))
|
|
73
|
+
sent = membersBefore(record, key)
|
|
74
|
+
if (sent !== null && held !== undefined) sent[key] = held
|
|
75
|
+
}
|
|
76
|
+
return sent ?? record
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* A copy of `record`'s own members ahead of `key`, in their order.
|
|
81
|
+
*
|
|
82
|
+
* NO PROTOTYPE, so an own `__proto__` member JSON parsed stays a member the schema reads rather
|
|
83
|
+
* than becoming the copy's prototype.
|
|
84
|
+
*/
|
|
85
|
+
function membersBefore(
|
|
86
|
+
record: Record<string, unknown>,
|
|
87
|
+
key: string,
|
|
88
|
+
): Record<string, unknown> {
|
|
89
|
+
const copy: Record<string, unknown> = Object.create(null)
|
|
90
|
+
for (const earlier in record) {
|
|
91
|
+
if (earlier === key) break
|
|
92
|
+
if (Object.hasOwn(record, earlier)) copy[earlier] = record[earlier]
|
|
93
|
+
}
|
|
94
|
+
return copy
|
|
26
95
|
}
|
package/src/shared/rpc.ts
CHANGED
|
@@ -407,6 +407,12 @@ export function declare(method: Method): Declare {
|
|
|
407
407
|
rungs.length === 0
|
|
408
408
|
? null
|
|
409
409
|
: compose(rungs, async (ctx: RpcContext) => {
|
|
410
|
+
// 16.7 behind rungs — the handler's spent answer is dropped HERE, once the
|
|
411
|
+
// rungs let the call through: reading an unkeyed value's cell builds this
|
|
412
|
+
// request's instance, which starts its body (1.7), and dropped in `call` that
|
|
413
|
+
// ran the body ahead of every rung (16.11, 16.16).
|
|
414
|
+
if (cacheOptions !== undefined && !keyed)
|
|
415
|
+
dropAnswered(value)
|
|
410
416
|
const reached = reach(argsOf(ctx))
|
|
411
417
|
const started = performance.now()
|
|
412
418
|
try {
|
|
@@ -464,8 +470,9 @@ export function declare(method: Method): Declare {
|
|
|
464
470
|
}
|
|
465
471
|
// 16.7 — a mutation's call is a write each time, even where the last one settled this
|
|
466
472
|
// turn: a keyed value's reach evicts an outlived entry itself, and an unkeyed one's
|
|
467
|
-
// spent answer is dropped here.
|
|
468
|
-
if (cacheOptions !== undefined && !keyed
|
|
473
|
+
// spent answer is dropped here — or, behind rungs, in the onion's center.
|
|
474
|
+
if (cacheOptions !== undefined && !keyed && wrapped === null)
|
|
475
|
+
dropAnswered(value)
|
|
469
476
|
const reached = callLoad(
|
|
470
477
|
reachThrough,
|
|
471
478
|
args,
|