@semiont/api-client 0.4.20 → 0.4.21

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/dist/index.js CHANGED
@@ -1,448 +1,522 @@
1
1
  import ky, { HTTPError } from 'ky';
2
- import { BehaviorSubject, Observable, merge } from 'rxjs';
3
- import { map, distinctUntilChanged, filter, takeUntil } from 'rxjs/operators';
4
- import { annotationId, resourceId, searchQuery, email, googleCredential, refreshToken } from '@semiont/core';
2
+ import { RESOURCE_BROADCAST_TYPES, PERSISTED_EVENT_TYPES, annotationId, resourceId, email, googleCredential, refreshToken, EventBus, accessToken, baseUrl, userDID, searchQuery } from '@semiont/core';
5
3
  export { getFragmentSelector, getSvgSelector, getTextPositionSelector, validateSvgMarkup } from '@semiont/core';
4
+ import { Subject, BehaviorSubject, merge, firstValueFrom, map as map$1, distinctUntilChanged, Observable, Subscription, of, filter as filter$1, take as take$1, timeout as timeout$1 } from 'rxjs';
5
+ import { share, filter, map, take, timeout, takeUntil, startWith, debounceTime, distinctUntilChanged as distinctUntilChanged$1, switchMap } from 'rxjs/operators';
6
6
 
7
7
  // src/client.ts
8
-
9
- // src/sse/stream.ts
10
- function createSSEStream(url, fetchOptions, config, logger) {
11
- let abortController = new AbortController();
12
- let closed = false;
8
+ function busLog(direction, channel, payload, scope) {
9
+ if (typeof globalThis === "undefined") return;
10
+ const g = globalThis;
11
+ if (!g.__SEMIONT_BUS_LOG__) return;
12
+ const cid = payload?.correlationId;
13
+ const tag = `[bus ${direction}] ${channel}` + (scope ? ` scope=${scope}` : "") + (cid ? ` cid=${String(cid).slice(0, 8)}` : "");
14
+ console.debug(tag, payload);
15
+ }
16
+ var DEGRADED_THRESHOLD_MS = 3e3;
17
+ var ALLOWED_TRANSITIONS = {
18
+ initial: ["connecting", "closed"],
19
+ connecting: ["open", "reconnecting", "closed"],
20
+ open: ["reconnecting", "closed"],
21
+ reconnecting: ["connecting", "degraded", "closed"],
22
+ degraded: ["connecting", "closed"],
23
+ closed: []
24
+ };
25
+ function createActorVM(options) {
26
+ const { baseUrl: baseUrl3, token: tokenOrGetter, channels: initialChannels, scope: initialScope, reconnectMs = 5e3 } = options;
27
+ const getToken = typeof tokenOrGetter === "function" ? tokenOrGetter : () => tokenOrGetter;
28
+ const g = globalThis;
29
+ g.__SEMIONT_ACTOR_INSTANCES__ = (g.__SEMIONT_ACTOR_INSTANCES__ ?? 0) + 1;
30
+ const actorSerial = g.__SEMIONT_ACTOR_INSTANCES__;
31
+ console.debug(`[diag] ActorVM #${actorSerial} constructed (baseUrl=${baseUrl3})`);
32
+ const globalChannels = new Set(initialChannels);
33
+ const scopedChannels = /* @__PURE__ */ new Set();
34
+ let activeScope = initialScope;
35
+ const events$ = new Subject();
36
+ const state$ = new BehaviorSubject("initial");
37
+ let currentState = "initial";
38
+ let degradedTimer = null;
39
+ const transition = (next) => {
40
+ if (currentState === next) return;
41
+ const allowed = ALLOWED_TRANSITIONS[currentState];
42
+ if (!allowed.includes(next)) {
43
+ throw new Error(`Invalid connection state transition: ${currentState} \u2192 ${next}`);
44
+ }
45
+ const prev = currentState;
46
+ currentState = next;
47
+ if (next === "reconnecting" && prev !== "reconnecting") {
48
+ if (degradedTimer) clearTimeout(degradedTimer);
49
+ degradedTimer = setTimeout(() => {
50
+ if (currentState === "reconnecting") transition("degraded");
51
+ }, DEGRADED_THRESHOLD_MS);
52
+ }
53
+ if (prev === "reconnecting" && next !== "reconnecting") {
54
+ if (degradedTimer) {
55
+ clearTimeout(degradedTimer);
56
+ degradedTimer = null;
57
+ }
58
+ }
59
+ state$.next(next);
60
+ };
61
+ let running = false;
62
+ const inflightControllers = /* @__PURE__ */ new Set();
63
+ let reconnectTimer = null;
13
64
  let lastEventId = null;
14
- const RECONNECT_INITIAL_MS = 1e3;
15
- const RECONNECT_MAX_MS = 3e4;
16
- let reconnectDelayMs = RECONNECT_INITIAL_MS;
65
+ const shared$ = events$.pipe(share());
66
+ const disconnect = () => {
67
+ for (const c of inflightControllers) {
68
+ try {
69
+ c.abort();
70
+ } catch {
71
+ }
72
+ }
73
+ inflightControllers.clear();
74
+ if (reconnectTimer) {
75
+ clearTimeout(reconnectTimer);
76
+ reconnectTimer = null;
77
+ }
78
+ };
17
79
  const connect = async () => {
18
- try {
19
- logger?.debug("SSE Stream Request", {
20
- type: "sse_request",
21
- url,
22
- method: fetchOptions.method || "GET",
23
- timestamp: Date.now()
24
- });
25
- const headers = {
26
- ...fetchOptions.headers,
27
- "Accept": "text/event-stream"
28
- };
29
- if (lastEventId !== null) {
30
- headers["Last-Event-ID"] = lastEventId;
80
+ transition("connecting");
81
+ for (const c of inflightControllers) {
82
+ try {
83
+ c.abort();
84
+ } catch {
31
85
  }
32
- const response = await fetch(url, {
33
- ...fetchOptions,
34
- signal: abortController.signal,
35
- headers
36
- });
37
- if (!response.ok) {
38
- const errorData = await response.json().catch(() => ({}));
39
- const error = new Error(errorData.message || `HTTP ${response.status}: ${response.statusText}`);
40
- logger?.error("SSE Stream Error", {
41
- type: "sse_error",
42
- url,
43
- error: error.message,
44
- status: response.status,
45
- phase: "connect"
46
- });
47
- throw error;
86
+ }
87
+ inflightControllers.clear();
88
+ const params = new URLSearchParams();
89
+ for (const ch of globalChannels) {
90
+ params.append("channel", ch);
91
+ }
92
+ if (activeScope && scopedChannels.size > 0) {
93
+ params.append("scope", activeScope);
94
+ for (const ch of scopedChannels) {
95
+ params.append("scoped", ch);
48
96
  }
49
- if (!response.body) {
50
- const error = new Error("Response body is null - server did not return a stream");
51
- logger?.error("SSE Stream Error", {
52
- type: "sse_error",
53
- url,
54
- error: error.message,
55
- phase: "connect"
56
- });
57
- throw error;
97
+ }
98
+ const url = `${baseUrl3}/bus/subscribe?${params.toString()}`;
99
+ const controller = new AbortController();
100
+ inflightControllers.add(controller);
101
+ try {
102
+ const headers = { Authorization: `Bearer ${getToken()}` };
103
+ if (lastEventId) headers["Last-Event-ID"] = lastEventId;
104
+ const response = await fetch(url, { headers, signal: controller.signal });
105
+ if (!response.ok || !response.body) {
106
+ throw new Error(`SSE connect failed: ${response.status}`);
58
107
  }
59
- logger?.info("SSE Stream Connected", {
60
- type: "sse_connected",
61
- url,
62
- status: response.status,
63
- contentType: response.headers.get("content-type") || "unknown"
64
- });
108
+ transition("open");
65
109
  const reader = response.body.getReader();
66
110
  const decoder = new TextDecoder();
67
111
  let buffer = "";
68
- let eventType = "";
69
- let eventData = "";
70
- let eventId = "";
71
- while (true) {
112
+ let currentEvent = "";
113
+ let currentData = "";
114
+ let currentId;
115
+ while (running && inflightControllers.has(controller)) {
72
116
  const { done, value } = await reader.read();
73
- if (done || closed) break;
74
- const chunk = decoder.decode(value, { stream: true });
75
- buffer += chunk;
117
+ if (done) break;
118
+ buffer += decoder.decode(value, { stream: true });
76
119
  const lines = buffer.split("\n");
77
- buffer = lines.pop() || "";
120
+ buffer = lines.pop() ?? "";
78
121
  for (const line of lines) {
79
- if (line.startsWith("event:")) {
80
- eventType = line.slice(6).trim();
81
- } else if (line.startsWith("data:")) {
82
- eventData = line.slice(5).trim();
83
- } else if (line.startsWith("id:")) {
84
- eventId = line.slice(3).trim();
122
+ if (line.startsWith("event: ")) {
123
+ currentEvent = line.slice(7);
124
+ } else if (line.startsWith("data: ")) {
125
+ currentData = line.slice(6);
126
+ } else if (line.startsWith("id: ")) {
127
+ currentId = line.slice(4);
85
128
  } else if (line === "") {
86
- if (eventData && !closed) {
87
- handleEvent(eventType, eventData, eventId);
88
- if (closed) break;
89
- eventType = "";
90
- eventData = "";
91
- eventId = "";
129
+ if (currentEvent === "bus-event" && currentData) {
130
+ if (currentId !== void 0) lastEventId = currentId;
131
+ const parsed = JSON.parse(currentData);
132
+ busLog("RECV", parsed.channel, parsed.payload, parsed.scope);
133
+ events$.next(parsed);
92
134
  }
135
+ currentEvent = "";
136
+ currentData = "";
137
+ currentId = void 0;
93
138
  }
94
139
  }
95
- if (closed) break;
96
- }
97
- logger?.info("SSE Stream Closed", {
98
- type: "sse_closed",
99
- url,
100
- reason: "complete"
101
- });
102
- } catch (error) {
103
- if (error instanceof Error && error.name !== "AbortError") {
104
- logger?.error("SSE Stream Error", {
105
- type: "sse_error",
106
- url,
107
- error: error.message,
108
- phase: "stream"
109
- });
110
- } else if (error instanceof Error && error.name === "AbortError") {
111
- logger?.info("SSE Stream Closed", {
112
- type: "sse_closed",
113
- url,
114
- reason: "abort"
115
- });
116
140
  }
141
+ } catch (err) {
142
+ if (err.name === "AbortError") return;
143
+ } finally {
144
+ inflightControllers.delete(controller);
117
145
  }
118
- };
119
- const handleEvent = (eventType, data, id) => {
120
- if (data.startsWith(":")) {
121
- return;
146
+ if (running) {
147
+ transition("reconnecting");
148
+ reconnectTimer = setTimeout(() => {
149
+ if (running) connect();
150
+ }, reconnectMs);
122
151
  }
123
- if (id) {
124
- lastEventId = id;
152
+ };
153
+ const reconnect = () => {
154
+ if (!running) return;
155
+ if (currentState === "open" || currentState === "connecting" || currentState === "degraded") {
156
+ transition("reconnecting");
125
157
  }
126
- try {
127
- const parsed = JSON.parse(data);
128
- logger?.debug("SSE Event Received", {
129
- type: "sse_event",
130
- url,
131
- event: eventType || "message",
132
- hasData: !!data
158
+ disconnect();
159
+ connect();
160
+ };
161
+ let reconnectTimer2 = null;
162
+ const RECONNECT_DEBOUNCE_MS = 100;
163
+ const scheduleReconnect = () => {
164
+ if (reconnectTimer2) clearTimeout(reconnectTimer2);
165
+ reconnectTimer2 = setTimeout(() => {
166
+ reconnectTimer2 = null;
167
+ reconnect();
168
+ }, RECONNECT_DEBOUNCE_MS);
169
+ };
170
+ return {
171
+ on$(channel) {
172
+ return shared$.pipe(
173
+ filter((e) => e.channel === channel),
174
+ map((e) => e.payload)
175
+ );
176
+ },
177
+ emit: async (channel, payload, emitScope) => {
178
+ busLog("EMIT", channel, payload, emitScope);
179
+ const body = { channel, payload };
180
+ if (emitScope) body.scope = emitScope;
181
+ await fetch(`${baseUrl3}/bus/emit`, {
182
+ method: "POST",
183
+ headers: {
184
+ "Content-Type": "application/json",
185
+ Authorization: `Bearer ${getToken()}`
186
+ },
187
+ body: JSON.stringify(body)
133
188
  });
134
- if (typeof parsed === "object" && parsed !== null && "metadata" in parsed) {
135
- config.eventBus.get(eventType).next(parsed);
136
- return;
189
+ },
190
+ state$: state$.asObservable(),
191
+ addChannels: (channels, scope) => {
192
+ let changed = false;
193
+ if (scope !== void 0) {
194
+ for (const ch of channels) {
195
+ if (!scopedChannels.has(ch)) {
196
+ scopedChannels.add(ch);
197
+ changed = true;
198
+ }
199
+ }
200
+ if (scope !== activeScope) {
201
+ activeScope = scope;
202
+ changed = true;
203
+ }
204
+ } else {
205
+ for (const ch of channels) {
206
+ if (!globalChannels.has(ch)) {
207
+ globalChannels.add(ch);
208
+ changed = true;
209
+ }
210
+ }
137
211
  }
138
- config.eventBus.get(eventType).next(parsed);
139
- if (config.completeEvent && eventType === config.completeEvent) {
140
- closed = true;
141
- abortController.abort();
142
- } else if (config.errorEvent && eventType === config.errorEvent) {
143
- closed = true;
144
- abortController.abort();
212
+ if (changed) scheduleReconnect();
213
+ },
214
+ removeChannels: (channels) => {
215
+ let changed = false;
216
+ for (const ch of channels) {
217
+ if (scopedChannels.delete(ch)) changed = true;
218
+ if (globalChannels.delete(ch)) changed = true;
145
219
  }
146
- } catch (error) {
147
- logger?.error("SSE Failed to parse event data", { error, eventType, data });
220
+ if (scopedChannels.size === 0) activeScope = void 0;
221
+ if (changed) scheduleReconnect();
222
+ },
223
+ start: () => {
224
+ if (running) return;
225
+ running = true;
226
+ connect();
227
+ },
228
+ stop: () => {
229
+ running = false;
230
+ if (currentState !== "closed") transition("closed");
231
+ if (reconnectTimer2) {
232
+ clearTimeout(reconnectTimer2);
233
+ reconnectTimer2 = null;
234
+ }
235
+ if (degradedTimer) {
236
+ clearTimeout(degradedTimer);
237
+ degradedTimer = null;
238
+ }
239
+ disconnect();
240
+ },
241
+ dispose: () => {
242
+ running = false;
243
+ if (currentState !== "closed") transition("closed");
244
+ if (reconnectTimer2) {
245
+ clearTimeout(reconnectTimer2);
246
+ reconnectTimer2 = null;
247
+ }
248
+ if (degradedTimer) {
249
+ clearTimeout(degradedTimer);
250
+ degradedTimer = null;
251
+ }
252
+ disconnect();
253
+ events$.complete();
254
+ state$.complete();
148
255
  }
149
256
  };
150
- const runConnect = async () => {
151
- if (!config.reconnect) {
152
- return connect();
153
- }
154
- while (!closed) {
155
- abortController = new AbortController();
156
- try {
157
- await connect();
158
- if (closed) return;
159
- logger?.info("SSE Stream ended cleanly; reconnecting", { url });
160
- } catch (error) {
161
- if (closed) return;
162
- if (error instanceof Error && error.name === "AbortError") return;
163
- logger?.warn("SSE Stream errored; reconnecting", {
164
- url,
165
- error: error instanceof Error ? error.message : String(error),
166
- delayMs: reconnectDelayMs
167
- });
168
- }
169
- await new Promise((resolve) => setTimeout(resolve, reconnectDelayMs));
170
- reconnectDelayMs = Math.min(reconnectDelayMs * 2, RECONNECT_MAX_MS);
257
+ }
258
+ var BusRequestError = class extends Error {
259
+ constructor(message) {
260
+ super(message);
261
+ this.name = "BusRequestError";
262
+ }
263
+ };
264
+ async function busRequest(actor, emitChannel, payload, resultChannel, failureChannel, timeoutMs = 3e4) {
265
+ const correlationId = crypto.randomUUID();
266
+ const fullPayload = { ...payload, correlationId };
267
+ const result$ = merge(
268
+ actor.on$(resultChannel).pipe(
269
+ filter((e) => e.correlationId === correlationId),
270
+ map((e) => ({ ok: true, response: e.response }))
271
+ ),
272
+ actor.on$(failureChannel).pipe(
273
+ filter((e) => e.correlationId === correlationId),
274
+ map((e) => ({ ok: false, error: new BusRequestError(e.message) }))
275
+ )
276
+ ).pipe(take(1), timeout(timeoutMs));
277
+ const resultPromise = firstValueFrom(result$);
278
+ await actor.emit(emitChannel, fullPayload);
279
+ const result = await resultPromise;
280
+ if (!result.ok) {
281
+ throw result.error;
282
+ }
283
+ return result.response;
284
+ }
285
+ function createCache(fetchFn) {
286
+ const store$ = new BehaviorSubject(/* @__PURE__ */ new Map());
287
+ const inflight = /* @__PURE__ */ new Set();
288
+ const obsCache = /* @__PURE__ */ new Map();
289
+ const doFetch = async (key) => {
290
+ if (inflight.has(key)) return;
291
+ inflight.add(key);
292
+ try {
293
+ const value = await fetchFn(key);
294
+ const next = new Map(store$.value);
295
+ next.set(key, value);
296
+ store$.next(next);
297
+ } catch {
298
+ } finally {
299
+ inflight.delete(key);
171
300
  }
172
301
  };
173
- void runConnect();
174
302
  return {
175
- close() {
176
- closed = true;
177
- abortController.abort();
303
+ observe(key) {
304
+ if (!store$.value.has(key) && !inflight.has(key)) {
305
+ void doFetch(key);
306
+ }
307
+ let obs = obsCache.get(key);
308
+ if (!obs) {
309
+ obs = store$.pipe(
310
+ map$1((m) => m.get(key)),
311
+ distinctUntilChanged()
312
+ );
313
+ obsCache.set(key, obs);
314
+ }
315
+ return obs;
316
+ },
317
+ get(key) {
318
+ return store$.value.get(key);
319
+ },
320
+ keys() {
321
+ return [...store$.value.keys()];
322
+ },
323
+ invalidate(key) {
324
+ inflight.delete(key);
325
+ void doFetch(key);
326
+ },
327
+ remove(key) {
328
+ const next = new Map(store$.value);
329
+ next.delete(key);
330
+ store$.next(next);
331
+ inflight.delete(key);
332
+ },
333
+ set(key, value) {
334
+ const next = new Map(store$.value);
335
+ next.set(key, value);
336
+ store$.next(next);
337
+ },
338
+ invalidateAll() {
339
+ for (const key of store$.value.keys()) {
340
+ inflight.delete(key);
341
+ void doFetch(key);
342
+ }
343
+ },
344
+ dispose() {
345
+ store$.complete();
346
+ obsCache.clear();
347
+ inflight.clear();
178
348
  }
179
349
  };
180
350
  }
181
351
 
182
- // src/sse/index.ts
183
- var SSE_STREAM_CONNECTED = "stream-connected";
184
- var SSEClient = class {
185
- baseUrl;
186
- logger;
187
- constructor(config) {
188
- this.baseUrl = config.baseUrl.endsWith("/") ? config.baseUrl.slice(0, -1) : config.baseUrl;
189
- this.logger = config.logger;
190
- }
191
- /**
192
- * Get common headers for SSE requests
193
- */
194
- getHeaders(auth) {
195
- const headers = {
196
- "Content-Type": "application/json"
197
- };
198
- if (auth) {
199
- headers["Authorization"] = `Bearer ${auth}`;
200
- }
201
- return headers;
202
- }
203
- /**
204
- * Subscribe to resource events (long-lived stream)
205
- *
206
- * Opens a long-lived SSE connection to receive real-time events for a resource.
207
- * Used for collaborative editing - see events from other users as they happen.
208
- *
209
- * This stream does NOT have a complete event - it stays open until explicitly closed.
210
- *
211
- * @param resourceId - Resource URI or ID to subscribe to
212
- * @param options - Request options (auth token)
213
- * @returns SSE stream controller with event callback
214
- *
215
- * @example
216
- * ```typescript
217
- * const stream = sseClient.resourceEvents(
218
- * 'http://localhost:4000/resources/doc-123',
219
- * { auth: 'your-token' }
220
- * );
221
- *
222
- * stream.onProgress((event) => {
223
- * console.log(`Event: ${event.type}`);
224
- * console.log(`User: ${event.userId}`);
225
- * console.log(`Sequence: ${event.metadata.sequenceNumber}`);
226
- * console.log(`Payload:`, event.payload);
227
- * });
228
- *
229
- * stream.onError((error) => {
230
- * console.error('Stream error:', error.message);
231
- * });
232
- *
233
- * // Close when no longer needed (e.g., component unmount)
234
- * stream.close();
235
- * ```
236
- */
237
- resourceEvents(resourceId, options) {
238
- const url = `${this.baseUrl}/resources/${resourceId}/events/stream`;
239
- const stream = createSSEStream(
240
- url,
241
- {
242
- method: "GET",
243
- headers: this.getHeaders(options.auth)
244
- },
245
- {
246
- progressEvents: ["*"],
247
- // Accept all event types (long-lived stream)
248
- completeEvent: null,
249
- // Never completes (long-lived)
250
- errorEvent: null,
251
- // No error event (errors throw)
252
- eventBus: options.eventBus,
253
- reconnect: true
254
- },
255
- this.logger
256
- );
257
- if (options.onConnected) {
258
- const sub = options.eventBus.get(SSE_STREAM_CONNECTED).subscribe(() => {
259
- options.onConnected();
260
- sub.unsubscribe();
261
- });
262
- }
263
- return stream;
264
- }
265
- /**
266
- * Subscribe to global system events (long-lived stream)
267
- *
268
- * Opens a long-lived SSE connection to receive system-level domain events
269
- * (entity type additions, etc.) that are not scoped to a specific resource.
270
- *
271
- * @param options - Request options (auth token, eventBus)
272
- * @returns SSE stream controller
273
- *
274
- * @example
275
- * ```typescript
276
- * const stream = sseClient.globalEvents({ auth: 'your-token', eventBus });
277
- *
278
- * // Events auto-emit to EventBus typed channels — subscribe there
279
- * eventBus.get('mark:entity-type-added').subscribe((stored) => {
280
- * // Invalidate entity types query
281
- * });
282
- *
283
- * // Close when no longer needed
284
- * stream.close();
285
- * ```
286
- */
287
- globalEvents(options) {
288
- const url = `${this.baseUrl}/api/events/stream`;
289
- const stream = createSSEStream(
290
- url,
291
- {
292
- method: "GET",
293
- headers: this.getHeaders(options.auth)
294
- },
295
- {
296
- progressEvents: ["*"],
297
- completeEvent: null,
298
- errorEvent: null,
299
- eventBus: options.eventBus,
300
- reconnect: true
301
- },
302
- this.logger
303
- );
304
- if (options.onConnected) {
305
- const sub = options.eventBus.get(SSE_STREAM_CONNECTED).subscribe(() => {
306
- options.onConnected();
307
- sub.unsubscribe();
308
- });
309
- }
310
- return stream;
311
- }
312
- /**
313
- * Subscribe to participant attention stream (long-lived stream)
314
- *
315
- * Opens a participant-scoped SSE connection to receive cross-participant
316
- * beckon signals. Signals are delivered as 'beckon:focus' events routed
317
- * to the EventBus — the existing scroll/highlight machinery handles them.
318
- *
319
- * Signals are ephemeral — delivered if connected, dropped if not.
320
- *
321
- * @param options - Request options (auth token, eventBus)
322
- * @returns SSE stream controller
323
- */
324
- attentionStream(options) {
325
- const url = `${this.baseUrl}/api/participants/me/attention-stream`;
326
- const stream = createSSEStream(
327
- url,
328
- {
329
- method: "GET",
330
- headers: this.getHeaders(options.auth)
331
- },
332
- {
333
- progressEvents: ["*"],
334
- completeEvent: null,
335
- errorEvent: null,
336
- eventBus: options.eventBus,
337
- reconnect: true
338
- },
339
- this.logger
340
- );
341
- if (options.onConnected) {
342
- const sub = options.eventBus.get(SSE_STREAM_CONNECTED).subscribe(() => {
343
- options.onConnected();
344
- sub.unsubscribe();
345
- });
346
- }
347
- return stream;
348
- }
349
- };
352
+ // src/namespaces/browse.ts
353
+ var ENTITY_TYPES_KEY = "_";
350
354
  var BrowseNamespace = class {
351
- constructor(http, eventBus, getToken) {
355
+ constructor(http, eventBus, getToken, actor) {
352
356
  this.http = http;
353
357
  this.eventBus = eventBus;
354
358
  this.getToken = getToken;
359
+ this.actor = actor;
360
+ const g = globalThis;
361
+ g.__SEMIONT_BROWSE_INSTANCES__ = (g.__SEMIONT_BROWSE_INSTANCES__ ?? 0) + 1;
362
+ const browseSerial = g.__SEMIONT_BROWSE_INSTANCES__;
363
+ this.__serial__ = browseSerial;
364
+ console.debug(`[diag] BrowseNamespace #${browseSerial} constructed`);
365
+ this.resourceCache = createCache(async (id) => {
366
+ const result = await busRequest(
367
+ this.actor,
368
+ "browse:resource-requested",
369
+ { resourceId: id },
370
+ "browse:resource-result",
371
+ "browse:resource-failed"
372
+ );
373
+ return result.resource;
374
+ });
375
+ this.resourceListCache = createCache(async (key) => {
376
+ const filters = this.resourceListFilters.get(key) ?? {};
377
+ const search = filters.search ? searchQuery(filters.search) : void 0;
378
+ const result = await busRequest(
379
+ this.actor,
380
+ "browse:resources-requested",
381
+ { search, archived: filters.archived, limit: filters.limit ?? 100, offset: 0 },
382
+ "browse:resources-result",
383
+ "browse:resources-failed"
384
+ );
385
+ return result.resources;
386
+ });
387
+ this.annotationListCache = createCache(async (resourceId) => {
388
+ return busRequest(
389
+ this.actor,
390
+ "browse:annotations-requested",
391
+ { resourceId },
392
+ "browse:annotations-result",
393
+ "browse:annotations-failed"
394
+ );
395
+ });
396
+ this.annotationDetailCache = createCache(async (annotationId) => {
397
+ const resourceId = this.annotationResources.get(annotationId);
398
+ if (!resourceId) {
399
+ throw new Error(`Cannot fetch annotation ${annotationId}: no resourceId known`);
400
+ }
401
+ const result = await busRequest(
402
+ this.actor,
403
+ "browse:annotation-requested",
404
+ { resourceId, annotationId },
405
+ "browse:annotation-result",
406
+ "browse:annotation-failed"
407
+ );
408
+ return result.annotation;
409
+ });
410
+ this.entityTypesCache = createCache(async () => {
411
+ const serial = this.__serial__;
412
+ console.debug(`[diag] BrowseNamespace#${serial} entityTypes fetchFn START`);
413
+ const result = await busRequest(
414
+ this.actor,
415
+ "browse:entity-types-requested",
416
+ {},
417
+ "browse:entity-types-result",
418
+ "browse:entity-types-failed"
419
+ );
420
+ console.debug(`[diag] BrowseNamespace#${serial} entityTypes fetchFn RESOLVE`, JSON.stringify(result.entityTypes).slice(0, 200));
421
+ return result.entityTypes;
422
+ });
423
+ this.referencedByCache = createCache(async (resourceId) => {
424
+ const result = await busRequest(
425
+ this.actor,
426
+ "browse:referenced-by-requested",
427
+ { resourceId },
428
+ "browse:referenced-by-result",
429
+ "browse:referenced-by-failed"
430
+ );
431
+ return result.referencedBy;
432
+ });
433
+ this.resourceEventsCache = createCache(async (resourceId) => {
434
+ const result = await busRequest(
435
+ this.actor,
436
+ "browse:events-requested",
437
+ { resourceId },
438
+ "browse:events-result",
439
+ "browse:events-failed"
440
+ );
441
+ return result.events;
442
+ });
355
443
  this.subscribeToEvents();
356
444
  }
357
- // ── Annotation list cache ───────────────────────────────────────────────
358
- annotationList$ = new BehaviorSubject(/* @__PURE__ */ new Map());
359
- fetchingAnnotationList = /* @__PURE__ */ new Set();
360
- annotationListObs$ = /* @__PURE__ */ new Map();
361
- // ── Annotation detail cache ─────────────────────────────────────────────
362
- annotationDetail$ = new BehaviorSubject(/* @__PURE__ */ new Map());
363
- fetchingAnnotationDetail = /* @__PURE__ */ new Set();
364
- annotationDetailObs$ = /* @__PURE__ */ new Map();
365
- // ── Resource detail cache ───────────────────────────────────────────────
366
- resourceDetail$ = new BehaviorSubject(/* @__PURE__ */ new Map());
367
- fetchingResourceDetail = /* @__PURE__ */ new Set();
368
- resourceDetailObs$ = /* @__PURE__ */ new Map();
369
- // ── Resource list cache ─────────────────────────────────────────────────
370
- resourceList$ = new BehaviorSubject(/* @__PURE__ */ new Map());
371
- fetchingResourceList = /* @__PURE__ */ new Set();
372
- resourceListObs$ = /* @__PURE__ */ new Map();
373
- // ── Entity types cache ──────────────────────────────────────────────────
374
- entityTypes$ = new BehaviorSubject(void 0);
375
- fetchingEntityTypes = false;
376
- // ── Referenced-by cache ─────────────────────────────────────────────────
377
- referencedBy$ = new BehaviorSubject(/* @__PURE__ */ new Map());
378
- fetchingReferencedBy = /* @__PURE__ */ new Set();
379
- referencedByObs$ = /* @__PURE__ */ new Map();
445
+ // ── Caches, backed by the RxJS-native `Cache<K, V>` primitive ───────────
446
+ //
447
+ // Each cache encapsulates the BehaviorSubject store, in-flight guard,
448
+ // and per-key observable memoization that was previously open-coded
449
+ // here. Behavioral contract: `packages/api-client/docs/CACHE-SEMANTICS.md`.
450
+ //
451
+ // Public surface (`resource()`, `annotations()`, etc.) is unchanged;
452
+ // the caches are an implementation detail of this namespace.
453
+ resourceCache;
454
+ resourceListCache;
455
+ annotationListCache;
456
+ /**
457
+ * Annotation-detail cache keyed by `annotationId` only — the resourceId
458
+ * is a routing hint for the backend fetch, not an identity component.
459
+ * We track the most recent resourceId per annotationId in a side-map
460
+ * so `mark:delete-ok` (which carries only `annotationId`) can reach
461
+ * the right cache entry. Aligns with the pre-refactor semantics.
462
+ */
463
+ annotationDetailCache;
464
+ annotationResources = /* @__PURE__ */ new Map();
465
+ entityTypesCache;
466
+ referencedByCache;
467
+ resourceEventsCache;
468
+ /** Filter-blob memory so `invalidateResourceLists` can replay per-key. */
469
+ resourceListFilters = /* @__PURE__ */ new Map();
470
+ /**
471
+ * Per-key memo for `annotations()` observables. The cache stores the
472
+ * full `AnnotationsListResponse`; the public shape is just the inner
473
+ * `Annotation[]`. Without this memo, every call to `annotations(rId)`
474
+ * would produce a fresh `.pipe(map(...))` observable, violating B4
475
+ * (per-key observable stability). Consumers that compare observable
476
+ * identity — React hooks depending on the observable reference,
477
+ * `distinctUntilChanged` at a higher level — would misbehave.
478
+ */
479
+ annotationListObs = /* @__PURE__ */ new Map();
380
480
  getToken;
481
+ actor;
381
482
  // ── Live queries ────────────────────────────────────────────────────────
382
483
  resource(resourceId) {
383
- if (!this.resourceDetail$.value.has(resourceId) && !this.fetchingResourceDetail.has(resourceId)) {
384
- this.fetchResourceDetail(resourceId);
385
- }
386
- let obs = this.resourceDetailObs$.get(resourceId);
387
- if (!obs) {
388
- obs = this.resourceDetail$.pipe(map((m) => m.get(resourceId)), distinctUntilChanged());
389
- this.resourceDetailObs$.set(resourceId, obs);
390
- }
391
- return obs;
484
+ return this.resourceCache.observe(resourceId);
392
485
  }
393
486
  resources(filters) {
394
487
  const key = JSON.stringify(filters ?? {});
395
- if (!this.resourceList$.value.has(key) && !this.fetchingResourceList.has(key)) {
396
- this.fetchResourceList(key, filters);
397
- }
398
- let obs = this.resourceListObs$.get(key);
399
- if (!obs) {
400
- obs = this.resourceList$.pipe(map((m) => m.get(key)), distinctUntilChanged());
401
- this.resourceListObs$.set(key, obs);
402
- }
403
- return obs;
488
+ this.resourceListFilters.set(key, filters ?? {});
489
+ return this.resourceListCache.observe(key);
404
490
  }
405
491
  annotations(resourceId) {
406
- if (!this.annotationList$.value.has(resourceId) && !this.fetchingAnnotationList.has(resourceId)) {
407
- this.fetchAnnotationList(resourceId);
408
- }
409
- let obs = this.annotationListObs$.get(resourceId);
492
+ let obs = this.annotationListObs.get(resourceId);
410
493
  if (!obs) {
411
- obs = this.annotationList$.pipe(
412
- map((m) => m.get(resourceId)?.annotations),
413
- distinctUntilChanged()
414
- );
415
- this.annotationListObs$.set(resourceId, obs);
494
+ obs = this.annotationListCache.observe(resourceId).pipe(map$1((r) => r?.annotations));
495
+ this.annotationListObs.set(resourceId, obs);
416
496
  }
417
497
  return obs;
418
498
  }
419
499
  annotation(resourceId, annotationId) {
420
- if (!this.annotationDetail$.value.has(annotationId) && !this.fetchingAnnotationDetail.has(annotationId)) {
421
- this.fetchAnnotationDetail(resourceId, annotationId);
422
- }
423
- let obs = this.annotationDetailObs$.get(annotationId);
424
- if (!obs) {
425
- obs = this.annotationDetail$.pipe(map((m) => m.get(annotationId)), distinctUntilChanged());
426
- this.annotationDetailObs$.set(annotationId, obs);
427
- }
428
- return obs;
500
+ this.annotationResources.set(annotationId, resourceId);
501
+ return this.annotationDetailCache.observe(annotationId);
429
502
  }
430
503
  entityTypes() {
431
- if (this.entityTypes$.value === void 0 && !this.fetchingEntityTypes) {
432
- this.fetchEntityTypes();
504
+ const serial = this.__serial__;
505
+ console.debug(`[diag] BrowseNamespace#${serial} entityTypes() called`);
506
+ const self = this;
507
+ if (!self.__entityTypesDiag__) {
508
+ self.__entityTypesDiag__ = this.entityTypesCache.observe(ENTITY_TYPES_KEY).pipe(map$1((v) => {
509
+ console.debug(`[diag] BrowseNamespace#${serial} entityTypes$ EMIT`, v === void 0 ? "undefined" : JSON.stringify(v).slice(0, 200));
510
+ return v;
511
+ }));
433
512
  }
434
- return this.entityTypes$.asObservable();
513
+ return self.__entityTypesDiag__;
435
514
  }
436
515
  referencedBy(resourceId) {
437
- if (!this.referencedBy$.value.has(resourceId) && !this.fetchingReferencedBy.has(resourceId)) {
438
- this.fetchReferencedBy(resourceId);
439
- }
440
- let obs = this.referencedByObs$.get(resourceId);
441
- if (!obs) {
442
- obs = this.referencedBy$.pipe(map((m) => m.get(resourceId)), distinctUntilChanged());
443
- this.referencedByObs$.set(resourceId, obs);
444
- }
445
- return obs;
516
+ return this.referencedByCache.observe(resourceId);
517
+ }
518
+ events(resourceId) {
519
+ return this.resourceEventsCache.observe(resourceId);
446
520
  }
447
521
  // ── One-shot reads ──────────────────────────────────────────────────────
448
522
  async resourceContent(resourceId) {
@@ -466,11 +540,23 @@ var BrowseNamespace = class {
466
540
  });
467
541
  }
468
542
  async resourceEvents(resourceId) {
469
- const result = await this.http.getResourceEvents(resourceId, { auth: this.getToken() });
543
+ const result = await busRequest(
544
+ this.actor,
545
+ "browse:events-requested",
546
+ { resourceId },
547
+ "browse:events-result",
548
+ "browse:events-failed"
549
+ );
470
550
  return result.events;
471
551
  }
472
552
  async annotationHistory(resourceId, annotationId) {
473
- return this.http.getAnnotationHistory(resourceId, annotationId, { auth: this.getToken() });
553
+ return busRequest(
554
+ this.actor,
555
+ "browse:annotation-history-requested",
556
+ { resourceId, annotationId },
557
+ "browse:annotation-history-result",
558
+ "browse:annotation-history-failed"
559
+ );
474
560
  }
475
561
  async connections(_resourceId) {
476
562
  throw new Error("Not implemented: connections endpoint does not exist yet");
@@ -482,302 +568,326 @@ var BrowseNamespace = class {
482
568
  throw new Error("Not implemented: resourcesByName endpoint does not exist yet");
483
569
  }
484
570
  async files(dirPath, sort) {
485
- return this.http.browseFiles(dirPath, sort, { auth: this.getToken() });
571
+ return busRequest(
572
+ this.actor,
573
+ "browse:directory-requested",
574
+ { path: dirPath ?? ".", sort: sort ?? "name" },
575
+ "browse:directory-result",
576
+ "browse:directory-failed"
577
+ );
486
578
  }
487
- // ── Invalidation (exposed for other namespaces) ─────────────────────────
579
+ // ── Cache-mutation API (used by the bus-event subscribers below and by
580
+ // other namespaces that know about specific updates) ─────────────────
581
+ //
582
+ // - `invalidate*` — SWR refetch (B7). Keeps prior value visible.
583
+ // - `removeAnnotationDetail` — drops the entry (B13a: entity gone).
584
+ // - `updateAnnotationInPlace` — write-through (B13b: new value known).
488
585
  invalidateAnnotationList(resourceId) {
489
- const next = new Map(this.annotationList$.value);
490
- next.delete(resourceId);
491
- this.annotationList$.next(next);
492
- this.fetchAnnotationList(resourceId);
586
+ this.annotationListCache.invalidate(resourceId);
493
587
  }
494
- invalidateAnnotationDetail(annotationId) {
495
- const next = new Map(this.annotationDetail$.value);
496
- next.delete(annotationId);
497
- this.annotationDetail$.next(next);
588
+ removeAnnotationDetail(annotationId) {
589
+ this.annotationDetailCache.remove(annotationId);
590
+ this.annotationResources.delete(annotationId);
498
591
  }
499
592
  invalidateResourceDetail(id) {
500
- const next = new Map(this.resourceDetail$.value);
501
- next.delete(id);
502
- this.resourceDetail$.next(next);
503
- this.fetchResourceDetail(id);
593
+ this.resourceCache.invalidate(id);
504
594
  }
505
595
  invalidateResourceLists() {
506
- this.resourceList$.next(/* @__PURE__ */ new Map());
596
+ this.resourceListCache.invalidateAll();
507
597
  }
508
598
  invalidateEntityTypes() {
509
- this.entityTypes$.next(void 0);
510
- this.fetchEntityTypes();
599
+ this.entityTypesCache.invalidate(ENTITY_TYPES_KEY);
511
600
  }
512
601
  invalidateReferencedBy(resourceId) {
513
- const next = new Map(this.referencedBy$.value);
514
- next.delete(resourceId);
515
- this.referencedBy$.next(next);
516
- this.fetchReferencedBy(resourceId);
602
+ this.referencedByCache.invalidate(resourceId);
603
+ }
604
+ invalidateResourceEvents(resourceId) {
605
+ this.resourceEventsCache.invalidate(resourceId);
517
606
  }
518
607
  updateAnnotationInPlace(resourceId, annotation) {
519
- const currentList = this.annotationList$.value.get(resourceId);
520
- if (!currentList) return;
521
- const existingIdx = currentList.annotations.findIndex((a) => a.id === annotation.id);
522
- const nextAnnotations = existingIdx >= 0 ? currentList.annotations.map((a, i) => i === existingIdx ? annotation : a) : [...currentList.annotations, annotation];
523
- const nextList = { ...currentList, annotations: nextAnnotations };
524
- const nextMap = new Map(this.annotationList$.value);
525
- nextMap.set(resourceId, nextList);
526
- this.annotationList$.next(nextMap);
608
+ const currentList = this.annotationListCache.get(resourceId);
609
+ if (currentList) {
610
+ const idx = currentList.annotations.findIndex((a) => a.id === annotation.id);
611
+ const nextAnnotations = idx >= 0 ? currentList.annotations.map((a, i) => i === idx ? annotation : a) : [...currentList.annotations, annotation];
612
+ this.annotationListCache.set(resourceId, { ...currentList, annotations: nextAnnotations });
613
+ }
614
+ const aId = annotationId(annotation.id);
615
+ this.annotationResources.set(aId, resourceId);
616
+ this.annotationDetailCache.set(aId, annotation);
527
617
  }
528
618
  // ── EventBus subscriptions ──────────────────────────────────────────────
619
+ /**
620
+ * Typed shorthand for `eventBus.get(channel).subscribe(handler)`.
621
+ * Preserves per-channel payload typing so handlers read
622
+ * `EventMap[K]` without any casts.
623
+ */
624
+ on(channel, handler) {
625
+ this.eventBus.get(channel).subscribe(handler);
626
+ }
627
+ /**
628
+ * Handler shared by `mark:entity-tag-added` and `mark:entity-tag-removed`.
629
+ * Both events carry the same effect: the annotation list, the
630
+ * resource descriptor, and the event log for that resource all may
631
+ * now reflect different entity tagging, so invalidate all three.
632
+ */
633
+ onEntityTagChanged = (stored) => {
634
+ if (!stored.resourceId) return;
635
+ this.invalidateAnnotationList(stored.resourceId);
636
+ this.invalidateResourceDetail(stored.resourceId);
637
+ this.invalidateResourceEvents(stored.resourceId);
638
+ };
639
+ /**
640
+ * Handler shared by `mark:archived` and `mark:unarchived`. Both
641
+ * change a resource's archived flag, which is stored on the resource
642
+ * descriptor and affects the resource-list filter.
643
+ */
644
+ onArchiveToggled = (stored) => {
645
+ if (!stored.resourceId) return;
646
+ this.invalidateResourceDetail(stored.resourceId);
647
+ this.invalidateResourceLists();
648
+ };
649
+ /**
650
+ * Handler shared by `yield:create-ok` and `yield:update-ok`. Both
651
+ * report a resource mutation with the resourceId as a string (not
652
+ * yet branded), so we brand and apply the same effect as
653
+ * `onArchiveToggled`.
654
+ */
655
+ onYieldResourceMutated = (event) => {
656
+ const rId = resourceId(event.resourceId);
657
+ this.invalidateResourceDetail(rId);
658
+ this.invalidateResourceLists();
659
+ };
529
660
  subscribeToEvents() {
530
- const bus = this.eventBus;
531
- bus.get("mark:delete-ok").subscribe((event) => {
532
- this.invalidateAnnotationDetail(annotationId(event.annotationId));
661
+ this.on("bus:resume-gap", (event) => {
662
+ const gapScope = event.scope;
663
+ if (gapScope) {
664
+ const rId = gapScope;
665
+ this.invalidateAnnotationList(rId);
666
+ this.invalidateResourceDetail(rId);
667
+ this.invalidateResourceEvents(rId);
668
+ this.invalidateReferencedBy(rId);
669
+ } else {
670
+ this.invalidateResourceLists();
671
+ for (const rId of this.annotationListCache.keys()) this.invalidateAnnotationList(rId);
672
+ for (const rId of this.resourceCache.keys()) this.invalidateResourceDetail(rId);
673
+ for (const rId of this.resourceEventsCache.keys()) this.invalidateResourceEvents(rId);
674
+ for (const rId of this.referencedByCache.keys()) this.invalidateReferencedBy(rId);
675
+ }
676
+ this.invalidateEntityTypes();
533
677
  });
534
- bus.get("mark:added").subscribe((stored) => {
678
+ this.on("mark:delete-ok", (event) => {
679
+ this.removeAnnotationDetail(annotationId(event.annotationId));
680
+ });
681
+ this.on("mark:added", (stored) => {
535
682
  if (stored.resourceId) {
536
683
  this.invalidateAnnotationList(stored.resourceId);
684
+ this.invalidateResourceEvents(stored.resourceId);
537
685
  }
538
686
  });
539
- bus.get("mark:removed").subscribe((stored) => {
687
+ this.on("mark:removed", (stored) => {
540
688
  if (stored.resourceId) {
541
689
  this.invalidateAnnotationList(stored.resourceId);
690
+ this.invalidateResourceEvents(stored.resourceId);
542
691
  }
543
- this.invalidateAnnotationDetail(annotationId(stored.payload.annotationId));
692
+ this.removeAnnotationDetail(annotationId(stored.payload.annotationId));
544
693
  });
545
- bus.get("mark:body-updated").subscribe((event) => {
694
+ this.on("mark:body-updated", (event) => {
546
695
  const enriched = event;
547
696
  if (!enriched.resourceId || !enriched.annotation) return;
548
697
  this.updateAnnotationInPlace(enriched.resourceId, enriched.annotation);
549
- this.invalidateAnnotationDetail(annotationId(enriched.annotation.id));
550
- });
551
- bus.get("mark:entity-tag-added").subscribe((stored) => {
552
- if (stored.resourceId) {
553
- this.invalidateAnnotationList(stored.resourceId);
554
- this.invalidateResourceDetail(stored.resourceId);
555
- }
556
- });
557
- bus.get("mark:entity-tag-removed").subscribe((stored) => {
558
- if (stored.resourceId) {
559
- this.invalidateAnnotationList(stored.resourceId);
560
- this.invalidateResourceDetail(stored.resourceId);
561
- }
698
+ this.invalidateResourceEvents(enriched.resourceId);
562
699
  });
563
- bus.get("replay-window-exceeded").subscribe((event) => {
700
+ this.on("mark:entity-tag-added", this.onEntityTagChanged);
701
+ this.on("mark:entity-tag-removed", this.onEntityTagChanged);
702
+ this.on("replay-window-exceeded", (event) => {
564
703
  if (event.resourceId) {
565
704
  this.invalidateAnnotationList(event.resourceId);
566
705
  }
567
706
  });
568
- bus.get("yield:create-ok").subscribe((event) => {
569
- this.fetchResourceDetail(resourceId(event.resourceId));
570
- this.invalidateResourceLists();
571
- });
572
- bus.get("yield:update-ok").subscribe((event) => {
573
- this.invalidateResourceDetail(resourceId(event.resourceId));
574
- this.invalidateResourceLists();
575
- });
576
- bus.get("mark:archived").subscribe((stored) => {
577
- if (stored.resourceId) {
578
- this.invalidateResourceDetail(stored.resourceId);
579
- this.invalidateResourceLists();
580
- }
581
- });
582
- bus.get("mark:unarchived").subscribe((stored) => {
583
- if (stored.resourceId) {
584
- this.invalidateResourceDetail(stored.resourceId);
585
- this.invalidateResourceLists();
586
- }
587
- });
588
- bus.get("mark:entity-type-added").subscribe(() => {
589
- this.invalidateEntityTypes();
590
- });
591
- }
592
- // ── Fetch helpers ───────────────────────────────────────────────────────
593
- async fetchAnnotationList(resourceId) {
594
- if (this.fetchingAnnotationList.has(resourceId)) return;
595
- this.fetchingAnnotationList.add(resourceId);
596
- try {
597
- const result = await this.http.browseAnnotations(resourceId, void 0, { auth: this.getToken() });
598
- const next = new Map(this.annotationList$.value);
599
- next.set(resourceId, result);
600
- this.annotationList$.next(next);
601
- } catch {
602
- } finally {
603
- this.fetchingAnnotationList.delete(resourceId);
604
- }
605
- }
606
- async fetchAnnotationDetail(resourceId, annotationId) {
607
- if (this.fetchingAnnotationDetail.has(annotationId)) return;
608
- this.fetchingAnnotationDetail.add(annotationId);
609
- try {
610
- const result = await this.http.browseAnnotation(resourceId, annotationId, { auth: this.getToken() });
611
- const next = new Map(this.annotationDetail$.value);
612
- next.set(annotationId, result.annotation);
613
- this.annotationDetail$.next(next);
614
- } catch {
615
- } finally {
616
- this.fetchingAnnotationDetail.delete(annotationId);
617
- }
618
- }
619
- async fetchResourceDetail(id) {
620
- if (this.fetchingResourceDetail.has(id)) return;
621
- this.fetchingResourceDetail.add(id);
622
- try {
623
- const result = await this.http.browseResource(id, { auth: this.getToken() });
624
- const next = new Map(this.resourceDetail$.value);
625
- next.set(id, result.resource);
626
- this.resourceDetail$.next(next);
627
- } catch {
628
- } finally {
629
- this.fetchingResourceDetail.delete(id);
630
- }
631
- }
632
- async fetchResourceList(key, filters) {
633
- if (this.fetchingResourceList.has(key)) return;
634
- this.fetchingResourceList.add(key);
635
- try {
636
- const search = filters?.search ? searchQuery(filters.search) : void 0;
637
- const result = await this.http.browseResources(filters?.limit, filters?.archived, search, { auth: this.getToken() });
638
- const next = new Map(this.resourceList$.value);
639
- next.set(key, result.resources);
640
- this.resourceList$.next(next);
641
- } catch {
642
- } finally {
643
- this.fetchingResourceList.delete(key);
644
- }
645
- }
646
- async fetchEntityTypes() {
647
- if (this.fetchingEntityTypes) return;
648
- this.fetchingEntityTypes = true;
649
- try {
650
- const result = await this.http.listEntityTypes({ auth: this.getToken() });
651
- this.entityTypes$.next(result.entityTypes);
652
- } catch {
653
- } finally {
654
- this.fetchingEntityTypes = false;
655
- }
656
- }
657
- async fetchReferencedBy(resourceId) {
658
- if (this.fetchingReferencedBy.has(resourceId)) return;
659
- this.fetchingReferencedBy.add(resourceId);
660
- try {
661
- const result = await this.http.browseReferences(resourceId, { auth: this.getToken() });
662
- const next = new Map(this.referencedBy$.value);
663
- next.set(resourceId, result.referencedBy);
664
- this.referencedBy$.next(next);
665
- } catch {
666
- } finally {
667
- this.fetchingReferencedBy.delete(resourceId);
668
- }
707
+ this.on("yield:create-ok", this.onYieldResourceMutated);
708
+ this.on("yield:update-ok", this.onYieldResourceMutated);
709
+ this.on("mark:archived", this.onArchiveToggled);
710
+ this.on("mark:unarchived", this.onArchiveToggled);
711
+ this.on("mark:entity-type-added", () => this.invalidateEntityTypes());
669
712
  }
670
713
  };
671
714
  var MarkNamespace = class {
672
- constructor(http, eventBus, getToken) {
715
+ constructor(http, eventBus, getToken, actor) {
673
716
  this.http = http;
674
717
  this.eventBus = eventBus;
675
718
  this.getToken = getToken;
719
+ this.actor = actor;
676
720
  }
677
721
  async annotation(resourceId, input) {
678
- return this.http.markAnnotation(resourceId, input, { auth: this.getToken() });
722
+ return busRequest(
723
+ this.actor,
724
+ "mark:create-request",
725
+ { resourceId, request: input },
726
+ "mark:create-ok",
727
+ "mark:create-failed"
728
+ );
679
729
  }
680
730
  async delete(resourceId, annotationId) {
681
- return this.http.deleteAnnotation(resourceId, annotationId, { auth: this.getToken() });
731
+ await this.actor.emit("mark:delete", { annotationId, resourceId });
682
732
  }
683
733
  async entityType(type) {
684
- return this.http.addEntityType(type, { auth: this.getToken() });
734
+ await this.actor.emit("mark:add-entity-type", { tag: type });
685
735
  }
686
736
  async entityTypes(types) {
687
- return this.http.addEntityTypesBulk(types, { auth: this.getToken() });
737
+ for (const tag of types) {
738
+ await this.actor.emit("mark:add-entity-type", { tag });
739
+ }
688
740
  }
689
741
  async updateResource(resourceId, data) {
690
742
  return this.http.updateResource(resourceId, data, { auth: this.getToken() });
691
743
  }
692
744
  async archive(resourceId) {
693
- return this.http.updateResource(resourceId, { archived: true }, { auth: this.getToken() });
745
+ await this.actor.emit("mark:archive", { resourceId });
694
746
  }
695
747
  async unarchive(resourceId) {
696
- return this.http.updateResource(resourceId, { archived: false }, { auth: this.getToken() });
748
+ await this.actor.emit("mark:unarchive", { resourceId });
697
749
  }
698
750
  assist(resourceId, motivation, options) {
699
751
  return new Observable((subscriber) => {
700
- const progress$ = this.eventBus.get("mark:progress").pipe(
701
- filter((e) => e.resourceId === resourceId)
752
+ let done = false;
753
+ let pollTimer = null;
754
+ let pollInterval = null;
755
+ const cleanup = () => {
756
+ done = true;
757
+ if (pollTimer) {
758
+ clearTimeout(pollTimer);
759
+ pollTimer = null;
760
+ }
761
+ if (pollInterval) {
762
+ clearInterval(pollInterval);
763
+ pollInterval = null;
764
+ }
765
+ };
766
+ const resetPollTimer = (jobId) => {
767
+ if (pollTimer) clearTimeout(pollTimer);
768
+ if (pollInterval) {
769
+ clearInterval(pollInterval);
770
+ pollInterval = null;
771
+ }
772
+ pollTimer = setTimeout(() => {
773
+ if (done) return;
774
+ pollInterval = setInterval(() => {
775
+ if (done) return;
776
+ busRequest(
777
+ this.actor,
778
+ "job:status-requested",
779
+ { jobId },
780
+ "job:status-result",
781
+ "job:status-failed"
782
+ ).then((status) => {
783
+ if (done) return;
784
+ if (status.status === "complete") {
785
+ cleanup();
786
+ subscriber.next({ motivation, resourceId, progress: status.result });
787
+ subscriber.complete();
788
+ } else if (status.status === "failed") {
789
+ cleanup();
790
+ subscriber.error(new Error(status.error ?? "Job failed"));
791
+ }
792
+ }).catch(() => {
793
+ });
794
+ }, 5e3);
795
+ }, 1e4);
796
+ };
797
+ let activeJobId = null;
798
+ const progress$ = this.eventBus.get("job:report-progress").pipe(
799
+ filter((e) => e.jobId === activeJobId)
702
800
  );
703
- const finished$ = this.eventBus.get("mark:assist-finished").pipe(
704
- filter((e) => e.resourceId === resourceId && e.motivation === motivation)
801
+ const complete$ = this.eventBus.get("job:complete").pipe(
802
+ filter((e) => e.jobId === activeJobId)
705
803
  );
706
- const failed$ = this.eventBus.get("mark:assist-failed").pipe(
707
- filter((e) => e.resourceId === resourceId)
804
+ const fail$ = this.eventBus.get("job:fail").pipe(
805
+ filter((e) => e.jobId === activeJobId)
708
806
  );
709
- const progressSub = progress$.pipe(takeUntil(merge(finished$, failed$))).subscribe((e) => subscriber.next(e));
710
- const finishedSub = finished$.subscribe((e) => {
711
- subscriber.next(e);
807
+ const progressSub = progress$.pipe(takeUntil(merge(complete$, fail$))).subscribe((e) => {
808
+ subscriber.next(e.progress);
809
+ if (activeJobId) resetPollTimer(activeJobId);
810
+ });
811
+ const completeSub = complete$.subscribe(() => {
812
+ cleanup();
712
813
  subscriber.complete();
713
814
  });
714
- const failedSub = failed$.subscribe((e) => {
715
- subscriber.error(new Error(e.message));
815
+ const failSub = fail$.subscribe((e) => {
816
+ cleanup();
817
+ subscriber.error(new Error(e.error));
716
818
  });
717
819
  const auth = this.getToken();
718
- const postPromise = this.dispatchAssist(resourceId, motivation, options, auth);
719
- postPromise.catch((error) => {
820
+ this.dispatchAssist(resourceId, motivation, options, auth).then(({ jobId }) => {
821
+ if (jobId && !done) {
822
+ activeJobId = jobId;
823
+ resetPollTimer(jobId);
824
+ }
825
+ }).catch((error) => {
826
+ cleanup();
720
827
  subscriber.error(error);
721
828
  });
722
829
  return () => {
830
+ cleanup();
723
831
  progressSub.unsubscribe();
724
- finishedSub.unsubscribe();
725
- failedSub.unsubscribe();
832
+ completeSub.unsubscribe();
833
+ failSub.unsubscribe();
726
834
  };
727
835
  });
728
836
  }
729
- async dispatchAssist(resourceId, motivation, options, auth) {
837
+ async dispatchAssist(resourceId, motivation, options, _auth) {
838
+ const jobTypeMap = {
839
+ tagging: "tag-annotation",
840
+ linking: "reference-annotation",
841
+ highlighting: "highlight-annotation",
842
+ assessing: "assessment-annotation",
843
+ commenting: "comment-annotation"
844
+ };
845
+ const jobType = jobTypeMap[motivation];
846
+ if (!jobType) throw new Error(`Unsupported motivation: ${motivation}`);
730
847
  if (motivation === "tagging") {
731
- const { schemaId, categories } = options;
732
- if (!schemaId || !categories?.length) throw new Error("Tag assist requires schemaId and categories");
733
- await this.http.annotateTags(resourceId, { schemaId, categories }, { auth });
848
+ if (!options.schemaId || !options.categories?.length) throw new Error("Tag assist requires schemaId and categories");
734
849
  } else if (motivation === "linking") {
735
- const { entityTypes, includeDescriptiveReferences } = options;
736
- if (!entityTypes?.length) throw new Error("Reference assist requires entityTypes");
737
- await this.http.annotateReferences(resourceId, {
738
- entityTypes,
739
- includeDescriptiveReferences: includeDescriptiveReferences ?? false
740
- }, { auth });
741
- } else if (motivation === "highlighting") {
742
- await this.http.annotateHighlights(resourceId, {
743
- instructions: options.instructions,
744
- density: options.density
745
- }, { auth });
746
- } else if (motivation === "assessing") {
747
- await this.http.annotateAssessments(resourceId, {
748
- instructions: options.instructions,
749
- tone: options.tone,
750
- density: options.density,
751
- language: options.language
752
- }, { auth });
753
- } else if (motivation === "commenting") {
754
- await this.http.annotateComments(resourceId, {
755
- instructions: options.instructions,
756
- tone: options.tone,
757
- density: options.density,
758
- language: options.language
759
- }, { auth });
850
+ if (!options.entityTypes?.length) throw new Error("Reference assist requires entityTypes");
760
851
  }
852
+ const params = {};
853
+ if (options.entityTypes) params.entityTypes = options.entityTypes;
854
+ if (options.includeDescriptiveReferences !== void 0) params.includeDescriptiveReferences = options.includeDescriptiveReferences;
855
+ if (options.instructions !== void 0) params.instructions = options.instructions;
856
+ if (options.density !== void 0) params.density = options.density;
857
+ if (options.tone !== void 0) params.tone = options.tone;
858
+ if (options.language !== void 0) params.language = options.language;
859
+ if (options.schemaId !== void 0) params.schemaId = options.schemaId;
860
+ if (options.categories !== void 0) params.categories = options.categories;
861
+ return busRequest(
862
+ this.actor,
863
+ "job:create",
864
+ { jobType, resourceId, params },
865
+ "job:created",
866
+ "job:create-failed"
867
+ );
761
868
  }
762
869
  };
763
870
 
764
871
  // src/namespaces/bind.ts
765
872
  var BindNamespace = class {
766
- constructor(http, getToken) {
767
- this.http = http;
768
- this.getToken = getToken;
873
+ constructor(actor) {
874
+ this.actor = actor;
769
875
  }
770
876
  async body(resourceId, annotationId, operations) {
771
- await this.http.bindAnnotation(resourceId, annotationId, { operations }, { auth: this.getToken() });
877
+ await this.actor.emit("bind:update-body", {
878
+ correlationId: crypto.randomUUID(),
879
+ annotationId,
880
+ resourceId,
881
+ operations
882
+ });
772
883
  }
773
884
  };
774
885
  var GatherNamespace = class {
775
- constructor(http, eventBus, getToken) {
776
- this.http = http;
886
+ constructor(eventBus, actor) {
777
887
  this.eventBus = eventBus;
778
- this.getToken = getToken;
888
+ this.actor = actor;
779
889
  }
780
- annotation(annotationId$1, resourceId, options) {
890
+ annotation(annotationId, resourceId, options) {
781
891
  return new Observable((subscriber) => {
782
892
  const correlationId = crypto.randomUUID();
783
893
  const complete$ = this.eventBus.get("gather:complete").pipe(
@@ -788,8 +898,7 @@ var GatherNamespace = class {
788
898
  );
789
899
  const sub = merge(
790
900
  this.eventBus.get("gather:annotation-progress").pipe(
791
- // Progress events don't carry correlationId, so match by annotationId
792
- filter((e) => e.annotationId === annotationId$1),
901
+ filter((e) => e.annotationId === annotationId),
793
902
  map((e) => e)
794
903
  ),
795
904
  complete$.pipe(map((e) => e))
@@ -804,12 +913,12 @@ var GatherNamespace = class {
804
913
  const failedSub = failed$.subscribe((e) => {
805
914
  subscriber.error(new Error(e.message));
806
915
  });
807
- this.http.gatherAnnotationContext(
916
+ this.actor.emit("gather:requested", {
917
+ correlationId,
918
+ annotationId,
808
919
  resourceId,
809
- annotationId(annotationId$1),
810
- { correlationId, contextWindow: options?.contextWindow ?? 2e3 },
811
- { auth: this.getToken() }
812
- ).catch((error) => {
920
+ contextWindow: options?.contextWindow ?? 2e3
921
+ }).catch((error) => {
813
922
  subscriber.error(error);
814
923
  });
815
924
  return () => {
@@ -824,10 +933,9 @@ var GatherNamespace = class {
824
933
  }
825
934
  };
826
935
  var MatchNamespace = class {
827
- constructor(http, eventBus, getToken) {
828
- this.http = http;
936
+ constructor(eventBus, actor) {
829
937
  this.eventBus = eventBus;
830
- this.getToken = getToken;
938
+ this.actor = actor;
831
939
  }
832
940
  search(resourceId, referenceId, context, options) {
833
941
  return new Observable((subscriber) => {
@@ -845,17 +953,14 @@ var MatchNamespace = class {
845
953
  const failedSub = failed$.subscribe((e) => {
846
954
  subscriber.error(new Error(e.error));
847
955
  });
848
- this.http.matchSearch(
956
+ this.actor.emit("match:search-requested", {
957
+ correlationId,
849
958
  resourceId,
850
- {
851
- correlationId,
852
- referenceId,
853
- context,
854
- limit: options?.limit,
855
- useSemanticScoring: options?.useSemanticScoring
856
- },
857
- { auth: this.getToken() }
858
- ).catch((error) => {
959
+ referenceId,
960
+ context,
961
+ limit: options?.limit ?? 10,
962
+ useSemanticScoring: options?.useSemanticScoring ?? true
963
+ }).catch((error) => {
859
964
  subscriber.error(error);
860
965
  });
861
966
  return () => {
@@ -866,105 +971,193 @@ var MatchNamespace = class {
866
971
  }
867
972
  };
868
973
  var YieldNamespace = class {
869
- constructor(http, eventBus, getToken) {
974
+ constructor(http, eventBus, getToken, actor) {
870
975
  this.http = http;
871
976
  this.eventBus = eventBus;
872
977
  this.getToken = getToken;
978
+ this.actor = actor;
873
979
  }
874
980
  async resource(data) {
875
981
  return this.http.yieldResource(data, { auth: this.getToken() });
876
982
  }
877
- fromAnnotation(resourceId$1, annotationId$1, options) {
983
+ fromAnnotation(resourceId, annotationId, options) {
878
984
  return new Observable((subscriber) => {
879
- const progress$ = this.eventBus.get("yield:progress").pipe(
880
- filter((e) => e.referenceId === annotationId$1)
985
+ let done = false;
986
+ let pollTimer = null;
987
+ let pollInterval = null;
988
+ const cleanup = () => {
989
+ done = true;
990
+ if (pollTimer) {
991
+ clearTimeout(pollTimer);
992
+ pollTimer = null;
993
+ }
994
+ if (pollInterval) {
995
+ clearInterval(pollInterval);
996
+ pollInterval = null;
997
+ }
998
+ };
999
+ const resetPollTimer = (jid) => {
1000
+ if (pollTimer) clearTimeout(pollTimer);
1001
+ if (pollInterval) {
1002
+ clearInterval(pollInterval);
1003
+ pollInterval = null;
1004
+ }
1005
+ pollTimer = setTimeout(() => {
1006
+ if (done) return;
1007
+ pollInterval = setInterval(() => {
1008
+ if (done) return;
1009
+ busRequest(
1010
+ this.actor,
1011
+ "job:status-requested",
1012
+ { jobId: jid },
1013
+ "job:status-result",
1014
+ "job:status-failed"
1015
+ ).then((status) => {
1016
+ if (done) return;
1017
+ if (status.status === "complete") {
1018
+ cleanup();
1019
+ subscriber.next({ stage: "complete", percentage: 100, message: "Generation complete" });
1020
+ subscriber.complete();
1021
+ } else if (status.status === "failed") {
1022
+ cleanup();
1023
+ subscriber.error(new Error(status.error ?? "Generation failed"));
1024
+ }
1025
+ }).catch(() => {
1026
+ });
1027
+ }, 5e3);
1028
+ }, 1e4);
1029
+ };
1030
+ let activeJobId = null;
1031
+ const progress$ = this.eventBus.get("job:report-progress").pipe(
1032
+ filter((e) => e.jobId === activeJobId)
881
1033
  );
882
- const finished$ = this.eventBus.get("yield:finished").pipe(
883
- filter((e) => e.referenceId === annotationId$1)
1034
+ const complete$ = this.eventBus.get("job:complete").pipe(
1035
+ filter((e) => e.jobId === activeJobId)
884
1036
  );
885
- const failed$ = this.eventBus.get("yield:failed").pipe(
886
- filter((e) => e.referenceId === annotationId$1)
1037
+ const fail$ = this.eventBus.get("job:fail").pipe(
1038
+ filter((e) => e.jobId === activeJobId)
887
1039
  );
888
- const progressSub = progress$.pipe(takeUntil(merge(finished$, failed$))).subscribe((e) => subscriber.next(e));
889
- const finishedSub = finished$.subscribe((event) => {
890
- subscriber.next(event);
1040
+ const progressSub = progress$.pipe(takeUntil(merge(complete$, fail$))).subscribe((e) => {
1041
+ subscriber.next(e.progress);
1042
+ if (activeJobId) resetPollTimer(activeJobId);
1043
+ });
1044
+ const completeSub = complete$.subscribe(() => {
1045
+ cleanup();
891
1046
  subscriber.complete();
892
- if (event.resourceId && event.referenceId && event.sourceResourceId) {
893
- this.eventBus.get("bind:update-body").next({
894
- correlationId: crypto.randomUUID(),
895
- annotationId: annotationId(event.referenceId),
896
- resourceId: resourceId(event.sourceResourceId),
897
- operations: [{ op: "add", item: { type: "SpecificResource", source: event.resourceId } }]
898
- });
899
- }
900
1047
  });
901
- const failedSub = failed$.subscribe((e) => {
902
- subscriber.error(new Error(e.error ?? e.message ?? "Generation failed"));
1048
+ const failSub = fail$.subscribe((e) => {
1049
+ cleanup();
1050
+ subscriber.error(new Error(e.error));
903
1051
  });
904
- this.http.yieldResourceFromAnnotation(
905
- resourceId$1,
906
- annotationId$1,
907
- options,
908
- { auth: this.getToken() }
909
- ).catch((error) => {
1052
+ busRequest(
1053
+ this.actor,
1054
+ "job:create",
1055
+ {
1056
+ jobType: "generation",
1057
+ resourceId,
1058
+ params: {
1059
+ referenceId: annotationId,
1060
+ title: options.title,
1061
+ prompt: options.prompt,
1062
+ language: options.language,
1063
+ temperature: options.temperature,
1064
+ maxTokens: options.maxTokens,
1065
+ storageUri: options.storageUri,
1066
+ context: options.context
1067
+ }
1068
+ },
1069
+ "job:created",
1070
+ "job:create-failed"
1071
+ ).then(({ jobId }) => {
1072
+ if (jobId && !done) {
1073
+ activeJobId = jobId;
1074
+ resetPollTimer(jobId);
1075
+ }
1076
+ }).catch((error) => {
1077
+ cleanup();
910
1078
  subscriber.error(error);
911
1079
  });
912
1080
  return () => {
1081
+ cleanup();
913
1082
  progressSub.unsubscribe();
914
- finishedSub.unsubscribe();
915
- failedSub.unsubscribe();
1083
+ completeSub.unsubscribe();
1084
+ failSub.unsubscribe();
916
1085
  };
917
1086
  });
918
1087
  }
919
1088
  async cloneToken(resourceId) {
920
- const result = await this.http.generateCloneToken(resourceId, { auth: this.getToken() });
921
- return result;
1089
+ return busRequest(
1090
+ this.actor,
1091
+ "yield:clone-token-requested",
1092
+ { resourceId },
1093
+ "yield:clone-token-generated",
1094
+ "yield:clone-token-failed"
1095
+ );
922
1096
  }
923
1097
  async fromToken(token) {
924
- const result = await this.http.getResourceByToken(token, { auth: this.getToken() });
1098
+ const result = await busRequest(
1099
+ this.actor,
1100
+ "yield:clone-resource-requested",
1101
+ { token },
1102
+ "yield:clone-resource-result",
1103
+ "yield:clone-resource-failed"
1104
+ );
925
1105
  return result.sourceResource;
926
1106
  }
927
1107
  async createFromToken(options) {
928
- return this.http.createResourceFromToken(options, { auth: this.getToken() });
1108
+ return busRequest(
1109
+ this.actor,
1110
+ "yield:clone-create",
1111
+ options,
1112
+ "yield:clone-created",
1113
+ "yield:clone-create-failed"
1114
+ );
929
1115
  }
930
1116
  };
931
1117
 
932
1118
  // src/namespaces/beckon.ts
933
1119
  var BeckonNamespace = class {
934
- constructor(http, getToken) {
935
- this.http = http;
936
- this.getToken = getToken;
1120
+ constructor(actor) {
1121
+ this.actor = actor;
937
1122
  }
938
1123
  attention(annotationId, resourceId) {
939
- this.http.beckonAttention(
940
- "me",
941
- // participantId — always 'me' for self-identification
942
- { annotationId, resourceId },
943
- { auth: this.getToken() }
944
- ).catch(() => {
1124
+ this.actor.emit("beckon:focus", { annotationId, resourceId }).catch(() => {
945
1125
  });
946
1126
  }
947
1127
  };
948
1128
 
949
1129
  // src/namespaces/job.ts
950
1130
  var JobNamespace = class {
951
- constructor(http, getToken) {
952
- this.http = http;
953
- this.getToken = getToken;
1131
+ constructor(actor) {
1132
+ this.actor = actor;
954
1133
  }
955
1134
  async status(jobId) {
956
- return this.http.getJobStatus(jobId, { auth: this.getToken() });
1135
+ return busRequest(
1136
+ this.actor,
1137
+ "job:status-requested",
1138
+ { jobId },
1139
+ "job:status-result",
1140
+ "job:status-failed"
1141
+ );
957
1142
  }
958
1143
  async pollUntilComplete(jobId, options) {
959
- return this.http.pollJobUntilComplete(jobId, {
960
- interval: options?.interval,
961
- timeout: options?.timeout,
962
- onProgress: options?.onProgress,
963
- auth: this.getToken()
964
- });
1144
+ const interval = options?.interval ?? 1e3;
1145
+ const timeout7 = options?.timeout ?? 6e4;
1146
+ const startTime = Date.now();
1147
+ while (true) {
1148
+ const status = await this.status(jobId);
1149
+ if (options?.onProgress) options.onProgress(status);
1150
+ if (status.status === "complete" || status.status === "failed" || status.status === "cancelled") {
1151
+ return status;
1152
+ }
1153
+ if (Date.now() - startTime > timeout7) {
1154
+ throw new Error(`Job polling timeout after ${timeout7}ms`);
1155
+ }
1156
+ await new Promise((resolve) => setTimeout(resolve, interval));
1157
+ }
965
1158
  }
966
1159
  async cancel(jobId, type) {
967
- throw new Error(`Not implemented: job.cancel(${jobId}, ${type}) \u2014 needs EventBus wiring`);
1160
+ await this.actor.emit("job:cancel-requested", { jobId, type });
968
1161
  }
969
1162
  };
970
1163
  var AuthNamespace = class {
@@ -1037,8 +1230,66 @@ var AdminNamespace = class {
1037
1230
  return this.http.importKnowledgeBase(file, { auth: this.getToken(), onProgress });
1038
1231
  }
1039
1232
  };
1040
-
1041
- // src/client.ts
1233
+ var RESOURCE_SCOPED_CHANNELS = [
1234
+ ...PERSISTED_EVENT_TYPES.filter((t) => t !== "mark:entity-type-added"),
1235
+ ...RESOURCE_BROADCAST_TYPES
1236
+ ];
1237
+ var BUS_RESULT_CHANNELS = [
1238
+ "browse:resources-result",
1239
+ "browse:resources-failed",
1240
+ "browse:resource-result",
1241
+ "browse:resource-failed",
1242
+ "browse:annotations-result",
1243
+ "browse:annotations-failed",
1244
+ "browse:annotation-result",
1245
+ "browse:annotation-failed",
1246
+ "browse:annotation-history-result",
1247
+ "browse:annotation-history-failed",
1248
+ "browse:events-result",
1249
+ "browse:events-failed",
1250
+ "browse:referenced-by-result",
1251
+ "browse:referenced-by-failed",
1252
+ "browse:entity-types-result",
1253
+ "browse:entity-types-failed",
1254
+ "browse:directory-result",
1255
+ "browse:directory-failed",
1256
+ "browse:annotation-context-result",
1257
+ "browse:annotation-context-failed",
1258
+ "mark:delete-ok",
1259
+ "mark:delete-failed",
1260
+ "mark:create-ok",
1261
+ "mark:create-failed",
1262
+ "match:search-results",
1263
+ "match:search-failed",
1264
+ "gather:complete",
1265
+ "gather:failed",
1266
+ "gather:annotation-progress",
1267
+ "gather:annotation-finished",
1268
+ "gather:summary-result",
1269
+ "gather:summary-failed",
1270
+ "bind:body-updated",
1271
+ "bind:body-update-failed",
1272
+ "job:report-progress",
1273
+ "job:complete",
1274
+ "job:fail",
1275
+ "job:status-result",
1276
+ "job:status-failed",
1277
+ "job:created",
1278
+ "job:create-failed",
1279
+ "job:claimed",
1280
+ "job:claim-failed",
1281
+ "yield:clone-token-generated",
1282
+ "yield:clone-token-failed",
1283
+ "yield:clone-resource-result",
1284
+ "yield:clone-resource-failed",
1285
+ "yield:clone-created",
1286
+ "yield:clone-create-failed",
1287
+ "mark:entity-type-added",
1288
+ "beckon:focus",
1289
+ "beckon:sparkle",
1290
+ "bus:resume-gap"
1291
+ ];
1292
+ var ACTOR_TO_LOCAL_BRIDGES = BUS_RESULT_CHANNELS;
1042
1293
  var APIError = class extends Error {
1043
1294
  constructor(message, status, statusText, details) {
1044
1295
  super(message);
@@ -1051,21 +1302,18 @@ var APIError = class extends Error {
1051
1302
  var SemiontApiClient = class {
1052
1303
  http;
1053
1304
  baseUrl;
1054
- /** The workspace-scoped EventBus this client was constructed with. */
1055
- eventBus;
1056
- logger;
1057
1305
  /**
1058
- * Shared mutable token getter. All verb namespaces read from this.
1059
- * Updated via setTokenGetter() from the React auth layer.
1306
+ * Workspace-scoped EventBus owned by the client, constructed
1307
+ * internally, never accepted from config. Private: all bus access
1308
+ * goes through `client.emit` / `client.on` / `client.stream`.
1060
1309
  */
1061
- _getToken = () => void 0;
1310
+ eventBus;
1311
+ logger;
1062
1312
  /**
1063
- * SSE streaming client for real-time operations
1064
- *
1065
- * Separate from the main HTTP client to clearly mark streaming endpoints.
1066
- * Uses native fetch() instead of ky for SSE support.
1313
+ * Observable token source. All auth reads from this.
1067
1314
  */
1068
- sse;
1315
+ token$;
1316
+ _actor = null;
1069
1317
  // ── Verb-oriented namespace API ──────────────────────────────────────────
1070
1318
  browse;
1071
1319
  mark;
@@ -1078,17 +1326,17 @@ var SemiontApiClient = class {
1078
1326
  auth;
1079
1327
  admin;
1080
1328
  constructor(config) {
1081
- const { baseUrl, eventBus, timeout = 3e4, retry = 2, logger, tokenRefresher } = config;
1082
- this.eventBus = eventBus;
1329
+ const { baseUrl: baseUrl3, timeout: timeout7 = 3e4, retry = 2, logger, tokenRefresher } = config;
1330
+ this.eventBus = new EventBus();
1083
1331
  this.logger = logger;
1084
- this.baseUrl = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
1332
+ this.baseUrl = baseUrl3.endsWith("/") ? baseUrl3.slice(0, -1) : baseUrl3;
1085
1333
  const retryConfig = tokenRefresher ? {
1086
1334
  limit: 1,
1087
1335
  methods: ["get", "post", "put", "patch", "delete", "head", "options"],
1088
1336
  statusCodes: [401, 408, 413, 429, 500, 502, 503, 504]
1089
1337
  } : retry;
1090
1338
  this.http = ky.create({
1091
- timeout,
1339
+ timeout: timeout7,
1092
1340
  retry: retryConfig,
1093
1341
  credentials: "include",
1094
1342
  hooks: {
@@ -1161,33 +1409,131 @@ var SemiontApiClient = class {
1161
1409
  ]
1162
1410
  }
1163
1411
  });
1164
- this.sse = new SSEClient({
1165
- baseUrl: this.baseUrl,
1166
- logger: this.logger
1167
- });
1168
- if (config.getToken) this._getToken = config.getToken;
1169
- const getToken = () => this._getToken();
1170
- this.browse = new BrowseNamespace(this, this.eventBus, getToken);
1171
- this.mark = new MarkNamespace(this, this.eventBus, getToken);
1172
- this.bind = new BindNamespace(this, getToken);
1173
- this.gather = new GatherNamespace(this, this.eventBus, getToken);
1174
- this.match = new MatchNamespace(this, this.eventBus, getToken);
1175
- this.yield = new YieldNamespace(this, this.eventBus, getToken);
1176
- this.beckon = new BeckonNamespace(this, getToken);
1177
- this.job = new JobNamespace(this, getToken);
1412
+ this.token$ = config.token$ ?? new BehaviorSubject(null);
1413
+ const getToken = () => this.token$.getValue() ?? void 0;
1414
+ this.browse = new BrowseNamespace(this, this.eventBus, getToken, this.actor);
1415
+ this.mark = new MarkNamespace(this, this.eventBus, getToken, this.actor);
1416
+ this.bind = new BindNamespace(this.actor);
1417
+ this.gather = new GatherNamespace(this.eventBus, this.actor);
1418
+ this.match = new MatchNamespace(this.eventBus, this.actor);
1419
+ this.yield = new YieldNamespace(this, this.eventBus, getToken, this.actor);
1420
+ this.beckon = new BeckonNamespace(this.actor);
1421
+ this.job = new JobNamespace(this.actor);
1178
1422
  this.auth = new AuthNamespace(this, getToken);
1179
1423
  this.admin = new AdminNamespace(this, getToken);
1424
+ this.token$.subscribe((token) => {
1425
+ if (token && !this._actorStarted) {
1426
+ this._actorStarted = true;
1427
+ this.actor.start();
1428
+ }
1429
+ });
1180
1430
  }
1431
+ _actorStarted = false;
1432
+ get actor() {
1433
+ if (!this._actor) {
1434
+ this._actor = createActorVM({
1435
+ baseUrl: this.baseUrl,
1436
+ token: () => this.token$.getValue() ?? "",
1437
+ channels: [...BUS_RESULT_CHANNELS]
1438
+ });
1439
+ for (const channel of ACTOR_TO_LOCAL_BRIDGES) {
1440
+ this._actor.on$(channel).subscribe((payload) => {
1441
+ this.eventBus.get(channel).next(payload);
1442
+ });
1443
+ }
1444
+ }
1445
+ return this._actor;
1446
+ }
1447
+ activeResource = null;
1181
1448
  /**
1182
- * Update the token getter for all verb namespaces.
1183
- * Called from the React auth layer when the token changes.
1184
- * All namespaces share this getter via closure — no per-namespace sync needed.
1449
+ * Subscribe the bus actor to the resource-scoped SSE stream for a single
1450
+ * resource and bridge incoming scoped events into the workspace event bus.
1451
+ *
1452
+ * **One distinct scope at a time**: the client supports subscriptions
1453
+ * to a single resource scope concurrently. Multiple calls with the
1454
+ * **same** resourceId are ref-counted — each returns an independent
1455
+ * unsubscribe; the underlying SSE scope is torn down only when the
1456
+ * last unsubscribe fires. Calling with a **different** resourceId
1457
+ * while a subscription is live throws. Widening this to multiple
1458
+ * concurrent scopes is deferred until a product requirement (e.g.
1459
+ * split-pane viewer, headless fleet-watcher) forces the design.
1460
+ *
1461
+ * @returns a disposer that decrements the ref count (and tears down
1462
+ * the SSE scope + bridges when it reaches zero).
1185
1463
  */
1186
- setTokenGetter(getter) {
1187
- this._getToken = getter;
1464
+ subscribeToResource(resourceId) {
1465
+ if (this.activeResource) {
1466
+ if (this.activeResource.resourceId !== resourceId) {
1467
+ throw new Error(
1468
+ `SemiontApiClient already subscribed to resource ${this.activeResource.resourceId}; call the unsubscribe returned from the previous subscribeToResource before subscribing to ${resourceId}.`
1469
+ );
1470
+ }
1471
+ this.activeResource.refCount++;
1472
+ return this.makeUnsubscriber();
1473
+ }
1474
+ this.actor.addChannels([...RESOURCE_SCOPED_CHANNELS], resourceId);
1475
+ const bridgeSubs = [];
1476
+ for (const channel of RESOURCE_SCOPED_CHANNELS) {
1477
+ bridgeSubs.push(
1478
+ this.actor.on$(channel).subscribe((payload) => {
1479
+ this.eventBus.get(channel).next(payload);
1480
+ })
1481
+ );
1482
+ }
1483
+ this.activeResource = { resourceId, refCount: 1, bridgeSubs };
1484
+ return this.makeUnsubscriber();
1485
+ }
1486
+ makeUnsubscriber() {
1487
+ let called = false;
1488
+ return () => {
1489
+ if (called) return;
1490
+ called = true;
1491
+ if (!this.activeResource) return;
1492
+ this.activeResource.refCount--;
1493
+ if (this.activeResource.refCount > 0) return;
1494
+ for (const sub of this.activeResource.bridgeSubs) sub.unsubscribe();
1495
+ this.actor.removeChannels([...RESOURCE_SCOPED_CHANNELS]);
1496
+ this.activeResource = null;
1497
+ };
1498
+ }
1499
+ dispose() {
1500
+ if (this.activeResource) {
1501
+ for (const sub of this.activeResource.bridgeSubs) sub.unsubscribe();
1502
+ this.activeResource = null;
1503
+ }
1504
+ if (this._actor) {
1505
+ this._actor.dispose();
1506
+ this._actor = null;
1507
+ }
1188
1508
  }
1509
+ // ── Event bus surface ─────────────────────────────────────────
1510
+ // The ONE public path to the workspace bus. VMs, session, and
1511
+ // components route through these methods; `this.eventBus` remains
1512
+ // the internal owner and will be privatized once all callers
1513
+ // have migrated.
1514
+ /** Emit an event on the internal bus. */
1515
+ emit(channel, payload) {
1516
+ this.eventBus.get(channel).next(payload);
1517
+ }
1518
+ /** Subscribe to an event on the internal bus; returns unsubscribe. */
1519
+ on(channel, handler) {
1520
+ const sub = this.eventBus.get(channel).subscribe(handler);
1521
+ return () => sub.unsubscribe();
1522
+ }
1523
+ /** Read-only observable for a bus channel. Consumers `.pipe(...)` over this. */
1524
+ stream(channel) {
1525
+ return this.eventBus.get(channel).asObservable();
1526
+ }
1527
+ /**
1528
+ * Build the `Authorization: Bearer <token>` header. If the caller passed
1529
+ * an explicit `{ auth }` it wins (used by session-internal throwaway
1530
+ * clients that need to run a validation request with a specific token).
1531
+ * Otherwise the current value of `this.token$` is used, so external
1532
+ * callers never have to plumb the token themselves.
1533
+ */
1189
1534
  authHeaders(options) {
1190
- return options?.auth ? { Authorization: `Bearer ${options.auth}` } : {};
1535
+ const token = options?.auth ?? this.token$.getValue() ?? void 0;
1536
+ return token ? { Authorization: `Bearer ${token}` } : {};
1191
1537
  }
1192
1538
  // ============================================================================
1193
1539
  // AUTHENTICATION
@@ -1254,6 +1600,8 @@ var SemiontApiClient = class {
1254
1600
  * @param data.creationMethod - Optional creation method
1255
1601
  * @param data.sourceAnnotationId - Optional source annotation ID
1256
1602
  * @param data.sourceResourceId - Optional source resource ID
1603
+ * @param data.generationPrompt - Optional prompt that drove AI generation
1604
+ * @param data.generator - Optional Agent(s) that generated the content
1257
1605
  * @param options - Request options including auth
1258
1606
  */
1259
1607
  async yieldResource(data, options) {
@@ -1284,15 +1632,22 @@ var SemiontApiClient = class {
1284
1632
  if (data.sourceResourceId) {
1285
1633
  formData.append("sourceResourceId", data.sourceResourceId);
1286
1634
  }
1635
+ if (data.generationPrompt) {
1636
+ formData.append("generationPrompt", data.generationPrompt);
1637
+ }
1638
+ if (data.generator) {
1639
+ formData.append("generator", JSON.stringify(data.generator));
1640
+ }
1641
+ if (data.isDraft !== void 0) {
1642
+ formData.append("isDraft", String(data.isDraft));
1643
+ }
1287
1644
  return this.http.post(`${this.baseUrl}/resources`, {
1288
1645
  body: formData,
1289
1646
  headers: this.authHeaders(options)
1290
1647
  }).json();
1291
1648
  }
1292
- async browseResource(id, options) {
1293
- return this.http.get(`${this.baseUrl}/resources/${id}`, {
1294
- headers: this.authHeaders(options)
1295
- }).json();
1649
+ async browseResource(id, _options) {
1650
+ return busRequest(this.actor, "browse:resource-requested", { resourceId: id }, "browse:resource-result", "browse:resource-failed");
1296
1651
  }
1297
1652
  /**
1298
1653
  * Get resource representation using W3C content negotiation
@@ -1376,15 +1731,14 @@ var SemiontApiClient = class {
1376
1731
  }
1377
1732
  return { stream: response.body, contentType };
1378
1733
  }
1379
- async browseResources(limit, archived, query, options) {
1380
- const searchParams = new URLSearchParams();
1381
- if (limit) searchParams.append("limit", limit.toString());
1382
- if (archived !== void 0) searchParams.append("archived", archived.toString());
1383
- if (query) searchParams.append("q", query);
1384
- return this.http.get(`${this.baseUrl}/resources`, {
1385
- searchParams,
1386
- headers: this.authHeaders(options)
1387
- }).json();
1734
+ async browseResources(limit, archived, query, _options) {
1735
+ return busRequest(
1736
+ this.actor,
1737
+ "browse:resources-requested",
1738
+ { search: query, archived, limit: limit ?? 100, offset: 0 },
1739
+ "browse:resources-result",
1740
+ "browse:resources-failed"
1741
+ );
1388
1742
  }
1389
1743
  async updateResource(id, data, options) {
1390
1744
  await this.http.patch(`${this.baseUrl}/resources/${id}`, {
@@ -1392,151 +1746,153 @@ var SemiontApiClient = class {
1392
1746
  headers: this.authHeaders(options)
1393
1747
  }).text();
1394
1748
  }
1395
- async getResourceEvents(id, options) {
1396
- return this.http.get(`${this.baseUrl}/resources/${id}/events`, {
1397
- headers: this.authHeaders(options)
1398
- }).json();
1749
+ async getResourceEvents(id, _options) {
1750
+ return busRequest(this.actor, "browse:events-requested", { resourceId: id }, "browse:events-result", "browse:events-failed");
1399
1751
  }
1400
- async browseReferences(id, options) {
1401
- return this.http.get(`${this.baseUrl}/resources/${id}/referenced-by`, {
1402
- headers: this.authHeaders(options)
1403
- }).json();
1752
+ async browseReferences(id, _options) {
1753
+ return busRequest(this.actor, "browse:referenced-by-requested", { resourceId: id }, "browse:referenced-by-result", "browse:referenced-by-failed");
1404
1754
  }
1405
- async generateCloneToken(id, options) {
1406
- return this.http.post(`${this.baseUrl}/resources/${id}/clone-with-token`, {
1407
- headers: this.authHeaders(options)
1408
- }).json();
1755
+ async generateCloneToken(id, _options) {
1756
+ return busRequest(this.actor, "yield:clone-token-requested", { resourceId: id }, "yield:clone-token-generated", "yield:clone-token-failed");
1409
1757
  }
1410
- async getResourceByToken(token, options) {
1411
- return this.http.get(`${this.baseUrl}/api/clone-tokens/${token}`, {
1412
- headers: this.authHeaders(options)
1413
- }).json();
1758
+ async getResourceByToken(token, _options) {
1759
+ return busRequest(this.actor, "yield:clone-resource-requested", { token }, "yield:clone-resource-result", "yield:clone-resource-failed");
1414
1760
  }
1415
- async createResourceFromToken(data, options) {
1416
- return this.http.post(`${this.baseUrl}/api/clone-tokens/create-resource`, {
1417
- json: data,
1418
- headers: this.authHeaders(options)
1419
- }).json();
1761
+ async createResourceFromToken(data, _options) {
1762
+ return busRequest(this.actor, "yield:clone-create", data, "yield:clone-created", "yield:clone-create-failed");
1420
1763
  }
1421
1764
  // ============================================================================
1422
1765
  // ANNOTATIONS
1423
1766
  // ============================================================================
1424
- async markAnnotation(id, data, options) {
1425
- return this.http.post(`${this.baseUrl}/resources/${id}/annotations`, {
1426
- json: data,
1427
- headers: this.authHeaders(options)
1428
- }).json();
1429
- }
1430
- async getAnnotation(id, options) {
1431
- return this.http.get(`${this.baseUrl}/annotations/${id}`, {
1432
- headers: this.authHeaders(options)
1433
- }).json();
1434
- }
1435
- async browseAnnotation(resourceId, annotationId, options) {
1436
- return this.http.get(`${this.baseUrl}/resources/${resourceId}/annotations/${annotationId}`, {
1437
- headers: this.authHeaders(options)
1438
- }).json();
1439
- }
1440
- async browseAnnotations(id, motivation, options) {
1441
- const searchParams = new URLSearchParams();
1442
- if (motivation) searchParams.append("motivation", motivation);
1443
- return this.http.get(`${this.baseUrl}/resources/${id}/annotations`, {
1444
- searchParams,
1445
- headers: this.authHeaders(options)
1446
- }).json();
1447
- }
1448
- async deleteAnnotation(resourceId, annotationId, options) {
1449
- await this.http.delete(`${this.baseUrl}/resources/${resourceId}/annotations/${annotationId}`, {
1450
- headers: this.authHeaders(options)
1451
- });
1767
+ async markAnnotation(id, data, _options) {
1768
+ return busRequest(
1769
+ this.actor,
1770
+ "mark:create-request",
1771
+ { resourceId: id, request: data },
1772
+ "mark:create-ok",
1773
+ "mark:create-failed"
1774
+ );
1452
1775
  }
1453
- async bindAnnotation(resourceId, annotationId, data, options) {
1454
- return this.http.post(`${this.baseUrl}/resources/${resourceId}/annotations/${annotationId}/bind`, {
1455
- json: { resourceId, ...data },
1456
- headers: this.authHeaders(options)
1457
- }).json();
1776
+ async getAnnotation(id, _options) {
1777
+ return busRequest(this.actor, "browse:annotation-requested", { annotationId: id }, "browse:annotation-result", "browse:annotation-failed");
1458
1778
  }
1459
- async getAnnotationHistory(resourceId, annotationId, options) {
1460
- return this.http.get(`${this.baseUrl}/resources/${resourceId}/annotations/${annotationId}/history`, {
1461
- headers: this.authHeaders(options)
1462
- }).json();
1779
+ async browseAnnotation(resourceId, annotationId, _options) {
1780
+ return busRequest(this.actor, "browse:annotation-requested", { resourceId, annotationId }, "browse:annotation-result", "browse:annotation-failed");
1463
1781
  }
1464
- async annotateReferences(resourceId, data, options) {
1465
- return this.http.post(`${this.baseUrl}/resources/${resourceId}/annotate-references`, {
1466
- json: data,
1467
- headers: this.authHeaders(options)
1468
- }).json();
1782
+ async browseAnnotations(id, _motivation, _options) {
1783
+ return busRequest(this.actor, "browse:annotations-requested", { resourceId: id }, "browse:annotations-result", "browse:annotations-failed");
1469
1784
  }
1470
- async annotateHighlights(resourceId, data, options) {
1471
- return this.http.post(`${this.baseUrl}/resources/${resourceId}/annotate-highlights`, {
1472
- json: data,
1473
- headers: this.authHeaders(options)
1474
- }).json();
1785
+ async deleteAnnotation(resourceId, annotationId, _options) {
1786
+ await this.actor.emit("mark:delete", { annotationId, resourceId });
1475
1787
  }
1476
- async annotateAssessments(resourceId, data, options) {
1477
- return this.http.post(`${this.baseUrl}/resources/${resourceId}/annotate-assessments`, {
1478
- json: data,
1479
- headers: this.authHeaders(options)
1480
- }).json();
1788
+ async bindAnnotation(resourceId, annotationId, data, _options) {
1789
+ const correlationId = crypto.randomUUID();
1790
+ await this.actor.emit("bind:update-body", { correlationId, annotationId, resourceId, operations: data.operations });
1791
+ return { correlationId };
1481
1792
  }
1482
- async annotateComments(resourceId, data, options) {
1483
- return this.http.post(`${this.baseUrl}/resources/${resourceId}/annotate-comments`, {
1484
- json: data,
1485
- headers: this.authHeaders(options)
1486
- }).json();
1793
+ async getAnnotationHistory(resourceId, annotationId, _options) {
1794
+ return busRequest(this.actor, "browse:annotation-history-requested", { resourceId, annotationId }, "browse:annotation-history-result", "browse:annotation-history-failed");
1487
1795
  }
1488
- async annotateTags(resourceId, data, options) {
1489
- return this.http.post(`${this.baseUrl}/resources/${resourceId}/annotate-tags`, {
1490
- json: data,
1491
- headers: this.authHeaders(options)
1492
- }).json();
1493
- }
1494
- async yieldResourceFromAnnotation(resourceId, annotationId, data, options) {
1495
- return this.http.post(`${this.baseUrl}/resources/${resourceId}/annotations/${annotationId}/yield-resource`, {
1496
- json: data,
1497
- headers: this.authHeaders(options)
1498
- }).json();
1499
- }
1500
- async gatherAnnotationContext(resourceId, annotationId, data, options) {
1501
- return this.http.post(`${this.baseUrl}/resources/${resourceId}/annotations/${annotationId}/gather`, {
1502
- json: data,
1503
- headers: this.authHeaders(options)
1504
- }).json();
1505
- }
1506
- async matchSearch(resourceId, data, options) {
1507
- return this.http.post(`${this.baseUrl}/resources/${resourceId}/match-search`, {
1508
- json: { resourceId, ...data },
1509
- headers: this.authHeaders(options)
1510
- }).json();
1796
+ async annotateReferences(resourceId, data, _options) {
1797
+ const { jobId } = await busRequest(
1798
+ this.actor,
1799
+ "job:create",
1800
+ { jobType: "reference-annotation", resourceId, params: data },
1801
+ "job:created",
1802
+ "job:create-failed"
1803
+ );
1804
+ return { correlationId: crypto.randomUUID(), jobId };
1805
+ }
1806
+ async annotateHighlights(resourceId, data, _options) {
1807
+ const { jobId } = await busRequest(
1808
+ this.actor,
1809
+ "job:create",
1810
+ { jobType: "highlight-annotation", resourceId, params: data },
1811
+ "job:created",
1812
+ "job:create-failed"
1813
+ );
1814
+ return { correlationId: crypto.randomUUID(), jobId };
1815
+ }
1816
+ async annotateAssessments(resourceId, data, _options) {
1817
+ const { jobId } = await busRequest(
1818
+ this.actor,
1819
+ "job:create",
1820
+ { jobType: "assessment-annotation", resourceId, params: data },
1821
+ "job:created",
1822
+ "job:create-failed"
1823
+ );
1824
+ return { correlationId: crypto.randomUUID(), jobId };
1825
+ }
1826
+ async annotateComments(resourceId, data, _options) {
1827
+ const { jobId } = await busRequest(
1828
+ this.actor,
1829
+ "job:create",
1830
+ { jobType: "comment-annotation", resourceId, params: data },
1831
+ "job:created",
1832
+ "job:create-failed"
1833
+ );
1834
+ return { correlationId: crypto.randomUUID(), jobId };
1835
+ }
1836
+ async annotateTags(resourceId, data, _options) {
1837
+ const { jobId } = await busRequest(
1838
+ this.actor,
1839
+ "job:create",
1840
+ { jobType: "tag-annotation", resourceId, params: data },
1841
+ "job:created",
1842
+ "job:create-failed"
1843
+ );
1844
+ return { correlationId: crypto.randomUUID(), jobId };
1845
+ }
1846
+ async yieldResourceFromAnnotation(resourceId, annotationId, data, _options) {
1847
+ const { jobId } = await busRequest(
1848
+ this.actor,
1849
+ "job:create",
1850
+ { jobType: "generation", resourceId, params: { referenceId: annotationId, ...data } },
1851
+ "job:created",
1852
+ "job:create-failed"
1853
+ );
1854
+ return { correlationId: crypto.randomUUID(), jobId };
1855
+ }
1856
+ async gatherAnnotationContext(resourceId, annotationId, data, _options) {
1857
+ await this.actor.emit("gather:requested", {
1858
+ correlationId: data.correlationId,
1859
+ annotationId,
1860
+ resourceId,
1861
+ contextWindow: data.contextWindow ?? 2e3
1862
+ });
1863
+ return { correlationId: data.correlationId };
1864
+ }
1865
+ async matchSearch(resourceId, data, _options) {
1866
+ await this.actor.emit("match:search-requested", {
1867
+ correlationId: data.correlationId,
1868
+ resourceId,
1869
+ referenceId: data.referenceId,
1870
+ context: data.context,
1871
+ limit: data.limit ?? 10,
1872
+ useSemanticScoring: data.useSemanticScoring ?? true
1873
+ });
1874
+ return { correlationId: data.correlationId };
1511
1875
  }
1512
1876
  // ============================================================================
1513
1877
  // ENTITY TYPES
1514
1878
  // ============================================================================
1515
- async addEntityType(type, options) {
1516
- await this.http.post(`${this.baseUrl}/api/entity-types`, {
1517
- json: { tag: type },
1518
- headers: this.authHeaders(options)
1519
- });
1879
+ async addEntityType(type, _options) {
1880
+ await this.actor.emit("mark:add-entity-type", { tag: type });
1520
1881
  }
1521
- async addEntityTypesBulk(types, options) {
1522
- await this.http.post(`${this.baseUrl}/api/entity-types/bulk`, {
1523
- json: { tags: types },
1524
- headers: this.authHeaders(options)
1525
- });
1882
+ async addEntityTypesBulk(types, _options) {
1883
+ for (const tag of types) {
1884
+ await this.actor.emit("mark:add-entity-type", { tag });
1885
+ }
1526
1886
  }
1527
- async listEntityTypes(options) {
1528
- return this.http.get(`${this.baseUrl}/api/entity-types`, {
1529
- headers: this.authHeaders(options)
1530
- }).json();
1887
+ async listEntityTypes(_options) {
1888
+ return busRequest(this.actor, "browse:entity-types-requested", {}, "browse:entity-types-result", "browse:entity-types-failed");
1531
1889
  }
1532
1890
  // ============================================================================
1533
1891
  // PARTICIPANTS
1534
1892
  // ============================================================================
1535
- async beckonAttention(participantId, data, options) {
1536
- return this.http.post(`${this.baseUrl}/api/participants/${participantId}/attention`, {
1537
- json: data,
1538
- headers: this.authHeaders(options)
1539
- }).json();
1893
+ async beckonAttention(_participantId, data, _options) {
1894
+ await this.actor.emit("beckon:focus", data);
1895
+ return {};
1540
1896
  }
1541
1897
  // ============================================================================
1542
1898
  // ADMIN
@@ -1642,58 +1998,1693 @@ var SemiontApiClient = class {
1642
1998
  // ============================================================================
1643
1999
  // JOB STATUS
1644
2000
  // ============================================================================
1645
- async getJobStatus(id, options) {
1646
- return this.http.get(`${this.baseUrl}/api/jobs/${id}`, {
2001
+ async getJobStatus(id, _options) {
2002
+ return busRequest(this.actor, "job:status-requested", { jobId: id }, "job:status-result", "job:status-failed");
2003
+ }
2004
+ /**
2005
+ * Poll a job until it completes or fails
2006
+ * @param id - The job ID to poll
2007
+ * @param options - Polling options
2008
+ * @returns The final job status
2009
+ */
2010
+ async pollJobUntilComplete(id, options) {
2011
+ const interval = options?.interval ?? 1e3;
2012
+ const timeout7 = options?.timeout ?? 6e4;
2013
+ const startTime = Date.now();
2014
+ while (true) {
2015
+ const status = await this.getJobStatus(id, { auth: options?.auth });
2016
+ if (options?.onProgress) {
2017
+ options.onProgress(status);
2018
+ }
2019
+ if (status.status === "complete" || status.status === "failed" || status.status === "cancelled") {
2020
+ return status;
2021
+ }
2022
+ if (Date.now() - startTime > timeout7) {
2023
+ throw new Error(`Job polling timeout after ${timeout7}ms`);
2024
+ }
2025
+ await new Promise((resolve) => setTimeout(resolve, interval));
2026
+ }
2027
+ }
2028
+ // ============================================================================
2029
+ // SYSTEM STATUS
2030
+ // ============================================================================
2031
+ async healthCheck(options) {
2032
+ return this.http.get(`${this.baseUrl}/api/health`, {
1647
2033
  headers: this.authHeaders(options)
1648
2034
  }).json();
1649
2035
  }
2036
+ async getStatus(options) {
2037
+ return this.http.get(`${this.baseUrl}/api/status`, {
2038
+ headers: this.authHeaders(options)
2039
+ }).json();
2040
+ }
2041
+ async browseFiles(dirPath, sort, _options) {
2042
+ return busRequest(
2043
+ this.actor,
2044
+ "browse:directory-requested",
2045
+ { path: dirPath ?? ".", sort: sort ?? "name" },
2046
+ "browse:directory-result",
2047
+ "browse:directory-failed"
2048
+ );
2049
+ }
2050
+ };
2051
+
2052
+ // src/session/storage.ts
2053
+ var SESSION_PREFIX = "semiont.session.";
2054
+ var STORAGE_KEY = "semiont.knowledgeBases";
2055
+ var ACTIVE_KEY = "semiont.activeKnowledgeBaseId";
2056
+ var REFRESH_BEFORE_EXP_MS = 5 * 60 * 1e3;
2057
+ function sessionKey(kbId) {
2058
+ return `${SESSION_PREFIX}${kbId}`;
2059
+ }
2060
+ function getStoredSession(storage, kbId) {
2061
+ const raw = storage.get(sessionKey(kbId));
2062
+ if (!raw) return null;
2063
+ try {
2064
+ const parsed = JSON.parse(raw);
2065
+ if (parsed && typeof parsed.access === "string" && typeof parsed.refresh === "string") {
2066
+ return { access: parsed.access, refresh: parsed.refresh };
2067
+ }
2068
+ } catch {
2069
+ }
2070
+ return null;
2071
+ }
2072
+ function setStoredSession(storage, kbId, session) {
2073
+ storage.set(sessionKey(kbId), JSON.stringify(session));
2074
+ }
2075
+ function clearStoredSession(storage, kbId) {
2076
+ storage.delete(sessionKey(kbId));
2077
+ }
2078
+ function parseJwtExpiry(token) {
2079
+ try {
2080
+ const parts = token.split(".");
2081
+ if (parts.length !== 3 || !parts[1]) return null;
2082
+ const payload = JSON.parse(atob(parts[1]));
2083
+ if (!payload.exp) return null;
2084
+ return new Date(payload.exp * 1e3);
2085
+ } catch {
2086
+ return null;
2087
+ }
2088
+ }
2089
+ function isJwtExpired(token) {
2090
+ const expiry = parseJwtExpiry(token);
2091
+ if (!expiry) return true;
2092
+ return expiry.getTime() < Date.now();
2093
+ }
2094
+ function migrateLegacyEntry(entry) {
2095
+ if (entry.host !== void 0) return entry;
2096
+ try {
2097
+ const url = new URL(entry.backendUrl);
2098
+ return {
2099
+ id: entry.id,
2100
+ label: entry.label,
2101
+ host: url.hostname,
2102
+ port: parseInt(url.port, 10) || (url.protocol === "https:" ? 443 : 80),
2103
+ protocol: url.protocol === "https:" ? "https" : "http",
2104
+ email: ""
2105
+ };
2106
+ } catch {
2107
+ return {
2108
+ id: entry.id,
2109
+ label: entry.label || "Unknown",
2110
+ host: "localhost",
2111
+ port: 4e3,
2112
+ protocol: "http",
2113
+ email: ""
2114
+ };
2115
+ }
2116
+ }
2117
+ function loadKnowledgeBases(storage) {
2118
+ try {
2119
+ const raw = storage.get(STORAGE_KEY);
2120
+ if (!raw) return [];
2121
+ const entries = JSON.parse(raw);
2122
+ return entries.map(migrateLegacyEntry);
2123
+ } catch {
2124
+ return [];
2125
+ }
2126
+ }
2127
+ function saveKnowledgeBases(storage, knowledgeBases) {
2128
+ storage.set(STORAGE_KEY, JSON.stringify(knowledgeBases));
2129
+ }
2130
+ function defaultProtocol(host) {
2131
+ return host === "localhost" || host === "127.0.0.1" ? "http" : "https";
2132
+ }
2133
+ var HOSTNAME_RE = /^(([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?|localhost|\d{1,3}(\.\d{1,3}){3})$/;
2134
+ function isValidHostname(host) {
2135
+ return HOSTNAME_RE.test(host);
2136
+ }
2137
+ function kbBackendUrl(kb) {
2138
+ if (!isValidHostname(kb.host)) {
2139
+ throw new Error(`Invalid KB hostname: "${kb.host}"`);
2140
+ }
2141
+ const url = new URL("http://x");
2142
+ url.protocol = kb.protocol + ":";
2143
+ url.hostname = kb.host;
2144
+ url.port = String(kb.port);
2145
+ return `${kb.protocol}://${url.hostname}:${kb.port}`;
2146
+ }
2147
+ function generateKbId() {
2148
+ return crypto.randomUUID();
2149
+ }
2150
+
2151
+ // src/session/errors.ts
2152
+ var SemiontError = class extends Error {
2153
+ code;
2154
+ kbId;
2155
+ constructor(code, message, kbId = null) {
2156
+ super(message);
2157
+ this.name = "SemiontError";
2158
+ this.code = code;
2159
+ this.kbId = kbId;
2160
+ }
2161
+ };
2162
+
2163
+ // src/session/semiont-session.ts
2164
+ var SemiontSession = class {
2165
+ kb;
2166
+ client;
2167
+ token$;
2168
+ user$;
2169
+ streamState$;
2170
+ /** Resolves after the initial validation round-trip completes (success or failure). */
2171
+ ready;
2172
+ storage;
2173
+ doRefresh;
2174
+ doValidate;
2175
+ onAuthFailed;
2176
+ onError;
2177
+ refreshTimer = null;
2178
+ unsubscribeStorage = null;
2179
+ disposed = false;
2180
+ constructor(config) {
2181
+ this.kb = config.kb;
2182
+ this.storage = config.storage;
2183
+ this.doRefresh = config.refresh;
2184
+ this.doValidate = config.validate;
2185
+ this.onAuthFailed = config.onAuthFailed ?? (() => {
2186
+ });
2187
+ this.onError = config.onError ?? (() => {
2188
+ });
2189
+ const stored = getStoredSession(this.storage, this.kb.id);
2190
+ const initialToken = stored && !isJwtExpired(stored.access) ? accessToken(stored.access) : null;
2191
+ this.token$ = new BehaviorSubject(initialToken);
2192
+ this.user$ = new BehaviorSubject(null);
2193
+ this.client = new SemiontApiClient({
2194
+ baseUrl: baseUrl(kbBackendUrl(this.kb)),
2195
+ token$: this.token$,
2196
+ tokenRefresher: () => this.refresh().then((t) => t ?? null)
2197
+ });
2198
+ this.streamState$ = this.client.actor.state$;
2199
+ if (initialToken) {
2200
+ this.scheduleProactiveRefresh(initialToken);
2201
+ }
2202
+ this.unsubscribeStorage = this.storage.subscribe?.((key, newValue) => {
2203
+ this.handleStorageChange(key, newValue);
2204
+ }) ?? null;
2205
+ this.ready = this.validate(stored);
2206
+ }
2207
+ /**
2208
+ * Run the initial mount-time validation. If a stored access token is
2209
+ * present and unexpired, call the configured `validate` with it to
2210
+ * confirm it still works and populate `user$`. If expired, try
2211
+ * refresh first. On 401 from validate, try refresh once. Surfaces
2212
+ * auth-failed on terminal failure.
2213
+ *
2214
+ * When no `validate` callback is provided (service principals), this
2215
+ * still runs through the refresh-if-expired step so the stored
2216
+ * token is current — it just skips the user-validation round trip.
2217
+ */
2218
+ async validate(stored) {
2219
+ if (!stored) return;
2220
+ const startToken = isJwtExpired(stored.access) ? await this.doRefresh() : stored.access;
2221
+ if (!startToken) {
2222
+ if (isJwtExpired(stored.access)) {
2223
+ clearStoredSession(this.storage, this.kb.id);
2224
+ }
2225
+ return;
2226
+ }
2227
+ if (startToken !== stored.access) {
2228
+ this.token$.next(accessToken(startToken));
2229
+ this.scheduleProactiveRefresh(startToken);
2230
+ }
2231
+ if (!this.doValidate) return;
2232
+ const attempt = async (token) => {
2233
+ if (this.disposed) return;
2234
+ try {
2235
+ const data = await this.doValidate(accessToken(token));
2236
+ if (this.disposed) return;
2237
+ this.user$.next(data);
2238
+ } catch (err) {
2239
+ if (this.disposed) return;
2240
+ if (err instanceof APIError && err.status === 401) {
2241
+ const refreshed = await this.doRefresh();
2242
+ if (this.disposed) return;
2243
+ if (refreshed) {
2244
+ this.token$.next(accessToken(refreshed));
2245
+ this.scheduleProactiveRefresh(refreshed);
2246
+ await attempt(refreshed);
2247
+ return;
2248
+ }
2249
+ clearStoredSession(this.storage, this.kb.id);
2250
+ this.token$.next(null);
2251
+ this.onAuthFailed("Your session has expired. Please sign in again.");
2252
+ } else {
2253
+ this.onError(
2254
+ new SemiontError(
2255
+ "session.auth-failed",
2256
+ err instanceof Error ? err.message : String(err),
2257
+ this.kb.id
2258
+ )
2259
+ );
2260
+ }
2261
+ }
2262
+ };
2263
+ await attempt(startToken);
2264
+ }
2265
+ /**
2266
+ * Refresh the access token via the configured `refresh` callback.
2267
+ * On success, pushes the new token into `token$` and schedules the
2268
+ * next proactive refresh. On failure, clears persisted state and
2269
+ * fires `onAuthFailed` — the frontend's wiring of that callback is
2270
+ * what surfaces the session-expired modal.
2271
+ */
2272
+ async refresh() {
2273
+ if (this.disposed) return null;
2274
+ const newAccess = await this.doRefresh();
2275
+ if (this.disposed) return null;
2276
+ if (newAccess) {
2277
+ const tok = accessToken(newAccess);
2278
+ this.token$.next(tok);
2279
+ this.scheduleProactiveRefresh(newAccess);
2280
+ return tok;
2281
+ }
2282
+ this.token$.next(null);
2283
+ clearStoredSession(this.storage, this.kb.id);
2284
+ this.onAuthFailed("Your session has expired. Please sign in again.");
2285
+ this.onError(
2286
+ new SemiontError("session.refresh-exhausted", "Token refresh failed", this.kb.id)
2287
+ );
2288
+ return null;
2289
+ }
2290
+ scheduleProactiveRefresh(token) {
2291
+ this.clearRefreshTimer();
2292
+ const expiresAt = parseJwtExpiry(token);
2293
+ if (!expiresAt) return;
2294
+ const refreshAt = expiresAt.getTime() - REFRESH_BEFORE_EXP_MS;
2295
+ const delay = Math.max(0, refreshAt - Date.now());
2296
+ this.refreshTimer = setTimeout(() => {
2297
+ this.refreshTimer = null;
2298
+ if (!this.disposed) void this.refresh();
2299
+ }, delay);
2300
+ }
2301
+ clearRefreshTimer() {
2302
+ if (this.refreshTimer) {
2303
+ clearTimeout(this.refreshTimer);
2304
+ this.refreshTimer = null;
2305
+ }
2306
+ }
2307
+ /**
2308
+ * Cross-context sync: another tab/process refreshed or signed out this
2309
+ * KB. Mirror the change into our in-memory state.
2310
+ */
2311
+ handleStorageChange(key, newValue) {
2312
+ if (this.disposed) return;
2313
+ if (key !== sessionKey(this.kb.id)) return;
2314
+ if (!newValue) {
2315
+ this.token$.next(null);
2316
+ this.user$.next(null);
2317
+ this.clearRefreshTimer();
2318
+ return;
2319
+ }
2320
+ try {
2321
+ const parsed = JSON.parse(newValue);
2322
+ if (typeof parsed.access === "string") {
2323
+ this.token$.next(accessToken(parsed.access));
2324
+ this.scheduleProactiveRefresh(parsed.access);
2325
+ }
2326
+ } catch {
2327
+ }
2328
+ }
2329
+ get expiresAt() {
2330
+ const token = this.token$.getValue();
2331
+ return token ? parseJwtExpiry(token) : null;
2332
+ }
2333
+ async dispose() {
2334
+ if (this.disposed) return;
2335
+ this.disposed = true;
2336
+ this.clearRefreshTimer();
2337
+ if (this.unsubscribeStorage) {
2338
+ this.unsubscribeStorage();
2339
+ this.unsubscribeStorage = null;
2340
+ }
2341
+ this.client.dispose();
2342
+ this.token$.complete();
2343
+ this.user$.complete();
2344
+ }
2345
+ };
2346
+
2347
+ // src/session/notify.ts
2348
+ var activeOnSessionExpired = null;
2349
+ var activeOnPermissionDenied = null;
2350
+ function notifySessionExpired(message) {
2351
+ activeOnSessionExpired?.(message);
2352
+ }
2353
+ function notifyPermissionDenied(message) {
2354
+ activeOnPermissionDenied?.(message);
2355
+ }
2356
+ function registerAuthNotifyHandlers(handlers) {
2357
+ activeOnSessionExpired = handlers.onSessionExpired;
2358
+ activeOnPermissionDenied = handlers.onPermissionDenied;
2359
+ return () => {
2360
+ activeOnSessionExpired = null;
2361
+ activeOnPermissionDenied = null;
2362
+ };
2363
+ }
2364
+ var FrontendSessionSignals = class {
2365
+ sessionExpiredAt$;
2366
+ sessionExpiredMessage$;
2367
+ permissionDeniedAt$;
2368
+ permissionDeniedMessage$;
2369
+ constructor() {
2370
+ this.sessionExpiredAt$ = new BehaviorSubject(null);
2371
+ this.sessionExpiredMessage$ = new BehaviorSubject(null);
2372
+ this.permissionDeniedAt$ = new BehaviorSubject(null);
2373
+ this.permissionDeniedMessage$ = new BehaviorSubject(null);
2374
+ }
2375
+ notifySessionExpired(message) {
2376
+ this.sessionExpiredMessage$.next(
2377
+ message ?? "Your session has expired. Please sign in again."
2378
+ );
2379
+ this.sessionExpiredAt$.next(Date.now());
2380
+ }
2381
+ notifyPermissionDenied(message) {
2382
+ this.permissionDeniedMessage$.next(
2383
+ message ?? "You do not have permission to perform this action."
2384
+ );
2385
+ this.permissionDeniedAt$.next(Date.now());
2386
+ }
2387
+ acknowledgeSessionExpired() {
2388
+ this.sessionExpiredAt$.next(null);
2389
+ this.sessionExpiredMessage$.next(null);
2390
+ }
2391
+ acknowledgePermissionDenied() {
2392
+ this.permissionDeniedAt$.next(null);
2393
+ this.permissionDeniedMessage$.next(null);
2394
+ }
2395
+ dispose() {
2396
+ this.sessionExpiredAt$.complete();
2397
+ this.sessionExpiredMessage$.complete();
2398
+ this.permissionDeniedAt$.complete();
2399
+ this.permissionDeniedMessage$.complete();
2400
+ }
2401
+ };
2402
+
2403
+ // src/session/semiont-browser.ts
2404
+ var OPEN_RESOURCES_KEY = "openDocuments";
2405
+ function sortOpenResources(resources) {
2406
+ return [...resources].sort((a, b) => {
2407
+ if (a.order !== void 0 && b.order !== void 0) return a.order - b.order;
2408
+ return a.openedAt - b.openedAt;
2409
+ });
2410
+ }
2411
+ function loadOpenResources(storage) {
2412
+ try {
2413
+ const stored = storage.get(OPEN_RESOURCES_KEY);
2414
+ if (stored) return sortOpenResources(JSON.parse(stored));
2415
+ } catch {
2416
+ }
2417
+ return [];
2418
+ }
2419
+ var SemiontBrowser = class {
2420
+ kbs$;
2421
+ activeKbId$;
2422
+ activeSession$;
2423
+ /**
2424
+ * Modal signals (session-expired / permission-denied) for the
2425
+ * currently-active session. Parallels `activeSession$` — always
2426
+ * non-null when `activeSession$` is non-null, always null when it
2427
+ * is. Extracted from the session itself so headless sessions
2428
+ * (workers, CLIs, tests) don't carry dead modal observables.
2429
+ * See [FrontendSessionSignals](./frontend-session-signals.ts).
2430
+ */
2431
+ activeSignals$;
2432
+ /**
2433
+ * True while a session is actively being constructed (setActiveKb /
2434
+ * signIn in flight, awaiting `session.ready`). Distinguishes the
2435
+ * "session about to arrive" intermediate state from "session
2436
+ * intentionally null" (after signOut, or when the active KB has no
2437
+ * stored credentials). UIs that want a loading spinner should gate
2438
+ * on this; otherwise they get stuck spinning after every signOut.
2439
+ */
2440
+ sessionActivating$;
2441
+ openResources$;
2442
+ error$;
2443
+ identityToken$;
2444
+ storage;
2445
+ /**
2446
+ * App-scoped EventBus. Hosts UI-shell events that must work regardless
2447
+ * of whether a KB session is active: panel toggles, sidebar state,
2448
+ * tab reorders, routing, settings, etc. Disjoint from the per-session
2449
+ * bus inside `SemiontApiClient`, which carries KB-content events
2450
+ * (mark:*, beckon:*, gather:*, match:*, bind:*, yield:*, browse:click).
2451
+ */
2452
+ eventBus = new EventBus();
2453
+ unregisterNotify = null;
2454
+ unsubscribeStorage = null;
2455
+ disposed = false;
2456
+ activating = null;
2457
+ /**
2458
+ * Per-KB in-flight refresh dedup. Simultaneous 401s for the same
2459
+ * KB converge on a single `/api/tokens/refresh` network call.
2460
+ * Was previously module-scoped in `refresh.ts`; moved here when
2461
+ * that file was deleted — SemiontBrowser is a singleton so the
2462
+ * scoping is equivalent.
2463
+ */
2464
+ inFlightRefreshes = /* @__PURE__ */ new Map();
2465
+ constructor(config) {
2466
+ this.storage = config.storage;
2467
+ const kbs = loadKnowledgeBases(this.storage);
2468
+ const storedActive = this.storage.get(ACTIVE_KEY);
2469
+ const initialActive = storedActive && kbs.some((kb) => kb.id === storedActive) ? storedActive : kbs[0]?.id ?? null;
2470
+ this.kbs$ = new BehaviorSubject(kbs);
2471
+ this.activeKbId$ = new BehaviorSubject(initialActive);
2472
+ this.activeSession$ = new BehaviorSubject(null);
2473
+ this.activeSignals$ = new BehaviorSubject(null);
2474
+ this.sessionActivating$ = new BehaviorSubject(false);
2475
+ this.openResources$ = new BehaviorSubject(loadOpenResources(this.storage));
2476
+ this.error$ = new Subject();
2477
+ this.identityToken$ = new BehaviorSubject(null);
2478
+ this.kbs$.subscribe((next) => saveKnowledgeBases(this.storage, next));
2479
+ this.activeKbId$.subscribe((id) => {
2480
+ if (id) this.storage.set(ACTIVE_KEY, id);
2481
+ else this.storage.delete(ACTIVE_KEY);
2482
+ });
2483
+ this.openResources$.subscribe((list) => {
2484
+ this.storage.set(OPEN_RESOURCES_KEY, JSON.stringify(list));
2485
+ });
2486
+ this.unsubscribeStorage = this.storage.subscribe?.((key, newValue) => {
2487
+ if (key !== OPEN_RESOURCES_KEY || !newValue) return;
2488
+ try {
2489
+ this.openResources$.next(sortOpenResources(JSON.parse(newValue)));
2490
+ } catch {
2491
+ }
2492
+ }) ?? null;
2493
+ this.unregisterNotify = registerAuthNotifyHandlers({
2494
+ onSessionExpired: (message) => {
2495
+ this.activeSignals$.getValue()?.notifySessionExpired(message ?? null);
2496
+ },
2497
+ onPermissionDenied: (message) => {
2498
+ this.activeSignals$.getValue()?.notifyPermissionDenied(message ?? null);
2499
+ }
2500
+ });
2501
+ if (initialActive) {
2502
+ void this.setActiveKb(initialActive);
2503
+ }
2504
+ }
2505
+ // ── App-scoped event bus ──────────────────────────────────────────────
2506
+ /** Emit an event on the browser's app-scoped bus. */
2507
+ emit(channel, payload) {
2508
+ if (this.disposed) return;
2509
+ this.eventBus.get(channel).next(payload);
2510
+ }
2511
+ /** Subscribe to an event; returns unsubscribe. */
2512
+ on(channel, handler) {
2513
+ const sub = this.eventBus.get(channel).subscribe(handler);
2514
+ return () => sub.unsubscribe();
2515
+ }
2516
+ /** Read-only observable for an app-scoped channel. */
2517
+ stream(channel) {
2518
+ return this.eventBus.get(channel).asObservable();
2519
+ }
2520
+ // ── Identity token (NextAuth bridge; D1) ──────────────────────────────
2521
+ /**
2522
+ * Set the app-level identity token (from NextAuth's useSession).
2523
+ * Called at the root layout via a single `useEffect`. No other site
2524
+ * in the codebase should call this.
2525
+ */
2526
+ setIdentityToken(token) {
2527
+ if (this.disposed) return;
2528
+ this.identityToken$.next(token);
2529
+ }
2530
+ // ── KB list management ────────────────────────────────────────────────
2531
+ addKb(input, access, refresh) {
2532
+ const kb = { id: generateKbId(), ...input };
2533
+ setStoredSession(this.storage, kb.id, { access, refresh });
2534
+ this.kbs$.next([...this.kbs$.getValue(), kb]);
2535
+ void this.setActiveKb(kb.id);
2536
+ return kb;
2537
+ }
2538
+ removeKb(id) {
2539
+ clearStoredSession(this.storage, id);
2540
+ const next = this.kbs$.getValue().filter((kb) => kb.id !== id);
2541
+ this.kbs$.next(next);
2542
+ if (this.activeKbId$.getValue() === id) {
2543
+ void this.setActiveKb(next[0]?.id ?? null);
2544
+ }
2545
+ }
2546
+ updateKb(id, updates) {
2547
+ this.kbs$.next(
2548
+ this.kbs$.getValue().map((kb) => kb.id === id ? { ...kb, ...updates } : kb)
2549
+ );
2550
+ }
2551
+ /**
2552
+ * Read the locally-stored credential status for a KB. Pure / synchronous —
2553
+ * does not subscribe to context changes. Used by KB-list UI to color status
2554
+ * dots without requiring re-renders on every tick.
2555
+ */
2556
+ getKbSessionStatus(kbId) {
2557
+ const stored = getStoredSession(this.storage, kbId);
2558
+ if (!stored) return "signed-out";
2559
+ return isJwtExpired(stored.access) ? "expired" : "authenticated";
2560
+ }
2561
+ /**
2562
+ * Switch the active KB. Follows the D2 disposal contract:
2563
+ * 1. Synchronously announce the new id on `activeKbId$` and null out
2564
+ * `activeSession$` so views see a safe empty state first.
2565
+ * 2. Serialize overlapping calls — if an activation is in flight, wait
2566
+ * for it before proceeding.
2567
+ * 3. Dispose whatever session is currently live.
2568
+ * 4. Construct the next session and await `session.ready`.
2569
+ * 5. Before emitting, re-check `activeKbId$` — if a newer call superseded
2570
+ * us while we waited, dispose our session and skip the emit.
2571
+ * 6. Emit the new session.
2572
+ */
2573
+ async setActiveKb(id) {
2574
+ if (this.disposed) return;
2575
+ const prevId = this.activeKbId$.getValue();
2576
+ const prevSession = this.activeSession$.getValue();
2577
+ if (id === prevId && prevSession) return;
2578
+ if (prevId !== id) this.activeKbId$.next(id);
2579
+ if (prevSession) {
2580
+ this.activeSession$.next(null);
2581
+ this.activeSignals$.next(null);
2582
+ }
2583
+ while (this.activating) {
2584
+ const current = this.activating;
2585
+ await current;
2586
+ if (this.disposed) return;
2587
+ if (this.activeKbId$.getValue() !== id) return;
2588
+ }
2589
+ const activation = (async () => {
2590
+ const toDispose = this.activeSession$.getValue();
2591
+ const signalsToDispose = this.activeSignals$.getValue();
2592
+ if (toDispose) {
2593
+ this.activeSession$.next(null);
2594
+ this.activeSignals$.next(null);
2595
+ await toDispose.dispose();
2596
+ signalsToDispose?.dispose();
2597
+ }
2598
+ if (!id) return;
2599
+ const kb = this.kbs$.getValue().find((k) => k.id === id);
2600
+ if (!kb) return;
2601
+ const signals = new FrontendSessionSignals();
2602
+ const session = new SemiontSession({
2603
+ kb,
2604
+ storage: this.storage,
2605
+ refresh: () => this.performRefresh(kb),
2606
+ validate: (token) => this.performValidate(kb, token),
2607
+ onAuthFailed: (msg) => signals.notifySessionExpired(msg),
2608
+ onError: (err) => this.error$.next(err)
2609
+ });
2610
+ try {
2611
+ await session.ready;
2612
+ } catch (err) {
2613
+ this.error$.next(
2614
+ new SemiontError(
2615
+ "session.construct-failed",
2616
+ err instanceof Error ? err.message : String(err),
2617
+ id
2618
+ )
2619
+ );
2620
+ await session.dispose();
2621
+ signals.dispose();
2622
+ return;
2623
+ }
2624
+ if (this.disposed || this.activeKbId$.getValue() !== id) {
2625
+ await session.dispose();
2626
+ signals.dispose();
2627
+ return;
2628
+ }
2629
+ this.activeSession$.next(session);
2630
+ this.activeSignals$.next(signals);
2631
+ })();
2632
+ this.activating = activation;
2633
+ this.sessionActivating$.next(true);
2634
+ try {
2635
+ await activation;
2636
+ } finally {
2637
+ if (this.activating === activation) {
2638
+ this.activating = null;
2639
+ this.sessionActivating$.next(false);
2640
+ }
2641
+ }
2642
+ }
2643
+ /**
2644
+ * Sign in to an existing KB: store the tokens and (re)activate the
2645
+ * session. If the KB is already active, the current session is disposed
2646
+ * and replaced so the new tokens take effect.
2647
+ */
2648
+ async signIn(id, access, refresh) {
2649
+ if (this.disposed) return;
2650
+ setStoredSession(this.storage, id, { access, refresh });
2651
+ if (this.activeKbId$.getValue() === id) {
2652
+ const prevSession = this.activeSession$.getValue();
2653
+ const prevSignals = this.activeSignals$.getValue();
2654
+ this.activeSession$.next(null);
2655
+ this.activeSignals$.next(null);
2656
+ if (prevSession) await prevSession.dispose();
2657
+ prevSignals?.dispose();
2658
+ await this.setActiveKb(id);
2659
+ return;
2660
+ }
2661
+ await this.setActiveKb(id);
2662
+ }
2663
+ /**
2664
+ * Sign out of a KB: clear stored tokens. If the KB is active, dispose
2665
+ * its session + signals and emit null for both.
2666
+ */
2667
+ async signOut(id) {
2668
+ if (this.disposed) return;
2669
+ clearStoredSession(this.storage, id);
2670
+ this.kbs$.next([...this.kbs$.getValue()]);
2671
+ if (this.activeKbId$.getValue() === id) {
2672
+ const prevSession = this.activeSession$.getValue();
2673
+ const prevSignals = this.activeSignals$.getValue();
2674
+ this.activeSession$.next(null);
2675
+ this.activeSignals$.next(null);
2676
+ if (prevSession) await prevSession.dispose();
2677
+ prevSignals?.dispose();
2678
+ }
2679
+ }
2680
+ // ── Open resources ────────────────────────────────────────────────────
2681
+ addOpenResource(id, name, mediaType, storageUri) {
2682
+ const existing = this.openResources$.getValue();
2683
+ const idx = existing.findIndex((r) => r.id === id);
2684
+ if (idx >= 0) {
2685
+ const prev = existing[idx];
2686
+ const updated = {
2687
+ ...prev,
2688
+ name,
2689
+ ...mediaType !== void 0 ? { mediaType } : {},
2690
+ ...storageUri !== void 0 ? { storageUri } : {}
2691
+ };
2692
+ const next = [...existing];
2693
+ next[idx] = updated;
2694
+ this.openResources$.next(next);
2695
+ return;
2696
+ }
2697
+ const resource = {
2698
+ id,
2699
+ name,
2700
+ openedAt: Date.now(),
2701
+ order: existing.length,
2702
+ ...mediaType !== void 0 ? { mediaType } : {},
2703
+ ...storageUri !== void 0 ? { storageUri } : {}
2704
+ };
2705
+ this.openResources$.next([...existing, resource]);
2706
+ }
2707
+ removeOpenResource(id) {
2708
+ this.openResources$.next(this.openResources$.getValue().filter((r) => r.id !== id));
2709
+ }
2710
+ updateOpenResourceName(id, name) {
2711
+ this.openResources$.next(
2712
+ this.openResources$.getValue().map((r) => r.id === id ? { ...r, name } : r)
2713
+ );
2714
+ }
2715
+ reorderOpenResources(oldIndex, newIndex) {
2716
+ const list = [...this.openResources$.getValue()];
2717
+ if (oldIndex < 0 || oldIndex >= list.length || newIndex < 0 || newIndex >= list.length) {
2718
+ return;
2719
+ }
2720
+ const [moved] = list.splice(oldIndex, 1);
2721
+ if (moved) list.splice(newIndex, 0, moved);
2722
+ this.openResources$.next(list);
2723
+ }
2724
+ // ── Auth callbacks bound per session ──────────────────────────────────
2725
+ //
2726
+ // These closures back the `refresh` and `validate` callbacks passed
2727
+ // to `SemiontSession` in `setActiveKb`. Factored out as methods
2728
+ // (rather than inline in the activation closure) so test-doubles
2729
+ // can override them cleanly, and so the in-flight dedup map
2730
+ // survives across activations of the same KB.
2731
+ /**
2732
+ * Refresh the active KB's access token. Returns the new token on
2733
+ * success, null on failure. Concurrent calls for the same KB
2734
+ * dedupe through `inFlightRefreshes`, so simultaneous 401s trigger
2735
+ * only one `/api/tokens/refresh` round trip.
2736
+ *
2737
+ * Uses a throwaway `SemiontApiClient` with no `tokenRefresher` —
2738
+ * a refresh call returning 401 would otherwise re-enter this
2739
+ * function infinitely.
2740
+ */
2741
+ async performRefresh(kb) {
2742
+ const existing = this.inFlightRefreshes.get(kb.id);
2743
+ if (existing) return existing;
2744
+ const promise = (async () => {
2745
+ const stored = getStoredSession(this.storage, kb.id);
2746
+ if (!stored) return null;
2747
+ const throwaway = new SemiontApiClient({
2748
+ baseUrl: baseUrl(kbBackendUrl(kb))
2749
+ });
2750
+ try {
2751
+ const response = await throwaway.refreshToken(refreshToken(stored.refresh));
2752
+ const newAccess = response.access_token;
2753
+ if (!newAccess) return null;
2754
+ setStoredSession(this.storage, kb.id, { access: newAccess, refresh: stored.refresh });
2755
+ return newAccess;
2756
+ } catch {
2757
+ return null;
2758
+ } finally {
2759
+ throwaway.dispose();
2760
+ }
2761
+ })();
2762
+ this.inFlightRefreshes.set(kb.id, promise);
2763
+ try {
2764
+ return await promise;
2765
+ } finally {
2766
+ this.inFlightRefreshes.delete(kb.id);
2767
+ }
2768
+ }
1650
2769
  /**
1651
- * Poll a job until it completes or fails
1652
- * @param id - The job ID to poll
1653
- * @param options - Polling options
1654
- * @returns The final job status
2770
+ * Validate an access token by calling `getMe` on a throwaway
2771
+ * client. The session uses this once at startup to populate
2772
+ * `user$`; 401 triggers a refresh-then-retry inside the session.
1655
2773
  */
1656
- async pollJobUntilComplete(id, options) {
1657
- const interval = options?.interval ?? 1e3;
1658
- const timeout = options?.timeout ?? 6e4;
1659
- const startTime = Date.now();
1660
- while (true) {
1661
- const status = await this.getJobStatus(id, { auth: options?.auth });
1662
- if (options?.onProgress) {
1663
- options.onProgress(status);
1664
- }
1665
- if (status.status === "complete" || status.status === "failed" || status.status === "cancelled") {
1666
- return status;
1667
- }
1668
- if (Date.now() - startTime > timeout) {
1669
- throw new Error(`Job polling timeout after ${timeout}ms`);
1670
- }
1671
- await new Promise((resolve) => setTimeout(resolve, interval));
2774
+ async performValidate(kb, token) {
2775
+ const throwaway = new SemiontApiClient({
2776
+ baseUrl: baseUrl(kbBackendUrl(kb))
2777
+ });
2778
+ try {
2779
+ const data = await throwaway.getMe({ auth: token });
2780
+ return data;
2781
+ } finally {
2782
+ throwaway.dispose();
1672
2783
  }
1673
2784
  }
1674
- // ============================================================================
1675
- // SYSTEM STATUS
1676
- // ============================================================================
1677
- async healthCheck(options) {
1678
- return this.http.get(`${this.baseUrl}/api/health`, {
1679
- headers: this.authHeaders(options)
1680
- }).json();
2785
+ // ── Lifecycle ─────────────────────────────────────────────────────────
2786
+ async dispose() {
2787
+ if (this.disposed) return;
2788
+ this.disposed = true;
2789
+ this.unregisterNotify?.();
2790
+ this.unregisterNotify = null;
2791
+ if (this.unsubscribeStorage) {
2792
+ this.unsubscribeStorage();
2793
+ this.unsubscribeStorage = null;
2794
+ }
2795
+ const prevSession = this.activeSession$.getValue();
2796
+ const prevSignals = this.activeSignals$.getValue();
2797
+ this.activeSession$.next(null);
2798
+ this.activeSignals$.next(null);
2799
+ if (prevSession) await prevSession.dispose();
2800
+ prevSignals?.dispose();
2801
+ this.kbs$.complete();
2802
+ this.activeKbId$.complete();
2803
+ this.activeSession$.complete();
2804
+ this.activeSignals$.complete();
2805
+ this.openResources$.complete();
2806
+ this.error$.complete();
2807
+ this.identityToken$.complete();
2808
+ this.eventBus.destroy();
1681
2809
  }
1682
- async getStatus(options) {
1683
- return this.http.get(`${this.baseUrl}/api/status`, {
1684
- headers: this.authHeaders(options)
1685
- }).json();
2810
+ };
2811
+
2812
+ // src/session/registry.ts
2813
+ var instance = null;
2814
+ function getBrowser(options) {
2815
+ if (!instance) {
2816
+ instance = new SemiontBrowser({ storage: options.storage });
1686
2817
  }
1687
- async browseFiles(dirPath, sort, options) {
1688
- const searchParams = new URLSearchParams();
1689
- if (dirPath) searchParams.append("path", dirPath);
1690
- if (sort) searchParams.append("sort", sort);
1691
- return this.http.get(`${this.baseUrl}/api/browse/files`, {
1692
- searchParams,
1693
- headers: this.authHeaders(options)
1694
- }).json();
2818
+ return instance;
2819
+ }
2820
+
2821
+ // src/session/session-storage.ts
2822
+ var InMemorySessionStorage = class {
2823
+ map = /* @__PURE__ */ new Map();
2824
+ get(key) {
2825
+ return this.map.has(key) ? this.map.get(key) : null;
2826
+ }
2827
+ set(key, value) {
2828
+ this.map.set(key, value);
1695
2829
  }
2830
+ delete(key) {
2831
+ this.map.delete(key);
2832
+ }
2833
+ };
2834
+ function createDisposer() {
2835
+ const sub = new Subscription();
2836
+ return {
2837
+ add: (item) => sub.add(typeof item === "function" ? item : () => item.dispose()),
2838
+ dispose: () => sub.unsubscribe()
2839
+ };
2840
+ }
2841
+ function createSearchPipeline(fetch2, options = {}) {
2842
+ const debounceMs = options.debounceMs ?? 250;
2843
+ const initial = options.initialQuery ?? "";
2844
+ const input$ = new Subject();
2845
+ const query$ = input$.pipe(startWith(initial));
2846
+ const state$ = input$.pipe(
2847
+ startWith(initial),
2848
+ debounceTime(debounceMs),
2849
+ distinctUntilChanged$1(),
2850
+ switchMap((q) => {
2851
+ const trimmed = q.trim();
2852
+ if (!trimmed) {
2853
+ return of({ results: [], isSearching: false });
2854
+ }
2855
+ return fetch2(trimmed).pipe(
2856
+ map((results) => ({
2857
+ results: results ?? [],
2858
+ isSearching: results === void 0
2859
+ })),
2860
+ startWith({ results: [], isSearching: true })
2861
+ );
2862
+ })
2863
+ );
2864
+ return {
2865
+ query$,
2866
+ state$,
2867
+ setQuery: (value) => input$.next(value),
2868
+ dispose: () => input$.complete()
2869
+ };
2870
+ }
2871
+ function createBeckonVM(client) {
2872
+ const subs = [];
2873
+ const hovered$ = new BehaviorSubject(null);
2874
+ subs.push(client.stream("beckon:hover").subscribe(({ annotationId }) => {
2875
+ hovered$.next(annotationId);
2876
+ if (annotationId) {
2877
+ client.emit("beckon:sparkle", { annotationId });
2878
+ }
2879
+ }));
2880
+ subs.push(client.stream("browse:click").subscribe(({ annotationId }) => {
2881
+ client.emit("beckon:focus", { annotationId });
2882
+ }));
2883
+ return {
2884
+ hoveredAnnotationId$: hovered$.asObservable(),
2885
+ hover: (annotationId) => client.emit("beckon:hover", { annotationId }),
2886
+ focus: (annotationId) => client.emit("beckon:focus", { annotationId }),
2887
+ sparkle: (annotationId) => client.emit("beckon:sparkle", { annotationId }),
2888
+ dispose() {
2889
+ subs.forEach((s) => s.unsubscribe());
2890
+ hovered$.complete();
2891
+ }
2892
+ };
2893
+ }
2894
+ var HOVER_DELAY_MS = 150;
2895
+ function createHoverHandlers(emit, delayMs) {
2896
+ let currentHover = null;
2897
+ let timer = null;
2898
+ const cancelTimer = () => {
2899
+ if (timer !== null) {
2900
+ clearTimeout(timer);
2901
+ timer = null;
2902
+ }
2903
+ };
2904
+ const handleMouseEnter = (annotationId) => {
2905
+ if (currentHover === annotationId) return;
2906
+ cancelTimer();
2907
+ timer = setTimeout(() => {
2908
+ timer = null;
2909
+ currentHover = annotationId;
2910
+ emit(annotationId);
2911
+ }, delayMs);
2912
+ };
2913
+ const handleMouseLeave = () => {
2914
+ cancelTimer();
2915
+ if (currentHover !== null) {
2916
+ currentHover = null;
2917
+ emit(null);
2918
+ }
2919
+ };
2920
+ return { handleMouseEnter, handleMouseLeave, cleanup: cancelTimer };
2921
+ }
2922
+ var COMMON_PANELS = ["knowledge-base", "user", "settings"];
2923
+ var RESOURCE_PANELS = ["history", "info", "annotations", "collaboration", "jsonld"];
2924
+ var MOTIVATION_TO_TAB = {
2925
+ "linking": "reference",
2926
+ "commenting": "comment",
2927
+ "tagging": "tag",
2928
+ "highlighting": "highlight",
2929
+ "assessing": "assessment"
1696
2930
  };
2931
+ var tabGenerationCounter = 0;
2932
+ function createShellVM(browser, options) {
2933
+ const subs = [];
2934
+ const activePanel$ = new BehaviorSubject(options?.initialPanel ?? null);
2935
+ const scrollToAnnotationId$ = new BehaviorSubject(null);
2936
+ const panelInitialTab$ = new BehaviorSubject(null);
2937
+ if (options?.onPanelChange) {
2938
+ const cb = options.onPanelChange;
2939
+ subs.push(activePanel$.subscribe(cb));
2940
+ }
2941
+ subs.push(browser.stream("panel:toggle").subscribe(({ panel }) => {
2942
+ const current = activePanel$.getValue();
2943
+ activePanel$.next(current === panel ? null : panel);
2944
+ }));
2945
+ subs.push(browser.stream("panel:open").subscribe(({ panel, scrollToAnnotationId, motivation }) => {
2946
+ if (scrollToAnnotationId) {
2947
+ scrollToAnnotationId$.next(scrollToAnnotationId);
2948
+ }
2949
+ if (motivation) {
2950
+ const tab = MOTIVATION_TO_TAB[motivation] || "highlight";
2951
+ panelInitialTab$.next({ tab, generation: ++tabGenerationCounter });
2952
+ }
2953
+ activePanel$.next(panel);
2954
+ }));
2955
+ subs.push(browser.stream("panel:close").subscribe(() => {
2956
+ activePanel$.next(null);
2957
+ }));
2958
+ return {
2959
+ activePanel$: activePanel$.asObservable(),
2960
+ scrollToAnnotationId$: scrollToAnnotationId$.asObservable(),
2961
+ panelInitialTab$: panelInitialTab$.asObservable(),
2962
+ openPanel: (panel) => browser.emit("panel:open", { panel }),
2963
+ closePanel: () => browser.emit("panel:close", void 0),
2964
+ togglePanel: (panel) => browser.emit("panel:toggle", { panel }),
2965
+ onScrollCompleted: () => scrollToAnnotationId$.next(null),
2966
+ dispose() {
2967
+ subs.forEach((s) => s.unsubscribe());
2968
+ activePanel$.complete();
2969
+ scrollToAnnotationId$.complete();
2970
+ panelInitialTab$.complete();
2971
+ }
2972
+ };
2973
+ }
2974
+ function createGatherVM(client, resourceId) {
2975
+ const subs = [];
2976
+ const context$ = new BehaviorSubject(null);
2977
+ const loading$ = new BehaviorSubject(false);
2978
+ const error$ = new BehaviorSubject(null);
2979
+ const annotationId$ = new BehaviorSubject(null);
2980
+ subs.push(client.stream("gather:requested").subscribe((event) => {
2981
+ loading$.next(true);
2982
+ error$.next(null);
2983
+ context$.next(null);
2984
+ annotationId$.next(annotationId(event.annotationId));
2985
+ const gatherSub = client.gather.annotation(
2986
+ annotationId(event.annotationId),
2987
+ resourceId,
2988
+ { contextWindow: event.options?.contextWindow ?? 2e3 }
2989
+ ).pipe(
2990
+ timeout(6e4)
2991
+ ).subscribe({
2992
+ next: (progress) => {
2993
+ if ("response" in progress && progress.response) {
2994
+ context$.next(
2995
+ progress.response.context ?? null
2996
+ );
2997
+ loading$.next(false);
2998
+ }
2999
+ },
3000
+ error: (err) => {
3001
+ error$.next(err instanceof Error ? err : new Error(String(err)));
3002
+ loading$.next(false);
3003
+ },
3004
+ complete: () => {
3005
+ loading$.next(false);
3006
+ }
3007
+ });
3008
+ subs.push(gatherSub);
3009
+ }));
3010
+ return {
3011
+ context$: context$.asObservable(),
3012
+ loading$: loading$.asObservable(),
3013
+ error$: error$.asObservable(),
3014
+ annotationId$: annotationId$.asObservable(),
3015
+ dispose() {
3016
+ subs.forEach((s) => s.unsubscribe());
3017
+ context$.complete();
3018
+ loading$.complete();
3019
+ error$.complete();
3020
+ annotationId$.complete();
3021
+ }
3022
+ };
3023
+ }
3024
+ function createMatchVM(client, _resourceId) {
3025
+ const subs = [];
3026
+ subs.push(client.stream("match:search-requested").subscribe((event) => {
3027
+ const searchSub = client.match.search(
3028
+ resourceId(event.resourceId),
3029
+ event.referenceId,
3030
+ event.context,
3031
+ { limit: event.limit, useSemanticScoring: event.useSemanticScoring }
3032
+ ).pipe(
3033
+ timeout(6e4)
3034
+ ).subscribe({
3035
+ next: (result) => client.emit("match:search-results", result),
3036
+ error: (err) => client.emit("match:search-failed", {
3037
+ correlationId: event.correlationId,
3038
+ referenceId: event.referenceId,
3039
+ error: err instanceof Error ? err.message : String(err)
3040
+ })
3041
+ });
3042
+ subs.push(searchSub);
3043
+ }));
3044
+ return {
3045
+ dispose() {
3046
+ subs.forEach((s) => s.unsubscribe());
3047
+ }
3048
+ };
3049
+ }
3050
+ function createYieldVM(client, resourceId$1, locale) {
3051
+ const subs = [];
3052
+ const isGenerating$ = new BehaviorSubject(false);
3053
+ const progress$ = new BehaviorSubject(null);
3054
+ let clearTimer = null;
3055
+ const generate = (referenceId, options) => {
3056
+ const genSub = client.yield.fromAnnotation(
3057
+ resourceId(resourceId$1),
3058
+ annotationId(referenceId),
3059
+ { ...options, language: options.language || locale }
3060
+ ).pipe(
3061
+ timeout({ each: 3e5 })
3062
+ ).subscribe({
3063
+ next: (chunk) => {
3064
+ progress$.next(chunk);
3065
+ isGenerating$.next(true);
3066
+ },
3067
+ complete: () => {
3068
+ isGenerating$.next(false);
3069
+ if (clearTimer) clearTimeout(clearTimer);
3070
+ clearTimer = setTimeout(() => {
3071
+ progress$.next(null);
3072
+ clearTimer = null;
3073
+ }, 2e3);
3074
+ },
3075
+ error: () => {
3076
+ progress$.next(null);
3077
+ isGenerating$.next(false);
3078
+ }
3079
+ });
3080
+ subs.push(genSub);
3081
+ };
3082
+ return {
3083
+ isGenerating$: isGenerating$.asObservable(),
3084
+ progress$: progress$.asObservable(),
3085
+ generate,
3086
+ dispose() {
3087
+ subs.forEach((s) => s.unsubscribe());
3088
+ if (clearTimer) clearTimeout(clearTimer);
3089
+ isGenerating$.complete();
3090
+ progress$.complete();
3091
+ }
3092
+ };
3093
+ }
3094
+ function selectionToSelector(selection) {
3095
+ if (selection.svgSelector) return { type: "SvgSelector", value: selection.svgSelector };
3096
+ if (selection.fragmentSelector) {
3097
+ const selectors = [{ type: "FragmentSelector", value: selection.fragmentSelector, ...selection.conformsTo && { conformsTo: selection.conformsTo } }];
3098
+ if (selection.exact) selectors.push({ type: "TextQuoteSelector", exact: selection.exact, ...selection.prefix && { prefix: selection.prefix }, ...selection.suffix && { suffix: selection.suffix } });
3099
+ return selectors;
3100
+ }
3101
+ return { type: "TextQuoteSelector", exact: selection.exact, ...selection.prefix && { prefix: selection.prefix }, ...selection.suffix && { suffix: selection.suffix } };
3102
+ }
3103
+ function createMarkVM(client, resourceId) {
3104
+ const subs = [];
3105
+ const pendingAnnotation$ = new BehaviorSubject(null);
3106
+ const assistingMotivation$ = new BehaviorSubject(null);
3107
+ const progress$ = new BehaviorSubject(null);
3108
+ let progressDismissTimer = null;
3109
+ const clearProgressTimer = () => {
3110
+ if (progressDismissTimer) {
3111
+ clearTimeout(progressDismissTimer);
3112
+ progressDismissTimer = null;
3113
+ }
3114
+ };
3115
+ const handleAnnotationRequested = (pending) => {
3116
+ pendingAnnotation$.next(pending);
3117
+ };
3118
+ subs.push(client.stream("mark:requested").subscribe(handleAnnotationRequested));
3119
+ subs.push(client.stream("mark:select-comment").subscribe((s) => handleAnnotationRequested({ selector: selectionToSelector(s), motivation: "commenting" })));
3120
+ subs.push(client.stream("mark:select-tag").subscribe((s) => handleAnnotationRequested({ selector: selectionToSelector(s), motivation: "tagging" })));
3121
+ subs.push(client.stream("mark:select-assessment").subscribe((s) => handleAnnotationRequested({ selector: selectionToSelector(s), motivation: "assessing" })));
3122
+ subs.push(client.stream("mark:select-reference").subscribe((s) => handleAnnotationRequested({ selector: selectionToSelector(s), motivation: "linking" })));
3123
+ subs.push(client.stream("mark:cancel-pending").subscribe(() => pendingAnnotation$.next(null)));
3124
+ subs.push(client.stream("mark:create-ok").subscribe(() => pendingAnnotation$.next(null)));
3125
+ subs.push(client.stream("mark:submit").subscribe(async (event) => {
3126
+ try {
3127
+ const result = await client.mark.annotation(resourceId, {
3128
+ motivation: event.motivation,
3129
+ target: { source: resourceId, selector: event.selector },
3130
+ body: event.body
3131
+ });
3132
+ client.emit("mark:create-ok", { annotationId: result.annotationId });
3133
+ } catch (error) {
3134
+ client.emit("mark:create-failed", { message: error instanceof Error ? error.message : String(error) });
3135
+ }
3136
+ }));
3137
+ subs.push(client.stream("mark:delete").subscribe(async (event) => {
3138
+ try {
3139
+ await client.mark.delete(resourceId, event.annotationId);
3140
+ client.emit("mark:delete-ok", { annotationId: event.annotationId });
3141
+ } catch (error) {
3142
+ client.emit("mark:delete-failed", { message: error instanceof Error ? error.message : String(error) });
3143
+ }
3144
+ }));
3145
+ subs.push(client.stream("mark:assist-request").subscribe((event) => {
3146
+ clearProgressTimer();
3147
+ assistingMotivation$.next(event.motivation);
3148
+ progress$.next(null);
3149
+ const assistSub = client.mark.assist(resourceId, event.motivation, event.options).pipe(
3150
+ timeout({ each: 18e4 })
3151
+ ).subscribe({
3152
+ next: (p) => progress$.next(p),
3153
+ complete: () => {
3154
+ assistingMotivation$.next(null);
3155
+ clearProgressTimer();
3156
+ progressDismissTimer = setTimeout(() => {
3157
+ progress$.next(null);
3158
+ progressDismissTimer = null;
3159
+ }, 5e3);
3160
+ },
3161
+ error: () => {
3162
+ clearProgressTimer();
3163
+ assistingMotivation$.next(null);
3164
+ progress$.next(null);
3165
+ }
3166
+ });
3167
+ subs.push(assistSub);
3168
+ }));
3169
+ subs.push(client.stream("mark:progress-dismiss").subscribe(() => {
3170
+ clearProgressTimer();
3171
+ progress$.next(null);
3172
+ }));
3173
+ return {
3174
+ pendingAnnotation$: pendingAnnotation$.asObservable(),
3175
+ assistingMotivation$: assistingMotivation$.asObservable(),
3176
+ progress$: progress$.asObservable(),
3177
+ dispose() {
3178
+ subs.forEach((s) => s.unsubscribe());
3179
+ clearProgressTimer();
3180
+ pendingAnnotation$.complete();
3181
+ assistingMotivation$.complete();
3182
+ progress$.complete();
3183
+ }
3184
+ };
3185
+ }
3186
+ var RECENT_LIMIT = 10;
3187
+ var SEARCH_LIMIT = 20;
3188
+ function createDiscoverVM(client, browse) {
3189
+ const disposer = createDisposer();
3190
+ const search = createSearchPipeline(
3191
+ (q) => client.browse.resources({ search: q, limit: SEARCH_LIMIT })
3192
+ );
3193
+ disposer.add(search);
3194
+ disposer.add(browse);
3195
+ const recent$ = client.browse.resources({ limit: RECENT_LIMIT, archived: false });
3196
+ const recentResources$ = recent$.pipe(
3197
+ map$1((r) => r ?? [])
3198
+ );
3199
+ const isLoadingRecent$ = recent$.pipe(
3200
+ map$1((r) => r === void 0)
3201
+ );
3202
+ const entityTypes$ = client.browse.entityTypes().pipe(
3203
+ map$1((e) => e ?? [])
3204
+ );
3205
+ return {
3206
+ browse,
3207
+ search,
3208
+ recentResources$,
3209
+ entityTypes$,
3210
+ isLoadingRecent$,
3211
+ dispose: () => disposer.dispose()
3212
+ };
3213
+ }
3214
+ function createEntityTagsVM(client, browse) {
3215
+ const disposer = createDisposer();
3216
+ disposer.add(browse);
3217
+ const newTag$ = new BehaviorSubject("");
3218
+ const error$ = new BehaviorSubject("");
3219
+ const isAdding$ = new BehaviorSubject(false);
3220
+ const raw$ = client.browse.entityTypes();
3221
+ const entityTypes$ = raw$.pipe(map$1((e) => e ?? []));
3222
+ const isLoading$ = raw$.pipe(map$1((e) => e === void 0));
3223
+ const addTag = async () => {
3224
+ const tag = newTag$.getValue().trim();
3225
+ if (!tag) return;
3226
+ error$.next("");
3227
+ isAdding$.next(true);
3228
+ try {
3229
+ await client.mark.entityType(tag);
3230
+ newTag$.next("");
3231
+ } catch (err) {
3232
+ error$.next(err instanceof Error ? err.message : "Failed to add entity type");
3233
+ } finally {
3234
+ isAdding$.next(false);
3235
+ }
3236
+ };
3237
+ return {
3238
+ browse,
3239
+ entityTypes$,
3240
+ isLoading$,
3241
+ newTag$: newTag$.asObservable(),
3242
+ error$: error$.asObservable(),
3243
+ isAdding$: isAdding$.asObservable(),
3244
+ setNewTag: (v) => newTag$.next(v),
3245
+ addTag,
3246
+ dispose: () => {
3247
+ newTag$.complete();
3248
+ error$.complete();
3249
+ isAdding$.complete();
3250
+ disposer.dispose();
3251
+ }
3252
+ };
3253
+ }
3254
+ function createExchangeVM(browse, exportFn, importFn) {
3255
+ const disposer = createDisposer();
3256
+ disposer.add(browse);
3257
+ const selectedFile$ = new BehaviorSubject(null);
3258
+ const preview$ = new BehaviorSubject(null);
3259
+ const importPhase$ = new BehaviorSubject(null);
3260
+ const importMessage$ = new BehaviorSubject(void 0);
3261
+ const importResult$ = new BehaviorSubject(void 0);
3262
+ const isExporting$ = new BehaviorSubject(false);
3263
+ const isImporting$ = new BehaviorSubject(false);
3264
+ const selectFile = (file) => {
3265
+ selectedFile$.next(file);
3266
+ importPhase$.next(null);
3267
+ importMessage$.next(void 0);
3268
+ importResult$.next(void 0);
3269
+ preview$.next({
3270
+ format: file.name.endsWith(".tar.gz") || file.name.endsWith(".gz") ? "semiont-linked-data" : "unknown",
3271
+ version: 1,
3272
+ sourceUrl: "",
3273
+ stats: {}
3274
+ });
3275
+ };
3276
+ const cancelImport = () => {
3277
+ selectedFile$.next(null);
3278
+ preview$.next(null);
3279
+ importPhase$.next(null);
3280
+ importMessage$.next(void 0);
3281
+ importResult$.next(void 0);
3282
+ };
3283
+ const doExport = async () => {
3284
+ isExporting$.next(true);
3285
+ try {
3286
+ const response = await exportFn();
3287
+ if (!response.ok) throw new Error(`Export failed: ${response.status} ${response.statusText}`);
3288
+ const blob = await response.blob();
3289
+ const contentDisposition = response.headers.get("Content-Disposition");
3290
+ const filename = contentDisposition?.match(/filename="(.+?)"/)?.[1] ?? `semiont-export-${Date.now()}.tar.gz`;
3291
+ return { blob, filename };
3292
+ } finally {
3293
+ isExporting$.next(false);
3294
+ }
3295
+ };
3296
+ const doImport = async () => {
3297
+ const file = selectedFile$.getValue();
3298
+ if (!file) return;
3299
+ isImporting$.next(true);
3300
+ importPhase$.next("started");
3301
+ importMessage$.next(void 0);
3302
+ importResult$.next(void 0);
3303
+ try {
3304
+ await importFn(file, {
3305
+ onProgress: (event) => {
3306
+ importPhase$.next(event.phase);
3307
+ importMessage$.next(event.message);
3308
+ if (event.result) importResult$.next(event.result);
3309
+ }
3310
+ });
3311
+ } finally {
3312
+ isImporting$.next(false);
3313
+ }
3314
+ };
3315
+ return {
3316
+ browse,
3317
+ selectedFile$: selectedFile$.asObservable(),
3318
+ preview$: preview$.asObservable(),
3319
+ importPhase$: importPhase$.asObservable(),
3320
+ importMessage$: importMessage$.asObservable(),
3321
+ importResult$: importResult$.asObservable(),
3322
+ isExporting$: isExporting$.asObservable(),
3323
+ isImporting$: isImporting$.asObservable(),
3324
+ selectFile,
3325
+ cancelImport,
3326
+ doExport,
3327
+ doImport,
3328
+ dispose: () => {
3329
+ selectedFile$.complete();
3330
+ preview$.complete();
3331
+ importPhase$.complete();
3332
+ importMessage$.complete();
3333
+ importResult$.complete();
3334
+ isExporting$.complete();
3335
+ isImporting$.complete();
3336
+ disposer.dispose();
3337
+ }
3338
+ };
3339
+ }
3340
+ function createAdminUsersVM(client, browse) {
3341
+ const disposer = createDisposer();
3342
+ disposer.add(browse);
3343
+ const users$ = new BehaviorSubject([]);
3344
+ const stats$ = new BehaviorSubject(null);
3345
+ const usersLoading$ = new BehaviorSubject(true);
3346
+ const statsLoading$ = new BehaviorSubject(true);
3347
+ const fetchUsers = () => {
3348
+ usersLoading$.next(true);
3349
+ client.listUsers().then((data) => {
3350
+ users$.next(data.users ?? []);
3351
+ usersLoading$.next(false);
3352
+ }).catch(() => usersLoading$.next(false));
3353
+ };
3354
+ const fetchStats = () => {
3355
+ statsLoading$.next(true);
3356
+ client.getUserStats().then((data) => {
3357
+ stats$.next(data.stats ?? null);
3358
+ statsLoading$.next(false);
3359
+ }).catch(() => statsLoading$.next(false));
3360
+ };
3361
+ fetchUsers();
3362
+ fetchStats();
3363
+ const updateUser = async (id, data) => {
3364
+ await client.updateUser(userDID(id), data);
3365
+ fetchUsers();
3366
+ fetchStats();
3367
+ };
3368
+ return {
3369
+ browse,
3370
+ users$: users$.asObservable(),
3371
+ stats$: stats$.asObservable(),
3372
+ usersLoading$: usersLoading$.asObservable(),
3373
+ statsLoading$: statsLoading$.asObservable(),
3374
+ updateUser,
3375
+ dispose: () => {
3376
+ users$.complete();
3377
+ stats$.complete();
3378
+ usersLoading$.complete();
3379
+ statsLoading$.complete();
3380
+ disposer.dispose();
3381
+ }
3382
+ };
3383
+ }
3384
+ function createAdminSecurityVM(client, browse) {
3385
+ const disposer = createDisposer();
3386
+ disposer.add(browse);
3387
+ const providers$ = new BehaviorSubject([]);
3388
+ const allowedDomains$ = new BehaviorSubject([]);
3389
+ const isLoading$ = new BehaviorSubject(true);
3390
+ client.getOAuthConfig().then((data) => {
3391
+ const config = data;
3392
+ providers$.next(config.providers ?? []);
3393
+ allowedDomains$.next(config.allowedDomains ?? []);
3394
+ isLoading$.next(false);
3395
+ }).catch(() => isLoading$.next(false));
3396
+ return {
3397
+ browse,
3398
+ providers$: providers$.asObservable(),
3399
+ allowedDomains$: allowedDomains$.asObservable(),
3400
+ isLoading$: isLoading$.asObservable(),
3401
+ dispose: () => {
3402
+ providers$.complete();
3403
+ allowedDomains$.complete();
3404
+ isLoading$.complete();
3405
+ disposer.dispose();
3406
+ }
3407
+ };
3408
+ }
3409
+ function createWelcomeVM(client) {
3410
+ const disposer = createDisposer();
3411
+ const userData$ = new BehaviorSubject(null);
3412
+ const isProcessing$ = new BehaviorSubject(false);
3413
+ client.getMe().then((data) => userData$.next(data)).catch(() => {
3414
+ });
3415
+ const acceptTerms = async () => {
3416
+ isProcessing$.next(true);
3417
+ try {
3418
+ await client.acceptTerms();
3419
+ userData$.next({ ...userData$.getValue(), termsAcceptedAt: (/* @__PURE__ */ new Date()).toISOString() });
3420
+ } finally {
3421
+ isProcessing$.next(false);
3422
+ }
3423
+ };
3424
+ return {
3425
+ userData$: userData$.asObservable(),
3426
+ isProcessing$: isProcessing$.asObservable(),
3427
+ acceptTerms,
3428
+ dispose: () => {
3429
+ userData$.complete();
3430
+ isProcessing$.complete();
3431
+ disposer.dispose();
3432
+ }
3433
+ };
3434
+ }
3435
+ function createResourceLoaderVM(client, resourceId) {
3436
+ const raw$ = client.browse.resource(resourceId);
3437
+ const resource$ = raw$;
3438
+ const isLoading$ = raw$.pipe(map$1((r) => r === void 0));
3439
+ return {
3440
+ resource$,
3441
+ isLoading$,
3442
+ invalidate: () => client.browse.invalidateResourceDetail(resourceId),
3443
+ dispose: () => {
3444
+ }
3445
+ };
3446
+ }
3447
+ function createSessionVM(client) {
3448
+ const isLoggingOut$ = new BehaviorSubject(false);
3449
+ const logout = async () => {
3450
+ isLoggingOut$.next(true);
3451
+ try {
3452
+ await client.logout();
3453
+ } catch {
3454
+ } finally {
3455
+ isLoggingOut$.next(false);
3456
+ }
3457
+ };
3458
+ return {
3459
+ isLoggingOut$: isLoggingOut$.asObservable(),
3460
+ logout,
3461
+ dispose: () => {
3462
+ isLoggingOut$.complete();
3463
+ }
3464
+ };
3465
+ }
3466
+ var SMELTER_CHANNELS = [
3467
+ "yield:created",
3468
+ "yield:updated",
3469
+ "yield:representation-added",
3470
+ "mark:archived",
3471
+ "mark:added",
3472
+ "mark:removed"
3473
+ ];
3474
+ function createSmelterActorVM(options) {
3475
+ const actor = createActorVM({
3476
+ baseUrl: options.baseUrl,
3477
+ token: options.token,
3478
+ channels: [...SMELTER_CHANNELS],
3479
+ reconnectMs: options.reconnectMs
3480
+ });
3481
+ const events$ = merge(
3482
+ ...SMELTER_CHANNELS.map(
3483
+ (channel) => actor.on$(channel).pipe(
3484
+ map((payload) => ({
3485
+ type: channel,
3486
+ resourceId: payload.resourceId,
3487
+ payload
3488
+ }))
3489
+ )
3490
+ )
3491
+ );
3492
+ return {
3493
+ events$,
3494
+ state$: actor.state$,
3495
+ emit: (channel, payload) => actor.emit(channel, payload),
3496
+ start: () => actor.start(),
3497
+ stop: () => actor.stop(),
3498
+ dispose: () => actor.dispose()
3499
+ };
3500
+ }
3501
+ function createJobClaimAdapter(options) {
3502
+ const { actor, jobTypes } = options;
3503
+ const activeJob$ = new BehaviorSubject(null);
3504
+ const isProcessing$ = new BehaviorSubject(false);
3505
+ const jobsCompleted$ = new BehaviorSubject(0);
3506
+ const errors$ = new Subject();
3507
+ let jobSubscription = null;
3508
+ let started = false;
3509
+ const claimJob = async (assignment) => {
3510
+ try {
3511
+ const correlationId = crypto.randomUUID();
3512
+ const result$ = merge(
3513
+ actor.on$("job:claimed").pipe(
3514
+ filter$1((e) => e.correlationId === correlationId),
3515
+ map$1((e) => ({ ok: true, response: e.response }))
3516
+ ),
3517
+ actor.on$("job:claim-failed").pipe(
3518
+ filter$1((e) => e.correlationId === correlationId),
3519
+ map$1(() => ({ ok: false }))
3520
+ )
3521
+ ).pipe(take$1(1), timeout$1(1e4));
3522
+ const resultPromise = firstValueFrom(result$);
3523
+ await actor.emit("job:claim", { correlationId, jobId: assignment.jobId });
3524
+ const result = await resultPromise;
3525
+ if (!result.ok) return null;
3526
+ const job = result.response;
3527
+ return {
3528
+ jobId: assignment.jobId,
3529
+ type: assignment.type,
3530
+ resourceId: assignment.resourceId,
3531
+ userId: job.metadata?.userId ?? "",
3532
+ params: job.params ?? {}
3533
+ };
3534
+ } catch {
3535
+ return null;
3536
+ }
3537
+ };
3538
+ return {
3539
+ activeJob$: activeJob$.asObservable(),
3540
+ isProcessing$: isProcessing$.asObservable(),
3541
+ jobsCompleted$: jobsCompleted$.asObservable(),
3542
+ errors$: errors$.asObservable(),
3543
+ start: () => {
3544
+ if (started) return;
3545
+ started = true;
3546
+ actor.addChannels(["job:queued"]);
3547
+ jobSubscription = actor.on$("job:queued").subscribe((event) => {
3548
+ const jobType = event.jobType;
3549
+ if (jobTypes.length > 0 && !jobTypes.includes(jobType)) return;
3550
+ if (isProcessing$.getValue()) return;
3551
+ isProcessing$.next(true);
3552
+ claimJob({ jobId: event.jobId, type: jobType, resourceId: event.resourceId }).then((job) => {
3553
+ if (job) {
3554
+ activeJob$.next(job);
3555
+ } else {
3556
+ isProcessing$.next(false);
3557
+ }
3558
+ }).catch(() => {
3559
+ isProcessing$.next(false);
3560
+ });
3561
+ });
3562
+ },
3563
+ stop: () => {
3564
+ jobSubscription?.unsubscribe();
3565
+ jobSubscription = null;
3566
+ started = false;
3567
+ },
3568
+ completeJob: () => {
3569
+ activeJob$.next(null);
3570
+ isProcessing$.next(false);
3571
+ jobsCompleted$.next(jobsCompleted$.getValue() + 1);
3572
+ },
3573
+ failJob: (jid, error) => {
3574
+ activeJob$.next(null);
3575
+ isProcessing$.next(false);
3576
+ errors$.next({ jobId: jid, error });
3577
+ },
3578
+ dispose: () => {
3579
+ jobSubscription?.unsubscribe();
3580
+ jobSubscription = null;
3581
+ started = false;
3582
+ activeJob$.complete();
3583
+ isProcessing$.complete();
3584
+ jobsCompleted$.complete();
3585
+ errors$.complete();
3586
+ }
3587
+ };
3588
+ }
3589
+ function createJobQueueVM(client) {
3590
+ const jobs$ = new BehaviorSubject([]);
3591
+ const jobCreated$ = new Subject();
3592
+ const jobCompleted$ = new Subject();
3593
+ const jobFailed$ = new Subject();
3594
+ const pendingByType$ = jobs$.pipe(
3595
+ map$1((all) => {
3596
+ const counts = /* @__PURE__ */ new Map();
3597
+ for (const j of all) {
3598
+ if (j.status === "pending") {
3599
+ counts.set(j.type, (counts.get(j.type) ?? 0) + 1);
3600
+ }
3601
+ }
3602
+ return counts;
3603
+ })
3604
+ );
3605
+ const runningJobs$ = jobs$.pipe(
3606
+ map$1((all) => all.filter((j) => j.status === "running"))
3607
+ );
3608
+ const addOrUpdate = (job) => {
3609
+ const current = jobs$.getValue();
3610
+ const idx = current.findIndex((j) => j.jobId === job.jobId);
3611
+ if (idx >= 0) {
3612
+ const next = [...current];
3613
+ next[idx] = job;
3614
+ jobs$.next(next);
3615
+ } else {
3616
+ jobs$.next([...current, job]);
3617
+ }
3618
+ };
3619
+ const subs = [
3620
+ client.stream("job:queued").subscribe((event) => {
3621
+ const job = {
3622
+ jobId: event.jobId,
3623
+ type: event.jobType,
3624
+ status: "pending",
3625
+ resourceId: event.resourceId ?? "",
3626
+ userId: "",
3627
+ created: (/* @__PURE__ */ new Date()).toISOString()
3628
+ };
3629
+ addOrUpdate(job);
3630
+ jobCreated$.next(job);
3631
+ }),
3632
+ client.stream("job:complete").subscribe((event) => {
3633
+ const job = {
3634
+ jobId: event.jobId,
3635
+ type: event.jobType ?? "",
3636
+ status: "complete",
3637
+ resourceId: event.resourceId ?? "",
3638
+ userId: event.userId ?? "",
3639
+ created: "",
3640
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
3641
+ result: event.result
3642
+ };
3643
+ addOrUpdate(job);
3644
+ jobCompleted$.next(job);
3645
+ }),
3646
+ client.stream("job:fail").subscribe((event) => {
3647
+ const job = {
3648
+ jobId: event.jobId,
3649
+ type: event.jobType ?? "",
3650
+ status: "failed",
3651
+ resourceId: event.resourceId ?? "",
3652
+ userId: event.userId ?? "",
3653
+ created: "",
3654
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
3655
+ error: event.error ?? "Unknown error"
3656
+ };
3657
+ addOrUpdate(job);
3658
+ jobFailed$.next(job);
3659
+ })
3660
+ ];
3661
+ return {
3662
+ jobs$: jobs$.asObservable(),
3663
+ pendingByType$,
3664
+ runningJobs$,
3665
+ jobCreated$: jobCreated$.asObservable(),
3666
+ jobCompleted$: jobCompleted$.asObservable(),
3667
+ jobFailed$: jobFailed$.asObservable(),
3668
+ dispose: () => {
3669
+ subs.forEach((s) => s.unsubscribe());
3670
+ jobs$.complete();
3671
+ jobCreated$.complete();
3672
+ jobCompleted$.complete();
3673
+ jobFailed$.complete();
3674
+ }
3675
+ };
3676
+ }
3677
+
3678
+ // src/utils/text-encoding.ts
3679
+ function extractCharset(mediaType) {
3680
+ const charsetMatch = mediaType.match(/charset=([^\s;]+)/i);
3681
+ return (charsetMatch?.[1] || "utf-8").toLowerCase();
3682
+ }
3683
+ function decodeWithCharset(buffer, mediaType) {
3684
+ const charset = extractCharset(mediaType);
3685
+ const decoder = new TextDecoder(charset);
3686
+ return decoder.decode(buffer);
3687
+ }
1697
3688
  function getBodySource(body) {
1698
3689
  if (Array.isArray(body)) {
1699
3690
  for (const item of body) {
@@ -1847,6 +3838,115 @@ function extractBoundingBox(svg) {
1847
3838
  return null;
1848
3839
  }
1849
3840
 
3841
+ // src/view-models/pages/resource-viewer-page-vm.ts
3842
+ var WIZARD_CLOSED = {
3843
+ open: false,
3844
+ annotationId: null,
3845
+ resourceId: null,
3846
+ defaultTitle: "",
3847
+ entityTypes: []
3848
+ };
3849
+ function createResourceViewerPageVM(client, resourceId, locale, browse, options) {
3850
+ const disposer = createDisposer();
3851
+ const beckon = createBeckonVM(client);
3852
+ const mark = createMarkVM(client, resourceId);
3853
+ const gather = createGatherVM(client, resourceId);
3854
+ const matchVM = createMatchVM(client);
3855
+ const yieldVM = createYieldVM(client, resourceId, locale);
3856
+ disposer.add(beckon);
3857
+ disposer.add(browse);
3858
+ disposer.add(mark);
3859
+ disposer.add(gather);
3860
+ disposer.add(matchVM);
3861
+ disposer.add(yieldVM);
3862
+ const annotations$ = client.browse.annotations(resourceId).pipe(
3863
+ map$1((a) => a ?? [])
3864
+ );
3865
+ const annotationGroups$ = annotations$.pipe(
3866
+ map$1((anns) => {
3867
+ const groups = { highlights: [], comments: [], assessments: [], references: [], tags: [] };
3868
+ for (const ann of anns) {
3869
+ if (isHighlight(ann)) groups.highlights.push(ann);
3870
+ else if (isComment(ann)) groups.comments.push(ann);
3871
+ else if (isAssessment(ann)) groups.assessments.push(ann);
3872
+ else if (isReference(ann)) groups.references.push(ann);
3873
+ else if (isTag(ann)) groups.tags.push(ann);
3874
+ }
3875
+ return groups;
3876
+ })
3877
+ );
3878
+ const entityTypes$ = client.browse.entityTypes().pipe(
3879
+ map$1((e) => e ?? [])
3880
+ );
3881
+ const events$ = client.browse.events(resourceId).pipe(
3882
+ map$1((e) => e ?? [])
3883
+ );
3884
+ const referencedBy$ = client.browse.referencedBy(resourceId).pipe(
3885
+ map$1((r) => r ?? [])
3886
+ );
3887
+ const content$ = new BehaviorSubject("");
3888
+ const contentLoading$ = new BehaviorSubject(false);
3889
+ const mediaToken$ = new BehaviorSubject(null);
3890
+ const mediaType = options?.mediaType || "text/plain";
3891
+ const isBinaryType = mediaType.startsWith("image/") || mediaType === "application/pdf";
3892
+ if (!isBinaryType && mediaType) {
3893
+ contentLoading$.next(true);
3894
+ client.browse.resourceRepresentation(resourceId, { accept: mediaType }).then(({ data }) => {
3895
+ content$.next(decodeWithCharset(data, mediaType));
3896
+ contentLoading$.next(false);
3897
+ }).catch(() => {
3898
+ contentLoading$.next(false);
3899
+ });
3900
+ }
3901
+ if (isBinaryType) {
3902
+ client.auth.mediaToken(resourceId).then(({ token }) => mediaToken$.next(token)).catch(() => {
3903
+ });
3904
+ }
3905
+ const wizard$ = new BehaviorSubject(WIZARD_CLOSED);
3906
+ const unsubscribeResource = client.subscribeToResource(resourceId);
3907
+ disposer.add(unsubscribeResource);
3908
+ const bindInitiateSub = client.stream("bind:initiate").subscribe((event) => {
3909
+ wizard$.next({
3910
+ open: true,
3911
+ annotationId: event.annotationId,
3912
+ resourceId: event.resourceId,
3913
+ defaultTitle: event.defaultTitle,
3914
+ entityTypes: event.entityTypes
3915
+ });
3916
+ client.emit("gather:requested", {
3917
+ correlationId: crypto.randomUUID(),
3918
+ annotationId: event.annotationId,
3919
+ resourceId: event.resourceId,
3920
+ options: { contextWindow: 2e3 }
3921
+ });
3922
+ });
3923
+ disposer.add(() => bindInitiateSub.unsubscribe());
3924
+ return {
3925
+ beckon,
3926
+ browse,
3927
+ mark,
3928
+ gather,
3929
+ yield: yieldVM,
3930
+ annotations$,
3931
+ annotationGroups$,
3932
+ entityTypes$,
3933
+ events$,
3934
+ referencedBy$,
3935
+ content$: content$.asObservable(),
3936
+ contentLoading$: contentLoading$.asObservable(),
3937
+ mediaToken$: mediaToken$.asObservable(),
3938
+ wizard$: wizard$.asObservable(),
3939
+ closeWizard: () => wizard$.next(WIZARD_CLOSED),
3940
+ dispose: () => {
3941
+ wizard$.complete();
3942
+ content$.complete();
3943
+ contentLoading$.complete();
3944
+ mediaToken$.complete();
3945
+ disposer.dispose();
3946
+ }
3947
+ };
3948
+ }
3949
+
1850
3950
  // src/utils/fuzzy-anchor.ts
1851
3951
  function normalizeText(text) {
1852
3952
  return text.replace(/\s+/g, " ").replace(/[\u2018\u2019]/g, "'").replace(/[\u201C\u201D]/g, '"').replace(/\u2014/g, "--").replace(/\u2013/g, "-").trim();
@@ -2277,17 +4377,6 @@ function validateAndCorrectOffsets(content, aiStart, aiEnd, exact) {
2277
4377
  };
2278
4378
  }
2279
4379
 
2280
- // src/utils/text-encoding.ts
2281
- function extractCharset(mediaType) {
2282
- const charsetMatch = mediaType.match(/charset=([^\s;]+)/i);
2283
- return (charsetMatch?.[1] || "utf-8").toLowerCase();
2284
- }
2285
- function decodeWithCharset(buffer, mediaType) {
2286
- const charset = extractCharset(mediaType);
2287
- const decoder = new TextDecoder(charset);
2288
- return decoder.decode(buffer);
2289
- }
2290
-
2291
4380
  // src/utils/validation.ts
2292
4381
  var JWTTokenSchema = {
2293
4382
  parse(token) {
@@ -2334,16 +4423,128 @@ function isValidEmail(email) {
2334
4423
  return emailRegex.test(email);
2335
4424
  }
2336
4425
 
4426
+ // src/view-models/pages/compose-page-vm.ts
4427
+ function createComposePageVM(client, browse, params, auth) {
4428
+ const disposer = createDisposer();
4429
+ disposer.add(browse);
4430
+ const isReferenceMode = Boolean(params.annotationUri && params.sourceDocumentId && params.name);
4431
+ const isCloneMode = params.mode === "clone" && Boolean(params.token);
4432
+ const pageMode = isCloneMode ? "clone" : isReferenceMode ? "reference" : "new";
4433
+ const mode$ = new BehaviorSubject(pageMode);
4434
+ const loading$ = new BehaviorSubject(true);
4435
+ const cloneData$ = new BehaviorSubject(null);
4436
+ const referenceData$ = new BehaviorSubject(null);
4437
+ const gatheredContext$ = new BehaviorSubject(null);
4438
+ const entityTypes$ = client.browse.entityTypes().pipe(
4439
+ map$1((e) => e ?? [])
4440
+ );
4441
+ if (isReferenceMode) {
4442
+ const entityTypes = params.entityTypes ? params.entityTypes.split(",") : [];
4443
+ referenceData$.next({
4444
+ annotationUri: params.annotationUri,
4445
+ sourceDocumentId: params.sourceDocumentId,
4446
+ name: params.name,
4447
+ entityTypes
4448
+ });
4449
+ if (params.storedContext) {
4450
+ try {
4451
+ gatheredContext$.next(JSON.parse(params.storedContext));
4452
+ } catch {
4453
+ }
4454
+ }
4455
+ loading$.next(false);
4456
+ } else if (isCloneMode) {
4457
+ void (async () => {
4458
+ try {
4459
+ const tokenResult = await client.yield.fromToken(params.token);
4460
+ if (tokenResult && auth) {
4461
+ const rId = resourceId(tokenResult["@id"]);
4462
+ const mediaType = getPrimaryMediaType(tokenResult) || "text/plain";
4463
+ const { data } = await client.getResourceRepresentation(rId, {
4464
+ accept: mediaType,
4465
+ auth
4466
+ });
4467
+ const content = decodeWithCharset(data, mediaType);
4468
+ cloneData$.next({ sourceResource: tokenResult, sourceContent: content });
4469
+ }
4470
+ } catch {
4471
+ }
4472
+ loading$.next(false);
4473
+ })();
4474
+ } else {
4475
+ loading$.next(false);
4476
+ }
4477
+ const save = async (saveParams) => {
4478
+ if (saveParams.mode === "clone") {
4479
+ const response2 = await client.yield.createFromToken({
4480
+ token: params.token,
4481
+ name: saveParams.name,
4482
+ content: saveParams.content,
4483
+ archiveOriginal: saveParams.archiveOriginal ?? true
4484
+ });
4485
+ return response2.resourceId;
4486
+ }
4487
+ let fileToUpload;
4488
+ let mimeType;
4489
+ if (saveParams.file) {
4490
+ fileToUpload = saveParams.file;
4491
+ mimeType = saveParams.format ?? "application/octet-stream";
4492
+ } else {
4493
+ const blob = new Blob([saveParams.content || ""], { type: saveParams.format ?? "application/octet-stream" });
4494
+ const extension = saveParams.format === "text/plain" ? ".txt" : saveParams.format === "text/html" ? ".html" : ".md";
4495
+ fileToUpload = new File([blob], saveParams.name + extension, { type: saveParams.format ?? "application/octet-stream" });
4496
+ mimeType = saveParams.format ?? "application/octet-stream";
4497
+ }
4498
+ const format = saveParams.charset && !saveParams.file ? `${mimeType}; charset=${saveParams.charset}` : mimeType;
4499
+ const response = await client.yield.resource({
4500
+ name: saveParams.name,
4501
+ file: fileToUpload,
4502
+ format,
4503
+ entityTypes: saveParams.entityTypes || [],
4504
+ language: saveParams.language,
4505
+ creationMethod: "ui",
4506
+ storageUri: saveParams.storageUri
4507
+ });
4508
+ const newResourceId = response.resourceId;
4509
+ if (saveParams.mode === "reference" && saveParams.annotationUri && saveParams.sourceDocumentId) {
4510
+ await client.bind.body(
4511
+ resourceId(saveParams.sourceDocumentId),
4512
+ annotationId(saveParams.annotationUri),
4513
+ [{ op: "add", item: { type: "SpecificResource", source: newResourceId, purpose: "linking" } }]
4514
+ );
4515
+ }
4516
+ return newResourceId;
4517
+ };
4518
+ return {
4519
+ browse,
4520
+ mode$: mode$.asObservable(),
4521
+ loading$: loading$.asObservable(),
4522
+ cloneData$: cloneData$.asObservable(),
4523
+ referenceData$: referenceData$.asObservable(),
4524
+ gatheredContext$: gatheredContext$.asObservable(),
4525
+ entityTypes$,
4526
+ save,
4527
+ dispose: () => {
4528
+ mode$.complete();
4529
+ loading$.complete();
4530
+ cloneData$.complete();
4531
+ referenceData$.complete();
4532
+ gatheredContext$.complete();
4533
+ disposer.dispose();
4534
+ }
4535
+ };
4536
+ }
4537
+
2337
4538
  // src/mime-utils.ts
2338
4539
  function getExtensionForMimeType(mimeType) {
2339
- const map3 = {
4540
+ const map15 = {
2340
4541
  "text/plain": "txt",
2341
4542
  "text/markdown": "md",
2342
4543
  "image/png": "png",
2343
4544
  "image/jpeg": "jpg",
2344
4545
  "application/pdf": "pdf"
2345
4546
  };
2346
- return map3[mimeType] || "dat";
4547
+ return map15[mimeType] || "dat";
2347
4548
  }
2348
4549
  function isImageMimeType(mimeType) {
2349
4550
  return mimeType === "image/png" || mimeType === "image/jpeg";
@@ -2364,6 +4565,6 @@ function getMimeCategory(mimeType) {
2364
4565
  return "unsupported";
2365
4566
  }
2366
4567
 
2367
- export { APIError, AdminNamespace, AuthNamespace, BeckonNamespace, BindNamespace, BrowseNamespace, GatherNamespace, JWTTokenSchema, JobNamespace, LOCALES, MarkNamespace, MatchNamespace, SSEClient, SSE_STREAM_CONNECTED, SemiontApiClient, YieldNamespace, buildContentCache, createCircleSvg, createPolygonSvg, createRectangleSvg, decodeRepresentation, decodeWithCharset, extractBoundingBox, extractCharset, extractContext, findBestTextMatch, findTextWithContext, formatLocaleDisplay, getAllLocaleCodes, getAnnotationExactText, getBodySource, getBodyType, getChecksum, getCommentText, getCreator, getDerivedFrom, getExactText, getExtensionForMimeType, getLanguage, getLocaleEnglishName, getLocaleInfo, getLocaleNativeName, getMimeCategory, getNodeEncoding, getPrimaryMediaType, getPrimaryRepresentation, getPrimarySelector, getResourceEntityTypes, getResourceId, getStorageUri, getTargetSelector, getTargetSource, getTextQuoteSelector, hasTargetSelector, isArchived, isAssessment, isBodyResolved, isComment, isDraft, isHighlight, isImageMimeType, isPdfMimeType, isReference, isResolvedReference, isStubReference, isTag, isTextMimeType, isValidEmail, normalizeCoordinates, normalizeText, parseSvgSelector, scaleSvgToNative, validateAndCorrectOffsets, validateData, verifyPosition };
4568
+ export { APIError, AdminNamespace, AuthNamespace, BeckonNamespace, BindNamespace, BrowseNamespace, BusRequestError, COMMON_PANELS, DEGRADED_THRESHOLD_MS, FrontendSessionSignals, GatherNamespace, HOVER_DELAY_MS, InMemorySessionStorage, JWTTokenSchema, JobNamespace, LOCALES, MarkNamespace, MatchNamespace, RESOURCE_PANELS, SemiontApiClient, SemiontBrowser, SemiontError, SemiontSession, YieldNamespace, buildContentCache, busRequest, createActorVM, createAdminSecurityVM, createAdminUsersVM, createBeckonVM, createCache, createCircleSvg, createComposePageVM, createDiscoverVM, createDisposer, createEntityTagsVM, createExchangeVM, createGatherVM, createHoverHandlers, createJobClaimAdapter, createJobQueueVM, createMarkVM, createMatchVM, createPolygonSvg, createRectangleSvg, createResourceLoaderVM, createResourceViewerPageVM, createSearchPipeline, createSessionVM, createShellVM, createSmelterActorVM, createWelcomeVM, createYieldVM, decodeRepresentation, decodeWithCharset, defaultProtocol, extractBoundingBox, extractCharset, extractContext, findBestTextMatch, findTextWithContext, formatLocaleDisplay, getAllLocaleCodes, getAnnotationExactText, getBodySource, getBodyType, getBrowser, getChecksum, getCommentText, getCreator, getDerivedFrom, getExactText, getExtensionForMimeType, getLanguage, getLocaleEnglishName, getLocaleInfo, getLocaleNativeName, getMimeCategory, getNodeEncoding, getPrimaryMediaType, getPrimaryRepresentation, getPrimarySelector, getResourceEntityTypes, getResourceId, getStorageUri, getTargetSelector, getTargetSource, getTextQuoteSelector, hasTargetSelector, isArchived, isAssessment, isBodyResolved, isComment, isDraft, isHighlight, isImageMimeType, isPdfMimeType, isReference, isResolvedReference, isStubReference, isTag, isTextMimeType, isValidEmail, isValidHostname, kbBackendUrl, normalizeCoordinates, normalizeText, notifyPermissionDenied, notifySessionExpired, parseSvgSelector, scaleSvgToNative, setStoredSession, validateAndCorrectOffsets, validateData, verifyPosition };
2368
4569
  //# sourceMappingURL=index.js.map
2369
4570
  //# sourceMappingURL=index.js.map