@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 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
@@ -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,GAUf,MAAM,iBAAiB,CAAA"}
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"}
@@ -223,13 +223,62 @@ interface Manifest {
223
223
  stage?: string;
224
224
  }[]>;
225
225
  }
226
- export declare function resolveManifest(pointer: AddressPointer, query: QueryFn): Promise<{
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
- } | null>;
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): Promise<ForeignObject | null>;
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): Promise<FolderProject | null>;
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
  *
@@ -1 +1 @@
1
- {"version":3,"file":"projection.d.ts","sourceRoot":"","sources":["../src/projection.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"projection.d.ts","sourceRoot":"","sources":["../src/projection.ts"],"names":[],"mappings":"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"}
@@ -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
- export async function resolveManifest(pointer, query) {
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
- // One round trip for the root, its changes and its comments. `#a` on both the
561
- // change and the comment kind, because both point at the object by *address*
562
- // rather than by event id — which is what makes them survive the author
563
- // replacing the root event.
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 = await resolveChildren({
626
- projection,
627
- root,
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
- * Resolve a projection's `list` slot into child objects.
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 `[]`, which
650
- * means "declared, and nothing matched". A renderer needs to tell "this holds
651
- * nothing" from "this holds no list".
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
- async function resolveChildren(args) {
654
- const { projection, root, manifest, query, depth, webTemplate, viaRecommendation } = args;
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 found.sort(byOrder).map((event) => buildChildObject({ root: event, manifest, projection: childProjection, records, webTemplate, viaRecommendation }));
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
- openUrl: naddr ? webTemplate?.replace('<bech32>', naddr) : undefined,
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
  }