@estiva-app/interop 0.1.2 → 0.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.
- package/CHANGELOG.md +84 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/projection.d.ts +91 -4
- package/dist/projection.d.ts.map +1 -1
- package/dist/projection.js +235 -34
- package/dist/projection.js.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +5 -0
- package/src/projection.ts +304 -44
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,90 @@ how a declaration is read, is a MAJOR — in `0.x`, a MINOR — even when no
|
|
|
6
6
|
TypeScript signature moved. A consumer upgrading must be able to tell whether
|
|
7
7
|
manifests already published still mean what they meant.
|
|
8
8
|
|
|
9
|
+
## 0.3.0 — 2026-09-01
|
|
10
|
+
|
|
11
|
+
**Manifest behaviour: unchanged.** Nothing a manifest may declare moved, and
|
|
12
|
+
nothing already published means anything different. What changes is how many
|
|
13
|
+
round trips reading one costs.
|
|
14
|
+
|
|
15
|
+
- **`createProjectionCache()`**, passed as the last argument to
|
|
16
|
+
`resolveForeignObject`, `resolveForeignEvent` and `resolveFolderProject`.
|
|
17
|
+
Omitting it is exactly the old behaviour.
|
|
18
|
+
|
|
19
|
+
A consumer that re-resolves on a timer paid the whole resolve every tick, and
|
|
20
|
+
**two of the four round trips were NIP-89 discovery** — the author's
|
|
21
|
+
`kind:31989` recommendation, then the `kind:31990` manifest. Those answer the
|
|
22
|
+
same thing until an app republishes. Memoised per kind and author with a
|
|
23
|
+
five-minute TTL; a negative answer is cached too, because "no app claims this
|
|
24
|
+
kind" costs the same two round trips and is just as stable.
|
|
25
|
+
|
|
26
|
+
**Owned by the caller.** A module-level cache would be invisible global state
|
|
27
|
+
shared by every consumer in a process, impossible to scope to a screen and
|
|
28
|
+
awkward to reset in a test.
|
|
29
|
+
|
|
30
|
+
Deliberately only the manifest. The object, its changes, its comments and its
|
|
31
|
+
children are what a refresh exists to notice — caching those is how a live
|
|
32
|
+
widget becomes a screenshot, which is the defect PRO-10 was filed for.
|
|
33
|
+
|
|
34
|
+
- **A `list` slot no longer costs a round trip of its own.** The child filter is
|
|
35
|
+
built from the manifest and the *pointer*, and an addressable event's `d` is
|
|
36
|
+
`pointer.identifier` by definition — it is what the root filter matches on. So
|
|
37
|
+
the second query never needed to wait for the first, and the children now ride
|
|
38
|
+
in the same request.
|
|
39
|
+
|
|
40
|
+
The two together: **10 requests a tick to 3** for three references on one Ship
|
|
41
|
+
issue, measured against production — one per reference — rendering identically
|
|
42
|
+
(title, kind, comment count, child count, widget). A comment posted between
|
|
43
|
+
ticks appeared on the next one-request resolve, so the widget is still live.
|
|
44
|
+
|
|
45
|
+
- **Comments and children are now matched on their filter's own criteria**, not
|
|
46
|
+
on kind. This is load-bearing rather than tidying: Peek's Topic declares
|
|
47
|
+
`kind:9` messages as children *and* `kind:9` as its comment kind, so merging
|
|
48
|
+
the two filters into one request puts both under one number and only the tag
|
|
49
|
+
separates them. Matching on kind alone would have made every message in a
|
|
50
|
+
Folder a comment on its own Topic. The predicate uses **any** matching tag,
|
|
51
|
+
as a relay's `#a` does — an event may carry several, and reading only the
|
|
52
|
+
first would silently drop a comment that references something else before its
|
|
53
|
+
parent.
|
|
54
|
+
|
|
55
|
+
Additive, with a changed request pattern, so a MINOR: ADR 0002 §4b puts the
|
|
56
|
+
break on MINOR within `0.x`.
|
|
57
|
+
|
|
58
|
+
## 0.2.0 — 2026-09-01
|
|
59
|
+
|
|
60
|
+
**Manifest behaviour: unchanged for addressable objects.** What changes is that
|
|
61
|
+
objects which have *no address* can now be resolved at all.
|
|
62
|
+
|
|
63
|
+
- **`resolveForeignEvent(reference, query)`** — resolve one event by id, from an
|
|
64
|
+
`nevent1…` or a bare 64-hex id.
|
|
65
|
+
|
|
66
|
+
The counterpart to `resolveForeignObject`, and PRO-11's reason to exist: a
|
|
67
|
+
`kind:9` message carries no `d`, so `(kind, pubkey, d)` cannot be built for it
|
|
68
|
+
and every resolver keyed on an address is blind to it. Measured on production
|
|
69
|
+
during PRO-6.
|
|
70
|
+
|
|
71
|
+
It is deliberately thinner. A regular event is immutable and has no folded
|
|
72
|
+
state, so there is no `records` rule to apply and no change events to fetch;
|
|
73
|
+
it cannot be the target of an `a` tag, so it has no comments addressed to it
|
|
74
|
+
and **no actions**. That last absence is the model being honest rather than a
|
|
75
|
+
gap — a change names its target by address, and there is nothing here to name.
|
|
76
|
+
|
|
77
|
+
Two round trips, and the order depends on the reference. A manifest is found
|
|
78
|
+
by kind; an `nevent` *may* carry its kind, and a bare id — which is what a
|
|
79
|
+
pasted `e` tag gives you — does not, so the event is read first to learn what
|
|
80
|
+
it is. Both arrive in practice.
|
|
81
|
+
|
|
82
|
+
- **`<bech32>` in a `web` template is substituted with whichever form the object
|
|
83
|
+
has.** NIP-89 says nothing about which NIP-19 entity a template is handed;
|
|
84
|
+
Ship's declares `naddr` because every Ship object is addressable. Substituting
|
|
85
|
+
the form the object *actually has* is what lets one template serve both, and
|
|
86
|
+
what stops a message linking to nothing.
|
|
87
|
+
|
|
88
|
+
- **`peerDependencies` rise to `@estiva-app/protocol >=0.3.0`**, which is where
|
|
89
|
+
`decodeNevent` lives. Unlike 0.1.0's `>=0.2.0`, this range is justified by
|
|
90
|
+
something the package uses — that one was a fact about the workspace and this
|
|
91
|
+
is a fact about the code.
|
|
92
|
+
|
|
9
93
|
## 0.1.2 — 2026-08-31
|
|
10
94
|
|
|
11
95
|
**Manifest behaviour: unchanged.** A build fix; 0.1.1 was tagged and never
|
package/dist/index.d.ts
CHANGED
|
@@ -24,5 +24,5 @@
|
|
|
24
24
|
* of every app that installed it, which is the objection that rules out
|
|
25
25
|
* iframes.
|
|
26
26
|
*/
|
|
27
|
-
export { resolveManifest, resolveForeignObject, resolveFolderProject, commentKindsOf, buildActionEvent, pickWidget, widgetChainProblem, CLOSED_WIDGETS, peopleViaRelay, type QueryFn, type PeopleFn, type People, type ForeignObject, type FolderProject, type ResolvedSlot, type ResolvedAction, type ManifestAction, type UnsignedActionEvent, } from './projection.js';
|
|
27
|
+
export { resolveManifest, resolveForeignObject, resolveForeignEvent, resolveFolderProject, commentKindsOf, buildActionEvent, pickWidget, widgetChainProblem, CLOSED_WIDGETS, peopleViaRelay, createProjectionCache, MANIFEST_TTL_MS, type ProjectionCache, type ResolvedManifest, type QueryFn, type PeopleFn, type People, type ForeignObject, type FolderProject, type ResolvedSlot, type ResolvedAction, type ManifestAction, type UnsignedActionEvent, } from './projection.js';
|
|
28
28
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,OAAO,EACL,eAAe,EACf,oBAAoB,EACpB,oBAAoB,EACpB,cAAc,EACd,gBAAgB,EAChB,UAAU,EACV,kBAAkB,EAClB,cAAc,EACd,cAAc,EACd,KAAK,OAAO,EACZ,KAAK,QAAQ,EACb,KAAK,MAAM,EACX,KAAK,aAAa,EAClB,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,KAAK,mBAAmB,GACzB,MAAM,iBAAiB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,OAAO,EACL,eAAe,EACf,oBAAoB,EACpB,mBAAmB,EACnB,oBAAoB,EACpB,cAAc,EACd,gBAAgB,EAChB,UAAU,EACV,kBAAkB,EAClB,cAAc,EACd,cAAc,EACd,qBAAqB,EACrB,eAAe,EACf,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,KAAK,OAAO,EACZ,KAAK,QAAQ,EACb,KAAK,MAAM,EACX,KAAK,aAAa,EAClB,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,KAAK,mBAAmB,GACzB,MAAM,iBAAiB,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -24,5 +24,5 @@
|
|
|
24
24
|
* of every app that installed it, which is the objection that rules out
|
|
25
25
|
* iframes.
|
|
26
26
|
*/
|
|
27
|
-
export { resolveManifest, resolveForeignObject, resolveFolderProject, commentKindsOf, buildActionEvent, pickWidget, widgetChainProblem, CLOSED_WIDGETS, peopleViaRelay, } from './projection.js';
|
|
27
|
+
export { resolveManifest, resolveForeignObject, resolveForeignEvent, resolveFolderProject, commentKindsOf, buildActionEvent, pickWidget, widgetChainProblem, CLOSED_WIDGETS, peopleViaRelay, createProjectionCache, MANIFEST_TTL_MS, } from './projection.js';
|
|
28
28
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,OAAO,EACL,eAAe,EACf,oBAAoB,EACpB,oBAAoB,EACpB,cAAc,EACd,gBAAgB,EAChB,UAAU,EACV,kBAAkB,EAClB,cAAc,EACd,cAAc,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,OAAO,EACL,eAAe,EACf,oBAAoB,EACpB,mBAAmB,EACnB,oBAAoB,EACpB,cAAc,EACd,gBAAgB,EAChB,UAAU,EACV,kBAAkB,EAClB,cAAc,EACd,cAAc,EACd,qBAAqB,EACrB,eAAe,GAYhB,MAAM,iBAAiB,CAAA"}
|
package/dist/projection.d.ts
CHANGED
|
@@ -223,13 +223,62 @@ interface Manifest {
|
|
|
223
223
|
stage?: string;
|
|
224
224
|
}[]>;
|
|
225
225
|
}
|
|
226
|
-
|
|
226
|
+
/** What {@link resolveManifest} answers with. */
|
|
227
|
+
export interface ResolvedManifest {
|
|
227
228
|
manifest: Manifest;
|
|
228
229
|
address: string;
|
|
229
230
|
viaRecommendation: boolean;
|
|
230
231
|
/** NIP-89 `web` template, `<bech32>` not yet substituted. */
|
|
231
232
|
webTemplate?: string;
|
|
232
|
-
}
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* A memo for the half of a resolve that does not change between refreshes.
|
|
236
|
+
*
|
|
237
|
+
* Resolving one reference costs four round trips, and **two of them are NIP-89
|
|
238
|
+
* discovery** — the author's `kind:31989` recommendation, then the `kind:31990`
|
|
239
|
+
* manifest itself. A consumer that re-resolves on a timer pays for both every
|
|
240
|
+
* time, and they answer the same thing until an app republishes its manifest.
|
|
241
|
+
* Measured on Ship, where a reference widget re-resolves on the poll: 4
|
|
242
|
+
* requests per reference per tick, identical on the second resolve, against a
|
|
243
|
+
* relay that meters reads at 300 a minute.
|
|
244
|
+
*
|
|
245
|
+
* **Owned by the caller, not this module.** A module-level cache would be
|
|
246
|
+
* invisible global state shared by every consumer in the process, impossible to
|
|
247
|
+
* scope to a screen and awkward to reset in a test. A caller that wants no
|
|
248
|
+
* caching passes nothing and gets exactly the old behaviour.
|
|
249
|
+
*
|
|
250
|
+
* Deliberately only the manifest. The object, its changes, its comments and its
|
|
251
|
+
* children are the parts a refresh exists to notice, and caching those is how
|
|
252
|
+
* a live widget becomes a screenshot.
|
|
253
|
+
*/
|
|
254
|
+
export interface ProjectionCache {
|
|
255
|
+
/** Forget everything. Worth calling after publishing a manifest. */
|
|
256
|
+
clear(): void;
|
|
257
|
+
/** @internal */
|
|
258
|
+
lookup(key: string, now: number): {
|
|
259
|
+
value: ResolvedManifest | null;
|
|
260
|
+
} | undefined;
|
|
261
|
+
/** @internal */
|
|
262
|
+
remember(key: string, value: ResolvedManifest | null, now: number): void;
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* How long a manifest may be believed without asking again.
|
|
266
|
+
*
|
|
267
|
+
* Five minutes is a compromise with one real cost: republish a manifest and
|
|
268
|
+
* consumers keep drawing the old projection for up to that long. That is
|
|
269
|
+
* recoverable and self-correcting, where the alternative — asking twice per
|
|
270
|
+
* reference per tick, for ever — is neither.
|
|
271
|
+
*/
|
|
272
|
+
export declare const MANIFEST_TTL_MS: number;
|
|
273
|
+
export declare function createProjectionCache(ttlMs?: number): ProjectionCache;
|
|
274
|
+
/**
|
|
275
|
+
* A negative answer is cached too.
|
|
276
|
+
*
|
|
277
|
+
* "No app claims this kind" costs the same two round trips as a hit and is just
|
|
278
|
+
* as stable. Caching only successes would leave the expensive case — a
|
|
279
|
+
* reference nothing can draw — paying full price on every tick for ever.
|
|
280
|
+
*/
|
|
281
|
+
export declare function resolveManifest(pointer: AddressPointer, query: QueryFn, cache?: ProjectionCache): Promise<ResolvedManifest | null>;
|
|
233
282
|
/**
|
|
234
283
|
* Which layout to draw, from a declared type or an ordered chain of them.
|
|
235
284
|
*
|
|
@@ -428,6 +477,37 @@ export interface ForeignObject {
|
|
|
428
477
|
* unresolvable reference in a chat message should degrade to plain text, not
|
|
429
478
|
* break the message around it.
|
|
430
479
|
*/
|
|
480
|
+
/**
|
|
481
|
+
* Resolve one event by id — `nevent1…`, or a bare 64-hex id.
|
|
482
|
+
*
|
|
483
|
+
* The counterpart to `resolveForeignObject`, and the reason it has to exist:
|
|
484
|
+
* **not every object has an address.** A `kind:9` message carries no `d`, so
|
|
485
|
+
* `(kind, pubkey, d)` cannot be built for it and every resolver keyed on an
|
|
486
|
+
* address is blind to it. Measured on production during PRO-6; PRO-11 is this.
|
|
487
|
+
*
|
|
488
|
+
* ## What it does *not* do, and why the function is short
|
|
489
|
+
*
|
|
490
|
+
* A regular event is immutable and has no folded state, so there is no `records`
|
|
491
|
+
* rule to apply, no change events to fetch, and no "current value" that differs
|
|
492
|
+
* from what is on the event. It also cannot be the target of an `a` tag, so it
|
|
493
|
+
* has no comments addressed to it and **no actions** — a change names its target
|
|
494
|
+
* by address, and there is nothing here to name. That absence is the model being
|
|
495
|
+
* honest rather than a gap to fill later.
|
|
496
|
+
*
|
|
497
|
+
* ## Two round trips, and the order depends on the reference
|
|
498
|
+
*
|
|
499
|
+
* A manifest is found by kind. An `nevent` *may* carry its kind, and when it
|
|
500
|
+
* does the manifest and the event can be fetched together. When it does not —
|
|
501
|
+
* a bare id, which is what a pasted `e` tag gives you — the event has to be
|
|
502
|
+
* read first to learn what kind it is. Both paths are supported because both
|
|
503
|
+
* arrive in practice, and a resolver that required the richer form would refuse
|
|
504
|
+
* references other clients legitimately produce.
|
|
505
|
+
*/
|
|
506
|
+
export declare function resolveForeignEvent(reference: string, query: QueryFn,
|
|
507
|
+
/** Defaults to asking the relay. The browser passes a cached lookup. */
|
|
508
|
+
lookupPeople?: PeopleFn,
|
|
509
|
+
/** See {@link ProjectionCache}. Omitting it is exactly the old behaviour. */
|
|
510
|
+
cache?: ProjectionCache): Promise<ForeignObject | null>;
|
|
431
511
|
export declare function resolveForeignObject(naddr: string, query: QueryFn,
|
|
432
512
|
/** Defaults to asking the relay. The browser passes a cached lookup. */
|
|
433
513
|
lookupPeople?: PeopleFn,
|
|
@@ -436,7 +516,12 @@ lookupPeople?: PeopleFn,
|
|
|
436
516
|
* module leave it at 0; it is the budget that stops a child's own `list`
|
|
437
517
|
* recursing forever. See `MAX_LIST_DEPTH`.
|
|
438
518
|
*/
|
|
439
|
-
depth?: number
|
|
519
|
+
depth?: number,
|
|
520
|
+
/**
|
|
521
|
+
* Optional memo for the NIP-89 discovery half. See {@link ProjectionCache} —
|
|
522
|
+
* omitting it is exactly the old behaviour.
|
|
523
|
+
*/
|
|
524
|
+
cache?: ProjectionCache): Promise<ForeignObject | null>;
|
|
440
525
|
/**
|
|
441
526
|
* A Folder's project and the tickets in motion in it.
|
|
442
527
|
*
|
|
@@ -484,7 +569,9 @@ lookupPeople?: PeopleFn,
|
|
|
484
569
|
* outside this module leave it at 0; it exists so that following a child's
|
|
485
570
|
* own `list` is a budget check rather than a thing nobody remembered.
|
|
486
571
|
*/
|
|
487
|
-
depth?: number
|
|
572
|
+
depth?: number,
|
|
573
|
+
/** See {@link ProjectionCache}. Omitting it is exactly the old behaviour. */
|
|
574
|
+
cache?: ProjectionCache): Promise<FolderProject | null>;
|
|
488
575
|
/**
|
|
489
576
|
* Build the event that performs a manifest-declared action.
|
|
490
577
|
*
|
package/dist/projection.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"projection.d.ts","sourceRoot":"","sources":["../src/projection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,
|
|
1
|
+
{"version":3,"file":"projection.d.ts","sourceRoot":"","sources":["../src/projection.ts"],"names":[],"mappings":"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"}
|
package/dist/projection.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* anything: it becomes an integration written against one app, which is the
|
|
13
13
|
* thing the whole exercise argues against.
|
|
14
14
|
*/
|
|
15
|
-
import { encodeNaddr, pointerToAddress, referenceToPointer } from '@estiva-app/protocol';
|
|
15
|
+
import { decodeNevent, encodeNaddr, encodeNevent, pointerToAddress, referenceToPointer } from '@estiva-app/protocol';
|
|
16
16
|
import { parseProfile } from '@estiva-app/protocol';
|
|
17
17
|
/** The default: one kind:0 query, straight to the relay. */
|
|
18
18
|
export function peopleViaRelay(query) {
|
|
@@ -124,6 +124,17 @@ function resolveActions(manifest, kind, folded) {
|
|
|
124
124
|
return out;
|
|
125
125
|
}
|
|
126
126
|
const tagValue = (e, name) => e.tags.find((t) => t[0] === name)?.[1];
|
|
127
|
+
/**
|
|
128
|
+
* Does the event carry this tag with this value — on **any** of them?
|
|
129
|
+
*
|
|
130
|
+
* `tagValue` reads the first tag with a given name, which is the wrong test for
|
|
131
|
+
* deciding whether an event would have matched a filter: a relay's `#a` matches
|
|
132
|
+
* if *any* `a` tag equals the wanted value, and an event may legitimately carry
|
|
133
|
+
* several. Used where a filter's own predicate is re-applied to a merged
|
|
134
|
+
* response (SHI-13), so that "did this come back because of that filter?" is
|
|
135
|
+
* answered the way the relay answered it.
|
|
136
|
+
*/
|
|
137
|
+
const hasTagValue = (e, name, value) => e.tags.some((t) => t[0] === name && t[1] === value);
|
|
127
138
|
/** A manifest event's `content`, or null when it is not parseable JSON. */
|
|
128
139
|
function parseManifest(event) {
|
|
129
140
|
try {
|
|
@@ -172,7 +183,54 @@ function webTemplate(event, entity) {
|
|
|
172
183
|
}
|
|
173
184
|
return undefined;
|
|
174
185
|
}
|
|
175
|
-
|
|
186
|
+
/**
|
|
187
|
+
* How long a manifest may be believed without asking again.
|
|
188
|
+
*
|
|
189
|
+
* Five minutes is a compromise with one real cost: republish a manifest and
|
|
190
|
+
* consumers keep drawing the old projection for up to that long. That is
|
|
191
|
+
* recoverable and self-correcting, where the alternative — asking twice per
|
|
192
|
+
* reference per tick, for ever — is neither.
|
|
193
|
+
*/
|
|
194
|
+
export const MANIFEST_TTL_MS = 5 * 60_000;
|
|
195
|
+
export function createProjectionCache(ttlMs = MANIFEST_TTL_MS) {
|
|
196
|
+
const entries = new Map();
|
|
197
|
+
return {
|
|
198
|
+
clear: () => entries.clear(),
|
|
199
|
+
lookup(key, now) {
|
|
200
|
+
const found = entries.get(key);
|
|
201
|
+
if (!found)
|
|
202
|
+
return undefined;
|
|
203
|
+
// `>=`, not `>`: a TTL of 0 must mean "never believe it", and an entry
|
|
204
|
+
// exactly at the boundary has expired rather than being on its last tick.
|
|
205
|
+
if (now - found.at >= ttlMs) {
|
|
206
|
+
entries.delete(key);
|
|
207
|
+
return undefined;
|
|
208
|
+
}
|
|
209
|
+
return { value: found.value };
|
|
210
|
+
},
|
|
211
|
+
remember(key, value, now) {
|
|
212
|
+
entries.set(key, { at: now, value });
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* A negative answer is cached too.
|
|
218
|
+
*
|
|
219
|
+
* "No app claims this kind" costs the same two round trips as a hit and is just
|
|
220
|
+
* as stable. Caching only successes would leave the expensive case — a
|
|
221
|
+
* reference nothing can draw — paying full price on every tick for ever.
|
|
222
|
+
*/
|
|
223
|
+
export async function resolveManifest(pointer, query, cache) {
|
|
224
|
+
const key = `${pointer.kind}:${pointer.pubkey}`;
|
|
225
|
+
const now = Date.now();
|
|
226
|
+
const memo = cache?.lookup(key, now);
|
|
227
|
+
if (memo)
|
|
228
|
+
return memo.value;
|
|
229
|
+
const answer = await resolveManifestUncached(pointer, query);
|
|
230
|
+
cache?.remember(key, answer, now);
|
|
231
|
+
return answer;
|
|
232
|
+
}
|
|
233
|
+
async function resolveManifestUncached(pointer, query) {
|
|
176
234
|
const parse = parseManifest;
|
|
177
235
|
const addressOf = (event) => `${event.kind}:${event.pubkey}:${tagValue(event, 'd') ?? ''}`;
|
|
178
236
|
const recommended = await query([
|
|
@@ -526,6 +584,77 @@ function buildObject(args) {
|
|
|
526
584
|
* unresolvable reference in a chat message should degrade to plain text, not
|
|
527
585
|
* break the message around it.
|
|
528
586
|
*/
|
|
587
|
+
/**
|
|
588
|
+
* Resolve one event by id — `nevent1…`, or a bare 64-hex id.
|
|
589
|
+
*
|
|
590
|
+
* The counterpart to `resolveForeignObject`, and the reason it has to exist:
|
|
591
|
+
* **not every object has an address.** A `kind:9` message carries no `d`, so
|
|
592
|
+
* `(kind, pubkey, d)` cannot be built for it and every resolver keyed on an
|
|
593
|
+
* address is blind to it. Measured on production during PRO-6; PRO-11 is this.
|
|
594
|
+
*
|
|
595
|
+
* ## What it does *not* do, and why the function is short
|
|
596
|
+
*
|
|
597
|
+
* A regular event is immutable and has no folded state, so there is no `records`
|
|
598
|
+
* rule to apply, no change events to fetch, and no "current value" that differs
|
|
599
|
+
* from what is on the event. It also cannot be the target of an `a` tag, so it
|
|
600
|
+
* has no comments addressed to it and **no actions** — a change names its target
|
|
601
|
+
* by address, and there is nothing here to name. That absence is the model being
|
|
602
|
+
* honest rather than a gap to fill later.
|
|
603
|
+
*
|
|
604
|
+
* ## Two round trips, and the order depends on the reference
|
|
605
|
+
*
|
|
606
|
+
* A manifest is found by kind. An `nevent` *may* carry its kind, and when it
|
|
607
|
+
* does the manifest and the event can be fetched together. When it does not —
|
|
608
|
+
* a bare id, which is what a pasted `e` tag gives you — the event has to be
|
|
609
|
+
* read first to learn what kind it is. Both paths are supported because both
|
|
610
|
+
* arrive in practice, and a resolver that required the richer form would refuse
|
|
611
|
+
* references other clients legitimately produce.
|
|
612
|
+
*/
|
|
613
|
+
export async function resolveForeignEvent(reference, query,
|
|
614
|
+
/** Defaults to asking the relay. The browser passes a cached lookup. */
|
|
615
|
+
lookupPeople,
|
|
616
|
+
/** See {@link ProjectionCache}. Omitting it is exactly the old behaviour. */
|
|
617
|
+
cache) {
|
|
618
|
+
let pointer;
|
|
619
|
+
try {
|
|
620
|
+
pointer = /^[0-9a-f]{64}$/i.test(reference.replace(/^nostr:/i, ''))
|
|
621
|
+
? { id: reference.replace(/^nostr:/i, '').toLowerCase(), relays: [] }
|
|
622
|
+
: decodeNevent(reference);
|
|
623
|
+
}
|
|
624
|
+
catch {
|
|
625
|
+
return null;
|
|
626
|
+
}
|
|
627
|
+
const [root] = await query([{ ids: [pointer.id], limit: 1 }]);
|
|
628
|
+
if (!root) {
|
|
629
|
+
// Nothing to draw and nothing to say about it: unlike an addressable
|
|
630
|
+
// object, there is no manifest resolved yet that could name the app or
|
|
631
|
+
// offer a way in. `unreachable` needs a projection to be a useful state.
|
|
632
|
+
return null;
|
|
633
|
+
}
|
|
634
|
+
// Whoever signed it is the app's own author, which is what a `#k` lookup
|
|
635
|
+
// needs when no recommendation exists. The pointer's `author` is a hint and
|
|
636
|
+
// may disagree with the event; the event wins, because it is the thing.
|
|
637
|
+
const resolved = await resolveManifest({ kind: root.kind, pubkey: root.pubkey, identifier: '', relays: pointer.relays }, query, cache);
|
|
638
|
+
if (!resolved)
|
|
639
|
+
return null;
|
|
640
|
+
const projection = resolved.manifest.projections?.[String(root.kind)];
|
|
641
|
+
if (!projection)
|
|
642
|
+
return null;
|
|
643
|
+
const object = buildChildObject({
|
|
644
|
+
root,
|
|
645
|
+
manifest: resolved.manifest,
|
|
646
|
+
projection,
|
|
647
|
+
records: foldRuleOf(resolved.manifest),
|
|
648
|
+
webTemplate: resolved.webTemplate,
|
|
649
|
+
viaRecommendation: resolved.viaRecommendation,
|
|
650
|
+
// The reference as given, so "open this in the app that owns it" points at
|
|
651
|
+
// the event rather than at nothing. A bare id is upgraded to an `nevent`
|
|
652
|
+
// carrying what we now know, which is more than the caller had.
|
|
653
|
+
nevent: encodeNevent({ id: root.id, relays: pointer.relays, pubkey: root.pubkey, kind: root.kind }),
|
|
654
|
+
});
|
|
655
|
+
const people = await (lookupPeople ?? peopleViaRelay(query))(pubkeysIn(object));
|
|
656
|
+
return { ...object, people };
|
|
657
|
+
}
|
|
529
658
|
export async function resolveForeignObject(naddr, query,
|
|
530
659
|
/** Defaults to asking the relay. The browser passes a cached lookup. */
|
|
531
660
|
lookupPeople,
|
|
@@ -534,7 +663,12 @@ lookupPeople,
|
|
|
534
663
|
* module leave it at 0; it is the budget that stops a child's own `list`
|
|
535
664
|
* recursing forever. See `MAX_LIST_DEPTH`.
|
|
536
665
|
*/
|
|
537
|
-
depth = 0
|
|
666
|
+
depth = 0,
|
|
667
|
+
/**
|
|
668
|
+
* Optional memo for the NIP-89 discovery half. See {@link ProjectionCache} —
|
|
669
|
+
* omitting it is exactly the old behaviour.
|
|
670
|
+
*/
|
|
671
|
+
cache) {
|
|
538
672
|
let pointer;
|
|
539
673
|
try {
|
|
540
674
|
// Either form: a body carries `naddr1…`, an `a` tag carries the plain
|
|
@@ -545,7 +679,7 @@ depth = 0) {
|
|
|
545
679
|
return null;
|
|
546
680
|
}
|
|
547
681
|
const address = pointerToAddress(pointer);
|
|
548
|
-
const resolved = await resolveManifest(pointer, query);
|
|
682
|
+
const resolved = await resolveManifest(pointer, query, cache);
|
|
549
683
|
if (!resolved)
|
|
550
684
|
return null;
|
|
551
685
|
const { manifest, viaRecommendation } = resolved;
|
|
@@ -557,15 +691,29 @@ depth = 0) {
|
|
|
557
691
|
if (!projection)
|
|
558
692
|
return null;
|
|
559
693
|
const records = foldRuleOf(manifest);
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
694
|
+
/*
|
|
695
|
+
One round trip for the root, its changes, its comments **and its children**.
|
|
696
|
+
|
|
697
|
+
`#a` on both the change and the comment kind, because both point at the
|
|
698
|
+
object by *address* rather than by event id — which is what makes them
|
|
699
|
+
survive the author replacing the root event.
|
|
700
|
+
|
|
701
|
+
The children used to be a second round trip, issued after the root came
|
|
702
|
+
back. They never needed to be: the child filter is built from the manifest
|
|
703
|
+
and the *pointer*, and an addressable event's `d` is `pointer.identifier` by
|
|
704
|
+
definition — it is what we just queried by. So once the manifest is known,
|
|
705
|
+
nothing about the child filter depends on the root's contents (SHI-13).
|
|
706
|
+
|
|
707
|
+
That is the difference between two requests per reference per refresh and
|
|
708
|
+
one, and with the manifest memoised it is the whole cost of a tick.
|
|
709
|
+
*/
|
|
710
|
+
const childFilter = childFilterFor({ projection, manifest, pointer, depth });
|
|
564
711
|
const events = await query([
|
|
565
712
|
{ kinds: [pointer.kind], authors: [pointer.pubkey], '#d': [pointer.identifier], limit: 1 },
|
|
566
713
|
// Only when the app actually declares a change kind — see `foldRuleOf`.
|
|
567
714
|
...(manifest.records ? [{ kinds: [manifest.records.changeKind], '#a': [address], limit: 500 }] : []),
|
|
568
715
|
{ kinds: commentKinds, '#a': [address], limit: 200 },
|
|
716
|
+
...(childFilter ? [childFilter.filter] : []),
|
|
569
717
|
]);
|
|
570
718
|
const root = events.find((e) => e.kind === pointer.kind && tagValue(e, 'd') === pointer.identifier);
|
|
571
719
|
if (!root) {
|
|
@@ -593,8 +741,19 @@ depth = 0) {
|
|
|
593
741
|
};
|
|
594
742
|
}
|
|
595
743
|
const folded = foldChanges(events.filter((e) => e.kind === records.changeKind && tagValue(e, records.targetTag) === address), records);
|
|
744
|
+
/*
|
|
745
|
+
Matched on the comment filter's own criteria — the kind **and** the `a` tag.
|
|
746
|
+
|
|
747
|
+
Kind alone was safe while this was its own query: the relay only returned
|
|
748
|
+
what the comment filter asked for. Now that the children ride in the same
|
|
749
|
+
request (SHI-13), a child sharing the comment kind would arrive here too and
|
|
750
|
+
be counted as a comment on its own parent. Peek is exactly that shape: its
|
|
751
|
+
Topic declares `kind:9` messages as children and `kind:9` as its comment
|
|
752
|
+
kind, so every message in the Folder would have become a comment on the
|
|
753
|
+
Topic — a widget silently showing a conversation twice.
|
|
754
|
+
*/
|
|
596
755
|
const comments = events
|
|
597
|
-
.filter((e) => commentKinds.includes(e.kind))
|
|
756
|
+
.filter((e) => commentKinds.includes(e.kind) && hasTagValue(e, 'a', address))
|
|
598
757
|
.sort(byOrder)
|
|
599
758
|
.map((e) => ({ id: e.id, author: e.pubkey, body: e.content, createdAt: e.created_at }));
|
|
600
759
|
const object = buildObject({
|
|
@@ -622,12 +781,10 @@ depth = 0) {
|
|
|
622
781
|
A child may be a regular event with no address of its own (Peek's messages
|
|
623
782
|
are), so this builds by event rather than by pointer.
|
|
624
783
|
*/
|
|
625
|
-
const children =
|
|
626
|
-
|
|
627
|
-
|
|
784
|
+
const children = childrenFrom({
|
|
785
|
+
events,
|
|
786
|
+
childFilter,
|
|
628
787
|
manifest,
|
|
629
|
-
query,
|
|
630
|
-
depth,
|
|
631
788
|
webTemplate: resolved.webTemplate,
|
|
632
789
|
viaRecommendation,
|
|
633
790
|
});
|
|
@@ -644,36 +801,69 @@ depth = 0) {
|
|
|
644
801
|
return { ...object, people, ...(children ? { children: children.map((c) => ({ ...c, people })) } : {}) };
|
|
645
802
|
}
|
|
646
803
|
/**
|
|
647
|
-
*
|
|
804
|
+
* The filter for a projection's `list` slot, or nothing.
|
|
805
|
+
*
|
|
806
|
+
* Split out from fetching so it can be built **before** the root event is in
|
|
807
|
+
* hand and merged into the object's own round trip (SHI-13). Everything it
|
|
808
|
+
* needs is in the manifest and the pointer: an addressable event's `d` *is*
|
|
809
|
+
* `pointer.identifier`, since that is what the root filter matches on, so
|
|
810
|
+
* reading it back off the root taught us nothing we did not already know.
|
|
648
811
|
*
|
|
649
|
-
* Returns undefined when no `list` is declared — distinct from
|
|
650
|
-
*
|
|
651
|
-
*
|
|
812
|
+
* Returns undefined when no `list` is declared — distinct from a declared list
|
|
813
|
+
* that matches nothing, which a renderer must be able to tell apart. The two
|
|
814
|
+
* other "declared but not renderable" cases are folded in here too, and both
|
|
815
|
+
* come back as `[]` from {@link childrenFrom}: a depth budget already spent,
|
|
816
|
+
* and a child kind the manifest never says how to draw.
|
|
652
817
|
*/
|
|
653
|
-
|
|
654
|
-
const { projection,
|
|
818
|
+
function childFilterFor(args) {
|
|
819
|
+
const { projection, manifest, pointer, depth } = args;
|
|
655
820
|
const spec = projection.slots.list;
|
|
656
821
|
const children = !Array.isArray(spec) ? spec?.children : undefined;
|
|
657
822
|
if (!children)
|
|
658
823
|
return undefined;
|
|
659
824
|
// The consumer's budget, not the manifest's — see MAX_LIST_DEPTH.
|
|
660
825
|
if (depth >= MAX_LIST_DEPTH)
|
|
661
|
-
return
|
|
662
|
-
const childProjection = manifest.projections?.[String(children.kind)];
|
|
826
|
+
return null;
|
|
663
827
|
// A declared list whose child kind has no projection is not renderable, and
|
|
664
828
|
// an empty list is the honest answer: the objects exist, this app has not
|
|
665
829
|
// said how to draw them.
|
|
830
|
+
if (!manifest.projections?.[String(children.kind)])
|
|
831
|
+
return null;
|
|
832
|
+
const parent = children.match === 'identifier'
|
|
833
|
+
? pointer.identifier
|
|
834
|
+
: pointerToAddress({ ...pointer, relays: [] });
|
|
835
|
+
return {
|
|
836
|
+
filter: { kinds: [children.kind], [`#${children.via}`]: [parent], limit: children.limit ?? 100 },
|
|
837
|
+
kind: children.kind,
|
|
838
|
+
via: children.via,
|
|
839
|
+
parent,
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
/**
|
|
843
|
+
* The child objects, picked back out of the merged result set.
|
|
844
|
+
*
|
|
845
|
+
* **Matched on the filter's own criteria, never on kind alone.** Peek's Topic
|
|
846
|
+
* declares `kind:9` messages as its children and `kind:9` as its comment kind,
|
|
847
|
+
* so a merged response carries both under one number and only the tag tells
|
|
848
|
+
* them apart. An event can honestly be both — Ship writes a `kind:9` with an
|
|
849
|
+
* `a` naming the object *and* an `h` naming the Folder — and it appeared in
|
|
850
|
+
* both result sets when these were two queries. Re-applying each filter's own
|
|
851
|
+
* predicate reproduces that, rather than making them compete.
|
|
852
|
+
*/
|
|
853
|
+
function childrenFrom(args) {
|
|
854
|
+
const { events, childFilter, manifest, webTemplate, viaRecommendation } = args;
|
|
855
|
+
if (childFilter === undefined)
|
|
856
|
+
return undefined;
|
|
857
|
+
if (childFilter === null)
|
|
858
|
+
return [];
|
|
859
|
+
const childProjection = manifest.projections?.[String(childFilter.kind)];
|
|
666
860
|
if (!childProjection)
|
|
667
861
|
return [];
|
|
668
|
-
const identifier = tagValue(root, 'd') ?? '';
|
|
669
|
-
const parent = children.match === 'identifier'
|
|
670
|
-
? identifier
|
|
671
|
-
: pointerToAddress({ kind: root.kind, pubkey: root.pubkey, identifier, relays: [] });
|
|
672
|
-
const found = await query([
|
|
673
|
-
{ kinds: [children.kind], [`#${children.via}`]: [parent], limit: children.limit ?? 100 },
|
|
674
|
-
]);
|
|
675
862
|
const records = foldRuleOf(manifest);
|
|
676
|
-
return
|
|
863
|
+
return events
|
|
864
|
+
.filter((e) => e.kind === childFilter.kind && hasTagValue(e, childFilter.via, childFilter.parent))
|
|
865
|
+
.sort(byOrder)
|
|
866
|
+
.map((event) => buildChildObject({ root: event, manifest, projection: childProjection, records, webTemplate, viaRecommendation }));
|
|
677
867
|
}
|
|
678
868
|
/**
|
|
679
869
|
* Build a `ForeignObject` from an event that may have no address of its own.
|
|
@@ -688,7 +878,7 @@ async function resolveChildren(args) {
|
|
|
688
878
|
* it changes, and a regular event cannot be named that way.
|
|
689
879
|
*/
|
|
690
880
|
function buildChildObject(args) {
|
|
691
|
-
const { root, manifest, projection, webTemplate, viaRecommendation } = args;
|
|
881
|
+
const { root, manifest, projection, webTemplate, viaRecommendation, nevent } = args;
|
|
692
882
|
const identifier = tagValue(root, 'd');
|
|
693
883
|
const addressable = identifier !== undefined;
|
|
694
884
|
const pointer = {
|
|
@@ -714,7 +904,16 @@ function buildChildObject(args) {
|
|
|
714
904
|
meta,
|
|
715
905
|
comments: [],
|
|
716
906
|
folder: tagValue(root, 'h'),
|
|
717
|
-
|
|
907
|
+
/*
|
|
908
|
+
`<bech32>` is whichever form this object actually has.
|
|
909
|
+
|
|
910
|
+
NIP-89's template says nothing about which NIP-19 entity it will be handed
|
|
911
|
+
— Ship's declares `naddr` in its own tag because every Ship object is
|
|
912
|
+
addressable, and a template for an app with non-addressable objects is
|
|
913
|
+
handed an `nevent`. Substituting the one the object *has* is what makes a
|
|
914
|
+
single template serve both, and what stops a message linking to nothing.
|
|
915
|
+
*/
|
|
916
|
+
openUrl: (naddr ?? nevent) ? webTemplate?.replace('<bech32>', (naddr ?? nevent)) : undefined,
|
|
718
917
|
// See the note above: nothing can be declared to act on a regular event.
|
|
719
918
|
actions: [],
|
|
720
919
|
viaRecommendation,
|
|
@@ -918,7 +1117,9 @@ lookupPeople,
|
|
|
918
1117
|
* outside this module leave it at 0; it exists so that following a child's
|
|
919
1118
|
* own `list` is a budget check rather than a thing nobody remembered.
|
|
920
1119
|
*/
|
|
921
|
-
depth = 0
|
|
1120
|
+
depth = 0,
|
|
1121
|
+
/** See {@link ProjectionCache}. Omitting it is exactly the old behaviour. */
|
|
1122
|
+
cache) {
|
|
922
1123
|
// The render loop this forbids is not hypothetical: two folders naming each
|
|
923
1124
|
// other's projects resolve forever, and the manifest declaring them is
|
|
924
1125
|
// another app's. See MAX_LIST_DEPTH.
|
|
@@ -1113,7 +1314,7 @@ depth = 0) {
|
|
|
1113
1314
|
const address = pointerToAddress(pointer);
|
|
1114
1315
|
// 3. Now the authoritative manifest — the object's author gets to say which
|
|
1115
1316
|
// app renders their project (kind:31989), same as for an inline reference.
|
|
1116
|
-
const resolved = await resolveManifest(pointer, query);
|
|
1317
|
+
const resolved = await resolveManifest(pointer, query, cache);
|
|
1117
1318
|
if (!resolved) {
|
|
1118
1319
|
return null;
|
|
1119
1320
|
}
|