mirror of
https://github.com/PR0M3TH3AN/Artstr-Studio.git
synced 2026-10-27 08:40:39 +00:00
Draft Linked Designs spec
Spec for the next big arc: reference-based cross-design imports. Adds a `kind: 'linked'` flavor of the existing design layer that stores only a NIP-01 addressable-event coordinate (30078:<pubkey>:<dTag>) and resolves to the latest source on render. Sits alongside the existing embedded import path; user picks at import time. Key decisions baked in (per scoping conversation): - Versioning: always-latest by default, with an opt-in "Pin to this version" toggle exposed per linked layer. - Nesting: up to 3 levels deep with cycle detection + visible depth-exceeded placeholders. - Encrypted: Phase 1 ships public-only; Phase 2 adds premium (renders if viewer's unlocked) and private (share-list aware). - Attribution chain is first-class — host publish emits one ['p', '<contributor pubkey>', '<relay-hint>', 'design-contributor'] tag per distinct linked-tree creator, plus an in-app Credits panel surfacing co-creators. Lets larger collaborative projects carry honest attribution natively. Architectural shape: - Resolver IIFE (linkedDesigns) with session cache, multi-relay union (mirrors _vaultRelayUnion), and a manual Refresh button to bypass cache. - renderDesignObjectIntoElement gets a linked branch that calls the resolver before the existing render path. - Layer-options panel exposes pin / refresh / unlink / replace. - "Publish + link" action converts an embedded layer to a freshly- published source + linked reference (Phase 3 polish). - Convert-back-to-embed action snapshots the current resolution. Phased implementation: - Phase 1: Public-only linking, schema, resolver, rendering, UI. - Phase 2: Premium + private link resolution with access control. - Phase 3: Disk cache, attribution `p`-tag emission, Credits panel, Publish + link workflow. Acceptance criterion for Phase 1: a public cover design linked into a public slide deck renders correctly; editing the source updates the host's render on next refresh. 10-link host event stays under typical relay size limits. Outstanding open questions to revisit during implementation: - Whether the depth limit should be 3 forever or configurable. - Whether the 256 KB per-source payload cap is the right number. - Whether to surface "last resolved" times in the layer name itself or only on hover.
This commit is contained in:
1 parent
59ae0e32ec
commit
87dd9426c2
1 file changed
+516
@@ -0,0 +1,516 @@
|
||||
# Artstr Studio Linked Designs
|
||||
|
||||
## Cross-Reference Imports + Auto-Update + Attribution Chain
|
||||
|
||||
### Status
|
||||
|
||||
Spec draft. Not yet implemented. This document is the canonical plan;
|
||||
when work begins it follows this doc, and updates land here first.
|
||||
|
||||
---
|
||||
|
||||
## 1. Feature Summary
|
||||
|
||||
Artstr currently supports **embedded** cross-template imports: any
|
||||
design can be pulled into another as a `type: 'design'` layer (or as
|
||||
a `type: 'pixelart'` layer, or as a deck slide, or into a disc-sheet
|
||||
slot). Every existing import is a deep `JSON.parse(JSON.stringify(...))`
|
||||
clone — once placed, the new layer has no relationship to its source
|
||||
event. Source edits don't propagate; host-event size grows linearly
|
||||
with embedded content.
|
||||
|
||||
This spec adds a parallel **linked** flavor of design imports.
|
||||
A linked layer stores only the NIP-01 addressable-event coordinate
|
||||
(`30078:<creatorPubkey>:<dTag>`) of its source. The renderer fetches
|
||||
the latest event at that address on demand and renders it in place.
|
||||
|
||||
Two architectural wins this unlocks:
|
||||
|
||||
1. **Auto-update.** The source author edits their design — every host
|
||||
design that links to it picks up the change next render. Brand
|
||||
assets, logos, common components stay in sync without coordination.
|
||||
2. **Smaller host events.** A host with N linked layers is the size
|
||||
of its own metadata + N coordinates, not N × source-payload. Side-
|
||||
steps the per-event relay size limit for large composite designs.
|
||||
|
||||
A third win, called out by the user during scoping: **attribution
|
||||
chain**. Every linked layer's source pubkey is computable at render
|
||||
time. The host's published event carries a `p` tag for every distinct
|
||||
contributing creator, so Nostr-aware clients + relays can surface
|
||||
"co-created by" without manual tagging.
|
||||
|
||||
Embedded imports stay as the default for everything that ships today.
|
||||
Linked imports are an opt-in alternative offered alongside, with a
|
||||
clear UI distinction.
|
||||
|
||||
---
|
||||
|
||||
## 2. Goals
|
||||
|
||||
- A new `design.kind: 'linked'` variant of the existing design layer.
|
||||
- Address-coordinate-based resolution that returns "always latest" by
|
||||
default, with an opt-in "pin to this version" toggle exposed in the
|
||||
layer-options panel.
|
||||
- Three levels of nesting allowed (linked design can itself contain
|
||||
linked layers, up to depth 3) with cycle detection.
|
||||
- Attribution chain: published host events carry one `p` tag per
|
||||
distinct contributing creator, plus an in-app "Credits" panel that
|
||||
lists who contributed.
|
||||
- Convert-direction commands: "Unlink to embed" (snapshot to current
|
||||
version) and "Publish + link" (extract an embedded layer into its
|
||||
own event and replace it with a link).
|
||||
- Graceful degradation when relays don't return the source (loading
|
||||
state → placeholder + optional cached `fallbackPreview`).
|
||||
|
||||
## 3. Non-Goals
|
||||
|
||||
- Linked premium / private designs in Phase 1. Encrypted-design link
|
||||
resolution requires the viewer's unlock state for premium and the
|
||||
share-list for private; both ship in Phase 2.
|
||||
- Real-time push when the source updates. The renderer polls on each
|
||||
render + offers a manual Refresh button; no relay subscription is
|
||||
held open for linked sources.
|
||||
- Editing the linked source through the host. A linked layer is
|
||||
read-only at the host level; to modify the source, the original
|
||||
creator edits the source event.
|
||||
- Cross-relay write replication. Source-author publishes as they do
|
||||
today; linked rendering reads from the host viewer's normal relay
|
||||
set.
|
||||
|
||||
---
|
||||
|
||||
## 4. Existing Artstr Fit
|
||||
|
||||
- The `type: 'design'` layer type and `renderDesignObjectIntoElement`
|
||||
renderer already accept any payload and render via the existing
|
||||
`renderDesignPayloadPreview` compact-mode path. Linked layers reuse
|
||||
the same renderer; only the source of the payload changes.
|
||||
- The purchase-vault arc already established multi-relay union reads
|
||||
via `_vaultRelayUnion()` for kind-30078 events. Linked-design
|
||||
resolution borrows the same pattern (NIP-65 write relays + a
|
||||
default fallback set), keyed by address.
|
||||
- Address-tolerant lookup helpers (`premiumAddressCoord`,
|
||||
`findUnlockedEntry`) already exist; the linked-design resolver
|
||||
follows the same shape.
|
||||
|
||||
---
|
||||
|
||||
## 5. Schema
|
||||
|
||||
Linked design layer:
|
||||
|
||||
```js
|
||||
{
|
||||
id: 'layer-...',
|
||||
type: 'design',
|
||||
name: 'Linked: <source title>',
|
||||
target: 'canvas',
|
||||
x, y, w, h,
|
||||
rotate, opacity, z,
|
||||
design: {
|
||||
kind: 'linked', // disambiguates from embedded
|
||||
ref: '30078:<creatorPubkey>:<dTag>', // NIP-01 addressable coord
|
||||
mode: 'cover', // hint for pre-fetch render
|
||||
pinnedEventId: null, // optional, locks to a specific event id
|
||||
fallbackPreview: 'data:image/jpeg;base64,...', // optional cached preview
|
||||
lastResolvedAt: 1748656800000, // ms timestamp of last successful fetch
|
||||
lastResolvedSize: { w: 1920, h: 1080 }, // hint for layout
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Embedded design layer (unchanged from today, made explicit for
|
||||
contrast):
|
||||
|
||||
```js
|
||||
{
|
||||
type: 'design',
|
||||
design: {
|
||||
kind: 'embedded', // implicit / absent on legacy layers
|
||||
mode: 'cover',
|
||||
payload: { /* full design JSON */ },
|
||||
},
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
Legacy layers without `design.kind` are treated as `embedded` for
|
||||
backwards compatibility.
|
||||
|
||||
Top-level host-event tags (added at publish time for linked-design
|
||||
attribution):
|
||||
|
||||
```
|
||||
['p', '<contributor pubkey>', '<relay-hint>', 'design-contributor']
|
||||
['p', '<contributor pubkey>', '<relay-hint>', 'design-contributor']
|
||||
...
|
||||
```
|
||||
|
||||
One `p` tag per *distinct* contributing pubkey discovered by walking
|
||||
the host's linked layers (and their linked layers, up to depth 3).
|
||||
The fourth element marks the tag as design-contributor specifically
|
||||
so it doesn't collide with `p` tags emitted for replies / mentions
|
||||
in other Nostr contexts.
|
||||
|
||||
---
|
||||
|
||||
## 6. Versioning Model
|
||||
|
||||
PPTX-style "always latest" is the default. Pinning is the advanced
|
||||
opt-in.
|
||||
|
||||
- **Default (always latest):** `pinnedEventId === null`. Renderer
|
||||
queries relays for `kinds: [30078], '#d': [dTag], authors: [pubkey]`
|
||||
and uses the most recent event. NIP-01 addressable semantics
|
||||
guarantee relays only retain the latest event per address, so this
|
||||
is essentially free.
|
||||
- **Pinned to version:** user toggles "Pin to this version" in the
|
||||
layer options. The current resolved `eventId` is written to
|
||||
`pinnedEventId`. Renderer queries by `ids: [<eventId>]` instead of
|
||||
by address — only that exact event is rendered. Source-author edits
|
||||
don't propagate until the host author manually un-pins or re-pins
|
||||
to a newer event.
|
||||
|
||||
The toggle is visible on every linked-design layer's right-panel.
|
||||
Switching states is non-destructive; flipping back to "always latest"
|
||||
just nulls `pinnedEventId`.
|
||||
|
||||
UI surfaces:
|
||||
|
||||
- Layer-options panel: "Source: <title> by <creator> · 🔗 latest"
|
||||
with a "Pin to this version" checkbox.
|
||||
- A "Last resolved <timestamp>" line so the user knows how fresh.
|
||||
- A "Refresh" button that re-queries even when the in-session cache
|
||||
is warm.
|
||||
|
||||
---
|
||||
|
||||
## 7. Caching Strategy
|
||||
|
||||
Per-session in-memory cache keyed by address (or by `eventId` when
|
||||
pinned). Three layers:
|
||||
|
||||
1. **Session cache** — `Map<addressOrEventId, { payload, fetchedAt }>`.
|
||||
First render fetches; subsequent renders within the same session
|
||||
hit cache. TTL: 5 minutes. Closing the tab clears it.
|
||||
2. **Disk cache** — None for Phase 1. If real-deck round-trips show
|
||||
render lag, Phase 3 polish adds a small localStorage-backed cache
|
||||
keyed by address + content-hash so a return visit doesn't re-fetch.
|
||||
3. **Manual refresh** — A "Refresh" button on the layer options bypasses
|
||||
the cache and re-queries unconditionally. Useful for the
|
||||
collaborative case where Adam knows the source author just edited.
|
||||
|
||||
Cache invalidation strategy:
|
||||
|
||||
- TTL on session cache (5 min).
|
||||
- "Refresh" button per-layer.
|
||||
- Switching from latest → pinned (or vice versa) busts the entry.
|
||||
- Editing the layer's `ref` busts the entry for the old ref.
|
||||
|
||||
Cache misses degrade gracefully:
|
||||
|
||||
- While fetching: render a loading placeholder ("⟳ Loading linked
|
||||
design...").
|
||||
- On fetch failure (no relay returned the event): render the
|
||||
`fallbackPreview` data URL if present, else a "🔗 Linked design
|
||||
not found — refresh to retry" placeholder.
|
||||
|
||||
---
|
||||
|
||||
## 8. Render Flow
|
||||
|
||||
```
|
||||
host design opened
|
||||
→ render() walks layers
|
||||
→ for each layer where layer.design.kind === 'linked':
|
||||
resolver.lookup(ref, pinnedEventId)
|
||||
→ check session cache → hit: return payload
|
||||
→ cache miss: queryRelays(union, filter)
|
||||
→ success: cache + return
|
||||
→ fail: return null + emit fallback
|
||||
renderDesignObjectIntoElement(el, { design: { payload } })
|
||||
```
|
||||
|
||||
Resolver lives in a `linkedDesigns` IIFE alongside the existing
|
||||
`Premium` and purchase-vault helpers in `src/index.html`. Exposes:
|
||||
|
||||
- `resolveLinked({ ref, pinnedEventId })` → `Promise<payload | null>`
|
||||
- `clearCache()` (used by the manual Refresh button)
|
||||
- `walkContributors(payload, opts)` → `Set<pubkey>` (used by the
|
||||
attribution-tagging path at publish time)
|
||||
|
||||
Renderer changes are minimal — `renderDesignObjectIntoElement`
|
||||
already handles "render a payload as a compact preview." The new
|
||||
path just fetches the payload async before calling it.
|
||||
|
||||
---
|
||||
|
||||
## 9. Nesting + Cycle Detection
|
||||
|
||||
Up to **three levels of linking** allowed. A links B links C is fine;
|
||||
C linking back to A is rejected at resolve time.
|
||||
|
||||
The resolver tracks `visited = Set<addressOrEventId>` through a single
|
||||
top-level render pass:
|
||||
|
||||
```js
|
||||
async function resolveLinkedTree(layer, depth, visited) {
|
||||
if (depth > MAX_LINK_DEPTH) return _depthLimitPlaceholder();
|
||||
const key = layer.design.pinnedEventId || layer.design.ref;
|
||||
if (visited.has(key)) return _cyclePlaceholder();
|
||||
visited.add(key);
|
||||
const payload = await resolveLinked(layer.design);
|
||||
// walk payload.layers for nested linked layers, recurse
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
`MAX_LINK_DEPTH = 3` (host counts as 0). Caps fetch storms + UX
|
||||
predictability.
|
||||
|
||||
Cycle and depth-exceeded placeholders show a small inline label so
|
||||
the user knows why nothing rendered.
|
||||
|
||||
---
|
||||
|
||||
## 10. Attribution Chain
|
||||
|
||||
The collaborative-project pitch is the main reason the user wants
|
||||
this feature, so attribution is first-class.
|
||||
|
||||
### 10.1 At-render attribution
|
||||
|
||||
The "Credits" panel (new, surfaced from the design's metadata
|
||||
sidebar) lists every distinct creator who contributed via a linked
|
||||
layer in the current design. Pulled from the resolved-tree walk; lazy,
|
||||
populated as resolves complete.
|
||||
|
||||
Displayed as: `@alice (3 designs)`, `@bob (1 design)`, etc., with
|
||||
each entry linking to the contributor's `/u/<npub>` profile page.
|
||||
|
||||
### 10.2 At-publish attribution
|
||||
|
||||
When the user publishes a host design that contains linked layers,
|
||||
the publish pipeline:
|
||||
|
||||
1. Walks the host's layers + their linked-tree resolutions (up to
|
||||
depth 3) to collect contributor pubkeys.
|
||||
2. Adds one `['p', '<pubkey>', '<relay-hint>', 'design-contributor']`
|
||||
tag per distinct contributor.
|
||||
3. Emits a `['link', '<address>', '<relay-hint>']` tag per linked
|
||||
reference so other indexers / readers can crawl the graph
|
||||
without re-fetching the design content.
|
||||
|
||||
NIP-aware clients picking up the host event can surface "co-created
|
||||
by" without parsing the design payload. Relays can index by `p` to
|
||||
let users find designs they've contributed to.
|
||||
|
||||
### 10.3 Embedded → linked conversions
|
||||
|
||||
When the user runs "Publish + link" on an embedded layer (see §11.3),
|
||||
the embedded layer's original `pubkey` is preserved as the new
|
||||
source author. Attribution survives the conversion.
|
||||
|
||||
---
|
||||
|
||||
## 11. UX / UI
|
||||
|
||||
### 11.1 Import flow
|
||||
|
||||
Existing flow: layer's right-panel + Community modal already expose
|
||||
"Browse community" and "Load from file." Add a third path:
|
||||
|
||||
- **"Link to community design"** — opens the community browser in
|
||||
link-mode. Selecting a row creates a `kind: 'linked'` layer with
|
||||
the source's address.
|
||||
- Existing "Browse community" remains and continues creating
|
||||
embedded layers (default).
|
||||
|
||||
A "Link this design" action also appears in each community-result
|
||||
card's row menu, so the user can pick link vs. embed at any entry
|
||||
point.
|
||||
|
||||
### 11.2 Visual distinction in the layer list
|
||||
|
||||
Linked layers show a 🔗 prefix in the layer list (e.g.
|
||||
`🔗 Brand logo (alice's design)`). Hover tooltip:
|
||||
`Linked to alice's design · latest version · refreshed 12s ago`.
|
||||
|
||||
### 11.3 Layer-options panel for linked designs
|
||||
|
||||
When a linked layer is selected, the right-side options panel shows:
|
||||
|
||||
- Source title + creator pubkey (clickable to profile)
|
||||
- `🔗 latest` vs `📌 pinned` toggle
|
||||
- "Refresh" button (forces re-fetch)
|
||||
- "Replace link…" button (opens community browser to swap target)
|
||||
- "Unlink to embed" button (snapshots the currently-resolved payload
|
||||
into an embedded layer + drops the link)
|
||||
- "Open source in new tab" link
|
||||
|
||||
Position / size / rotation / opacity stay editable like any layer.
|
||||
Inner layers of the linked design are NOT editable — that's the
|
||||
trade-off for source updates working.
|
||||
|
||||
### 11.4 "Publish + link"
|
||||
|
||||
A right-click action on any embedded design layer (or in the layer
|
||||
menu): "Publish this layer as its own design and link to it."
|
||||
Pipeline:
|
||||
|
||||
1. Extract the embedded payload into a fresh design.
|
||||
2. Publish it as a normal kind-30078 from the current user.
|
||||
3. Replace the host's embedded layer with a linked layer pointing at
|
||||
the new address.
|
||||
|
||||
Lets a user organically refactor a self-contained sub-design into
|
||||
a reusable linked asset.
|
||||
|
||||
---
|
||||
|
||||
## 12. Publishing Considerations
|
||||
|
||||
A host design with linked layers publishes as a normal kind-30078,
|
||||
just with a smaller content payload (linked layers carry only the
|
||||
ref, not the full sub-payload).
|
||||
|
||||
- Host content shape stays loadable by old viewers that don't know
|
||||
about linked layers. Legacy clients render the layer's
|
||||
`fallbackPreview` (when present) or a generic "Linked design" stub.
|
||||
- Attribution tags (§10.2) are additive — clients that don't parse
|
||||
them ignore them. NIP-01 compliant.
|
||||
- The host stays Nostr-publishable at standard relay size limits even
|
||||
when linking dozens of components.
|
||||
|
||||
### 12.1 Premium / private design boundary (Phase 2)
|
||||
|
||||
When a linked layer points at a premium design, the host viewer's
|
||||
unlock state controls render:
|
||||
|
||||
- Unlocked → render the decrypted payload from the viewer's purchase
|
||||
vault.
|
||||
- Locked → render a placeholder ("🔒 Linked premium design — unlock
|
||||
to render") with an unlock CTA inline.
|
||||
|
||||
For private designs (`payload.visibility === 'private'`), Phase 2
|
||||
checks the share-list at fetch time; rendering only happens for the
|
||||
source author or an explicitly-shared pubkey.
|
||||
|
||||
Phase 1 detects an encrypted source and renders the
|
||||
"Linked design — currently unsupported (premium / private)"
|
||||
placeholder.
|
||||
|
||||
---
|
||||
|
||||
## 13. Security
|
||||
|
||||
- Sanitize text content from linked payloads the same way as forks
|
||||
today — `safeTextLinkHref` etc. — since linked content arrives
|
||||
from an arbitrary third party.
|
||||
- Reject layer types that don't pass the existing
|
||||
`migrateLegacyLayerTargets` allowlist.
|
||||
- Cap fetched payload size at 256 KB per linked event to avoid pulling
|
||||
in adversarially huge sub-designs.
|
||||
- Depth cap (§9) prevents fetch-storm DOS.
|
||||
- Cycle detection (§9) prevents stack-blowing infinite loops.
|
||||
- No external URL fetching introduced — only relay queries.
|
||||
|
||||
---
|
||||
|
||||
## 14. Implementation Plan
|
||||
|
||||
### Phase 1 — Public-only linking
|
||||
|
||||
- Schema additions (`design.kind`, `design.ref`, `design.pinnedEventId`,
|
||||
`design.fallbackPreview`, `design.lastResolvedAt`).
|
||||
- `linkedDesigns` resolver IIFE with session cache + multi-relay
|
||||
union queries (mirror of `_vaultRelayUnion`).
|
||||
- Render-time wiring in `renderDesignObjectIntoElement` for the
|
||||
linked branch.
|
||||
- Nesting + cycle detection.
|
||||
- Layer-options panel: pin / refresh / unlink / replace.
|
||||
- Community browser "Link this design" path.
|
||||
- Loading + missing-source + cycle placeholders.
|
||||
- Acceptance: a public cover design linked into a public slide deck
|
||||
renders correctly; editing the source updates the host's render
|
||||
next refresh.
|
||||
|
||||
### Phase 2 — Premium + private
|
||||
|
||||
- Premium link: check `findUnlockedEntry` for the source's address
|
||||
before rendering; show unlock CTA inline when locked.
|
||||
- Private link: respect `payload.visibility === 'private'` + the
|
||||
shared-with list; render only for authorized viewers.
|
||||
- Acceptance: a premium design linked into a host renders for buyers
|
||||
who've unlocked it and shows an unlock CTA for those who haven't.
|
||||
|
||||
### Phase 3 — Polish
|
||||
|
||||
- localStorage-backed disk cache for return-visit performance.
|
||||
- Attribution `p`-tag emission at host publish time.
|
||||
- Credits panel UI in the host design's sidebar.
|
||||
- "Publish + link" workflow (convert embedded → linked).
|
||||
- Optional batching of the resolver's relay queries to reduce
|
||||
round-trips when a host has many linked layers.
|
||||
|
||||
---
|
||||
|
||||
## 15. Risks
|
||||
|
||||
- **Source disappears.** If the source author tombstones their
|
||||
design (kind-5 deletion) or all relays drop the event, the host
|
||||
renders the fallback preview or a placeholder. Document this in
|
||||
the link-creation UI: "Linked designs depend on the source staying
|
||||
on relays."
|
||||
- **Performance on large compositions.** A host with 50 linked
|
||||
layers triggers 50 fetches on first render. Mitigation: in-session
|
||||
cache + (Phase 3) disk cache + (Phase 3) batched queries.
|
||||
- **Latency on first paint.** Each linked layer flashes a loading
|
||||
placeholder before the resolved payload arrives. Mitigation:
|
||||
parallel kick-off + per-layer fallback preview.
|
||||
- **Embed / link confusion.** Two visually-similar import paths
|
||||
with very different semantics. Mitigation: 🔗 prefix in layer
|
||||
list, distinct icons in the import modal, clear copy in both
|
||||
buttons.
|
||||
- **Attribution griefing.** A malicious user could embed a target
|
||||
pubkey via a linked layer to spuriously attribute work. Mitigation:
|
||||
attribution `p` tag includes the `'design-contributor'` marker so
|
||||
it's distinguishable from arbitrary mention tags, and the source
|
||||
author can issue a kind-5 deletion of any host event that
|
||||
falsely credits them.
|
||||
|
||||
---
|
||||
|
||||
## 16. Acceptance Criteria for Phase 1
|
||||
|
||||
- Linked layers render the latest version of their source on each
|
||||
open of the host design.
|
||||
- Pinned linked layers freeze at a specific event id and don't pick
|
||||
up later edits.
|
||||
- Up to 3 levels of nested linking works; depth-4 and cycles emit
|
||||
visible placeholders without breaking the host.
|
||||
- Refresh button forces a re-fetch.
|
||||
- Missing-source events render either the cached `fallbackPreview`
|
||||
or a clear "not found" placeholder.
|
||||
- An embedded layer + its linked-twin layer render identically when
|
||||
pointing at the same source.
|
||||
- Layer panel shows 🔗 prefix + creator name.
|
||||
- Host event published with linked layers stays under typical relay
|
||||
size limits (verified with a 10-link example).
|
||||
|
||||
---
|
||||
|
||||
## 17. Future Enhancements (not in initial release)
|
||||
|
||||
- **Real-time updates** — subscribe to kind-30078 with the linked
|
||||
address filter so source edits notify the host viewer instantly.
|
||||
- **Relay subscription budgets** — coordinate fetches across hosts
|
||||
that share a tab to avoid duplicate work.
|
||||
- **Designer comments / pull-requests** — let a viewer suggest an
|
||||
edit to a linked source via a kind-1 comment (out of scope, but
|
||||
the link graph makes this navigable).
|
||||
- **Author allowlists** — a host design optionally restricts which
|
||||
authors' linked layers it'll render, for safety in adversarial
|
||||
contexts.
|
||||
- **Diff-aware fallback** — store last-N source previews so the host
|
||||
can "fall back to last known good" when the new source is broken.
|
||||
Reference in new issue
Block a user