@semiont/sdk 0.5.5 → 0.5.7

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/README.md CHANGED
@@ -87,7 +87,7 @@ const client = new SemiontClient(localTransport, localContentTransport);
87
87
  - **Collaboration primitives** — fire-and-forget signals on the verb namespaces (`beckon.hover`, `bind.initiate`, `mark.changeShape`, `browse.click`, ...) coordinate attention and intent across participants. Not afterthoughts, not browser-app fluff: they're how a multi-participant session stays coherent.
88
88
  - **Session layer** — `SemiontSession` (per-KB authentication, token refresh, lifecycle), `SemiontBrowser` (multi-KB orchestration), and `SessionStorage` adapters (`InMemorySessionStorage`, plus a browser-backed one in `@semiont/react-ui`).
89
89
  - **Flow state machines** — RxJS-based factories (`createMarkStateUnit`, `createGatherStateUnit`, `createMatchStateUnit`, `createYieldStateUnit`, `createBeckonStateUnit`) that wrap each long-running flow with `loading$` / `error$` / progress observables. UI-shape-agnostic — any consumer (browser, terminal, mobile, daemon) can subscribe.
90
- - **`WorkerBus`** — the transport-neutral channel-bus interface that worker-side adapters consume. Domain-specific worker adapters live with their domain — `createJobClaimAdapter` and `createJobQueueStateUnit` in [`@semiont/jobs`](https://github.com/The-AI-Alliance/semiont/tree/main/packages/jobs); `createSmelterActorStateUnit` in [`@semiont/make-meaning`](https://github.com/The-AI-Alliance/semiont/tree/main/packages/make-meaning) — and consume `WorkerBus` from here.
90
+ - **`WorkerBus`** — the transport-neutral channel-bus interface that worker-side adapters consume. Domain-specific worker adapters live with their domain — `createJobClaimAdapter` in [`@semiont/jobs`](https://github.com/The-AI-Alliance/semiont/tree/main/packages/jobs); `createSmelterActorStateUnit` in [`@semiont/make-meaning`](https://github.com/The-AI-Alliance/semiont/tree/main/packages/make-meaning) — and consume `WorkerBus` from here.
91
91
  - **Helpers** — `bus-request` (correlation-ID request/reply), the cache primitive backing live queries, and `createSearchPipeline` (debounced-search RxJS pipeline).
92
92
 
93
93
  Page-shaped state machines (admin tables, compose page, resource viewer page, etc.) live in [`@semiont/react-ui`](https://github.com/The-AI-Alliance/semiont/tree/main/packages/react-ui), alongside the components that render them. Those are framework-neutral but tied to the Semiont web frontend's specific page taxonomy; they don't apply to non-web consumers.
@@ -104,7 +104,7 @@ Page-shaped state machines (admin tables, compose page, resource viewer page, et
104
104
  - The transport-neutral `WorkerBus` interface (worker adapters live in their domain packages — `@semiont/jobs`, `@semiont/make-meaning`)
105
105
  - Branded ID types, the unified error hierarchy, the `TransportErrorCode` neutral vocabulary
106
106
 
107
- Nothing page-shaped, nothing web-shell-shaped. A TUI, mobile reader, daemon, or AI agent installs `@semiont/sdk` alone (plus a transport package — `@semiont/api-client` for HTTP, `@semiont/make-meaning` for in-process).
107
+ Nothing page-shaped, nothing web-shell-shaped. A TUI, mobile reader, daemon, or AI agent installs `@semiont/sdk` alone (plus a transport package — `@semiont/http-transport` for HTTP, `@semiont/make-meaning` for in-process).
108
108
 
109
109
  ## Installation
110
110
 
@@ -245,7 +245,7 @@ Apache-2.0 — see [LICENSE](https://github.com/The-AI-Alliance/semiont/blob/mai
245
245
  ## Related packages
246
246
 
247
247
  - [`@semiont/core`](https://github.com/The-AI-Alliance/semiont/tree/main/packages/core) — domain types, `ITransport` contract, OpenAPI-derived schemas
248
- - [`@semiont/api-client`](https://github.com/The-AI-Alliance/semiont/tree/main/packages/api-client) — HTTP transport (`HttpTransport`, `HttpContentTransport`)
248
+ - [`@semiont/http-transport`](https://github.com/The-AI-Alliance/semiont/tree/main/packages/http-transport) — HTTP transport (`HttpTransport`, `HttpContentTransport`)
249
249
  - [`@semiont/make-meaning`](https://github.com/The-AI-Alliance/semiont/tree/main/packages/make-meaning) — in-process transport (`LocalTransport`) and the actor model behind it
250
250
  - [`@semiont/observability`](https://github.com/The-AI-Alliance/semiont/tree/main/packages/observability) — OpenTelemetry tracing the SDK propagates across the bus
251
251
  - [`@semiont/react-ui`](https://github.com/The-AI-Alliance/semiont/tree/main/packages/react-ui) — React bindings (`useStateUnit`, web `SessionStorage`)
package/dist/index.d.ts CHANGED
@@ -4,7 +4,7 @@ export { firstValueFrom, lastValueFrom } from 'rxjs';
4
4
  import * as _semiont_core from '@semiont/core';
5
5
  import { ResourceId, components, UserDID, paths, BackendDownload, ProgressEvent, AnnotationId, BodyOperation, EventMap, ResourceDescriptor, Annotation, TagSchema, GraphConnection, Motivation, GatheredContext, JobId, ITransport, EventBus, IContentTransport, IBackendOperations, BaseUrl, AccessToken, SemiontError, ConnectionState, Selector } from '@semiont/core';
6
6
  export { AccessToken, Annotation, AnnotationId, BaseUrl, BodyItem, BodyOperation, ConnectionState, EntityType, EventMap, GatheredContext, IContentTransport, ITransport, Logger, Motivation, RefreshToken, ResourceDescriptor, ResourceId, SemiontError, TagCategory, TagSchema, UserId, accessToken, annotationId, baseUrl, entityType, refreshToken, resourceId, userId } from '@semiont/core';
7
- export { APIError, HttpContentTransport, HttpTransport, HttpTransportConfig, TokenRefresher } from '@semiont/api-client';
7
+ export { APIError, HttpContentTransport, HttpTransport, HttpTransportConfig, TokenRefresher } from '@semiont/http-transport';
8
8
 
9
9
  /**
10
10
  * Thenable Observable subclasses.
@@ -45,9 +45,12 @@ declare class StreamObservable<T> extends Observable<T> implements PromiseLike<T
45
45
  * cache entry. Used by Browse live-query methods (`browse.resource`,
46
46
  * `browse.annotations`, etc.).
47
47
  *
48
- * Awaiting resolves to the **first non-undefined** value (waits past the
49
- * loading state). Subscribing yields the full sequence including the
50
- * initial `undefined`, so reactive consumers can render a loading state.
48
+ * Awaiting (the one-shot path) fetches a **fresh** value via the optional
49
+ * `fetchFresh` action and rejects on failure a re-read reflects writes
50
+ * (#847). Subscribing yields the SWR sequence: the initial `undefined`, the
51
+ * loaded value, and re-emits on invalidation. (Without a `fetchFresh` action
52
+ * — e.g. a non-cache wrapper — the await falls back to the first
53
+ * non-undefined emission.)
51
54
  *
52
55
  * The class is parameterized as `CacheObservable<T>` even though the
53
56
  * stream's element type is `T | undefined` — `T` is what the consumer
@@ -56,10 +59,22 @@ declare class StreamObservable<T> extends Observable<T> implements PromiseLike<T
56
59
  * `.pipe` in the natural way.
57
60
  */
58
61
  declare class CacheObservable<T> extends Observable<T | undefined> implements PromiseLike<T> {
62
+ /**
63
+ * Optional one-shot fresh-fetch action. When present, `then()` (the await
64
+ * path) resolves to a freshly fetched value and rejects on fetch failure —
65
+ * so a re-read reflects writes (#847). `.subscribe(...)` never uses it: it
66
+ * keeps the stale-while-revalidate cached view over `source`.
67
+ */
68
+ private fetchFresh?;
59
69
  then<R1 = T, R2 = never>(onfulfilled?: ((v: T) => R1 | PromiseLike<R1>) | null, onrejected?: ((e: unknown) => R2 | PromiseLike<R2>) | null): PromiseLike<R1 | R2>;
60
70
  /**
61
71
  * Wrap an existing Observable's subscribe behavior in a `CacheObservable`.
62
72
  *
73
+ * `fetchFresh`, when supplied, backs the await path: `await` resolves to a
74
+ * freshly fetched value (rejecting on failure), so a one-shot read reflects
75
+ * writes without a scoped subscription (#847). `.subscribe(...)` consumers
76
+ * keep the SWR view over `source`.
77
+ *
63
78
  * Memoizes on source identity: passing the same `source` returns the same
64
79
  * wrapper instance. The Browse cache primitive already returns a stable
65
80
  * Observable per key (its B4 contract), so this preserves that contract
@@ -69,7 +84,7 @@ declare class CacheObservable<T> extends Observable<T | undefined> implements Pr
69
84
  *
70
85
  * Backed by a `WeakMap`, so wrappers are GC'd when their source is.
71
86
  */
72
- static from<T>(source: Observable<T | undefined>): CacheObservable<T>;
87
+ static from<T>(source: Observable<T | undefined>, fetchFresh?: () => Promise<T>): CacheObservable<T>;
73
88
  }
74
89
  /**
75
90
  * Discriminated phases of an upload's lifecycle.
@@ -137,6 +152,7 @@ declare class UploadObservable extends Observable<UploadProgress> implements Pro
137
152
  */
138
153
 
139
154
  type StoredEventResponse$1 = components['schemas']['StoredEventResponse'];
155
+ type GetResourceResponse$1 = components['schemas']['GetResourceResponse'];
140
156
  type GatherProgress = components['schemas']['GatherProgress'];
141
157
  type MatchSearchResult = components['schemas']['MatchSearchResult'];
142
158
  type JobProgress$2 = components['schemas']['JobProgress'];
@@ -294,6 +310,7 @@ interface BrowseNamespace$1 {
294
310
  limit?: number;
295
311
  archived?: boolean;
296
312
  search?: string;
313
+ entityType?: string;
297
314
  }): CacheObservable<ResourceDescriptor[]>;
298
315
  annotations(resourceId: ResourceId): CacheObservable<Annotation[]>;
299
316
  annotation(resourceId: ResourceId, annotationId: AnnotationId): CacheObservable<Annotation>;
@@ -302,15 +319,12 @@ interface BrowseNamespace$1 {
302
319
  referencedBy(resourceId: ResourceId): CacheObservable<ReferencedByEntry[]>;
303
320
  events(resourceId: ResourceId): CacheObservable<StoredEventResponse$1[]>;
304
321
  resourceContent(resourceId: ResourceId): Promise<string>;
305
- resourceRepresentation(resourceId: ResourceId, options?: {
306
- accept?: string;
307
- }): Promise<{
322
+ resourceGraph(resourceId: ResourceId): Promise<GetResourceResponse$1>;
323
+ resourceRepresentation(resourceId: ResourceId): Promise<{
308
324
  data: ArrayBuffer;
309
325
  contentType: string;
310
326
  }>;
311
- resourceRepresentationStream(resourceId: ResourceId, options?: {
312
- accept?: string;
313
- }): Promise<{
327
+ resourceRepresentationStream(resourceId: ResourceId): Promise<{
314
328
  stream: ReadableStream<Uint8Array>;
315
329
  contentType: string;
316
330
  }>;
@@ -541,10 +555,12 @@ interface AdminNamespace$1 {
541
555
  }
542
556
 
543
557
  type StoredEventResponse = components['schemas']['StoredEventResponse'];
558
+ type GetResourceResponse = components['schemas']['GetResourceResponse'];
544
559
  type ResourceListFilters = {
545
560
  limit?: number;
546
561
  archived?: boolean;
547
562
  search?: string;
563
+ entityType?: string;
548
564
  };
549
565
  declare class BrowseNamespace implements BrowseNamespace$1 {
550
566
  private readonly transport;
@@ -578,7 +594,33 @@ declare class BrowseNamespace implements BrowseNamespace$1 {
578
594
  * `distinctUntilChanged` at a higher level — would misbehave.
579
595
  */
580
596
  private readonly annotationListObs;
597
+ /**
598
+ * Per-source memo for the scope-acquiring wrapper (#847 Phase 4), keyed by
599
+ * the underlying (stable, per-key) cache observable so the wrapped
600
+ * observable is itself stable per key — preserving B4/B11 referential
601
+ * identity through to `CacheObservable.from`'s own memo.
602
+ */
603
+ private readonly scopedSources;
581
604
  constructor(transport: ITransport, bus: EventBus, content: IContentTransport);
605
+ /**
606
+ * Wrap a resource-scoped live query's source so that *subscribing* acquires
607
+ * the resource's scope (via the transport's ref-counted
608
+ * `subscribeToResource`) and the last unsubscribe releases it (#847 Phase 4).
609
+ * Freshness follows observation: a `.subscribe()` keeps `rId`'s scoped
610
+ * events flowing — so `mark:*` / entity-tag invalidations reach this cache —
611
+ * with no separate `subscribeToResource` call from the consumer.
612
+ *
613
+ * The one-shot `await` path does NOT go through here (it resolves via the
614
+ * cache's `fetch` — see `CacheObservable.from`'s `fetchFresh`), so a
615
+ * one-shot read acquires no scope.
616
+ *
617
+ * Memoized per source so the wrapped observable is stable per key (B4/B11).
618
+ * Each subscription calls `subscribeToResource(rId)`; the transport
619
+ * ref-counts concurrent subscriptions for the same resource onto a single
620
+ * SSE scope. Single-scope model unchanged — multi-scope is deferred (see
621
+ * `.plans/MULTI-RESOURCE-SCOPE.md`).
622
+ */
623
+ private withScope;
582
624
  resource(resourceId: ResourceId): CacheObservable<ResourceDescriptor>;
583
625
  resources(filters?: ResourceListFilters): CacheObservable<ResourceDescriptor[]>;
584
626
  annotations(resourceId: ResourceId): CacheObservable<Annotation[]>;
@@ -588,15 +630,18 @@ declare class BrowseNamespace implements BrowseNamespace$1 {
588
630
  referencedBy(resourceId: ResourceId): CacheObservable<ReferencedByEntry[]>;
589
631
  events(resourceId: ResourceId): CacheObservable<StoredEventResponse[]>;
590
632
  resourceContent(resourceId: ResourceId): Promise<string>;
591
- resourceRepresentation(resourceId: ResourceId, options?: {
592
- accept?: string;
593
- }): Promise<{
633
+ /**
634
+ * Fetch the resource's JSON-LD metadata graph (descriptor + annotations +
635
+ * inbound entity references). One-shot, uncached, dereferenced via the
636
+ * transport's HTTP `/jsonld` face (bus-free) — the LD view an external
637
+ * linked-data client gets. See `.plans/SIMPLER-JSON-LD.md` §5.
638
+ */
639
+ resourceGraph(resourceId: ResourceId): Promise<GetResourceResponse>;
640
+ resourceRepresentation(resourceId: ResourceId): Promise<{
594
641
  data: ArrayBuffer;
595
642
  contentType: string;
596
643
  }>;
597
- resourceRepresentationStream(resourceId: ResourceId, options?: {
598
- accept?: string;
599
- }): Promise<{
644
+ resourceRepresentationStream(resourceId: ResourceId): Promise<{
600
645
  stream: ReadableStream<Uint8Array>;
601
646
  contentType: string;
602
647
  }>;
@@ -768,9 +813,9 @@ declare class JobNamespace implements JobNamespace$1 {
768
813
  get queued$(): Observable<EventMap['job:queued']>;
769
814
  /** Live stream of `job:report-progress` events. */
770
815
  get progress$(): Observable<EventMap['job:report-progress']>;
771
- /** Live stream of `job:complete` events. */
816
+ /** Live stream of `job:complete` events (global; filter by `jobId`). */
772
817
  get complete$(): Observable<EventMap['job:complete']>;
773
- /** Live stream of `job:fail` events. */
818
+ /** Live stream of `job:fail` events (global; filter by `jobId`). */
774
819
  get fail$(): Observable<EventMap['job:fail']>;
775
820
  status(jobId: JobId): Promise<JobStatusResponse>;
776
821
  pollUntilComplete(jobId: JobId, options?: {
@@ -879,7 +924,6 @@ declare class SemiontClient {
879
924
  constructor(transport: ITransport, content: IContentTransport, backend?: IBackendOperations);
880
925
  /** Transport-level connection state. HTTP reflects SSE health; local is always 'connected'. */
881
926
  get state$(): rxjs.Observable<_semiont_core.ConnectionState>;
882
- subscribeToResource(resourceId: ResourceId): () => void;
883
927
  dispose(): void;
884
928
  /**
885
929
  * Convenience factory for the default HTTP setup. Constructs a
@@ -1678,7 +1722,7 @@ declare function createSearchPipeline<T>(fetch: (query: string) => Observable<T[
1678
1722
  * (e.g. `JobClaimAdapter` in `@semiont/jobs`, `SmelterActorStateUnit` in
1679
1723
  * `@semiont/make-meaning`) need.
1680
1724
  *
1681
- * Transport-neutral by design. HTTP `ActorStateUnit` (from `@semiont/api-client`)
1725
+ * Transport-neutral by design. HTTP `ActorStateUnit` (from `@semiont/http-transport`)
1682
1726
  * satisfies it directly; an in-process worker can pass a small shim around
1683
1727
  * an `EventBus` with a `() => Promise<void>` `emit` that calls into the
1684
1728
  * actor system.
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
- import { SemiontError, annotationId, resourceId, email, googleCredential, refreshToken, EventBus, baseUrl, accessToken, searchQuery } from '@semiont/core';
1
+ import { SemiontError, decodeWithCharset, annotationId, resourceId, email, googleCredential, refreshToken, EventBus, baseUrl, accessToken, searchQuery } from '@semiont/core';
2
2
  export { SemiontError, accessToken, annotationId, baseUrl, entityType, refreshToken, resourceId, userId } from '@semiont/core';
3
3
  import { Observable, lastValueFrom, firstValueFrom, merge, TimeoutError, throwError, map as map$1, BehaviorSubject, Subject, Subscription, of, distinctUntilChanged as distinctUntilChanged$1 } from 'rxjs';
4
4
  export { firstValueFrom, lastValueFrom } from 'rxjs';
5
5
  import { filter, map, take, timeout, catchError, takeUntil, startWith, debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';
6
- import { HttpTransport, HttpContentTransport, APIError } from '@semiont/api-client';
7
- export { APIError, HttpContentTransport, HttpTransport } from '@semiont/api-client';
6
+ import { HttpTransport, HttpContentTransport, APIError } from '@semiont/http-transport';
7
+ export { APIError, HttpContentTransport, HttpTransport } from '@semiont/http-transport';
8
8
 
9
9
  // src/client.ts
10
10
  var StreamObservable = class _StreamObservable extends Observable {
@@ -17,12 +17,27 @@ var StreamObservable = class _StreamObservable extends Observable {
17
17
  }
18
18
  };
19
19
  var CacheObservable = class _CacheObservable extends Observable {
20
+ /**
21
+ * Optional one-shot fresh-fetch action. When present, `then()` (the await
22
+ * path) resolves to a freshly fetched value and rejects on fetch failure —
23
+ * so a re-read reflects writes (#847). `.subscribe(...)` never uses it: it
24
+ * keeps the stale-while-revalidate cached view over `source`.
25
+ */
26
+ fetchFresh;
20
27
  then(onfulfilled, onrejected) {
28
+ if (this.fetchFresh) {
29
+ return this.fetchFresh().then(onfulfilled, onrejected);
30
+ }
21
31
  return firstValueFrom(this.pipe(filter((v) => v !== void 0))).then(onfulfilled, onrejected);
22
32
  }
23
33
  /**
24
34
  * Wrap an existing Observable's subscribe behavior in a `CacheObservable`.
25
35
  *
36
+ * `fetchFresh`, when supplied, backs the await path: `await` resolves to a
37
+ * freshly fetched value (rejecting on failure), so a one-shot read reflects
38
+ * writes without a scoped subscription (#847). `.subscribe(...)` consumers
39
+ * keep the SWR view over `source`.
40
+ *
26
41
  * Memoizes on source identity: passing the same `source` returns the same
27
42
  * wrapper instance. The Browse cache primitive already returns a stable
28
43
  * Observable per key (its B4 contract), so this preserves that contract
@@ -32,10 +47,11 @@ var CacheObservable = class _CacheObservable extends Observable {
32
47
  *
33
48
  * Backed by a `WeakMap`, so wrappers are GC'd when their source is.
34
49
  */
35
- static from(source) {
50
+ static from(source, fetchFresh) {
36
51
  let wrapper = wrapperCache.get(source);
37
52
  if (!wrapper) {
38
53
  wrapper = new _CacheObservable((subscriber) => source.subscribe(subscriber));
54
+ wrapper.fetchFresh = fetchFresh;
39
55
  wrapperCache.set(source, wrapper);
40
56
  }
41
57
  return wrapper;
@@ -110,25 +126,31 @@ async function busRequest(bus, emitChannel, payload, resultChannel, failureChann
110
126
  }
111
127
  function createCache(fetchFn) {
112
128
  const store$ = new BehaviorSubject(/* @__PURE__ */ new Map());
113
- const inflight = /* @__PURE__ */ new Set();
129
+ const inflight = /* @__PURE__ */ new Map();
114
130
  const obsCache = /* @__PURE__ */ new Map();
115
- const doFetch = async (key) => {
116
- if (inflight.has(key)) return;
117
- inflight.add(key);
118
- try {
119
- const value = await fetchFn(key);
120
- const next = new Map(store$.value);
121
- next.set(key, value);
122
- store$.next(next);
123
- } catch {
124
- } finally {
125
- inflight.delete(key);
126
- }
131
+ const runFetch = (key) => {
132
+ const existing = inflight.get(key);
133
+ if (existing) return existing;
134
+ let p;
135
+ p = (async () => {
136
+ try {
137
+ const value = await fetchFn(key);
138
+ const next = new Map(store$.value);
139
+ next.set(key, value);
140
+ store$.next(next);
141
+ return value;
142
+ } finally {
143
+ if (inflight.get(key) === p) inflight.delete(key);
144
+ }
145
+ })();
146
+ inflight.set(key, p);
147
+ return p;
127
148
  };
128
149
  return {
129
150
  observe(key) {
130
151
  if (!store$.value.has(key) && !inflight.has(key)) {
131
- void doFetch(key);
152
+ void runFetch(key).catch(() => {
153
+ });
132
154
  }
133
155
  let obs = obsCache.get(key);
134
156
  if (!obs) {
@@ -140,6 +162,9 @@ function createCache(fetchFn) {
140
162
  }
141
163
  return obs;
142
164
  },
165
+ fetch(key) {
166
+ return runFetch(key);
167
+ },
143
168
  get(key) {
144
169
  return store$.value.get(key);
145
170
  },
@@ -148,7 +173,8 @@ function createCache(fetchFn) {
148
173
  },
149
174
  invalidate(key) {
150
175
  inflight.delete(key);
151
- void doFetch(key);
176
+ void runFetch(key).catch(() => {
177
+ });
152
178
  },
153
179
  remove(key) {
154
180
  const next = new Map(store$.value);
@@ -164,7 +190,8 @@ function createCache(fetchFn) {
164
190
  invalidateAll() {
165
191
  for (const key of store$.value.keys()) {
166
192
  inflight.delete(key);
167
- void doFetch(key);
193
+ void runFetch(key).catch(() => {
194
+ });
168
195
  }
169
196
  },
170
197
  dispose() {
@@ -199,7 +226,13 @@ var BrowseNamespace = class {
199
226
  const result = await busRequest(
200
227
  this.transport,
201
228
  "browse:resources-requested",
202
- { search, archived: filters.archived, limit: filters.limit ?? 100, offset: 0 },
229
+ {
230
+ search,
231
+ archived: filters.archived,
232
+ entityType: filters.entityType,
233
+ limit: filters.limit ?? 100,
234
+ offset: 0
235
+ },
203
236
  "browse:resources-result",
204
237
  "browse:resources-failed"
205
238
  );
@@ -270,6 +303,9 @@ var BrowseNamespace = class {
270
303
  });
271
304
  this.subscribeToEvents();
272
305
  }
306
+ transport;
307
+ bus;
308
+ content;
273
309
  // ── Caches, backed by the RxJS-native `Cache<K, V>` primitive ───────────
274
310
  //
275
311
  // Each cache encapsulates the BehaviorSubject store, in-flight guard,
@@ -306,18 +342,58 @@ var BrowseNamespace = class {
306
342
  * `distinctUntilChanged` at a higher level — would misbehave.
307
343
  */
308
344
  annotationListObs = /* @__PURE__ */ new Map();
345
+ /**
346
+ * Per-source memo for the scope-acquiring wrapper (#847 Phase 4), keyed by
347
+ * the underlying (stable, per-key) cache observable so the wrapped
348
+ * observable is itself stable per key — preserving B4/B11 referential
349
+ * identity through to `CacheObservable.from`'s own memo.
350
+ */
351
+ scopedSources = /* @__PURE__ */ new WeakMap();
352
+ /**
353
+ * Wrap a resource-scoped live query's source so that *subscribing* acquires
354
+ * the resource's scope (via the transport's ref-counted
355
+ * `subscribeToResource`) and the last unsubscribe releases it (#847 Phase 4).
356
+ * Freshness follows observation: a `.subscribe()` keeps `rId`'s scoped
357
+ * events flowing — so `mark:*` / entity-tag invalidations reach this cache —
358
+ * with no separate `subscribeToResource` call from the consumer.
359
+ *
360
+ * The one-shot `await` path does NOT go through here (it resolves via the
361
+ * cache's `fetch` — see `CacheObservable.from`'s `fetchFresh`), so a
362
+ * one-shot read acquires no scope.
363
+ *
364
+ * Memoized per source so the wrapped observable is stable per key (B4/B11).
365
+ * Each subscription calls `subscribeToResource(rId)`; the transport
366
+ * ref-counts concurrent subscriptions for the same resource onto a single
367
+ * SSE scope. Single-scope model unchanged — multi-scope is deferred (see
368
+ * `.plans/MULTI-RESOURCE-SCOPE.md`).
369
+ */
370
+ withScope(rId, source) {
371
+ let scoped = this.scopedSources.get(source);
372
+ if (!scoped) {
373
+ scoped = new Observable((subscriber) => {
374
+ const release = this.transport.subscribeToResource(rId);
375
+ const inner = source.subscribe(subscriber);
376
+ return () => {
377
+ inner.unsubscribe();
378
+ release();
379
+ };
380
+ });
381
+ this.scopedSources.set(source, scoped);
382
+ }
383
+ return scoped;
384
+ }
309
385
  // ── Live queries ────────────────────────────────────────────────────────
310
386
  //
311
387
  // These return `CacheObservable<T>`: subscribers see `T | undefined`
312
388
  // (with `undefined` during initial load), and `await` resolves to the
313
389
  // first non-undefined value.
314
390
  resource(resourceId2) {
315
- return CacheObservable.from(this.resourceCache.observe(resourceId2));
391
+ return CacheObservable.from(this.withScope(resourceId2, this.resourceCache.observe(resourceId2)), () => this.resourceCache.fetch(resourceId2));
316
392
  }
317
393
  resources(filters) {
318
394
  const key = JSON.stringify(filters ?? {});
319
395
  this.resourceListFilters.set(key, filters ?? {});
320
- return CacheObservable.from(this.resourceListCache.observe(key));
396
+ return CacheObservable.from(this.resourceListCache.observe(key), () => this.resourceListCache.fetch(key));
321
397
  }
322
398
  annotations(resourceId2) {
323
399
  let obs = this.annotationListObs.get(resourceId2);
@@ -325,35 +401,43 @@ var BrowseNamespace = class {
325
401
  obs = this.annotationListCache.observe(resourceId2).pipe(map$1((r) => r?.annotations));
326
402
  this.annotationListObs.set(resourceId2, obs);
327
403
  }
328
- return CacheObservable.from(obs);
404
+ return CacheObservable.from(this.withScope(resourceId2, obs), () => this.annotationListCache.fetch(resourceId2).then((r) => r.annotations));
329
405
  }
330
406
  annotation(resourceId2, annotationId2) {
331
407
  this.annotationResources.set(annotationId2, resourceId2);
332
- return CacheObservable.from(this.annotationDetailCache.observe(annotationId2));
408
+ return CacheObservable.from(this.withScope(resourceId2, this.annotationDetailCache.observe(annotationId2)), () => this.annotationDetailCache.fetch(annotationId2));
333
409
  }
334
410
  entityTypes() {
335
- return CacheObservable.from(this.entityTypesCache.observe(ENTITY_TYPES_KEY));
411
+ return CacheObservable.from(this.entityTypesCache.observe(ENTITY_TYPES_KEY), () => this.entityTypesCache.fetch(ENTITY_TYPES_KEY));
336
412
  }
337
413
  tagSchemas() {
338
- return CacheObservable.from(this.tagSchemasCache.observe(TAG_SCHEMAS_KEY));
414
+ return CacheObservable.from(this.tagSchemasCache.observe(TAG_SCHEMAS_KEY), () => this.tagSchemasCache.fetch(TAG_SCHEMAS_KEY));
339
415
  }
340
416
  referencedBy(resourceId2) {
341
- return CacheObservable.from(this.referencedByCache.observe(resourceId2));
417
+ return CacheObservable.from(this.withScope(resourceId2, this.referencedByCache.observe(resourceId2)), () => this.referencedByCache.fetch(resourceId2));
342
418
  }
343
419
  events(resourceId2) {
344
- return CacheObservable.from(this.resourceEventsCache.observe(resourceId2));
420
+ return CacheObservable.from(this.withScope(resourceId2, this.resourceEventsCache.observe(resourceId2)), () => this.resourceEventsCache.fetch(resourceId2));
345
421
  }
346
422
  // ── One-shot reads ──────────────────────────────────────────────────────
347
423
  async resourceContent(resourceId2) {
348
- const result = await this.content.getBinary(resourceId2, { accept: "text/plain" });
349
- const decoder = new TextDecoder();
350
- return decoder.decode(result.data);
424
+ const result = await this.content.getBinary(resourceId2);
425
+ return decodeWithCharset(result.data, result.contentType);
426
+ }
427
+ /**
428
+ * Fetch the resource's JSON-LD metadata graph (descriptor + annotations +
429
+ * inbound entity references). One-shot, uncached, dereferenced via the
430
+ * transport's HTTP `/jsonld` face (bus-free) — the LD view an external
431
+ * linked-data client gets. See `.plans/SIMPLER-JSON-LD.md` §5.
432
+ */
433
+ async resourceGraph(resourceId2) {
434
+ return this.content.getResourceGraph(resourceId2);
351
435
  }
352
- async resourceRepresentation(resourceId2, options) {
353
- return this.content.getBinary(resourceId2, options?.accept ? { accept: options.accept } : void 0);
436
+ async resourceRepresentation(resourceId2) {
437
+ return this.content.getBinary(resourceId2);
354
438
  }
355
- async resourceRepresentationStream(resourceId2, options) {
356
- return this.content.getBinaryStream(resourceId2, options?.accept ? { accept: options.accept } : void 0);
439
+ async resourceRepresentationStream(resourceId2) {
440
+ return this.content.getBinaryStream(resourceId2);
357
441
  }
358
442
  async resourceEvents(resourceId2) {
359
443
  const result = await busRequest(
@@ -544,6 +628,8 @@ var MarkNamespace = class {
544
628
  this.transport = transport;
545
629
  this.bus = bus;
546
630
  }
631
+ transport;
632
+ bus;
547
633
  async annotation(input) {
548
634
  const resourceId2 = resourceId(input.target.source);
549
635
  const result = await busRequest(
@@ -569,7 +655,6 @@ var MarkNamespace = class {
569
655
  let done = false;
570
656
  let pollTimer = null;
571
657
  let pollInterval = null;
572
- let unsubscribeResource = this.transport.subscribeToResource(resourceId2);
573
658
  const cleanup = () => {
574
659
  done = true;
575
660
  if (pollTimer) {
@@ -580,10 +665,6 @@ var MarkNamespace = class {
580
665
  clearInterval(pollInterval);
581
666
  pollInterval = null;
582
667
  }
583
- if (unsubscribeResource) {
584
- unsubscribeResource();
585
- unsubscribeResource = null;
586
- }
587
668
  };
588
669
  const resetPollTimer = (jobId) => {
589
670
  if (pollTimer) clearTimeout(pollTimer);
@@ -742,6 +823,8 @@ var BindNamespace = class {
742
823
  this.transport = transport;
743
824
  this.bus = bus;
744
825
  }
826
+ transport;
827
+ bus;
745
828
  async body(resourceId2, annotationId2, operations) {
746
829
  await this.transport.emit("bind:update-body", {
747
830
  correlationId: crypto.randomUUID(),
@@ -759,6 +842,8 @@ var GatherNamespace = class {
759
842
  this.transport = transport;
760
843
  this.bus = bus;
761
844
  }
845
+ transport;
846
+ bus;
762
847
  annotation(resourceId2, annotationId2, options) {
763
848
  return new StreamObservable((subscriber) => {
764
849
  const correlationId = crypto.randomUUID();
@@ -810,6 +895,8 @@ var MatchNamespace = class {
810
895
  this.transport = transport;
811
896
  this.bus = bus;
812
897
  }
898
+ transport;
899
+ bus;
813
900
  requestSearch(input) {
814
901
  this.bus.get("match:search-requested").next(input);
815
902
  }
@@ -853,6 +940,9 @@ var YieldNamespace = class {
853
940
  this.bus = bus;
854
941
  this.content = content;
855
942
  }
943
+ transport;
944
+ bus;
945
+ content;
856
946
  resource(data) {
857
947
  const totalBytes = typeof Buffer !== "undefined" && data.file instanceof Buffer ? data.file.length : data.file.size;
858
948
  return new UploadObservable((subscriber) => {
@@ -905,7 +995,6 @@ var YieldNamespace = class {
905
995
  let done = false;
906
996
  let pollTimer = null;
907
997
  let pollInterval = null;
908
- let unsubscribeResource = this.transport.subscribeToResource(resourceId2);
909
998
  const cleanup = () => {
910
999
  done = true;
911
1000
  if (pollTimer) {
@@ -916,10 +1005,6 @@ var YieldNamespace = class {
916
1005
  clearInterval(pollInterval);
917
1006
  pollInterval = null;
918
1007
  }
919
- if (unsubscribeResource) {
920
- unsubscribeResource();
921
- unsubscribeResource = null;
922
- }
923
1008
  };
924
1009
  const resetPollTimer = (jid) => {
925
1010
  if (pollTimer) clearTimeout(pollTimer);
@@ -1062,6 +1147,8 @@ var BeckonNamespace = class {
1062
1147
  this.transport = transport;
1063
1148
  this.bus = bus;
1064
1149
  }
1150
+ transport;
1151
+ bus;
1065
1152
  attention(resourceId2, annotationId2) {
1066
1153
  void this.transport.emit("beckon:focus", { annotationId: annotationId2, resourceId: resourceId2 });
1067
1154
  }
@@ -1078,6 +1165,7 @@ var FrameNamespace = class {
1078
1165
  constructor(transport) {
1079
1166
  this.transport = transport;
1080
1167
  }
1168
+ transport;
1081
1169
  async addEntityType(type) {
1082
1170
  await this.transport.emit("frame:add-entity-type", { tag: type });
1083
1171
  }
@@ -1097,6 +1185,8 @@ var JobNamespace = class {
1097
1185
  this.transport = transport;
1098
1186
  this.bus = bus;
1099
1187
  }
1188
+ transport;
1189
+ bus;
1100
1190
  /**
1101
1191
  * Live stream of `job:queued` events. Surfaces a typed view onto the
1102
1192
  * underlying bus channel for consumers (CLIs, MCP handlers, widgets)
@@ -1109,11 +1199,11 @@ var JobNamespace = class {
1109
1199
  get progress$() {
1110
1200
  return this.bus.get("job:report-progress");
1111
1201
  }
1112
- /** Live stream of `job:complete` events. */
1202
+ /** Live stream of `job:complete` events (global; filter by `jobId`). */
1113
1203
  get complete$() {
1114
1204
  return this.bus.get("job:complete");
1115
1205
  }
1116
- /** Live stream of `job:fail` events. */
1206
+ /** Live stream of `job:fail` events (global; filter by `jobId`). */
1117
1207
  get fail$() {
1118
1208
  return this.bus.get("job:fail");
1119
1209
  }
@@ -1153,6 +1243,7 @@ var AuthNamespace = class {
1153
1243
  constructor(backend) {
1154
1244
  this.backend = backend;
1155
1245
  }
1246
+ backend;
1156
1247
  async password(emailStr, passwordStr) {
1157
1248
  return this.backend.authenticatePassword(email(emailStr), passwordStr);
1158
1249
  }
@@ -1184,6 +1275,7 @@ var AdminNamespace = class {
1184
1275
  constructor(backend) {
1185
1276
  this.backend = backend;
1186
1277
  }
1278
+ backend;
1187
1279
  async users() {
1188
1280
  const result = await this.backend.listUsers();
1189
1281
  return result.users;
@@ -1306,9 +1398,6 @@ var SemiontClient = class _SemiontClient {
1306
1398
  get state$() {
1307
1399
  return this.transport.state$;
1308
1400
  }
1309
- subscribeToResource(resourceId2) {
1310
- return this.transport.subscribeToResource(resourceId2);
1311
- }
1312
1401
  dispose() {
1313
1402
  this.transport.dispose();
1314
1403
  this.content.dispose();