@semiont/sdk 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/README.md +115 -0
- package/dist/index.d.ts +1688 -0
- package/dist/index.js +3030 -0
- package/dist/index.js.map +1 -0
- package/package.json +60 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3030 @@
|
|
|
1
|
+
import { annotationId, resourceId, email, googleCredential, refreshToken, EventBus, accessToken, baseUrl, isHighlight, isComment, isAssessment, isReference, isTag, decodeWithCharset, getPrimaryMediaType, userDID, searchQuery } from '@semiont/core';
|
|
2
|
+
import { merge, firstValueFrom, BehaviorSubject, map as map$1, distinctUntilChanged, Observable, Subject, Subscription, of, filter as filter$1, take as take$1, timeout as timeout$1 } from 'rxjs';
|
|
3
|
+
import { filter, map, take, timeout, takeUntil, startWith, debounceTime, distinctUntilChanged as distinctUntilChanged$1, switchMap } from 'rxjs/operators';
|
|
4
|
+
import { HttpTransport, HttpContentTransport, createActorVM, APIError } from '@semiont/api-client';
|
|
5
|
+
export { APIError, DEGRADED_THRESHOLD_MS, HttpContentTransport, HttpTransport, createActorVM } from '@semiont/api-client';
|
|
6
|
+
|
|
7
|
+
// src/client.ts
|
|
8
|
+
var BusRequestError = class extends Error {
|
|
9
|
+
constructor(message) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = "BusRequestError";
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
async function busRequest(bus, emitChannel, payload, resultChannel, failureChannel, timeoutMs = 3e4) {
|
|
15
|
+
const correlationId = crypto.randomUUID();
|
|
16
|
+
const fullPayload = { ...payload, correlationId };
|
|
17
|
+
const result$ = merge(
|
|
18
|
+
bus.stream(resultChannel).pipe(
|
|
19
|
+
filter((e) => e.correlationId === correlationId),
|
|
20
|
+
map((e) => ({ ok: true, response: e.response }))
|
|
21
|
+
),
|
|
22
|
+
bus.stream(failureChannel).pipe(
|
|
23
|
+
filter((e) => e.correlationId === correlationId),
|
|
24
|
+
map((e) => ({ ok: false, error: new BusRequestError(e.message) }))
|
|
25
|
+
)
|
|
26
|
+
).pipe(take(1), timeout(timeoutMs));
|
|
27
|
+
const resultPromise = firstValueFrom(result$);
|
|
28
|
+
await bus.emit(emitChannel, fullPayload);
|
|
29
|
+
const result = await resultPromise;
|
|
30
|
+
if (!result.ok) {
|
|
31
|
+
throw result.error;
|
|
32
|
+
}
|
|
33
|
+
return result.response;
|
|
34
|
+
}
|
|
35
|
+
function createCache(fetchFn) {
|
|
36
|
+
const store$ = new BehaviorSubject(/* @__PURE__ */ new Map());
|
|
37
|
+
const inflight = /* @__PURE__ */ new Set();
|
|
38
|
+
const obsCache = /* @__PURE__ */ new Map();
|
|
39
|
+
const doFetch = async (key) => {
|
|
40
|
+
if (inflight.has(key)) return;
|
|
41
|
+
inflight.add(key);
|
|
42
|
+
try {
|
|
43
|
+
const value = await fetchFn(key);
|
|
44
|
+
const next = new Map(store$.value);
|
|
45
|
+
next.set(key, value);
|
|
46
|
+
store$.next(next);
|
|
47
|
+
} catch {
|
|
48
|
+
} finally {
|
|
49
|
+
inflight.delete(key);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
return {
|
|
53
|
+
observe(key) {
|
|
54
|
+
if (!store$.value.has(key) && !inflight.has(key)) {
|
|
55
|
+
void doFetch(key);
|
|
56
|
+
}
|
|
57
|
+
let obs = obsCache.get(key);
|
|
58
|
+
if (!obs) {
|
|
59
|
+
obs = store$.pipe(
|
|
60
|
+
map$1((m) => m.get(key)),
|
|
61
|
+
distinctUntilChanged()
|
|
62
|
+
);
|
|
63
|
+
obsCache.set(key, obs);
|
|
64
|
+
}
|
|
65
|
+
return obs;
|
|
66
|
+
},
|
|
67
|
+
get(key) {
|
|
68
|
+
return store$.value.get(key);
|
|
69
|
+
},
|
|
70
|
+
keys() {
|
|
71
|
+
return [...store$.value.keys()];
|
|
72
|
+
},
|
|
73
|
+
invalidate(key) {
|
|
74
|
+
inflight.delete(key);
|
|
75
|
+
void doFetch(key);
|
|
76
|
+
},
|
|
77
|
+
remove(key) {
|
|
78
|
+
const next = new Map(store$.value);
|
|
79
|
+
next.delete(key);
|
|
80
|
+
store$.next(next);
|
|
81
|
+
inflight.delete(key);
|
|
82
|
+
},
|
|
83
|
+
set(key, value) {
|
|
84
|
+
const next = new Map(store$.value);
|
|
85
|
+
next.set(key, value);
|
|
86
|
+
store$.next(next);
|
|
87
|
+
},
|
|
88
|
+
invalidateAll() {
|
|
89
|
+
for (const key of store$.value.keys()) {
|
|
90
|
+
inflight.delete(key);
|
|
91
|
+
void doFetch(key);
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
dispose() {
|
|
95
|
+
store$.complete();
|
|
96
|
+
obsCache.clear();
|
|
97
|
+
inflight.clear();
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// src/namespaces/browse.ts
|
|
103
|
+
var ENTITY_TYPES_KEY = "_";
|
|
104
|
+
var BrowseNamespace = class {
|
|
105
|
+
constructor(transport, bus, content) {
|
|
106
|
+
this.transport = transport;
|
|
107
|
+
this.bus = bus;
|
|
108
|
+
this.content = content;
|
|
109
|
+
const g = globalThis;
|
|
110
|
+
g.__SEMIONT_BROWSE_INSTANCES__ = (g.__SEMIONT_BROWSE_INSTANCES__ ?? 0) + 1;
|
|
111
|
+
const browseSerial = g.__SEMIONT_BROWSE_INSTANCES__;
|
|
112
|
+
this.__serial__ = browseSerial;
|
|
113
|
+
console.debug(`[diag] BrowseNamespace #${browseSerial} constructed`);
|
|
114
|
+
this.resourceCache = createCache(async (id) => {
|
|
115
|
+
const result = await busRequest(
|
|
116
|
+
this.transport,
|
|
117
|
+
"browse:resource-requested",
|
|
118
|
+
{ resourceId: id },
|
|
119
|
+
"browse:resource-result",
|
|
120
|
+
"browse:resource-failed"
|
|
121
|
+
);
|
|
122
|
+
return result.resource;
|
|
123
|
+
});
|
|
124
|
+
this.resourceListCache = createCache(async (key) => {
|
|
125
|
+
const filters = this.resourceListFilters.get(key) ?? {};
|
|
126
|
+
const search = filters.search ? searchQuery(filters.search) : void 0;
|
|
127
|
+
const result = await busRequest(
|
|
128
|
+
this.transport,
|
|
129
|
+
"browse:resources-requested",
|
|
130
|
+
{ search, archived: filters.archived, limit: filters.limit ?? 100, offset: 0 },
|
|
131
|
+
"browse:resources-result",
|
|
132
|
+
"browse:resources-failed"
|
|
133
|
+
);
|
|
134
|
+
return result.resources;
|
|
135
|
+
});
|
|
136
|
+
this.annotationListCache = createCache(async (resourceId) => {
|
|
137
|
+
return busRequest(
|
|
138
|
+
this.transport,
|
|
139
|
+
"browse:annotations-requested",
|
|
140
|
+
{ resourceId },
|
|
141
|
+
"browse:annotations-result",
|
|
142
|
+
"browse:annotations-failed"
|
|
143
|
+
);
|
|
144
|
+
});
|
|
145
|
+
this.annotationDetailCache = createCache(async (annotationId) => {
|
|
146
|
+
const resourceId = this.annotationResources.get(annotationId);
|
|
147
|
+
if (!resourceId) {
|
|
148
|
+
throw new Error(`Cannot fetch annotation ${annotationId}: no resourceId known`);
|
|
149
|
+
}
|
|
150
|
+
const result = await busRequest(
|
|
151
|
+
this.transport,
|
|
152
|
+
"browse:annotation-requested",
|
|
153
|
+
{ resourceId, annotationId },
|
|
154
|
+
"browse:annotation-result",
|
|
155
|
+
"browse:annotation-failed"
|
|
156
|
+
);
|
|
157
|
+
return result.annotation;
|
|
158
|
+
});
|
|
159
|
+
this.entityTypesCache = createCache(async () => {
|
|
160
|
+
const serial = this.__serial__;
|
|
161
|
+
console.debug(`[diag] BrowseNamespace#${serial} entityTypes fetchFn START`);
|
|
162
|
+
const result = await busRequest(
|
|
163
|
+
this.transport,
|
|
164
|
+
"browse:entity-types-requested",
|
|
165
|
+
{},
|
|
166
|
+
"browse:entity-types-result",
|
|
167
|
+
"browse:entity-types-failed"
|
|
168
|
+
);
|
|
169
|
+
console.debug(`[diag] BrowseNamespace#${serial} entityTypes fetchFn RESOLVE`, JSON.stringify(result.entityTypes).slice(0, 200));
|
|
170
|
+
return result.entityTypes;
|
|
171
|
+
});
|
|
172
|
+
this.referencedByCache = createCache(async (resourceId) => {
|
|
173
|
+
const result = await busRequest(
|
|
174
|
+
this.transport,
|
|
175
|
+
"browse:referenced-by-requested",
|
|
176
|
+
{ resourceId },
|
|
177
|
+
"browse:referenced-by-result",
|
|
178
|
+
"browse:referenced-by-failed"
|
|
179
|
+
);
|
|
180
|
+
return result.referencedBy;
|
|
181
|
+
});
|
|
182
|
+
this.resourceEventsCache = createCache(async (resourceId) => {
|
|
183
|
+
const result = await busRequest(
|
|
184
|
+
this.transport,
|
|
185
|
+
"browse:events-requested",
|
|
186
|
+
{ resourceId },
|
|
187
|
+
"browse:events-result",
|
|
188
|
+
"browse:events-failed"
|
|
189
|
+
);
|
|
190
|
+
return result.events;
|
|
191
|
+
});
|
|
192
|
+
this.subscribeToEvents();
|
|
193
|
+
}
|
|
194
|
+
// ── Caches, backed by the RxJS-native `Cache<K, V>` primitive ───────────
|
|
195
|
+
//
|
|
196
|
+
// Each cache encapsulates the BehaviorSubject store, in-flight guard,
|
|
197
|
+
// and per-key observable memoization that was previously open-coded
|
|
198
|
+
// here. Behavioral contract: `packages/api-client/docs/CACHE-SEMANTICS.md`.
|
|
199
|
+
//
|
|
200
|
+
// Public surface (`resource()`, `annotations()`, etc.) is unchanged;
|
|
201
|
+
// the caches are an implementation detail of this namespace.
|
|
202
|
+
resourceCache;
|
|
203
|
+
resourceListCache;
|
|
204
|
+
annotationListCache;
|
|
205
|
+
/**
|
|
206
|
+
* Annotation-detail cache keyed by `annotationId` only — the resourceId
|
|
207
|
+
* is a routing hint for the backend fetch, not an identity component.
|
|
208
|
+
* We track the most recent resourceId per annotationId in a side-map
|
|
209
|
+
* so `mark:delete-ok` (which carries only `annotationId`) can reach
|
|
210
|
+
* the right cache entry. Aligns with the pre-refactor semantics.
|
|
211
|
+
*/
|
|
212
|
+
annotationDetailCache;
|
|
213
|
+
annotationResources = /* @__PURE__ */ new Map();
|
|
214
|
+
entityTypesCache;
|
|
215
|
+
referencedByCache;
|
|
216
|
+
resourceEventsCache;
|
|
217
|
+
/** Filter-blob memory so `invalidateResourceLists` can replay per-key. */
|
|
218
|
+
resourceListFilters = /* @__PURE__ */ new Map();
|
|
219
|
+
/**
|
|
220
|
+
* Per-key memo for `annotations()` observables. The cache stores the
|
|
221
|
+
* full `AnnotationsListResponse`; the public shape is just the inner
|
|
222
|
+
* `Annotation[]`. Without this memo, every call to `annotations(rId)`
|
|
223
|
+
* would produce a fresh `.pipe(map(...))` observable, violating B4
|
|
224
|
+
* (per-key observable stability). Consumers that compare observable
|
|
225
|
+
* identity — React hooks depending on the observable reference,
|
|
226
|
+
* `distinctUntilChanged` at a higher level — would misbehave.
|
|
227
|
+
*/
|
|
228
|
+
annotationListObs = /* @__PURE__ */ new Map();
|
|
229
|
+
// ── Live queries ────────────────────────────────────────────────────────
|
|
230
|
+
resource(resourceId) {
|
|
231
|
+
return this.resourceCache.observe(resourceId);
|
|
232
|
+
}
|
|
233
|
+
resources(filters) {
|
|
234
|
+
const key = JSON.stringify(filters ?? {});
|
|
235
|
+
this.resourceListFilters.set(key, filters ?? {});
|
|
236
|
+
return this.resourceListCache.observe(key);
|
|
237
|
+
}
|
|
238
|
+
annotations(resourceId) {
|
|
239
|
+
let obs = this.annotationListObs.get(resourceId);
|
|
240
|
+
if (!obs) {
|
|
241
|
+
obs = this.annotationListCache.observe(resourceId).pipe(map$1((r) => r?.annotations));
|
|
242
|
+
this.annotationListObs.set(resourceId, obs);
|
|
243
|
+
}
|
|
244
|
+
return obs;
|
|
245
|
+
}
|
|
246
|
+
annotation(resourceId, annotationId) {
|
|
247
|
+
this.annotationResources.set(annotationId, resourceId);
|
|
248
|
+
return this.annotationDetailCache.observe(annotationId);
|
|
249
|
+
}
|
|
250
|
+
entityTypes() {
|
|
251
|
+
const serial = this.__serial__;
|
|
252
|
+
console.debug(`[diag] BrowseNamespace#${serial} entityTypes() called`);
|
|
253
|
+
const self = this;
|
|
254
|
+
if (!self.__entityTypesDiag__) {
|
|
255
|
+
self.__entityTypesDiag__ = this.entityTypesCache.observe(ENTITY_TYPES_KEY).pipe(map$1((v) => {
|
|
256
|
+
console.debug(`[diag] BrowseNamespace#${serial} entityTypes$ EMIT`, v === void 0 ? "undefined" : JSON.stringify(v).slice(0, 200));
|
|
257
|
+
return v;
|
|
258
|
+
}));
|
|
259
|
+
}
|
|
260
|
+
return self.__entityTypesDiag__;
|
|
261
|
+
}
|
|
262
|
+
referencedBy(resourceId) {
|
|
263
|
+
return this.referencedByCache.observe(resourceId);
|
|
264
|
+
}
|
|
265
|
+
events(resourceId) {
|
|
266
|
+
return this.resourceEventsCache.observe(resourceId);
|
|
267
|
+
}
|
|
268
|
+
// ── One-shot reads ──────────────────────────────────────────────────────
|
|
269
|
+
async resourceContent(resourceId) {
|
|
270
|
+
const result = await this.content.getBinary(resourceId, { accept: "text/plain" });
|
|
271
|
+
const decoder = new TextDecoder();
|
|
272
|
+
return decoder.decode(result.data);
|
|
273
|
+
}
|
|
274
|
+
async resourceRepresentation(resourceId, options) {
|
|
275
|
+
return this.content.getBinary(resourceId, options?.accept ? { accept: options.accept } : void 0);
|
|
276
|
+
}
|
|
277
|
+
async resourceRepresentationStream(resourceId, options) {
|
|
278
|
+
return this.content.getBinaryStream(resourceId, options?.accept ? { accept: options.accept } : void 0);
|
|
279
|
+
}
|
|
280
|
+
async resourceEvents(resourceId) {
|
|
281
|
+
const result = await busRequest(
|
|
282
|
+
this.transport,
|
|
283
|
+
"browse:events-requested",
|
|
284
|
+
{ resourceId },
|
|
285
|
+
"browse:events-result",
|
|
286
|
+
"browse:events-failed"
|
|
287
|
+
);
|
|
288
|
+
return result.events;
|
|
289
|
+
}
|
|
290
|
+
async annotationHistory(resourceId, annotationId) {
|
|
291
|
+
return busRequest(
|
|
292
|
+
this.transport,
|
|
293
|
+
"browse:annotation-history-requested",
|
|
294
|
+
{ resourceId, annotationId },
|
|
295
|
+
"browse:annotation-history-result",
|
|
296
|
+
"browse:annotation-history-failed"
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
async connections(_resourceId) {
|
|
300
|
+
throw new Error("Not implemented: connections endpoint does not exist yet");
|
|
301
|
+
}
|
|
302
|
+
async backlinks(_resourceId) {
|
|
303
|
+
throw new Error("Not implemented: backlinks endpoint does not exist yet");
|
|
304
|
+
}
|
|
305
|
+
async resourcesByName(_query, _limit) {
|
|
306
|
+
throw new Error("Not implemented: resourcesByName endpoint does not exist yet");
|
|
307
|
+
}
|
|
308
|
+
async files(dirPath, sort) {
|
|
309
|
+
return busRequest(
|
|
310
|
+
this.transport,
|
|
311
|
+
"browse:directory-requested",
|
|
312
|
+
{ path: dirPath ?? ".", sort: sort ?? "name" },
|
|
313
|
+
"browse:directory-result",
|
|
314
|
+
"browse:directory-failed"
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
// ── UI signals (local bus fan-out) ────────────────────────────────────
|
|
318
|
+
click(annotationId, motivation) {
|
|
319
|
+
this.bus.get("browse:click").next({ annotationId, motivation });
|
|
320
|
+
}
|
|
321
|
+
navigateReference(resourceId) {
|
|
322
|
+
this.bus.get("browse:reference-navigate").next({ resourceId });
|
|
323
|
+
}
|
|
324
|
+
// ── Cache-mutation API (used by the bus-event subscribers below and by
|
|
325
|
+
// other namespaces that know about specific updates) ─────────────────
|
|
326
|
+
//
|
|
327
|
+
// - `invalidate*` — SWR refetch (B7). Keeps prior value visible.
|
|
328
|
+
// - `removeAnnotationDetail` — drops the entry (B13a: entity gone).
|
|
329
|
+
// - `updateAnnotationInPlace` — write-through (B13b: new value known).
|
|
330
|
+
invalidateAnnotationList(resourceId) {
|
|
331
|
+
this.annotationListCache.invalidate(resourceId);
|
|
332
|
+
}
|
|
333
|
+
removeAnnotationDetail(annotationId) {
|
|
334
|
+
this.annotationDetailCache.remove(annotationId);
|
|
335
|
+
this.annotationResources.delete(annotationId);
|
|
336
|
+
}
|
|
337
|
+
invalidateResourceDetail(id) {
|
|
338
|
+
this.resourceCache.invalidate(id);
|
|
339
|
+
}
|
|
340
|
+
invalidateResourceLists() {
|
|
341
|
+
this.resourceListCache.invalidateAll();
|
|
342
|
+
}
|
|
343
|
+
invalidateEntityTypes() {
|
|
344
|
+
this.entityTypesCache.invalidate(ENTITY_TYPES_KEY);
|
|
345
|
+
}
|
|
346
|
+
invalidateReferencedBy(resourceId) {
|
|
347
|
+
this.referencedByCache.invalidate(resourceId);
|
|
348
|
+
}
|
|
349
|
+
invalidateResourceEvents(resourceId) {
|
|
350
|
+
this.resourceEventsCache.invalidate(resourceId);
|
|
351
|
+
}
|
|
352
|
+
updateAnnotationInPlace(resourceId, annotation) {
|
|
353
|
+
const currentList = this.annotationListCache.get(resourceId);
|
|
354
|
+
if (currentList) {
|
|
355
|
+
const idx = currentList.annotations.findIndex((a) => a.id === annotation.id);
|
|
356
|
+
const nextAnnotations = idx >= 0 ? currentList.annotations.map((a, i) => i === idx ? annotation : a) : [...currentList.annotations, annotation];
|
|
357
|
+
this.annotationListCache.set(resourceId, { ...currentList, annotations: nextAnnotations });
|
|
358
|
+
}
|
|
359
|
+
const aId = annotationId(annotation.id);
|
|
360
|
+
this.annotationResources.set(aId, resourceId);
|
|
361
|
+
this.annotationDetailCache.set(aId, annotation);
|
|
362
|
+
}
|
|
363
|
+
// ── EventBus subscriptions ──────────────────────────────────────────────
|
|
364
|
+
/**
|
|
365
|
+
* Typed shorthand for `eventBus.get(channel).subscribe(handler)`.
|
|
366
|
+
* Preserves per-channel payload typing so handlers read
|
|
367
|
+
* `EventMap[K]` without any casts.
|
|
368
|
+
*/
|
|
369
|
+
on(channel, handler) {
|
|
370
|
+
this.bus.get(channel).subscribe(handler);
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Handler shared by `mark:entity-tag-added` and `mark:entity-tag-removed`.
|
|
374
|
+
* Both events carry the same effect: the annotation list, the
|
|
375
|
+
* resource descriptor, and the event log for that resource all may
|
|
376
|
+
* now reflect different entity tagging, so invalidate all three.
|
|
377
|
+
*/
|
|
378
|
+
onEntityTagChanged = (stored) => {
|
|
379
|
+
if (!stored.resourceId) return;
|
|
380
|
+
this.invalidateAnnotationList(stored.resourceId);
|
|
381
|
+
this.invalidateResourceDetail(stored.resourceId);
|
|
382
|
+
this.invalidateResourceEvents(stored.resourceId);
|
|
383
|
+
};
|
|
384
|
+
/**
|
|
385
|
+
* Handler shared by `mark:archived` and `mark:unarchived`. Both
|
|
386
|
+
* change a resource's archived flag, which is stored on the resource
|
|
387
|
+
* descriptor and affects the resource-list filter.
|
|
388
|
+
*/
|
|
389
|
+
onArchiveToggled = (stored) => {
|
|
390
|
+
if (!stored.resourceId) return;
|
|
391
|
+
this.invalidateResourceDetail(stored.resourceId);
|
|
392
|
+
this.invalidateResourceLists();
|
|
393
|
+
};
|
|
394
|
+
/**
|
|
395
|
+
* Handler shared by `yield:create-ok` and `yield:update-ok`. Both
|
|
396
|
+
* report a resource mutation with the resourceId as a string (not
|
|
397
|
+
* yet branded), so we brand and apply the same effect as
|
|
398
|
+
* `onArchiveToggled`.
|
|
399
|
+
*/
|
|
400
|
+
onYieldResourceMutated = (event) => {
|
|
401
|
+
const rId = resourceId(event.resourceId);
|
|
402
|
+
this.invalidateResourceDetail(rId);
|
|
403
|
+
this.invalidateResourceLists();
|
|
404
|
+
};
|
|
405
|
+
subscribeToEvents() {
|
|
406
|
+
this.on("bus:resume-gap", (event) => {
|
|
407
|
+
const gapScope = event.scope;
|
|
408
|
+
if (gapScope) {
|
|
409
|
+
const rId = gapScope;
|
|
410
|
+
this.invalidateAnnotationList(rId);
|
|
411
|
+
this.invalidateResourceDetail(rId);
|
|
412
|
+
this.invalidateResourceEvents(rId);
|
|
413
|
+
this.invalidateReferencedBy(rId);
|
|
414
|
+
} else {
|
|
415
|
+
this.invalidateResourceLists();
|
|
416
|
+
for (const rId of this.annotationListCache.keys()) this.invalidateAnnotationList(rId);
|
|
417
|
+
for (const rId of this.resourceCache.keys()) this.invalidateResourceDetail(rId);
|
|
418
|
+
for (const rId of this.resourceEventsCache.keys()) this.invalidateResourceEvents(rId);
|
|
419
|
+
for (const rId of this.referencedByCache.keys()) this.invalidateReferencedBy(rId);
|
|
420
|
+
}
|
|
421
|
+
this.invalidateEntityTypes();
|
|
422
|
+
});
|
|
423
|
+
this.on("mark:delete-ok", (event) => {
|
|
424
|
+
this.removeAnnotationDetail(annotationId(event.annotationId));
|
|
425
|
+
});
|
|
426
|
+
this.on("mark:added", (stored) => {
|
|
427
|
+
if (stored.resourceId) {
|
|
428
|
+
this.invalidateAnnotationList(stored.resourceId);
|
|
429
|
+
this.invalidateResourceEvents(stored.resourceId);
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
this.on("mark:removed", (stored) => {
|
|
433
|
+
if (stored.resourceId) {
|
|
434
|
+
this.invalidateAnnotationList(stored.resourceId);
|
|
435
|
+
this.invalidateResourceEvents(stored.resourceId);
|
|
436
|
+
}
|
|
437
|
+
this.removeAnnotationDetail(annotationId(stored.payload.annotationId));
|
|
438
|
+
});
|
|
439
|
+
this.on("mark:body-updated", (event) => {
|
|
440
|
+
const enriched = event;
|
|
441
|
+
if (!enriched.resourceId || !enriched.annotation) return;
|
|
442
|
+
this.updateAnnotationInPlace(enriched.resourceId, enriched.annotation);
|
|
443
|
+
this.invalidateResourceEvents(enriched.resourceId);
|
|
444
|
+
});
|
|
445
|
+
this.on("mark:entity-tag-added", this.onEntityTagChanged);
|
|
446
|
+
this.on("mark:entity-tag-removed", this.onEntityTagChanged);
|
|
447
|
+
this.on("replay-window-exceeded", (event) => {
|
|
448
|
+
if (event.resourceId) {
|
|
449
|
+
this.invalidateAnnotationList(event.resourceId);
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
this.on("yield:create-ok", this.onYieldResourceMutated);
|
|
453
|
+
this.on("yield:update-ok", this.onYieldResourceMutated);
|
|
454
|
+
this.on("mark:archived", this.onArchiveToggled);
|
|
455
|
+
this.on("mark:unarchived", this.onArchiveToggled);
|
|
456
|
+
this.on("mark:entity-type-added", () => this.invalidateEntityTypes());
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
var MarkNamespace = class {
|
|
460
|
+
constructor(transport, bus) {
|
|
461
|
+
this.transport = transport;
|
|
462
|
+
this.bus = bus;
|
|
463
|
+
}
|
|
464
|
+
async annotation(resourceId, input) {
|
|
465
|
+
return busRequest(
|
|
466
|
+
this.transport,
|
|
467
|
+
"mark:create-request",
|
|
468
|
+
{ resourceId, request: input },
|
|
469
|
+
"mark:create-ok",
|
|
470
|
+
"mark:create-failed"
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
async delete(resourceId, annotationId) {
|
|
474
|
+
await this.transport.emit("mark:delete", { annotationId, resourceId });
|
|
475
|
+
}
|
|
476
|
+
async entityType(type) {
|
|
477
|
+
await this.transport.emit("mark:add-entity-type", { tag: type });
|
|
478
|
+
}
|
|
479
|
+
async entityTypes(types) {
|
|
480
|
+
for (const tag of types) {
|
|
481
|
+
await this.transport.emit("mark:add-entity-type", { tag });
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
async archive(resourceId) {
|
|
485
|
+
await this.transport.emit("mark:archive", { resourceId });
|
|
486
|
+
}
|
|
487
|
+
async unarchive(resourceId) {
|
|
488
|
+
await this.transport.emit("mark:unarchive", { resourceId });
|
|
489
|
+
}
|
|
490
|
+
assist(resourceId, motivation, options) {
|
|
491
|
+
return new Observable((subscriber) => {
|
|
492
|
+
let done = false;
|
|
493
|
+
let pollTimer = null;
|
|
494
|
+
let pollInterval = null;
|
|
495
|
+
const cleanup = () => {
|
|
496
|
+
done = true;
|
|
497
|
+
if (pollTimer) {
|
|
498
|
+
clearTimeout(pollTimer);
|
|
499
|
+
pollTimer = null;
|
|
500
|
+
}
|
|
501
|
+
if (pollInterval) {
|
|
502
|
+
clearInterval(pollInterval);
|
|
503
|
+
pollInterval = null;
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
const resetPollTimer = (jobId) => {
|
|
507
|
+
if (pollTimer) clearTimeout(pollTimer);
|
|
508
|
+
if (pollInterval) {
|
|
509
|
+
clearInterval(pollInterval);
|
|
510
|
+
pollInterval = null;
|
|
511
|
+
}
|
|
512
|
+
pollTimer = setTimeout(() => {
|
|
513
|
+
if (done) return;
|
|
514
|
+
pollInterval = setInterval(() => {
|
|
515
|
+
if (done) return;
|
|
516
|
+
busRequest(
|
|
517
|
+
this.transport,
|
|
518
|
+
"job:status-requested",
|
|
519
|
+
{ jobId },
|
|
520
|
+
"job:status-result",
|
|
521
|
+
"job:status-failed"
|
|
522
|
+
).then((status) => {
|
|
523
|
+
if (done) return;
|
|
524
|
+
if (status.status === "complete") {
|
|
525
|
+
cleanup();
|
|
526
|
+
subscriber.next({
|
|
527
|
+
kind: "complete",
|
|
528
|
+
data: {
|
|
529
|
+
jobId,
|
|
530
|
+
jobType: status.jobType ?? "annotation",
|
|
531
|
+
resourceId,
|
|
532
|
+
result: status.result
|
|
533
|
+
}
|
|
534
|
+
});
|
|
535
|
+
subscriber.complete();
|
|
536
|
+
} else if (status.status === "failed") {
|
|
537
|
+
cleanup();
|
|
538
|
+
subscriber.error(new Error(status.error ?? "Job failed"));
|
|
539
|
+
}
|
|
540
|
+
}).catch(() => {
|
|
541
|
+
});
|
|
542
|
+
}, 5e3);
|
|
543
|
+
}, 1e4);
|
|
544
|
+
};
|
|
545
|
+
let activeJobId = null;
|
|
546
|
+
const progress$ = this.bus.get("job:report-progress").pipe(
|
|
547
|
+
filter((e) => e.jobId === activeJobId)
|
|
548
|
+
);
|
|
549
|
+
const complete$ = this.bus.get("job:complete").pipe(
|
|
550
|
+
filter((e) => e.jobId === activeJobId)
|
|
551
|
+
);
|
|
552
|
+
const fail$ = this.bus.get("job:fail").pipe(
|
|
553
|
+
filter((e) => e.jobId === activeJobId)
|
|
554
|
+
);
|
|
555
|
+
const progressSub = progress$.pipe(takeUntil(merge(complete$, fail$))).subscribe((e) => {
|
|
556
|
+
if (e.progress) subscriber.next({ kind: "progress", data: e.progress });
|
|
557
|
+
if (activeJobId) resetPollTimer(activeJobId);
|
|
558
|
+
});
|
|
559
|
+
const completeSub = complete$.subscribe((e) => {
|
|
560
|
+
cleanup();
|
|
561
|
+
subscriber.next({ kind: "complete", data: e });
|
|
562
|
+
subscriber.complete();
|
|
563
|
+
});
|
|
564
|
+
const failSub = fail$.subscribe((e) => {
|
|
565
|
+
cleanup();
|
|
566
|
+
subscriber.error(new Error(e.error));
|
|
567
|
+
});
|
|
568
|
+
this.dispatchAssist(resourceId, motivation, options).then(({ jobId }) => {
|
|
569
|
+
if (jobId && !done) {
|
|
570
|
+
activeJobId = jobId;
|
|
571
|
+
resetPollTimer(jobId);
|
|
572
|
+
}
|
|
573
|
+
}).catch((error) => {
|
|
574
|
+
cleanup();
|
|
575
|
+
subscriber.error(error);
|
|
576
|
+
});
|
|
577
|
+
return () => {
|
|
578
|
+
cleanup();
|
|
579
|
+
progressSub.unsubscribe();
|
|
580
|
+
completeSub.unsubscribe();
|
|
581
|
+
failSub.unsubscribe();
|
|
582
|
+
};
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
request(selector, motivation) {
|
|
586
|
+
this.bus.get("mark:requested").next({ selector, motivation });
|
|
587
|
+
}
|
|
588
|
+
requestAssist(motivation, options, correlationId) {
|
|
589
|
+
this.bus.get("mark:assist-request").next({
|
|
590
|
+
motivation,
|
|
591
|
+
options,
|
|
592
|
+
...correlationId ? { correlationId } : {}
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
submit(input) {
|
|
596
|
+
this.bus.get("mark:submit").next(input);
|
|
597
|
+
}
|
|
598
|
+
cancelPending() {
|
|
599
|
+
this.bus.get("mark:cancel-pending").next(void 0);
|
|
600
|
+
}
|
|
601
|
+
dismissProgress() {
|
|
602
|
+
this.bus.get("mark:progress-dismiss").next(void 0);
|
|
603
|
+
}
|
|
604
|
+
changeSelection(motivation) {
|
|
605
|
+
this.bus.get("mark:selection-changed").next({ motivation });
|
|
606
|
+
}
|
|
607
|
+
changeClick(action) {
|
|
608
|
+
this.bus.get("mark:click-changed").next({ action });
|
|
609
|
+
}
|
|
610
|
+
changeShape(shape) {
|
|
611
|
+
this.bus.get("mark:shape-changed").next({ shape });
|
|
612
|
+
}
|
|
613
|
+
toggleMode() {
|
|
614
|
+
this.bus.get("mark:mode-toggled").next(void 0);
|
|
615
|
+
}
|
|
616
|
+
async dispatchAssist(resourceId, motivation, options) {
|
|
617
|
+
const jobTypeMap = {
|
|
618
|
+
tagging: "tag-annotation",
|
|
619
|
+
linking: "reference-annotation",
|
|
620
|
+
highlighting: "highlight-annotation",
|
|
621
|
+
assessing: "assessment-annotation",
|
|
622
|
+
commenting: "comment-annotation"
|
|
623
|
+
};
|
|
624
|
+
const jobType = jobTypeMap[motivation];
|
|
625
|
+
if (!jobType) throw new Error(`Unsupported motivation: ${motivation}`);
|
|
626
|
+
if (motivation === "tagging") {
|
|
627
|
+
if (!options.schemaId || !options.categories?.length) throw new Error("Tag assist requires schemaId and categories");
|
|
628
|
+
} else if (motivation === "linking") {
|
|
629
|
+
if (!options.entityTypes?.length) throw new Error("Reference assist requires entityTypes");
|
|
630
|
+
}
|
|
631
|
+
const params = {};
|
|
632
|
+
if (options.entityTypes) params.entityTypes = options.entityTypes;
|
|
633
|
+
if (options.includeDescriptiveReferences !== void 0) params.includeDescriptiveReferences = options.includeDescriptiveReferences;
|
|
634
|
+
if (options.instructions !== void 0) params.instructions = options.instructions;
|
|
635
|
+
if (options.density !== void 0) params.density = options.density;
|
|
636
|
+
if (options.tone !== void 0) params.tone = options.tone;
|
|
637
|
+
if (options.language !== void 0) params.language = options.language;
|
|
638
|
+
if (options.schemaId !== void 0) params.schemaId = options.schemaId;
|
|
639
|
+
if (options.categories !== void 0) params.categories = options.categories;
|
|
640
|
+
return busRequest(
|
|
641
|
+
this.transport,
|
|
642
|
+
"job:create",
|
|
643
|
+
{ jobType, resourceId, params },
|
|
644
|
+
"job:created",
|
|
645
|
+
"job:create-failed"
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
};
|
|
649
|
+
|
|
650
|
+
// src/namespaces/bind.ts
|
|
651
|
+
var BindNamespace = class {
|
|
652
|
+
constructor(transport, bus) {
|
|
653
|
+
this.transport = transport;
|
|
654
|
+
this.bus = bus;
|
|
655
|
+
}
|
|
656
|
+
async body(resourceId, annotationId, operations) {
|
|
657
|
+
await this.transport.emit("bind:update-body", {
|
|
658
|
+
correlationId: crypto.randomUUID(),
|
|
659
|
+
annotationId,
|
|
660
|
+
resourceId,
|
|
661
|
+
operations
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
initiate(input) {
|
|
665
|
+
this.bus.get("bind:initiate").next(input);
|
|
666
|
+
}
|
|
667
|
+
};
|
|
668
|
+
var GatherNamespace = class {
|
|
669
|
+
constructor(transport, bus) {
|
|
670
|
+
this.transport = transport;
|
|
671
|
+
this.bus = bus;
|
|
672
|
+
}
|
|
673
|
+
annotation(annotationId, resourceId, options) {
|
|
674
|
+
return new Observable((subscriber) => {
|
|
675
|
+
const correlationId = crypto.randomUUID();
|
|
676
|
+
const complete$ = this.bus.get("gather:complete").pipe(
|
|
677
|
+
filter((e) => e.correlationId === correlationId)
|
|
678
|
+
);
|
|
679
|
+
const failed$ = this.bus.get("gather:failed").pipe(
|
|
680
|
+
filter((e) => e.correlationId === correlationId)
|
|
681
|
+
);
|
|
682
|
+
const sub = merge(
|
|
683
|
+
this.bus.get("gather:annotation-progress").pipe(
|
|
684
|
+
filter((e) => e.annotationId === annotationId),
|
|
685
|
+
map((e) => e)
|
|
686
|
+
),
|
|
687
|
+
complete$.pipe(map((e) => e))
|
|
688
|
+
).pipe(takeUntil(merge(complete$, failed$))).subscribe({
|
|
689
|
+
next: (v) => subscriber.next(v),
|
|
690
|
+
error: (e) => subscriber.error(e)
|
|
691
|
+
});
|
|
692
|
+
const completeSub = complete$.subscribe((e) => {
|
|
693
|
+
subscriber.next(e);
|
|
694
|
+
subscriber.complete();
|
|
695
|
+
});
|
|
696
|
+
const failedSub = failed$.subscribe((e) => {
|
|
697
|
+
subscriber.error(new Error(e.message));
|
|
698
|
+
});
|
|
699
|
+
this.transport.emit("gather:requested", {
|
|
700
|
+
correlationId,
|
|
701
|
+
annotationId,
|
|
702
|
+
resourceId,
|
|
703
|
+
options: { contextWindow: options?.contextWindow ?? 2e3 }
|
|
704
|
+
}).catch((error) => {
|
|
705
|
+
subscriber.error(error);
|
|
706
|
+
});
|
|
707
|
+
return () => {
|
|
708
|
+
sub.unsubscribe();
|
|
709
|
+
completeSub.unsubscribe();
|
|
710
|
+
failedSub.unsubscribe();
|
|
711
|
+
};
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
resource(_resourceId, _options) {
|
|
715
|
+
throw new Error("Not implemented: gather.resource() \u2014 no backend route yet");
|
|
716
|
+
}
|
|
717
|
+
};
|
|
718
|
+
var MatchNamespace = class {
|
|
719
|
+
constructor(transport, bus) {
|
|
720
|
+
this.transport = transport;
|
|
721
|
+
this.bus = bus;
|
|
722
|
+
}
|
|
723
|
+
requestSearch(input) {
|
|
724
|
+
this.bus.get("match:search-requested").next(input);
|
|
725
|
+
}
|
|
726
|
+
search(resourceId, referenceId, context, options) {
|
|
727
|
+
return new Observable((subscriber) => {
|
|
728
|
+
const correlationId = crypto.randomUUID();
|
|
729
|
+
const result$ = this.bus.get("match:search-results").pipe(
|
|
730
|
+
filter((e) => e.correlationId === correlationId)
|
|
731
|
+
);
|
|
732
|
+
const failed$ = this.bus.get("match:search-failed").pipe(
|
|
733
|
+
filter((e) => e.correlationId === correlationId)
|
|
734
|
+
);
|
|
735
|
+
const resultSub = result$.subscribe((e) => {
|
|
736
|
+
subscriber.next(e);
|
|
737
|
+
subscriber.complete();
|
|
738
|
+
});
|
|
739
|
+
const failedSub = failed$.subscribe((e) => {
|
|
740
|
+
subscriber.error(new Error(e.error));
|
|
741
|
+
});
|
|
742
|
+
this.transport.emit("match:search-requested", {
|
|
743
|
+
correlationId,
|
|
744
|
+
resourceId,
|
|
745
|
+
referenceId,
|
|
746
|
+
context,
|
|
747
|
+
limit: options?.limit ?? 10,
|
|
748
|
+
useSemanticScoring: options?.useSemanticScoring ?? true
|
|
749
|
+
}).catch((error) => {
|
|
750
|
+
subscriber.error(error);
|
|
751
|
+
});
|
|
752
|
+
return () => {
|
|
753
|
+
resultSub.unsubscribe();
|
|
754
|
+
failedSub.unsubscribe();
|
|
755
|
+
};
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
};
|
|
759
|
+
var YieldNamespace = class {
|
|
760
|
+
constructor(transport, bus, content) {
|
|
761
|
+
this.transport = transport;
|
|
762
|
+
this.bus = bus;
|
|
763
|
+
this.content = content;
|
|
764
|
+
}
|
|
765
|
+
async resource(data) {
|
|
766
|
+
const result = await this.content.putBinary({
|
|
767
|
+
name: data.name,
|
|
768
|
+
file: data.file,
|
|
769
|
+
format: data.format,
|
|
770
|
+
storageUri: data.storageUri,
|
|
771
|
+
...data.entityTypes ? { entityTypes: data.entityTypes } : {},
|
|
772
|
+
...data.language ? { language: data.language } : {},
|
|
773
|
+
...data.creationMethod ? { creationMethod: data.creationMethod } : {},
|
|
774
|
+
...data.sourceAnnotationId ? { sourceAnnotationId: data.sourceAnnotationId } : {},
|
|
775
|
+
...data.sourceResourceId ? { sourceResourceId: data.sourceResourceId } : {},
|
|
776
|
+
...data.generationPrompt ? { generationPrompt: data.generationPrompt } : {},
|
|
777
|
+
...data.generator ? { generator: data.generator } : {},
|
|
778
|
+
...data.isDraft !== void 0 ? { isDraft: data.isDraft } : {}
|
|
779
|
+
});
|
|
780
|
+
return { resourceId: result.resourceId };
|
|
781
|
+
}
|
|
782
|
+
fromAnnotation(resourceId, annotationId, options) {
|
|
783
|
+
return new Observable((subscriber) => {
|
|
784
|
+
let done = false;
|
|
785
|
+
let pollTimer = null;
|
|
786
|
+
let pollInterval = null;
|
|
787
|
+
const cleanup = () => {
|
|
788
|
+
done = true;
|
|
789
|
+
if (pollTimer) {
|
|
790
|
+
clearTimeout(pollTimer);
|
|
791
|
+
pollTimer = null;
|
|
792
|
+
}
|
|
793
|
+
if (pollInterval) {
|
|
794
|
+
clearInterval(pollInterval);
|
|
795
|
+
pollInterval = null;
|
|
796
|
+
}
|
|
797
|
+
};
|
|
798
|
+
const resetPollTimer = (jid) => {
|
|
799
|
+
if (pollTimer) clearTimeout(pollTimer);
|
|
800
|
+
if (pollInterval) {
|
|
801
|
+
clearInterval(pollInterval);
|
|
802
|
+
pollInterval = null;
|
|
803
|
+
}
|
|
804
|
+
pollTimer = setTimeout(() => {
|
|
805
|
+
if (done) return;
|
|
806
|
+
pollInterval = setInterval(() => {
|
|
807
|
+
if (done) return;
|
|
808
|
+
busRequest(
|
|
809
|
+
this.transport,
|
|
810
|
+
"job:status-requested",
|
|
811
|
+
{ jobId: jid },
|
|
812
|
+
"job:status-result",
|
|
813
|
+
"job:status-failed"
|
|
814
|
+
).then((status) => {
|
|
815
|
+
if (done) return;
|
|
816
|
+
if (status.status === "complete") {
|
|
817
|
+
cleanup();
|
|
818
|
+
subscriber.next({
|
|
819
|
+
kind: "complete",
|
|
820
|
+
data: {
|
|
821
|
+
jobId: jid,
|
|
822
|
+
jobType: status.jobType ?? "generation",
|
|
823
|
+
resourceId,
|
|
824
|
+
result: status.result
|
|
825
|
+
}
|
|
826
|
+
});
|
|
827
|
+
subscriber.complete();
|
|
828
|
+
} else if (status.status === "failed") {
|
|
829
|
+
cleanup();
|
|
830
|
+
subscriber.error(new Error(status.error ?? "Generation failed"));
|
|
831
|
+
}
|
|
832
|
+
}).catch(() => {
|
|
833
|
+
});
|
|
834
|
+
}, 5e3);
|
|
835
|
+
}, 1e4);
|
|
836
|
+
};
|
|
837
|
+
let activeJobId = null;
|
|
838
|
+
const progress$ = this.bus.get("job:report-progress").pipe(
|
|
839
|
+
filter((e) => e.jobId === activeJobId)
|
|
840
|
+
);
|
|
841
|
+
const complete$ = this.bus.get("job:complete").pipe(
|
|
842
|
+
filter((e) => e.jobId === activeJobId)
|
|
843
|
+
);
|
|
844
|
+
const fail$ = this.bus.get("job:fail").pipe(
|
|
845
|
+
filter((e) => e.jobId === activeJobId)
|
|
846
|
+
);
|
|
847
|
+
const progressSub = progress$.pipe(takeUntil(merge(complete$, fail$))).subscribe((e) => {
|
|
848
|
+
if (e.progress) subscriber.next({ kind: "progress", data: e.progress });
|
|
849
|
+
if (activeJobId) resetPollTimer(activeJobId);
|
|
850
|
+
});
|
|
851
|
+
const completeSub = complete$.subscribe((e) => {
|
|
852
|
+
cleanup();
|
|
853
|
+
subscriber.next({ kind: "complete", data: e });
|
|
854
|
+
subscriber.complete();
|
|
855
|
+
});
|
|
856
|
+
const failSub = fail$.subscribe((e) => {
|
|
857
|
+
cleanup();
|
|
858
|
+
subscriber.error(new Error(e.error));
|
|
859
|
+
});
|
|
860
|
+
busRequest(
|
|
861
|
+
this.transport,
|
|
862
|
+
"job:create",
|
|
863
|
+
{
|
|
864
|
+
jobType: "generation",
|
|
865
|
+
resourceId,
|
|
866
|
+
params: {
|
|
867
|
+
referenceId: annotationId,
|
|
868
|
+
title: options.title,
|
|
869
|
+
prompt: options.prompt,
|
|
870
|
+
language: options.language,
|
|
871
|
+
temperature: options.temperature,
|
|
872
|
+
maxTokens: options.maxTokens,
|
|
873
|
+
storageUri: options.storageUri,
|
|
874
|
+
context: options.context
|
|
875
|
+
}
|
|
876
|
+
},
|
|
877
|
+
"job:created",
|
|
878
|
+
"job:create-failed"
|
|
879
|
+
).then(({ jobId }) => {
|
|
880
|
+
if (jobId && !done) {
|
|
881
|
+
activeJobId = jobId;
|
|
882
|
+
resetPollTimer(jobId);
|
|
883
|
+
}
|
|
884
|
+
}).catch((error) => {
|
|
885
|
+
cleanup();
|
|
886
|
+
subscriber.error(error);
|
|
887
|
+
});
|
|
888
|
+
return () => {
|
|
889
|
+
cleanup();
|
|
890
|
+
progressSub.unsubscribe();
|
|
891
|
+
completeSub.unsubscribe();
|
|
892
|
+
failSub.unsubscribe();
|
|
893
|
+
};
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
async cloneToken(resourceId) {
|
|
897
|
+
return busRequest(
|
|
898
|
+
this.transport,
|
|
899
|
+
"yield:clone-token-requested",
|
|
900
|
+
{ resourceId },
|
|
901
|
+
"yield:clone-token-generated",
|
|
902
|
+
"yield:clone-token-failed"
|
|
903
|
+
);
|
|
904
|
+
}
|
|
905
|
+
async fromToken(token) {
|
|
906
|
+
const result = await busRequest(
|
|
907
|
+
this.transport,
|
|
908
|
+
"yield:clone-resource-requested",
|
|
909
|
+
{ token },
|
|
910
|
+
"yield:clone-resource-result",
|
|
911
|
+
"yield:clone-resource-failed"
|
|
912
|
+
);
|
|
913
|
+
return result.sourceResource;
|
|
914
|
+
}
|
|
915
|
+
async createFromToken(options) {
|
|
916
|
+
return busRequest(
|
|
917
|
+
this.transport,
|
|
918
|
+
"yield:clone-create",
|
|
919
|
+
options,
|
|
920
|
+
"yield:clone-created",
|
|
921
|
+
"yield:clone-create-failed"
|
|
922
|
+
);
|
|
923
|
+
}
|
|
924
|
+
clone() {
|
|
925
|
+
this.bus.get("yield:clone").next(void 0);
|
|
926
|
+
}
|
|
927
|
+
};
|
|
928
|
+
|
|
929
|
+
// src/namespaces/beckon.ts
|
|
930
|
+
var BeckonNamespace = class {
|
|
931
|
+
constructor(transport, bus) {
|
|
932
|
+
this.transport = transport;
|
|
933
|
+
this.bus = bus;
|
|
934
|
+
}
|
|
935
|
+
attention(annotationId, resourceId) {
|
|
936
|
+
void this.transport.emit("beckon:focus", { annotationId, resourceId });
|
|
937
|
+
}
|
|
938
|
+
hover(annotationId) {
|
|
939
|
+
this.bus.get("beckon:hover").next({ annotationId });
|
|
940
|
+
}
|
|
941
|
+
sparkle(annotationId) {
|
|
942
|
+
this.bus.get("beckon:sparkle").next({ annotationId });
|
|
943
|
+
}
|
|
944
|
+
};
|
|
945
|
+
|
|
946
|
+
// src/namespaces/job.ts
|
|
947
|
+
var JobNamespace = class {
|
|
948
|
+
constructor(transport, bus) {
|
|
949
|
+
this.transport = transport;
|
|
950
|
+
this.bus = bus;
|
|
951
|
+
}
|
|
952
|
+
/**
|
|
953
|
+
* Live stream of `job:queued` events. Surfaces a typed view onto the
|
|
954
|
+
* underlying bus channel for consumers (CLIs, MCP handlers, widgets)
|
|
955
|
+
* that orchestrate jobs and need to react to lifecycle transitions.
|
|
956
|
+
*/
|
|
957
|
+
get queued$() {
|
|
958
|
+
return this.bus.get("job:queued");
|
|
959
|
+
}
|
|
960
|
+
/** Live stream of `job:report-progress` events. */
|
|
961
|
+
get progress$() {
|
|
962
|
+
return this.bus.get("job:report-progress");
|
|
963
|
+
}
|
|
964
|
+
/** Live stream of `job:complete` events. */
|
|
965
|
+
get complete$() {
|
|
966
|
+
return this.bus.get("job:complete");
|
|
967
|
+
}
|
|
968
|
+
/** Live stream of `job:fail` events. */
|
|
969
|
+
get fail$() {
|
|
970
|
+
return this.bus.get("job:fail");
|
|
971
|
+
}
|
|
972
|
+
async status(jobId) {
|
|
973
|
+
return busRequest(
|
|
974
|
+
this.transport,
|
|
975
|
+
"job:status-requested",
|
|
976
|
+
{ jobId },
|
|
977
|
+
"job:status-result",
|
|
978
|
+
"job:status-failed"
|
|
979
|
+
);
|
|
980
|
+
}
|
|
981
|
+
async pollUntilComplete(jobId, options) {
|
|
982
|
+
const interval = options?.interval ?? 1e3;
|
|
983
|
+
const timeout7 = options?.timeout ?? 6e4;
|
|
984
|
+
const startTime = Date.now();
|
|
985
|
+
while (true) {
|
|
986
|
+
const status = await this.status(jobId);
|
|
987
|
+
if (options?.onProgress) options.onProgress(status);
|
|
988
|
+
if (status.status === "complete" || status.status === "failed" || status.status === "cancelled") {
|
|
989
|
+
return status;
|
|
990
|
+
}
|
|
991
|
+
if (Date.now() - startTime > timeout7) {
|
|
992
|
+
throw new Error(`Job polling timeout after ${timeout7}ms`);
|
|
993
|
+
}
|
|
994
|
+
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
async cancel(_jobId, type) {
|
|
998
|
+
await this.transport.emit("job:cancel-requested", {
|
|
999
|
+
jobType: type === "generation" ? "generation" : "annotation"
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
1002
|
+
cancelRequest(jobType) {
|
|
1003
|
+
this.bus.get("job:cancel-requested").next({ jobType });
|
|
1004
|
+
}
|
|
1005
|
+
};
|
|
1006
|
+
var AuthNamespace = class {
|
|
1007
|
+
constructor(transport) {
|
|
1008
|
+
this.transport = transport;
|
|
1009
|
+
}
|
|
1010
|
+
async password(emailStr, passwordStr) {
|
|
1011
|
+
return this.transport.authenticatePassword(email(emailStr), passwordStr);
|
|
1012
|
+
}
|
|
1013
|
+
async google(credential) {
|
|
1014
|
+
return this.transport.authenticateGoogle(googleCredential(credential));
|
|
1015
|
+
}
|
|
1016
|
+
async refresh(token) {
|
|
1017
|
+
return this.transport.refreshAccessToken(refreshToken(token));
|
|
1018
|
+
}
|
|
1019
|
+
async logout() {
|
|
1020
|
+
await this.transport.logout();
|
|
1021
|
+
}
|
|
1022
|
+
async me() {
|
|
1023
|
+
return this.transport.getCurrentUser();
|
|
1024
|
+
}
|
|
1025
|
+
async acceptTerms() {
|
|
1026
|
+
await this.transport.acceptTerms();
|
|
1027
|
+
}
|
|
1028
|
+
async mcpToken() {
|
|
1029
|
+
return this.transport.generateMcpToken();
|
|
1030
|
+
}
|
|
1031
|
+
async mediaToken(resourceId) {
|
|
1032
|
+
return this.transport.getMediaToken(resourceId);
|
|
1033
|
+
}
|
|
1034
|
+
};
|
|
1035
|
+
|
|
1036
|
+
// src/namespaces/admin.ts
|
|
1037
|
+
var AdminNamespace = class {
|
|
1038
|
+
constructor(transport) {
|
|
1039
|
+
this.transport = transport;
|
|
1040
|
+
}
|
|
1041
|
+
async users() {
|
|
1042
|
+
const result = await this.transport.listUsers();
|
|
1043
|
+
return result.users;
|
|
1044
|
+
}
|
|
1045
|
+
async userStats() {
|
|
1046
|
+
return this.transport.getUserStats();
|
|
1047
|
+
}
|
|
1048
|
+
async updateUser(userId, data) {
|
|
1049
|
+
const result = await this.transport.updateUser(userId, data);
|
|
1050
|
+
return result.user;
|
|
1051
|
+
}
|
|
1052
|
+
async oauthConfig() {
|
|
1053
|
+
return this.transport.getOAuthConfig();
|
|
1054
|
+
}
|
|
1055
|
+
async healthCheck() {
|
|
1056
|
+
return this.transport.healthCheck();
|
|
1057
|
+
}
|
|
1058
|
+
async status() {
|
|
1059
|
+
return this.transport.getStatus();
|
|
1060
|
+
}
|
|
1061
|
+
async backup() {
|
|
1062
|
+
return this.transport.backupKnowledgeBase();
|
|
1063
|
+
}
|
|
1064
|
+
async restore(file, onProgress) {
|
|
1065
|
+
return this.transport.restoreKnowledgeBase(file, onProgress);
|
|
1066
|
+
}
|
|
1067
|
+
async exportKnowledgeBase(params) {
|
|
1068
|
+
return this.transport.exportKnowledgeBase(params);
|
|
1069
|
+
}
|
|
1070
|
+
async importKnowledgeBase(file, onProgress) {
|
|
1071
|
+
return this.transport.importKnowledgeBase(file, onProgress);
|
|
1072
|
+
}
|
|
1073
|
+
};
|
|
1074
|
+
var SemiontClient = class {
|
|
1075
|
+
/**
|
|
1076
|
+
* The wire-facing transport. Owns bus actor, HTTP, auth, admin, exchange,
|
|
1077
|
+
* system. Exposed for advanced consumers (workers, custom job adapters)
|
|
1078
|
+
* that need raw `transport.emit(channel, payload, scope)` access. Ordinary
|
|
1079
|
+
* consumers go through typed namespace methods.
|
|
1080
|
+
*/
|
|
1081
|
+
transport;
|
|
1082
|
+
/** Binary I/O transport. */
|
|
1083
|
+
content;
|
|
1084
|
+
/**
|
|
1085
|
+
* Per-client local EventBus. Wire events flow in via the transport
|
|
1086
|
+
* bridge. Read-only public so `SemiontSession.subscribe(channel, …)`
|
|
1087
|
+
* can wire arbitrary-channel subscriptions; everything else uses
|
|
1088
|
+
* typed namespace methods.
|
|
1089
|
+
*/
|
|
1090
|
+
bus;
|
|
1091
|
+
baseUrl;
|
|
1092
|
+
// ── Verb-oriented namespace API ──────────────────────────────────────────
|
|
1093
|
+
browse;
|
|
1094
|
+
mark;
|
|
1095
|
+
bind;
|
|
1096
|
+
gather;
|
|
1097
|
+
match;
|
|
1098
|
+
yield;
|
|
1099
|
+
beckon;
|
|
1100
|
+
job;
|
|
1101
|
+
auth;
|
|
1102
|
+
admin;
|
|
1103
|
+
/**
|
|
1104
|
+
* The client *owns* its bus. The constructor creates a fresh `EventBus`
|
|
1105
|
+
* and hands it to the transport via `transport.bridgeInto(this.bus)`.
|
|
1106
|
+
* The reference flows client → transport, never the other way:
|
|
1107
|
+
* the transport stores the reference and publishes the events it
|
|
1108
|
+
* receives onto that bus. `HttpTransport` does so for every channel
|
|
1109
|
+
* delivered on its SSE wire; in-process transports adapt their
|
|
1110
|
+
* internal source.
|
|
1111
|
+
*
|
|
1112
|
+
* Callers do not pass a bus in. If they need to interact with the bus
|
|
1113
|
+
* (e.g. for tests or to subscribe to arbitrary channels), they read it
|
|
1114
|
+
* back via `client.bus`.
|
|
1115
|
+
*/
|
|
1116
|
+
constructor(transport, content) {
|
|
1117
|
+
this.transport = transport;
|
|
1118
|
+
this.content = content;
|
|
1119
|
+
this.baseUrl = transport.baseUrl;
|
|
1120
|
+
this.bus = new EventBus();
|
|
1121
|
+
this.transport.bridgeInto(this.bus);
|
|
1122
|
+
this.browse = new BrowseNamespace(this.transport, this.bus, this.content);
|
|
1123
|
+
this.mark = new MarkNamespace(this.transport, this.bus);
|
|
1124
|
+
this.bind = new BindNamespace(this.transport, this.bus);
|
|
1125
|
+
this.gather = new GatherNamespace(this.transport, this.bus);
|
|
1126
|
+
this.match = new MatchNamespace(this.transport, this.bus);
|
|
1127
|
+
this.yield = new YieldNamespace(this.transport, this.bus, this.content);
|
|
1128
|
+
this.beckon = new BeckonNamespace(this.transport, this.bus);
|
|
1129
|
+
this.job = new JobNamespace(this.transport, this.bus);
|
|
1130
|
+
this.auth = new AuthNamespace(this.transport);
|
|
1131
|
+
this.admin = new AdminNamespace(this.transport);
|
|
1132
|
+
}
|
|
1133
|
+
/** Transport-level connection state. HTTP reflects SSE health; local is always 'connected'. */
|
|
1134
|
+
get state$() {
|
|
1135
|
+
return this.transport.state$;
|
|
1136
|
+
}
|
|
1137
|
+
subscribeToResource(resourceId) {
|
|
1138
|
+
return this.transport.subscribeToResource(resourceId);
|
|
1139
|
+
}
|
|
1140
|
+
dispose() {
|
|
1141
|
+
this.transport.dispose();
|
|
1142
|
+
this.content.dispose();
|
|
1143
|
+
}
|
|
1144
|
+
};
|
|
1145
|
+
|
|
1146
|
+
// src/session/storage.ts
|
|
1147
|
+
var SESSION_PREFIX = "semiont.session.";
|
|
1148
|
+
var STORAGE_KEY = "semiont.knowledgeBases";
|
|
1149
|
+
var ACTIVE_KEY = "semiont.activeKnowledgeBaseId";
|
|
1150
|
+
var REFRESH_BEFORE_EXP_MS = 5 * 60 * 1e3;
|
|
1151
|
+
function sessionKey(kbId) {
|
|
1152
|
+
return `${SESSION_PREFIX}${kbId}`;
|
|
1153
|
+
}
|
|
1154
|
+
function getStoredSession(storage, kbId) {
|
|
1155
|
+
const raw = storage.get(sessionKey(kbId));
|
|
1156
|
+
if (!raw) return null;
|
|
1157
|
+
try {
|
|
1158
|
+
const parsed = JSON.parse(raw);
|
|
1159
|
+
if (parsed && typeof parsed.access === "string" && typeof parsed.refresh === "string") {
|
|
1160
|
+
return { access: parsed.access, refresh: parsed.refresh };
|
|
1161
|
+
}
|
|
1162
|
+
} catch {
|
|
1163
|
+
}
|
|
1164
|
+
return null;
|
|
1165
|
+
}
|
|
1166
|
+
function setStoredSession(storage, kbId, session) {
|
|
1167
|
+
storage.set(sessionKey(kbId), JSON.stringify(session));
|
|
1168
|
+
}
|
|
1169
|
+
function clearStoredSession(storage, kbId) {
|
|
1170
|
+
storage.delete(sessionKey(kbId));
|
|
1171
|
+
}
|
|
1172
|
+
function parseJwtExpiry(token) {
|
|
1173
|
+
try {
|
|
1174
|
+
const parts = token.split(".");
|
|
1175
|
+
if (parts.length !== 3 || !parts[1]) return null;
|
|
1176
|
+
const payload = JSON.parse(atob(parts[1]));
|
|
1177
|
+
if (!payload.exp) return null;
|
|
1178
|
+
return new Date(payload.exp * 1e3);
|
|
1179
|
+
} catch {
|
|
1180
|
+
return null;
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
function isJwtExpired(token) {
|
|
1184
|
+
const expiry = parseJwtExpiry(token);
|
|
1185
|
+
if (!expiry) return true;
|
|
1186
|
+
return expiry.getTime() < Date.now();
|
|
1187
|
+
}
|
|
1188
|
+
function migrateLegacyEntry(entry) {
|
|
1189
|
+
if (entry.host !== void 0) return entry;
|
|
1190
|
+
try {
|
|
1191
|
+
const url = new URL(entry.backendUrl);
|
|
1192
|
+
return {
|
|
1193
|
+
id: entry.id,
|
|
1194
|
+
label: entry.label,
|
|
1195
|
+
host: url.hostname,
|
|
1196
|
+
port: parseInt(url.port, 10) || (url.protocol === "https:" ? 443 : 80),
|
|
1197
|
+
protocol: url.protocol === "https:" ? "https" : "http",
|
|
1198
|
+
email: ""
|
|
1199
|
+
};
|
|
1200
|
+
} catch {
|
|
1201
|
+
return {
|
|
1202
|
+
id: entry.id,
|
|
1203
|
+
label: entry.label || "Unknown",
|
|
1204
|
+
host: "localhost",
|
|
1205
|
+
port: 4e3,
|
|
1206
|
+
protocol: "http",
|
|
1207
|
+
email: ""
|
|
1208
|
+
};
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
function loadKnowledgeBases(storage) {
|
|
1212
|
+
try {
|
|
1213
|
+
const raw = storage.get(STORAGE_KEY);
|
|
1214
|
+
if (!raw) return [];
|
|
1215
|
+
const entries = JSON.parse(raw);
|
|
1216
|
+
return entries.map(migrateLegacyEntry);
|
|
1217
|
+
} catch {
|
|
1218
|
+
return [];
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
function saveKnowledgeBases(storage, knowledgeBases) {
|
|
1222
|
+
storage.set(STORAGE_KEY, JSON.stringify(knowledgeBases));
|
|
1223
|
+
}
|
|
1224
|
+
function defaultProtocol(host) {
|
|
1225
|
+
return host === "localhost" || host === "127.0.0.1" ? "http" : "https";
|
|
1226
|
+
}
|
|
1227
|
+
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})$/;
|
|
1228
|
+
function isValidHostname(host) {
|
|
1229
|
+
return HOSTNAME_RE.test(host);
|
|
1230
|
+
}
|
|
1231
|
+
function kbBackendUrl(kb) {
|
|
1232
|
+
if (!isValidHostname(kb.host)) {
|
|
1233
|
+
throw new Error(`Invalid KB hostname: "${kb.host}"`);
|
|
1234
|
+
}
|
|
1235
|
+
const url = new URL("http://x");
|
|
1236
|
+
url.protocol = kb.protocol + ":";
|
|
1237
|
+
url.hostname = kb.host;
|
|
1238
|
+
url.port = String(kb.port);
|
|
1239
|
+
return `${kb.protocol}://${url.hostname}:${kb.port}`;
|
|
1240
|
+
}
|
|
1241
|
+
function generateKbId() {
|
|
1242
|
+
return crypto.randomUUID();
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
// src/session/errors.ts
|
|
1246
|
+
var SemiontError = class extends Error {
|
|
1247
|
+
code;
|
|
1248
|
+
kbId;
|
|
1249
|
+
constructor(code, message, kbId = null) {
|
|
1250
|
+
super(message);
|
|
1251
|
+
this.name = "SemiontError";
|
|
1252
|
+
this.code = code;
|
|
1253
|
+
this.kbId = kbId;
|
|
1254
|
+
}
|
|
1255
|
+
};
|
|
1256
|
+
|
|
1257
|
+
// src/session/semiont-session.ts
|
|
1258
|
+
var SemiontSession = class {
|
|
1259
|
+
kb;
|
|
1260
|
+
client;
|
|
1261
|
+
token$;
|
|
1262
|
+
user$;
|
|
1263
|
+
streamState$;
|
|
1264
|
+
/** Resolves after the initial validation round-trip completes (success or failure). */
|
|
1265
|
+
ready;
|
|
1266
|
+
storage;
|
|
1267
|
+
doRefresh;
|
|
1268
|
+
doValidate;
|
|
1269
|
+
onAuthFailed;
|
|
1270
|
+
onError;
|
|
1271
|
+
refreshTimer = null;
|
|
1272
|
+
unsubscribeStorage = null;
|
|
1273
|
+
disposed = false;
|
|
1274
|
+
constructor(config) {
|
|
1275
|
+
this.kb = config.kb;
|
|
1276
|
+
this.storage = config.storage;
|
|
1277
|
+
this.doRefresh = config.refresh;
|
|
1278
|
+
this.doValidate = config.validate;
|
|
1279
|
+
this.onAuthFailed = config.onAuthFailed ?? (() => {
|
|
1280
|
+
});
|
|
1281
|
+
this.onError = config.onError ?? (() => {
|
|
1282
|
+
});
|
|
1283
|
+
this.client = config.client;
|
|
1284
|
+
this.token$ = config.token$;
|
|
1285
|
+
this.user$ = new BehaviorSubject(null);
|
|
1286
|
+
const stored = getStoredSession(this.storage, this.kb.id);
|
|
1287
|
+
if (stored && !isJwtExpired(stored.access) && this.token$.getValue() === null) {
|
|
1288
|
+
this.token$.next(accessToken(stored.access));
|
|
1289
|
+
}
|
|
1290
|
+
const initialToken = this.token$.getValue();
|
|
1291
|
+
this.streamState$ = this.client.state$;
|
|
1292
|
+
if (initialToken) {
|
|
1293
|
+
this.scheduleProactiveRefresh(initialToken);
|
|
1294
|
+
}
|
|
1295
|
+
this.unsubscribeStorage = this.storage.subscribe?.((key, newValue) => {
|
|
1296
|
+
this.handleStorageChange(key, newValue);
|
|
1297
|
+
}) ?? null;
|
|
1298
|
+
this.ready = this.validate(stored);
|
|
1299
|
+
}
|
|
1300
|
+
/**
|
|
1301
|
+
* Run the initial mount-time validation. If a stored access token is
|
|
1302
|
+
* present and unexpired, call the configured `validate` with it to
|
|
1303
|
+
* confirm it still works and populate `user$`. If expired, try
|
|
1304
|
+
* refresh first. On 401 from validate, try refresh once. Surfaces
|
|
1305
|
+
* auth-failed on terminal failure.
|
|
1306
|
+
*
|
|
1307
|
+
* When no `validate` callback is provided (service principals), this
|
|
1308
|
+
* still runs through the refresh-if-expired step so the stored
|
|
1309
|
+
* token is current — it just skips the user-validation round trip.
|
|
1310
|
+
*/
|
|
1311
|
+
async validate(stored) {
|
|
1312
|
+
if (!stored) return;
|
|
1313
|
+
const startToken = isJwtExpired(stored.access) ? this.doRefresh ? await this.doRefresh() : null : stored.access;
|
|
1314
|
+
if (!startToken) {
|
|
1315
|
+
if (isJwtExpired(stored.access)) {
|
|
1316
|
+
clearStoredSession(this.storage, this.kb.id);
|
|
1317
|
+
}
|
|
1318
|
+
return;
|
|
1319
|
+
}
|
|
1320
|
+
if (startToken !== stored.access) {
|
|
1321
|
+
this.token$.next(accessToken(startToken));
|
|
1322
|
+
this.scheduleProactiveRefresh(startToken);
|
|
1323
|
+
}
|
|
1324
|
+
if (!this.doValidate) return;
|
|
1325
|
+
const attempt = async (token) => {
|
|
1326
|
+
if (this.disposed) return;
|
|
1327
|
+
try {
|
|
1328
|
+
const data = await this.doValidate(accessToken(token));
|
|
1329
|
+
if (this.disposed) return;
|
|
1330
|
+
this.user$.next(data);
|
|
1331
|
+
} catch (err) {
|
|
1332
|
+
if (this.disposed) return;
|
|
1333
|
+
if (err instanceof APIError && err.status === 401) {
|
|
1334
|
+
const refreshed = this.doRefresh ? await this.doRefresh() : null;
|
|
1335
|
+
if (this.disposed) return;
|
|
1336
|
+
if (refreshed) {
|
|
1337
|
+
this.token$.next(accessToken(refreshed));
|
|
1338
|
+
this.scheduleProactiveRefresh(refreshed);
|
|
1339
|
+
await attempt(refreshed);
|
|
1340
|
+
return;
|
|
1341
|
+
}
|
|
1342
|
+
clearStoredSession(this.storage, this.kb.id);
|
|
1343
|
+
this.token$.next(null);
|
|
1344
|
+
this.onAuthFailed("Your session has expired. Please sign in again.");
|
|
1345
|
+
} else {
|
|
1346
|
+
this.onError(
|
|
1347
|
+
new SemiontError(
|
|
1348
|
+
"session.auth-failed",
|
|
1349
|
+
err instanceof Error ? err.message : String(err),
|
|
1350
|
+
this.kb.id
|
|
1351
|
+
)
|
|
1352
|
+
);
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
};
|
|
1356
|
+
await attempt(startToken);
|
|
1357
|
+
}
|
|
1358
|
+
/**
|
|
1359
|
+
* Refresh the access token via the configured `refresh` callback.
|
|
1360
|
+
* On success, pushes the new token into `token$` and schedules the
|
|
1361
|
+
* next proactive refresh. On failure, clears persisted state and
|
|
1362
|
+
* fires `onAuthFailed` — the frontend's wiring of that callback is
|
|
1363
|
+
* what surfaces the session-expired modal.
|
|
1364
|
+
*/
|
|
1365
|
+
async refresh() {
|
|
1366
|
+
if (this.disposed) return null;
|
|
1367
|
+
if (!this.doRefresh) return null;
|
|
1368
|
+
const newAccess = await this.doRefresh();
|
|
1369
|
+
if (this.disposed) return null;
|
|
1370
|
+
if (newAccess) {
|
|
1371
|
+
const tok = accessToken(newAccess);
|
|
1372
|
+
this.token$.next(tok);
|
|
1373
|
+
this.scheduleProactiveRefresh(newAccess);
|
|
1374
|
+
return tok;
|
|
1375
|
+
}
|
|
1376
|
+
this.token$.next(null);
|
|
1377
|
+
clearStoredSession(this.storage, this.kb.id);
|
|
1378
|
+
this.onAuthFailed("Your session has expired. Please sign in again.");
|
|
1379
|
+
this.onError(
|
|
1380
|
+
new SemiontError("session.refresh-exhausted", "Token refresh failed", this.kb.id)
|
|
1381
|
+
);
|
|
1382
|
+
return null;
|
|
1383
|
+
}
|
|
1384
|
+
scheduleProactiveRefresh(token) {
|
|
1385
|
+
this.clearRefreshTimer();
|
|
1386
|
+
const expiresAt = parseJwtExpiry(token);
|
|
1387
|
+
if (!expiresAt) return;
|
|
1388
|
+
const refreshAt = expiresAt.getTime() - REFRESH_BEFORE_EXP_MS;
|
|
1389
|
+
const delay = Math.max(0, refreshAt - Date.now());
|
|
1390
|
+
this.refreshTimer = setTimeout(() => {
|
|
1391
|
+
this.refreshTimer = null;
|
|
1392
|
+
if (!this.disposed) void this.refresh();
|
|
1393
|
+
}, delay);
|
|
1394
|
+
}
|
|
1395
|
+
clearRefreshTimer() {
|
|
1396
|
+
if (this.refreshTimer) {
|
|
1397
|
+
clearTimeout(this.refreshTimer);
|
|
1398
|
+
this.refreshTimer = null;
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
/**
|
|
1402
|
+
* Cross-context sync: another tab/process refreshed or signed out this
|
|
1403
|
+
* KB. Mirror the change into our in-memory state.
|
|
1404
|
+
*/
|
|
1405
|
+
handleStorageChange(key, newValue) {
|
|
1406
|
+
if (this.disposed) return;
|
|
1407
|
+
if (key !== sessionKey(this.kb.id)) return;
|
|
1408
|
+
if (!newValue) {
|
|
1409
|
+
this.token$.next(null);
|
|
1410
|
+
this.user$.next(null);
|
|
1411
|
+
this.clearRefreshTimer();
|
|
1412
|
+
return;
|
|
1413
|
+
}
|
|
1414
|
+
try {
|
|
1415
|
+
const parsed = JSON.parse(newValue);
|
|
1416
|
+
if (typeof parsed.access === "string") {
|
|
1417
|
+
this.token$.next(accessToken(parsed.access));
|
|
1418
|
+
this.scheduleProactiveRefresh(parsed.access);
|
|
1419
|
+
}
|
|
1420
|
+
} catch {
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
get expiresAt() {
|
|
1424
|
+
const token = this.token$.getValue();
|
|
1425
|
+
return token ? parseJwtExpiry(token) : null;
|
|
1426
|
+
}
|
|
1427
|
+
/**
|
|
1428
|
+
* Subscribe to a session-bus channel. The single sanctioned escape hatch
|
|
1429
|
+
* for generic-channel subscription (the case `useEventSubscription` needs
|
|
1430
|
+
* — channel name is a hook parameter, not known statically). All other
|
|
1431
|
+
* consumers must call typed namespace methods (e.g. `session.client.mark.archive(...)`).
|
|
1432
|
+
*
|
|
1433
|
+
* @returns disposer that unsubscribes the handler.
|
|
1434
|
+
*/
|
|
1435
|
+
subscribe(channel, handler) {
|
|
1436
|
+
const sub = this.client.bus.get(channel).subscribe(handler);
|
|
1437
|
+
return () => sub.unsubscribe();
|
|
1438
|
+
}
|
|
1439
|
+
async dispose() {
|
|
1440
|
+
if (this.disposed) return;
|
|
1441
|
+
this.disposed = true;
|
|
1442
|
+
this.clearRefreshTimer();
|
|
1443
|
+
if (this.unsubscribeStorage) {
|
|
1444
|
+
this.unsubscribeStorage();
|
|
1445
|
+
this.unsubscribeStorage = null;
|
|
1446
|
+
}
|
|
1447
|
+
this.client.dispose();
|
|
1448
|
+
this.token$.complete();
|
|
1449
|
+
this.user$.complete();
|
|
1450
|
+
}
|
|
1451
|
+
};
|
|
1452
|
+
|
|
1453
|
+
// src/session/notify.ts
|
|
1454
|
+
var activeOnSessionExpired = null;
|
|
1455
|
+
var activeOnPermissionDenied = null;
|
|
1456
|
+
function notifySessionExpired(message) {
|
|
1457
|
+
activeOnSessionExpired?.(message);
|
|
1458
|
+
}
|
|
1459
|
+
function notifyPermissionDenied(message) {
|
|
1460
|
+
activeOnPermissionDenied?.(message);
|
|
1461
|
+
}
|
|
1462
|
+
function registerAuthNotifyHandlers(handlers) {
|
|
1463
|
+
activeOnSessionExpired = handlers.onSessionExpired;
|
|
1464
|
+
activeOnPermissionDenied = handlers.onPermissionDenied;
|
|
1465
|
+
return () => {
|
|
1466
|
+
activeOnSessionExpired = null;
|
|
1467
|
+
activeOnPermissionDenied = null;
|
|
1468
|
+
};
|
|
1469
|
+
}
|
|
1470
|
+
var FrontendSessionSignals = class {
|
|
1471
|
+
sessionExpiredAt$;
|
|
1472
|
+
sessionExpiredMessage$;
|
|
1473
|
+
permissionDeniedAt$;
|
|
1474
|
+
permissionDeniedMessage$;
|
|
1475
|
+
constructor() {
|
|
1476
|
+
this.sessionExpiredAt$ = new BehaviorSubject(null);
|
|
1477
|
+
this.sessionExpiredMessage$ = new BehaviorSubject(null);
|
|
1478
|
+
this.permissionDeniedAt$ = new BehaviorSubject(null);
|
|
1479
|
+
this.permissionDeniedMessage$ = new BehaviorSubject(null);
|
|
1480
|
+
}
|
|
1481
|
+
notifySessionExpired(message) {
|
|
1482
|
+
this.sessionExpiredMessage$.next(
|
|
1483
|
+
message ?? "Your session has expired. Please sign in again."
|
|
1484
|
+
);
|
|
1485
|
+
this.sessionExpiredAt$.next(Date.now());
|
|
1486
|
+
}
|
|
1487
|
+
notifyPermissionDenied(message) {
|
|
1488
|
+
this.permissionDeniedMessage$.next(
|
|
1489
|
+
message ?? "You do not have permission to perform this action."
|
|
1490
|
+
);
|
|
1491
|
+
this.permissionDeniedAt$.next(Date.now());
|
|
1492
|
+
}
|
|
1493
|
+
acknowledgeSessionExpired() {
|
|
1494
|
+
this.sessionExpiredAt$.next(null);
|
|
1495
|
+
this.sessionExpiredMessage$.next(null);
|
|
1496
|
+
}
|
|
1497
|
+
acknowledgePermissionDenied() {
|
|
1498
|
+
this.permissionDeniedAt$.next(null);
|
|
1499
|
+
this.permissionDeniedMessage$.next(null);
|
|
1500
|
+
}
|
|
1501
|
+
dispose() {
|
|
1502
|
+
this.sessionExpiredAt$.complete();
|
|
1503
|
+
this.sessionExpiredMessage$.complete();
|
|
1504
|
+
this.permissionDeniedAt$.complete();
|
|
1505
|
+
this.permissionDeniedMessage$.complete();
|
|
1506
|
+
}
|
|
1507
|
+
};
|
|
1508
|
+
|
|
1509
|
+
// src/session/semiont-browser.ts
|
|
1510
|
+
var OPEN_RESOURCES_KEY = "openDocuments";
|
|
1511
|
+
function sortOpenResources(resources) {
|
|
1512
|
+
return [...resources].sort((a, b) => {
|
|
1513
|
+
if (a.order !== void 0 && b.order !== void 0) return a.order - b.order;
|
|
1514
|
+
return a.openedAt - b.openedAt;
|
|
1515
|
+
});
|
|
1516
|
+
}
|
|
1517
|
+
function loadOpenResources(storage) {
|
|
1518
|
+
try {
|
|
1519
|
+
const stored = storage.get(OPEN_RESOURCES_KEY);
|
|
1520
|
+
if (stored) return sortOpenResources(JSON.parse(stored));
|
|
1521
|
+
} catch {
|
|
1522
|
+
}
|
|
1523
|
+
return [];
|
|
1524
|
+
}
|
|
1525
|
+
var SemiontBrowser = class {
|
|
1526
|
+
kbs$;
|
|
1527
|
+
activeKbId$;
|
|
1528
|
+
activeSession$;
|
|
1529
|
+
/**
|
|
1530
|
+
* Modal signals (session-expired / permission-denied) for the
|
|
1531
|
+
* currently-active session. Parallels `activeSession$` — always
|
|
1532
|
+
* non-null when `activeSession$` is non-null, always null when it
|
|
1533
|
+
* is. Extracted from the session itself so headless sessions
|
|
1534
|
+
* (workers, CLIs, tests) don't carry dead modal observables.
|
|
1535
|
+
* See [FrontendSessionSignals](./frontend-session-signals.ts).
|
|
1536
|
+
*/
|
|
1537
|
+
activeSignals$;
|
|
1538
|
+
/**
|
|
1539
|
+
* True while a session is actively being constructed (setActiveKb /
|
|
1540
|
+
* signIn in flight, awaiting `session.ready`). Distinguishes the
|
|
1541
|
+
* "session about to arrive" intermediate state from "session
|
|
1542
|
+
* intentionally null" (after signOut, or when the active KB has no
|
|
1543
|
+
* stored credentials). UIs that want a loading spinner should gate
|
|
1544
|
+
* on this; otherwise they get stuck spinning after every signOut.
|
|
1545
|
+
*/
|
|
1546
|
+
sessionActivating$;
|
|
1547
|
+
openResources$;
|
|
1548
|
+
error$;
|
|
1549
|
+
identityToken$;
|
|
1550
|
+
storage;
|
|
1551
|
+
/**
|
|
1552
|
+
* App-scoped EventBus. Hosts UI-shell events that must work regardless
|
|
1553
|
+
* of whether a KB session is active: panel toggles, sidebar state,
|
|
1554
|
+
* tab reorders, routing, settings, etc. Disjoint from the per-session
|
|
1555
|
+
* bus inside `SemiontClient`, which carries KB-content events
|
|
1556
|
+
* (mark:*, beckon:*, gather:*, match:*, bind:*, yield:*, browse:click).
|
|
1557
|
+
*/
|
|
1558
|
+
eventBus = new EventBus();
|
|
1559
|
+
unregisterNotify = null;
|
|
1560
|
+
unsubscribeStorage = null;
|
|
1561
|
+
disposed = false;
|
|
1562
|
+
activating = null;
|
|
1563
|
+
/**
|
|
1564
|
+
* Per-KB in-flight refresh dedup. Simultaneous 401s for the same
|
|
1565
|
+
* KB converge on a single `/api/tokens/refresh` network call.
|
|
1566
|
+
* Was previously module-scoped in `refresh.ts`; moved here when
|
|
1567
|
+
* that file was deleted — SemiontBrowser is a singleton so the
|
|
1568
|
+
* scoping is equivalent.
|
|
1569
|
+
*/
|
|
1570
|
+
inFlightRefreshes = /* @__PURE__ */ new Map();
|
|
1571
|
+
constructor(config) {
|
|
1572
|
+
this.storage = config.storage;
|
|
1573
|
+
const kbs = loadKnowledgeBases(this.storage);
|
|
1574
|
+
const storedActive = this.storage.get(ACTIVE_KEY);
|
|
1575
|
+
const initialActive = storedActive && kbs.some((kb) => kb.id === storedActive) ? storedActive : kbs[0]?.id ?? null;
|
|
1576
|
+
this.kbs$ = new BehaviorSubject(kbs);
|
|
1577
|
+
this.activeKbId$ = new BehaviorSubject(initialActive);
|
|
1578
|
+
this.activeSession$ = new BehaviorSubject(null);
|
|
1579
|
+
this.activeSignals$ = new BehaviorSubject(null);
|
|
1580
|
+
this.sessionActivating$ = new BehaviorSubject(false);
|
|
1581
|
+
this.openResources$ = new BehaviorSubject(loadOpenResources(this.storage));
|
|
1582
|
+
this.error$ = new Subject();
|
|
1583
|
+
this.identityToken$ = new BehaviorSubject(null);
|
|
1584
|
+
this.kbs$.subscribe((next) => saveKnowledgeBases(this.storage, next));
|
|
1585
|
+
this.activeKbId$.subscribe((id) => {
|
|
1586
|
+
if (id) this.storage.set(ACTIVE_KEY, id);
|
|
1587
|
+
else this.storage.delete(ACTIVE_KEY);
|
|
1588
|
+
});
|
|
1589
|
+
this.openResources$.subscribe((list) => {
|
|
1590
|
+
this.storage.set(OPEN_RESOURCES_KEY, JSON.stringify(list));
|
|
1591
|
+
});
|
|
1592
|
+
this.unsubscribeStorage = this.storage.subscribe?.((key, newValue) => {
|
|
1593
|
+
if (key !== OPEN_RESOURCES_KEY || !newValue) return;
|
|
1594
|
+
try {
|
|
1595
|
+
this.openResources$.next(sortOpenResources(JSON.parse(newValue)));
|
|
1596
|
+
} catch {
|
|
1597
|
+
}
|
|
1598
|
+
}) ?? null;
|
|
1599
|
+
this.unregisterNotify = registerAuthNotifyHandlers({
|
|
1600
|
+
onSessionExpired: (message) => {
|
|
1601
|
+
this.activeSignals$.getValue()?.notifySessionExpired(message ?? null);
|
|
1602
|
+
},
|
|
1603
|
+
onPermissionDenied: (message) => {
|
|
1604
|
+
this.activeSignals$.getValue()?.notifyPermissionDenied(message ?? null);
|
|
1605
|
+
}
|
|
1606
|
+
});
|
|
1607
|
+
if (initialActive) {
|
|
1608
|
+
void this.setActiveKb(initialActive);
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
// ── App-scoped event bus ──────────────────────────────────────────────
|
|
1612
|
+
/** Emit an event on the browser's app-scoped bus. */
|
|
1613
|
+
emit(channel, payload) {
|
|
1614
|
+
if (this.disposed) return;
|
|
1615
|
+
this.eventBus.get(channel).next(payload);
|
|
1616
|
+
}
|
|
1617
|
+
/** Subscribe to an event; returns unsubscribe. */
|
|
1618
|
+
on(channel, handler) {
|
|
1619
|
+
const sub = this.eventBus.get(channel).subscribe(handler);
|
|
1620
|
+
return () => sub.unsubscribe();
|
|
1621
|
+
}
|
|
1622
|
+
/** Read-only observable for an app-scoped channel. */
|
|
1623
|
+
stream(channel) {
|
|
1624
|
+
return this.eventBus.get(channel).asObservable();
|
|
1625
|
+
}
|
|
1626
|
+
// ── Identity token (NextAuth bridge; D1) ──────────────────────────────
|
|
1627
|
+
/**
|
|
1628
|
+
* Set the app-level identity token (from NextAuth's useSession).
|
|
1629
|
+
* Called at the root layout via a single `useEffect`. No other site
|
|
1630
|
+
* in the codebase should call this.
|
|
1631
|
+
*/
|
|
1632
|
+
setIdentityToken(token) {
|
|
1633
|
+
if (this.disposed) return;
|
|
1634
|
+
this.identityToken$.next(token);
|
|
1635
|
+
}
|
|
1636
|
+
// ── KB list management ────────────────────────────────────────────────
|
|
1637
|
+
addKb(input, access, refresh) {
|
|
1638
|
+
const kb = { id: generateKbId(), ...input };
|
|
1639
|
+
setStoredSession(this.storage, kb.id, { access, refresh });
|
|
1640
|
+
this.kbs$.next([...this.kbs$.getValue(), kb]);
|
|
1641
|
+
void this.setActiveKb(kb.id);
|
|
1642
|
+
return kb;
|
|
1643
|
+
}
|
|
1644
|
+
removeKb(id) {
|
|
1645
|
+
clearStoredSession(this.storage, id);
|
|
1646
|
+
const next = this.kbs$.getValue().filter((kb) => kb.id !== id);
|
|
1647
|
+
this.kbs$.next(next);
|
|
1648
|
+
if (this.activeKbId$.getValue() === id) {
|
|
1649
|
+
void this.setActiveKb(next[0]?.id ?? null);
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
updateKb(id, updates) {
|
|
1653
|
+
this.kbs$.next(
|
|
1654
|
+
this.kbs$.getValue().map((kb) => kb.id === id ? { ...kb, ...updates } : kb)
|
|
1655
|
+
);
|
|
1656
|
+
}
|
|
1657
|
+
/**
|
|
1658
|
+
* Read the locally-stored credential status for a KB. Pure / synchronous —
|
|
1659
|
+
* does not subscribe to context changes. Used by KB-list UI to color status
|
|
1660
|
+
* dots without requiring re-renders on every tick.
|
|
1661
|
+
*/
|
|
1662
|
+
getKbSessionStatus(kbId) {
|
|
1663
|
+
const stored = getStoredSession(this.storage, kbId);
|
|
1664
|
+
if (!stored) return "signed-out";
|
|
1665
|
+
return isJwtExpired(stored.access) ? "expired" : "authenticated";
|
|
1666
|
+
}
|
|
1667
|
+
/**
|
|
1668
|
+
* Switch the active KB. Follows the D2 disposal contract:
|
|
1669
|
+
* 1. Synchronously announce the new id on `activeKbId$` and null out
|
|
1670
|
+
* `activeSession$` so views see a safe empty state first.
|
|
1671
|
+
* 2. Serialize overlapping calls — if an activation is in flight, wait
|
|
1672
|
+
* for it before proceeding.
|
|
1673
|
+
* 3. Dispose whatever session is currently live.
|
|
1674
|
+
* 4. Construct the next session and await `session.ready`.
|
|
1675
|
+
* 5. Before emitting, re-check `activeKbId$` — if a newer call superseded
|
|
1676
|
+
* us while we waited, dispose our session and skip the emit.
|
|
1677
|
+
* 6. Emit the new session.
|
|
1678
|
+
*/
|
|
1679
|
+
async setActiveKb(id) {
|
|
1680
|
+
if (this.disposed) return;
|
|
1681
|
+
const prevId = this.activeKbId$.getValue();
|
|
1682
|
+
const prevSession = this.activeSession$.getValue();
|
|
1683
|
+
if (id === prevId && prevSession) return;
|
|
1684
|
+
if (prevId !== id) this.activeKbId$.next(id);
|
|
1685
|
+
if (prevSession) {
|
|
1686
|
+
this.activeSession$.next(null);
|
|
1687
|
+
this.activeSignals$.next(null);
|
|
1688
|
+
}
|
|
1689
|
+
while (this.activating) {
|
|
1690
|
+
const current = this.activating;
|
|
1691
|
+
await current;
|
|
1692
|
+
if (this.disposed) return;
|
|
1693
|
+
if (this.activeKbId$.getValue() !== id) return;
|
|
1694
|
+
}
|
|
1695
|
+
const activation = (async () => {
|
|
1696
|
+
const toDispose = this.activeSession$.getValue();
|
|
1697
|
+
const signalsToDispose = this.activeSignals$.getValue();
|
|
1698
|
+
if (toDispose) {
|
|
1699
|
+
this.activeSession$.next(null);
|
|
1700
|
+
this.activeSignals$.next(null);
|
|
1701
|
+
await toDispose.dispose();
|
|
1702
|
+
signalsToDispose?.dispose();
|
|
1703
|
+
}
|
|
1704
|
+
if (!id) return;
|
|
1705
|
+
const kb = this.kbs$.getValue().find((k) => k.id === id);
|
|
1706
|
+
if (!kb) return;
|
|
1707
|
+
const signals = new FrontendSessionSignals();
|
|
1708
|
+
const token$ = new BehaviorSubject(null);
|
|
1709
|
+
let session;
|
|
1710
|
+
const transport = new HttpTransport({
|
|
1711
|
+
baseUrl: baseUrl(kbBackendUrl(kb)),
|
|
1712
|
+
token$,
|
|
1713
|
+
tokenRefresher: () => session.refresh().then((t) => t ?? null)
|
|
1714
|
+
});
|
|
1715
|
+
const content = new HttpContentTransport(transport);
|
|
1716
|
+
const client = new SemiontClient(transport, content);
|
|
1717
|
+
session = new SemiontSession({
|
|
1718
|
+
kb,
|
|
1719
|
+
storage: this.storage,
|
|
1720
|
+
client,
|
|
1721
|
+
token$,
|
|
1722
|
+
refresh: () => this.performRefresh(kb),
|
|
1723
|
+
validate: (token) => this.performValidate(kb, token),
|
|
1724
|
+
onAuthFailed: (msg) => signals.notifySessionExpired(msg),
|
|
1725
|
+
onError: (err) => this.error$.next(err)
|
|
1726
|
+
});
|
|
1727
|
+
try {
|
|
1728
|
+
await session.ready;
|
|
1729
|
+
} catch (err) {
|
|
1730
|
+
this.error$.next(
|
|
1731
|
+
new SemiontError(
|
|
1732
|
+
"session.construct-failed",
|
|
1733
|
+
err instanceof Error ? err.message : String(err),
|
|
1734
|
+
id
|
|
1735
|
+
)
|
|
1736
|
+
);
|
|
1737
|
+
await session.dispose();
|
|
1738
|
+
signals.dispose();
|
|
1739
|
+
return;
|
|
1740
|
+
}
|
|
1741
|
+
if (this.disposed || this.activeKbId$.getValue() !== id) {
|
|
1742
|
+
await session.dispose();
|
|
1743
|
+
signals.dispose();
|
|
1744
|
+
return;
|
|
1745
|
+
}
|
|
1746
|
+
this.activeSession$.next(session);
|
|
1747
|
+
this.activeSignals$.next(signals);
|
|
1748
|
+
})();
|
|
1749
|
+
this.activating = activation;
|
|
1750
|
+
this.sessionActivating$.next(true);
|
|
1751
|
+
try {
|
|
1752
|
+
await activation;
|
|
1753
|
+
} finally {
|
|
1754
|
+
if (this.activating === activation) {
|
|
1755
|
+
this.activating = null;
|
|
1756
|
+
this.sessionActivating$.next(false);
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
/**
|
|
1761
|
+
* Sign in to an existing KB: store the tokens and (re)activate the
|
|
1762
|
+
* session. If the KB is already active, the current session is disposed
|
|
1763
|
+
* and replaced so the new tokens take effect.
|
|
1764
|
+
*/
|
|
1765
|
+
async signIn(id, access, refresh) {
|
|
1766
|
+
if (this.disposed) return;
|
|
1767
|
+
setStoredSession(this.storage, id, { access, refresh });
|
|
1768
|
+
if (this.activeKbId$.getValue() === id) {
|
|
1769
|
+
const prevSession = this.activeSession$.getValue();
|
|
1770
|
+
const prevSignals = this.activeSignals$.getValue();
|
|
1771
|
+
this.activeSession$.next(null);
|
|
1772
|
+
this.activeSignals$.next(null);
|
|
1773
|
+
if (prevSession) await prevSession.dispose();
|
|
1774
|
+
prevSignals?.dispose();
|
|
1775
|
+
await this.setActiveKb(id);
|
|
1776
|
+
return;
|
|
1777
|
+
}
|
|
1778
|
+
await this.setActiveKb(id);
|
|
1779
|
+
}
|
|
1780
|
+
/**
|
|
1781
|
+
* Sign out of a KB: clear stored tokens. If the KB is active, dispose
|
|
1782
|
+
* its session + signals and emit null for both.
|
|
1783
|
+
*/
|
|
1784
|
+
async signOut(id) {
|
|
1785
|
+
if (this.disposed) return;
|
|
1786
|
+
clearStoredSession(this.storage, id);
|
|
1787
|
+
this.kbs$.next([...this.kbs$.getValue()]);
|
|
1788
|
+
if (this.activeKbId$.getValue() === id) {
|
|
1789
|
+
const prevSession = this.activeSession$.getValue();
|
|
1790
|
+
const prevSignals = this.activeSignals$.getValue();
|
|
1791
|
+
this.activeSession$.next(null);
|
|
1792
|
+
this.activeSignals$.next(null);
|
|
1793
|
+
if (prevSession) await prevSession.dispose();
|
|
1794
|
+
prevSignals?.dispose();
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
// ── Open resources ────────────────────────────────────────────────────
|
|
1798
|
+
addOpenResource(id, name, mediaType, storageUri) {
|
|
1799
|
+
const existing = this.openResources$.getValue();
|
|
1800
|
+
const idx = existing.findIndex((r) => r.id === id);
|
|
1801
|
+
if (idx >= 0) {
|
|
1802
|
+
const prev = existing[idx];
|
|
1803
|
+
const updated = {
|
|
1804
|
+
...prev,
|
|
1805
|
+
name,
|
|
1806
|
+
...mediaType !== void 0 ? { mediaType } : {},
|
|
1807
|
+
...storageUri !== void 0 ? { storageUri } : {}
|
|
1808
|
+
};
|
|
1809
|
+
const next = [...existing];
|
|
1810
|
+
next[idx] = updated;
|
|
1811
|
+
this.openResources$.next(next);
|
|
1812
|
+
return;
|
|
1813
|
+
}
|
|
1814
|
+
const resource = {
|
|
1815
|
+
id,
|
|
1816
|
+
name,
|
|
1817
|
+
openedAt: Date.now(),
|
|
1818
|
+
order: existing.length,
|
|
1819
|
+
...mediaType !== void 0 ? { mediaType } : {},
|
|
1820
|
+
...storageUri !== void 0 ? { storageUri } : {}
|
|
1821
|
+
};
|
|
1822
|
+
this.openResources$.next([...existing, resource]);
|
|
1823
|
+
}
|
|
1824
|
+
removeOpenResource(id) {
|
|
1825
|
+
this.openResources$.next(this.openResources$.getValue().filter((r) => r.id !== id));
|
|
1826
|
+
}
|
|
1827
|
+
updateOpenResourceName(id, name) {
|
|
1828
|
+
this.openResources$.next(
|
|
1829
|
+
this.openResources$.getValue().map((r) => r.id === id ? { ...r, name } : r)
|
|
1830
|
+
);
|
|
1831
|
+
}
|
|
1832
|
+
reorderOpenResources(oldIndex, newIndex) {
|
|
1833
|
+
const list = [...this.openResources$.getValue()];
|
|
1834
|
+
if (oldIndex < 0 || oldIndex >= list.length || newIndex < 0 || newIndex >= list.length) {
|
|
1835
|
+
return;
|
|
1836
|
+
}
|
|
1837
|
+
const [moved] = list.splice(oldIndex, 1);
|
|
1838
|
+
if (moved) list.splice(newIndex, 0, moved);
|
|
1839
|
+
this.openResources$.next(list);
|
|
1840
|
+
}
|
|
1841
|
+
// ── Auth callbacks bound per session ──────────────────────────────────
|
|
1842
|
+
//
|
|
1843
|
+
// These closures back the `refresh` and `validate` callbacks passed
|
|
1844
|
+
// to `SemiontSession` in `setActiveKb`. Factored out as methods
|
|
1845
|
+
// (rather than inline in the activation closure) so test-doubles
|
|
1846
|
+
// can override them cleanly, and so the in-flight dedup map
|
|
1847
|
+
// survives across activations of the same KB.
|
|
1848
|
+
/**
|
|
1849
|
+
* Refresh the active KB's access token. Returns the new token on
|
|
1850
|
+
* success, null on failure. Concurrent calls for the same KB
|
|
1851
|
+
* dedupe through `inFlightRefreshes`, so simultaneous 401s trigger
|
|
1852
|
+
* only one `/api/tokens/refresh` round trip.
|
|
1853
|
+
*
|
|
1854
|
+
* Uses a throwaway `SemiontClient` with no `tokenRefresher` —
|
|
1855
|
+
* a refresh call returning 401 would otherwise re-enter this
|
|
1856
|
+
* function infinitely.
|
|
1857
|
+
*/
|
|
1858
|
+
async performRefresh(kb) {
|
|
1859
|
+
const existing = this.inFlightRefreshes.get(kb.id);
|
|
1860
|
+
if (existing) return existing;
|
|
1861
|
+
const promise = (async () => {
|
|
1862
|
+
const stored = getStoredSession(this.storage, kb.id);
|
|
1863
|
+
if (!stored) return null;
|
|
1864
|
+
const throwawayTransport = new HttpTransport({ baseUrl: baseUrl(kbBackendUrl(kb)) });
|
|
1865
|
+
const throwaway = new SemiontClient(throwawayTransport, new HttpContentTransport(throwawayTransport));
|
|
1866
|
+
try {
|
|
1867
|
+
const response = await throwaway.auth.refresh(stored.refresh);
|
|
1868
|
+
const newAccess = response.access_token;
|
|
1869
|
+
if (!newAccess) return null;
|
|
1870
|
+
setStoredSession(this.storage, kb.id, { access: newAccess, refresh: stored.refresh });
|
|
1871
|
+
return newAccess;
|
|
1872
|
+
} catch {
|
|
1873
|
+
return null;
|
|
1874
|
+
} finally {
|
|
1875
|
+
throwaway.dispose();
|
|
1876
|
+
}
|
|
1877
|
+
})();
|
|
1878
|
+
this.inFlightRefreshes.set(kb.id, promise);
|
|
1879
|
+
try {
|
|
1880
|
+
return await promise;
|
|
1881
|
+
} finally {
|
|
1882
|
+
this.inFlightRefreshes.delete(kb.id);
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
/**
|
|
1886
|
+
* Validate an access token by calling `auth.me` on a throwaway
|
|
1887
|
+
* client. The session uses this once at startup to populate
|
|
1888
|
+
* `user$`; 401 triggers a refresh-then-retry inside the session.
|
|
1889
|
+
*
|
|
1890
|
+
* The throwaway transport is seeded with the specific token to
|
|
1891
|
+
* validate so the request actually carries it (HttpTransport
|
|
1892
|
+
* sources `Authorization` from its `token$`).
|
|
1893
|
+
*/
|
|
1894
|
+
async performValidate(kb, token) {
|
|
1895
|
+
const tokenSubject = new BehaviorSubject(token);
|
|
1896
|
+
const throwawayTransport = new HttpTransport({
|
|
1897
|
+
baseUrl: baseUrl(kbBackendUrl(kb)),
|
|
1898
|
+
token$: tokenSubject
|
|
1899
|
+
});
|
|
1900
|
+
const throwaway = new SemiontClient(throwawayTransport, new HttpContentTransport(throwawayTransport));
|
|
1901
|
+
try {
|
|
1902
|
+
const data = await throwaway.auth.me();
|
|
1903
|
+
return data;
|
|
1904
|
+
} finally {
|
|
1905
|
+
throwaway.dispose();
|
|
1906
|
+
tokenSubject.complete();
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
// ── Lifecycle ─────────────────────────────────────────────────────────
|
|
1910
|
+
async dispose() {
|
|
1911
|
+
if (this.disposed) return;
|
|
1912
|
+
this.disposed = true;
|
|
1913
|
+
this.unregisterNotify?.();
|
|
1914
|
+
this.unregisterNotify = null;
|
|
1915
|
+
if (this.unsubscribeStorage) {
|
|
1916
|
+
this.unsubscribeStorage();
|
|
1917
|
+
this.unsubscribeStorage = null;
|
|
1918
|
+
}
|
|
1919
|
+
const prevSession = this.activeSession$.getValue();
|
|
1920
|
+
const prevSignals = this.activeSignals$.getValue();
|
|
1921
|
+
this.activeSession$.next(null);
|
|
1922
|
+
this.activeSignals$.next(null);
|
|
1923
|
+
if (prevSession) await prevSession.dispose();
|
|
1924
|
+
prevSignals?.dispose();
|
|
1925
|
+
this.kbs$.complete();
|
|
1926
|
+
this.activeKbId$.complete();
|
|
1927
|
+
this.activeSession$.complete();
|
|
1928
|
+
this.activeSignals$.complete();
|
|
1929
|
+
this.openResources$.complete();
|
|
1930
|
+
this.error$.complete();
|
|
1931
|
+
this.identityToken$.complete();
|
|
1932
|
+
this.eventBus.destroy();
|
|
1933
|
+
}
|
|
1934
|
+
};
|
|
1935
|
+
|
|
1936
|
+
// src/session/registry.ts
|
|
1937
|
+
var instance = null;
|
|
1938
|
+
function getBrowser(options) {
|
|
1939
|
+
if (!instance) {
|
|
1940
|
+
instance = new SemiontBrowser({ storage: options.storage });
|
|
1941
|
+
}
|
|
1942
|
+
return instance;
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
// src/session/session-storage.ts
|
|
1946
|
+
var InMemorySessionStorage = class {
|
|
1947
|
+
map = /* @__PURE__ */ new Map();
|
|
1948
|
+
get(key) {
|
|
1949
|
+
return this.map.has(key) ? this.map.get(key) : null;
|
|
1950
|
+
}
|
|
1951
|
+
set(key, value) {
|
|
1952
|
+
this.map.set(key, value);
|
|
1953
|
+
}
|
|
1954
|
+
delete(key) {
|
|
1955
|
+
this.map.delete(key);
|
|
1956
|
+
}
|
|
1957
|
+
};
|
|
1958
|
+
function createDisposer() {
|
|
1959
|
+
const sub = new Subscription();
|
|
1960
|
+
return {
|
|
1961
|
+
add: (item) => sub.add(typeof item === "function" ? item : () => item.dispose()),
|
|
1962
|
+
dispose: () => sub.unsubscribe()
|
|
1963
|
+
};
|
|
1964
|
+
}
|
|
1965
|
+
function createSearchPipeline(fetch, options = {}) {
|
|
1966
|
+
const debounceMs = options.debounceMs ?? 250;
|
|
1967
|
+
const initial = options.initialQuery ?? "";
|
|
1968
|
+
const input$ = new Subject();
|
|
1969
|
+
const query$ = input$.pipe(startWith(initial));
|
|
1970
|
+
const state$ = input$.pipe(
|
|
1971
|
+
startWith(initial),
|
|
1972
|
+
debounceTime(debounceMs),
|
|
1973
|
+
distinctUntilChanged$1(),
|
|
1974
|
+
switchMap((q) => {
|
|
1975
|
+
const trimmed = q.trim();
|
|
1976
|
+
if (!trimmed) {
|
|
1977
|
+
return of({ results: [], isSearching: false });
|
|
1978
|
+
}
|
|
1979
|
+
return fetch(trimmed).pipe(
|
|
1980
|
+
map((results) => ({
|
|
1981
|
+
results: results ?? [],
|
|
1982
|
+
isSearching: results === void 0
|
|
1983
|
+
})),
|
|
1984
|
+
startWith({ results: [], isSearching: true })
|
|
1985
|
+
);
|
|
1986
|
+
})
|
|
1987
|
+
);
|
|
1988
|
+
return {
|
|
1989
|
+
query$,
|
|
1990
|
+
state$,
|
|
1991
|
+
setQuery: (value) => input$.next(value),
|
|
1992
|
+
dispose: () => input$.complete()
|
|
1993
|
+
};
|
|
1994
|
+
}
|
|
1995
|
+
function createBeckonVM(client) {
|
|
1996
|
+
const subs = [];
|
|
1997
|
+
const hovered$ = new BehaviorSubject(null);
|
|
1998
|
+
subs.push(client.bus.get("beckon:hover").subscribe(({ annotationId }) => {
|
|
1999
|
+
hovered$.next(annotationId);
|
|
2000
|
+
if (annotationId) {
|
|
2001
|
+
client.bus.get("beckon:sparkle").next({ annotationId });
|
|
2002
|
+
}
|
|
2003
|
+
}));
|
|
2004
|
+
subs.push(client.bus.get("browse:click").subscribe(({ annotationId }) => {
|
|
2005
|
+
client.bus.get("beckon:focus").next({ annotationId });
|
|
2006
|
+
}));
|
|
2007
|
+
return {
|
|
2008
|
+
hoveredAnnotationId$: hovered$.asObservable(),
|
|
2009
|
+
hover: (annotationId) => client.bus.get("beckon:hover").next({ annotationId }),
|
|
2010
|
+
focus: (annotationId) => client.bus.get("beckon:focus").next({ annotationId }),
|
|
2011
|
+
sparkle: (annotationId) => client.bus.get("beckon:sparkle").next({ annotationId }),
|
|
2012
|
+
dispose() {
|
|
2013
|
+
subs.forEach((s) => s.unsubscribe());
|
|
2014
|
+
hovered$.complete();
|
|
2015
|
+
}
|
|
2016
|
+
};
|
|
2017
|
+
}
|
|
2018
|
+
var HOVER_DELAY_MS = 150;
|
|
2019
|
+
function createHoverHandlers(emit, delayMs) {
|
|
2020
|
+
let currentHover = null;
|
|
2021
|
+
let timer = null;
|
|
2022
|
+
const cancelTimer = () => {
|
|
2023
|
+
if (timer !== null) {
|
|
2024
|
+
clearTimeout(timer);
|
|
2025
|
+
timer = null;
|
|
2026
|
+
}
|
|
2027
|
+
};
|
|
2028
|
+
const handleMouseEnter = (annotationId) => {
|
|
2029
|
+
if (currentHover === annotationId) return;
|
|
2030
|
+
cancelTimer();
|
|
2031
|
+
timer = setTimeout(() => {
|
|
2032
|
+
timer = null;
|
|
2033
|
+
currentHover = annotationId;
|
|
2034
|
+
emit(annotationId);
|
|
2035
|
+
}, delayMs);
|
|
2036
|
+
};
|
|
2037
|
+
const handleMouseLeave = () => {
|
|
2038
|
+
cancelTimer();
|
|
2039
|
+
if (currentHover !== null) {
|
|
2040
|
+
currentHover = null;
|
|
2041
|
+
emit(null);
|
|
2042
|
+
}
|
|
2043
|
+
};
|
|
2044
|
+
return { handleMouseEnter, handleMouseLeave, cleanup: cancelTimer };
|
|
2045
|
+
}
|
|
2046
|
+
var COMMON_PANELS = ["knowledge-base", "user", "settings"];
|
|
2047
|
+
var RESOURCE_PANELS = ["history", "info", "annotations", "collaboration", "jsonld"];
|
|
2048
|
+
var MOTIVATION_TO_TAB = {
|
|
2049
|
+
"linking": "reference",
|
|
2050
|
+
"commenting": "comment",
|
|
2051
|
+
"tagging": "tag",
|
|
2052
|
+
"highlighting": "highlight",
|
|
2053
|
+
"assessing": "assessment"
|
|
2054
|
+
};
|
|
2055
|
+
var tabGenerationCounter = 0;
|
|
2056
|
+
function createShellVM(browser, options) {
|
|
2057
|
+
const subs = [];
|
|
2058
|
+
const activePanel$ = new BehaviorSubject(options?.initialPanel ?? null);
|
|
2059
|
+
const scrollToAnnotationId$ = new BehaviorSubject(null);
|
|
2060
|
+
const panelInitialTab$ = new BehaviorSubject(null);
|
|
2061
|
+
if (options?.onPanelChange) {
|
|
2062
|
+
const cb = options.onPanelChange;
|
|
2063
|
+
subs.push(activePanel$.subscribe(cb));
|
|
2064
|
+
}
|
|
2065
|
+
subs.push(browser.stream("panel:toggle").subscribe(({ panel }) => {
|
|
2066
|
+
const current = activePanel$.getValue();
|
|
2067
|
+
activePanel$.next(current === panel ? null : panel);
|
|
2068
|
+
}));
|
|
2069
|
+
subs.push(browser.stream("panel:open").subscribe(({ panel, scrollToAnnotationId, motivation }) => {
|
|
2070
|
+
if (scrollToAnnotationId) {
|
|
2071
|
+
scrollToAnnotationId$.next(scrollToAnnotationId);
|
|
2072
|
+
}
|
|
2073
|
+
if (motivation) {
|
|
2074
|
+
const tab = MOTIVATION_TO_TAB[motivation] || "highlight";
|
|
2075
|
+
panelInitialTab$.next({ tab, generation: ++tabGenerationCounter });
|
|
2076
|
+
}
|
|
2077
|
+
activePanel$.next(panel);
|
|
2078
|
+
}));
|
|
2079
|
+
subs.push(browser.stream("panel:close").subscribe(() => {
|
|
2080
|
+
activePanel$.next(null);
|
|
2081
|
+
}));
|
|
2082
|
+
return {
|
|
2083
|
+
activePanel$: activePanel$.asObservable(),
|
|
2084
|
+
scrollToAnnotationId$: scrollToAnnotationId$.asObservable(),
|
|
2085
|
+
panelInitialTab$: panelInitialTab$.asObservable(),
|
|
2086
|
+
openPanel: (panel) => browser.emit("panel:open", { panel }),
|
|
2087
|
+
closePanel: () => browser.emit("panel:close", void 0),
|
|
2088
|
+
togglePanel: (panel) => browser.emit("panel:toggle", { panel }),
|
|
2089
|
+
onScrollCompleted: () => scrollToAnnotationId$.next(null),
|
|
2090
|
+
dispose() {
|
|
2091
|
+
subs.forEach((s) => s.unsubscribe());
|
|
2092
|
+
activePanel$.complete();
|
|
2093
|
+
scrollToAnnotationId$.complete();
|
|
2094
|
+
panelInitialTab$.complete();
|
|
2095
|
+
}
|
|
2096
|
+
};
|
|
2097
|
+
}
|
|
2098
|
+
function createGatherVM(client, resourceId) {
|
|
2099
|
+
const subs = [];
|
|
2100
|
+
const context$ = new BehaviorSubject(null);
|
|
2101
|
+
const loading$ = new BehaviorSubject(false);
|
|
2102
|
+
const error$ = new BehaviorSubject(null);
|
|
2103
|
+
const annotationId$ = new BehaviorSubject(null);
|
|
2104
|
+
subs.push(client.bus.get("gather:requested").subscribe((event) => {
|
|
2105
|
+
loading$.next(true);
|
|
2106
|
+
error$.next(null);
|
|
2107
|
+
context$.next(null);
|
|
2108
|
+
annotationId$.next(annotationId(event.annotationId));
|
|
2109
|
+
const gatherSub = client.gather.annotation(
|
|
2110
|
+
annotationId(event.annotationId),
|
|
2111
|
+
resourceId,
|
|
2112
|
+
{ contextWindow: event.options?.contextWindow ?? 2e3 }
|
|
2113
|
+
).pipe(
|
|
2114
|
+
timeout(6e4)
|
|
2115
|
+
).subscribe({
|
|
2116
|
+
next: (progress) => {
|
|
2117
|
+
if ("response" in progress && progress.response) {
|
|
2118
|
+
context$.next(
|
|
2119
|
+
progress.response.context ?? null
|
|
2120
|
+
);
|
|
2121
|
+
loading$.next(false);
|
|
2122
|
+
}
|
|
2123
|
+
},
|
|
2124
|
+
error: (err) => {
|
|
2125
|
+
error$.next(err instanceof Error ? err : new Error(String(err)));
|
|
2126
|
+
loading$.next(false);
|
|
2127
|
+
},
|
|
2128
|
+
complete: () => {
|
|
2129
|
+
loading$.next(false);
|
|
2130
|
+
}
|
|
2131
|
+
});
|
|
2132
|
+
subs.push(gatherSub);
|
|
2133
|
+
}));
|
|
2134
|
+
return {
|
|
2135
|
+
context$: context$.asObservable(),
|
|
2136
|
+
loading$: loading$.asObservable(),
|
|
2137
|
+
error$: error$.asObservable(),
|
|
2138
|
+
annotationId$: annotationId$.asObservable(),
|
|
2139
|
+
dispose() {
|
|
2140
|
+
subs.forEach((s) => s.unsubscribe());
|
|
2141
|
+
context$.complete();
|
|
2142
|
+
loading$.complete();
|
|
2143
|
+
error$.complete();
|
|
2144
|
+
annotationId$.complete();
|
|
2145
|
+
}
|
|
2146
|
+
};
|
|
2147
|
+
}
|
|
2148
|
+
function createMatchVM(client, _resourceId) {
|
|
2149
|
+
const subs = [];
|
|
2150
|
+
subs.push(client.bus.get("match:search-requested").subscribe((event) => {
|
|
2151
|
+
const searchSub = client.match.search(
|
|
2152
|
+
resourceId(event.resourceId),
|
|
2153
|
+
event.referenceId,
|
|
2154
|
+
event.context,
|
|
2155
|
+
{ limit: event.limit, useSemanticScoring: event.useSemanticScoring }
|
|
2156
|
+
).pipe(
|
|
2157
|
+
timeout(6e4)
|
|
2158
|
+
).subscribe({
|
|
2159
|
+
next: (result) => client.bus.get("match:search-results").next(result),
|
|
2160
|
+
error: (err) => client.bus.get("match:search-failed").next({
|
|
2161
|
+
correlationId: event.correlationId,
|
|
2162
|
+
referenceId: event.referenceId,
|
|
2163
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2164
|
+
})
|
|
2165
|
+
});
|
|
2166
|
+
subs.push(searchSub);
|
|
2167
|
+
}));
|
|
2168
|
+
return {
|
|
2169
|
+
dispose() {
|
|
2170
|
+
subs.forEach((s) => s.unsubscribe());
|
|
2171
|
+
}
|
|
2172
|
+
};
|
|
2173
|
+
}
|
|
2174
|
+
function createYieldVM(client, resourceId$1, locale) {
|
|
2175
|
+
const subs = [];
|
|
2176
|
+
const isGenerating$ = new BehaviorSubject(false);
|
|
2177
|
+
const progress$ = new BehaviorSubject(null);
|
|
2178
|
+
let clearTimer = null;
|
|
2179
|
+
const generate = (referenceId, options) => {
|
|
2180
|
+
const genSub = client.yield.fromAnnotation(
|
|
2181
|
+
resourceId(resourceId$1),
|
|
2182
|
+
annotationId(referenceId),
|
|
2183
|
+
{ ...options, language: options.language || locale }
|
|
2184
|
+
).pipe(
|
|
2185
|
+
timeout({ each: 3e5 })
|
|
2186
|
+
).subscribe({
|
|
2187
|
+
next: (e) => {
|
|
2188
|
+
if (e.kind === "progress") {
|
|
2189
|
+
progress$.next(e.data);
|
|
2190
|
+
isGenerating$.next(true);
|
|
2191
|
+
}
|
|
2192
|
+
},
|
|
2193
|
+
complete: () => {
|
|
2194
|
+
isGenerating$.next(false);
|
|
2195
|
+
if (clearTimer) clearTimeout(clearTimer);
|
|
2196
|
+
clearTimer = setTimeout(() => {
|
|
2197
|
+
progress$.next(null);
|
|
2198
|
+
clearTimer = null;
|
|
2199
|
+
}, 2e3);
|
|
2200
|
+
},
|
|
2201
|
+
error: () => {
|
|
2202
|
+
progress$.next(null);
|
|
2203
|
+
isGenerating$.next(false);
|
|
2204
|
+
}
|
|
2205
|
+
});
|
|
2206
|
+
subs.push(genSub);
|
|
2207
|
+
};
|
|
2208
|
+
return {
|
|
2209
|
+
isGenerating$: isGenerating$.asObservable(),
|
|
2210
|
+
progress$: progress$.asObservable(),
|
|
2211
|
+
generate,
|
|
2212
|
+
dispose() {
|
|
2213
|
+
subs.forEach((s) => s.unsubscribe());
|
|
2214
|
+
if (clearTimer) clearTimeout(clearTimer);
|
|
2215
|
+
isGenerating$.complete();
|
|
2216
|
+
progress$.complete();
|
|
2217
|
+
}
|
|
2218
|
+
};
|
|
2219
|
+
}
|
|
2220
|
+
function selectionToSelector(selection) {
|
|
2221
|
+
if (selection.svgSelector) return { type: "SvgSelector", value: selection.svgSelector };
|
|
2222
|
+
if (selection.fragmentSelector) {
|
|
2223
|
+
const selectors = [{ type: "FragmentSelector", value: selection.fragmentSelector, ...selection.conformsTo && { conformsTo: selection.conformsTo } }];
|
|
2224
|
+
if (selection.exact) selectors.push({ type: "TextQuoteSelector", exact: selection.exact, ...selection.prefix && { prefix: selection.prefix }, ...selection.suffix && { suffix: selection.suffix } });
|
|
2225
|
+
return selectors;
|
|
2226
|
+
}
|
|
2227
|
+
return { type: "TextQuoteSelector", exact: selection.exact, ...selection.prefix && { prefix: selection.prefix }, ...selection.suffix && { suffix: selection.suffix } };
|
|
2228
|
+
}
|
|
2229
|
+
function createMarkVM(client, resourceId) {
|
|
2230
|
+
const subs = [];
|
|
2231
|
+
const pendingAnnotation$ = new BehaviorSubject(null);
|
|
2232
|
+
const assistingMotivation$ = new BehaviorSubject(null);
|
|
2233
|
+
const progress$ = new BehaviorSubject(null);
|
|
2234
|
+
let progressDismissTimer = null;
|
|
2235
|
+
const clearProgressTimer = () => {
|
|
2236
|
+
if (progressDismissTimer) {
|
|
2237
|
+
clearTimeout(progressDismissTimer);
|
|
2238
|
+
progressDismissTimer = null;
|
|
2239
|
+
}
|
|
2240
|
+
};
|
|
2241
|
+
const handleAnnotationRequested = (pending) => {
|
|
2242
|
+
pendingAnnotation$.next(pending);
|
|
2243
|
+
};
|
|
2244
|
+
subs.push(client.bus.get("mark:requested").subscribe(handleAnnotationRequested));
|
|
2245
|
+
subs.push(client.bus.get("mark:select-comment").subscribe((s) => handleAnnotationRequested({ selector: selectionToSelector(s), motivation: "commenting" })));
|
|
2246
|
+
subs.push(client.bus.get("mark:select-tag").subscribe((s) => handleAnnotationRequested({ selector: selectionToSelector(s), motivation: "tagging" })));
|
|
2247
|
+
subs.push(client.bus.get("mark:select-assessment").subscribe((s) => handleAnnotationRequested({ selector: selectionToSelector(s), motivation: "assessing" })));
|
|
2248
|
+
subs.push(client.bus.get("mark:select-reference").subscribe((s) => handleAnnotationRequested({ selector: selectionToSelector(s), motivation: "linking" })));
|
|
2249
|
+
subs.push(client.bus.get("mark:cancel-pending").subscribe(() => pendingAnnotation$.next(null)));
|
|
2250
|
+
subs.push(client.bus.get("mark:create-ok").subscribe(() => pendingAnnotation$.next(null)));
|
|
2251
|
+
subs.push(client.bus.get("mark:submit").subscribe(async (event) => {
|
|
2252
|
+
try {
|
|
2253
|
+
const result = await client.mark.annotation(resourceId, {
|
|
2254
|
+
motivation: event.motivation,
|
|
2255
|
+
target: { source: resourceId, selector: event.selector },
|
|
2256
|
+
body: event.body
|
|
2257
|
+
});
|
|
2258
|
+
client.bus.get("mark:create-ok").next({ annotationId: result.annotationId });
|
|
2259
|
+
} catch (error) {
|
|
2260
|
+
client.bus.get("mark:create-failed").next({ message: error instanceof Error ? error.message : String(error) });
|
|
2261
|
+
}
|
|
2262
|
+
}));
|
|
2263
|
+
subs.push(client.bus.get("mark:delete").subscribe(async (event) => {
|
|
2264
|
+
try {
|
|
2265
|
+
await client.mark.delete(resourceId, event.annotationId);
|
|
2266
|
+
client.bus.get("mark:delete-ok").next({ annotationId: event.annotationId });
|
|
2267
|
+
} catch (error) {
|
|
2268
|
+
client.bus.get("mark:delete-failed").next({ message: error instanceof Error ? error.message : String(error) });
|
|
2269
|
+
}
|
|
2270
|
+
}));
|
|
2271
|
+
subs.push(client.bus.get("mark:assist-request").subscribe((event) => {
|
|
2272
|
+
clearProgressTimer();
|
|
2273
|
+
assistingMotivation$.next(event.motivation);
|
|
2274
|
+
progress$.next(null);
|
|
2275
|
+
const assistSub = client.mark.assist(resourceId, event.motivation, event.options).pipe(
|
|
2276
|
+
timeout({ each: 18e4 })
|
|
2277
|
+
).subscribe({
|
|
2278
|
+
next: (e) => {
|
|
2279
|
+
if (e.kind === "progress") progress$.next(e.data);
|
|
2280
|
+
},
|
|
2281
|
+
complete: () => {
|
|
2282
|
+
assistingMotivation$.next(null);
|
|
2283
|
+
clearProgressTimer();
|
|
2284
|
+
progressDismissTimer = setTimeout(() => {
|
|
2285
|
+
progress$.next(null);
|
|
2286
|
+
progressDismissTimer = null;
|
|
2287
|
+
}, 5e3);
|
|
2288
|
+
},
|
|
2289
|
+
error: () => {
|
|
2290
|
+
clearProgressTimer();
|
|
2291
|
+
assistingMotivation$.next(null);
|
|
2292
|
+
progress$.next(null);
|
|
2293
|
+
}
|
|
2294
|
+
});
|
|
2295
|
+
subs.push(assistSub);
|
|
2296
|
+
}));
|
|
2297
|
+
subs.push(client.bus.get("mark:progress-dismiss").subscribe(() => {
|
|
2298
|
+
clearProgressTimer();
|
|
2299
|
+
progress$.next(null);
|
|
2300
|
+
}));
|
|
2301
|
+
return {
|
|
2302
|
+
pendingAnnotation$: pendingAnnotation$.asObservable(),
|
|
2303
|
+
assistingMotivation$: assistingMotivation$.asObservable(),
|
|
2304
|
+
progress$: progress$.asObservable(),
|
|
2305
|
+
dispose() {
|
|
2306
|
+
subs.forEach((s) => s.unsubscribe());
|
|
2307
|
+
clearProgressTimer();
|
|
2308
|
+
pendingAnnotation$.complete();
|
|
2309
|
+
assistingMotivation$.complete();
|
|
2310
|
+
progress$.complete();
|
|
2311
|
+
}
|
|
2312
|
+
};
|
|
2313
|
+
}
|
|
2314
|
+
var RECENT_LIMIT = 10;
|
|
2315
|
+
var SEARCH_LIMIT = 20;
|
|
2316
|
+
function createDiscoverVM(client, browse) {
|
|
2317
|
+
const disposer = createDisposer();
|
|
2318
|
+
const search = createSearchPipeline(
|
|
2319
|
+
(q) => client.browse.resources({ search: q, limit: SEARCH_LIMIT })
|
|
2320
|
+
);
|
|
2321
|
+
disposer.add(search);
|
|
2322
|
+
disposer.add(browse);
|
|
2323
|
+
const recent$ = client.browse.resources({ limit: RECENT_LIMIT, archived: false });
|
|
2324
|
+
const recentResources$ = recent$.pipe(
|
|
2325
|
+
map$1((r) => r ?? [])
|
|
2326
|
+
);
|
|
2327
|
+
const isLoadingRecent$ = recent$.pipe(
|
|
2328
|
+
map$1((r) => r === void 0)
|
|
2329
|
+
);
|
|
2330
|
+
const entityTypes$ = client.browse.entityTypes().pipe(
|
|
2331
|
+
map$1((e) => e ?? [])
|
|
2332
|
+
);
|
|
2333
|
+
return {
|
|
2334
|
+
browse,
|
|
2335
|
+
search,
|
|
2336
|
+
recentResources$,
|
|
2337
|
+
entityTypes$,
|
|
2338
|
+
isLoadingRecent$,
|
|
2339
|
+
dispose: () => disposer.dispose()
|
|
2340
|
+
};
|
|
2341
|
+
}
|
|
2342
|
+
function createEntityTagsVM(client, browse) {
|
|
2343
|
+
const disposer = createDisposer();
|
|
2344
|
+
disposer.add(browse);
|
|
2345
|
+
const newTag$ = new BehaviorSubject("");
|
|
2346
|
+
const error$ = new BehaviorSubject("");
|
|
2347
|
+
const isAdding$ = new BehaviorSubject(false);
|
|
2348
|
+
const raw$ = client.browse.entityTypes();
|
|
2349
|
+
const entityTypes$ = raw$.pipe(map$1((e) => e ?? []));
|
|
2350
|
+
const isLoading$ = raw$.pipe(map$1((e) => e === void 0));
|
|
2351
|
+
const addTag = async () => {
|
|
2352
|
+
const tag = newTag$.getValue().trim();
|
|
2353
|
+
if (!tag) return;
|
|
2354
|
+
error$.next("");
|
|
2355
|
+
isAdding$.next(true);
|
|
2356
|
+
try {
|
|
2357
|
+
await client.mark.entityType(tag);
|
|
2358
|
+
newTag$.next("");
|
|
2359
|
+
} catch (err) {
|
|
2360
|
+
error$.next(err instanceof Error ? err.message : "Failed to add entity type");
|
|
2361
|
+
} finally {
|
|
2362
|
+
isAdding$.next(false);
|
|
2363
|
+
}
|
|
2364
|
+
};
|
|
2365
|
+
return {
|
|
2366
|
+
browse,
|
|
2367
|
+
entityTypes$,
|
|
2368
|
+
isLoading$,
|
|
2369
|
+
newTag$: newTag$.asObservable(),
|
|
2370
|
+
error$: error$.asObservable(),
|
|
2371
|
+
isAdding$: isAdding$.asObservable(),
|
|
2372
|
+
setNewTag: (v) => newTag$.next(v),
|
|
2373
|
+
addTag,
|
|
2374
|
+
dispose: () => {
|
|
2375
|
+
newTag$.complete();
|
|
2376
|
+
error$.complete();
|
|
2377
|
+
isAdding$.complete();
|
|
2378
|
+
disposer.dispose();
|
|
2379
|
+
}
|
|
2380
|
+
};
|
|
2381
|
+
}
|
|
2382
|
+
function createExchangeVM(browse, exportFn, importFn) {
|
|
2383
|
+
const disposer = createDisposer();
|
|
2384
|
+
disposer.add(browse);
|
|
2385
|
+
const selectedFile$ = new BehaviorSubject(null);
|
|
2386
|
+
const preview$ = new BehaviorSubject(null);
|
|
2387
|
+
const importPhase$ = new BehaviorSubject(null);
|
|
2388
|
+
const importMessage$ = new BehaviorSubject(void 0);
|
|
2389
|
+
const importResult$ = new BehaviorSubject(void 0);
|
|
2390
|
+
const isExporting$ = new BehaviorSubject(false);
|
|
2391
|
+
const isImporting$ = new BehaviorSubject(false);
|
|
2392
|
+
const selectFile = (file) => {
|
|
2393
|
+
selectedFile$.next(file);
|
|
2394
|
+
importPhase$.next(null);
|
|
2395
|
+
importMessage$.next(void 0);
|
|
2396
|
+
importResult$.next(void 0);
|
|
2397
|
+
preview$.next({
|
|
2398
|
+
format: file.name.endsWith(".tar.gz") || file.name.endsWith(".gz") ? "semiont-linked-data" : "unknown",
|
|
2399
|
+
version: 1,
|
|
2400
|
+
sourceUrl: "",
|
|
2401
|
+
stats: {}
|
|
2402
|
+
});
|
|
2403
|
+
};
|
|
2404
|
+
const cancelImport = () => {
|
|
2405
|
+
selectedFile$.next(null);
|
|
2406
|
+
preview$.next(null);
|
|
2407
|
+
importPhase$.next(null);
|
|
2408
|
+
importMessage$.next(void 0);
|
|
2409
|
+
importResult$.next(void 0);
|
|
2410
|
+
};
|
|
2411
|
+
const doExport = async () => {
|
|
2412
|
+
isExporting$.next(true);
|
|
2413
|
+
try {
|
|
2414
|
+
const response = await exportFn();
|
|
2415
|
+
if (!response.ok) throw new Error(`Export failed: ${response.status} ${response.statusText}`);
|
|
2416
|
+
const blob = await response.blob();
|
|
2417
|
+
const contentDisposition = response.headers.get("Content-Disposition");
|
|
2418
|
+
const filename = contentDisposition?.match(/filename="(.+?)"/)?.[1] ?? `semiont-export-${Date.now()}.tar.gz`;
|
|
2419
|
+
return { blob, filename };
|
|
2420
|
+
} finally {
|
|
2421
|
+
isExporting$.next(false);
|
|
2422
|
+
}
|
|
2423
|
+
};
|
|
2424
|
+
const doImport = async () => {
|
|
2425
|
+
const file = selectedFile$.getValue();
|
|
2426
|
+
if (!file) return;
|
|
2427
|
+
isImporting$.next(true);
|
|
2428
|
+
importPhase$.next("started");
|
|
2429
|
+
importMessage$.next(void 0);
|
|
2430
|
+
importResult$.next(void 0);
|
|
2431
|
+
try {
|
|
2432
|
+
await importFn(file, {
|
|
2433
|
+
onProgress: (event) => {
|
|
2434
|
+
importPhase$.next(event.phase);
|
|
2435
|
+
importMessage$.next(event.message);
|
|
2436
|
+
if (event.result) importResult$.next(event.result);
|
|
2437
|
+
}
|
|
2438
|
+
});
|
|
2439
|
+
} finally {
|
|
2440
|
+
isImporting$.next(false);
|
|
2441
|
+
}
|
|
2442
|
+
};
|
|
2443
|
+
return {
|
|
2444
|
+
browse,
|
|
2445
|
+
selectedFile$: selectedFile$.asObservable(),
|
|
2446
|
+
preview$: preview$.asObservable(),
|
|
2447
|
+
importPhase$: importPhase$.asObservable(),
|
|
2448
|
+
importMessage$: importMessage$.asObservable(),
|
|
2449
|
+
importResult$: importResult$.asObservable(),
|
|
2450
|
+
isExporting$: isExporting$.asObservable(),
|
|
2451
|
+
isImporting$: isImporting$.asObservable(),
|
|
2452
|
+
selectFile,
|
|
2453
|
+
cancelImport,
|
|
2454
|
+
doExport,
|
|
2455
|
+
doImport,
|
|
2456
|
+
dispose: () => {
|
|
2457
|
+
selectedFile$.complete();
|
|
2458
|
+
preview$.complete();
|
|
2459
|
+
importPhase$.complete();
|
|
2460
|
+
importMessage$.complete();
|
|
2461
|
+
importResult$.complete();
|
|
2462
|
+
isExporting$.complete();
|
|
2463
|
+
isImporting$.complete();
|
|
2464
|
+
disposer.dispose();
|
|
2465
|
+
}
|
|
2466
|
+
};
|
|
2467
|
+
}
|
|
2468
|
+
function createAdminUsersVM(client, browse) {
|
|
2469
|
+
const disposer = createDisposer();
|
|
2470
|
+
disposer.add(browse);
|
|
2471
|
+
const users$ = new BehaviorSubject([]);
|
|
2472
|
+
const stats$ = new BehaviorSubject(null);
|
|
2473
|
+
const usersLoading$ = new BehaviorSubject(true);
|
|
2474
|
+
const statsLoading$ = new BehaviorSubject(true);
|
|
2475
|
+
const fetchUsers = () => {
|
|
2476
|
+
usersLoading$.next(true);
|
|
2477
|
+
client.admin.users().then((data) => {
|
|
2478
|
+
users$.next(data.users ?? []);
|
|
2479
|
+
usersLoading$.next(false);
|
|
2480
|
+
}).catch(() => usersLoading$.next(false));
|
|
2481
|
+
};
|
|
2482
|
+
const fetchStats = () => {
|
|
2483
|
+
statsLoading$.next(true);
|
|
2484
|
+
client.admin.userStats().then((data) => {
|
|
2485
|
+
stats$.next(data.stats ?? null);
|
|
2486
|
+
statsLoading$.next(false);
|
|
2487
|
+
}).catch(() => statsLoading$.next(false));
|
|
2488
|
+
};
|
|
2489
|
+
fetchUsers();
|
|
2490
|
+
fetchStats();
|
|
2491
|
+
const updateUser = async (id, data) => {
|
|
2492
|
+
await client.admin.updateUser(userDID(id), data);
|
|
2493
|
+
fetchUsers();
|
|
2494
|
+
fetchStats();
|
|
2495
|
+
};
|
|
2496
|
+
return {
|
|
2497
|
+
browse,
|
|
2498
|
+
users$: users$.asObservable(),
|
|
2499
|
+
stats$: stats$.asObservable(),
|
|
2500
|
+
usersLoading$: usersLoading$.asObservable(),
|
|
2501
|
+
statsLoading$: statsLoading$.asObservable(),
|
|
2502
|
+
updateUser,
|
|
2503
|
+
dispose: () => {
|
|
2504
|
+
users$.complete();
|
|
2505
|
+
stats$.complete();
|
|
2506
|
+
usersLoading$.complete();
|
|
2507
|
+
statsLoading$.complete();
|
|
2508
|
+
disposer.dispose();
|
|
2509
|
+
}
|
|
2510
|
+
};
|
|
2511
|
+
}
|
|
2512
|
+
function createAdminSecurityVM(client, browse) {
|
|
2513
|
+
const disposer = createDisposer();
|
|
2514
|
+
disposer.add(browse);
|
|
2515
|
+
const providers$ = new BehaviorSubject([]);
|
|
2516
|
+
const allowedDomains$ = new BehaviorSubject([]);
|
|
2517
|
+
const isLoading$ = new BehaviorSubject(true);
|
|
2518
|
+
client.admin.oauthConfig().then((data) => {
|
|
2519
|
+
const config = data;
|
|
2520
|
+
providers$.next(config.providers ?? []);
|
|
2521
|
+
allowedDomains$.next(config.allowedDomains ?? []);
|
|
2522
|
+
isLoading$.next(false);
|
|
2523
|
+
}).catch(() => isLoading$.next(false));
|
|
2524
|
+
return {
|
|
2525
|
+
browse,
|
|
2526
|
+
providers$: providers$.asObservable(),
|
|
2527
|
+
allowedDomains$: allowedDomains$.asObservable(),
|
|
2528
|
+
isLoading$: isLoading$.asObservable(),
|
|
2529
|
+
dispose: () => {
|
|
2530
|
+
providers$.complete();
|
|
2531
|
+
allowedDomains$.complete();
|
|
2532
|
+
isLoading$.complete();
|
|
2533
|
+
disposer.dispose();
|
|
2534
|
+
}
|
|
2535
|
+
};
|
|
2536
|
+
}
|
|
2537
|
+
function createWelcomeVM(client) {
|
|
2538
|
+
const disposer = createDisposer();
|
|
2539
|
+
const userData$ = new BehaviorSubject(null);
|
|
2540
|
+
const isProcessing$ = new BehaviorSubject(false);
|
|
2541
|
+
client.auth.me().then((data) => userData$.next(data)).catch(() => {
|
|
2542
|
+
});
|
|
2543
|
+
const acceptTerms = async () => {
|
|
2544
|
+
isProcessing$.next(true);
|
|
2545
|
+
try {
|
|
2546
|
+
await client.auth.acceptTerms();
|
|
2547
|
+
userData$.next({ ...userData$.getValue(), termsAcceptedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
2548
|
+
} finally {
|
|
2549
|
+
isProcessing$.next(false);
|
|
2550
|
+
}
|
|
2551
|
+
};
|
|
2552
|
+
return {
|
|
2553
|
+
userData$: userData$.asObservable(),
|
|
2554
|
+
isProcessing$: isProcessing$.asObservable(),
|
|
2555
|
+
acceptTerms,
|
|
2556
|
+
dispose: () => {
|
|
2557
|
+
userData$.complete();
|
|
2558
|
+
isProcessing$.complete();
|
|
2559
|
+
disposer.dispose();
|
|
2560
|
+
}
|
|
2561
|
+
};
|
|
2562
|
+
}
|
|
2563
|
+
function createResourceLoaderVM(client, resourceId) {
|
|
2564
|
+
const raw$ = client.browse.resource(resourceId);
|
|
2565
|
+
const resource$ = raw$;
|
|
2566
|
+
const isLoading$ = raw$.pipe(map$1((r) => r === void 0));
|
|
2567
|
+
return {
|
|
2568
|
+
resource$,
|
|
2569
|
+
isLoading$,
|
|
2570
|
+
invalidate: () => client.browse.invalidateResourceDetail(resourceId),
|
|
2571
|
+
dispose: () => {
|
|
2572
|
+
}
|
|
2573
|
+
};
|
|
2574
|
+
}
|
|
2575
|
+
function createSessionVM(client) {
|
|
2576
|
+
const isLoggingOut$ = new BehaviorSubject(false);
|
|
2577
|
+
const logout = async () => {
|
|
2578
|
+
isLoggingOut$.next(true);
|
|
2579
|
+
try {
|
|
2580
|
+
await client.auth.logout();
|
|
2581
|
+
} catch {
|
|
2582
|
+
} finally {
|
|
2583
|
+
isLoggingOut$.next(false);
|
|
2584
|
+
}
|
|
2585
|
+
};
|
|
2586
|
+
return {
|
|
2587
|
+
isLoggingOut$: isLoggingOut$.asObservable(),
|
|
2588
|
+
logout,
|
|
2589
|
+
dispose: () => {
|
|
2590
|
+
isLoggingOut$.complete();
|
|
2591
|
+
}
|
|
2592
|
+
};
|
|
2593
|
+
}
|
|
2594
|
+
var SMELTER_CHANNELS = [
|
|
2595
|
+
"yield:created",
|
|
2596
|
+
"yield:updated",
|
|
2597
|
+
"yield:representation-added",
|
|
2598
|
+
"mark:archived",
|
|
2599
|
+
"mark:added",
|
|
2600
|
+
"mark:removed"
|
|
2601
|
+
];
|
|
2602
|
+
function createSmelterActorVM(options) {
|
|
2603
|
+
const actor = createActorVM({
|
|
2604
|
+
baseUrl: options.baseUrl,
|
|
2605
|
+
token: options.token,
|
|
2606
|
+
channels: [...SMELTER_CHANNELS],
|
|
2607
|
+
reconnectMs: options.reconnectMs
|
|
2608
|
+
});
|
|
2609
|
+
const events$ = merge(
|
|
2610
|
+
...SMELTER_CHANNELS.map(
|
|
2611
|
+
(channel) => actor.on$(channel).pipe(
|
|
2612
|
+
map((payload) => ({
|
|
2613
|
+
type: channel,
|
|
2614
|
+
resourceId: payload.resourceId,
|
|
2615
|
+
payload
|
|
2616
|
+
}))
|
|
2617
|
+
)
|
|
2618
|
+
)
|
|
2619
|
+
);
|
|
2620
|
+
return {
|
|
2621
|
+
events$,
|
|
2622
|
+
state$: actor.state$,
|
|
2623
|
+
emit: (channel, payload) => actor.emit(channel, payload),
|
|
2624
|
+
start: () => actor.start(),
|
|
2625
|
+
stop: () => actor.stop(),
|
|
2626
|
+
dispose: () => actor.dispose()
|
|
2627
|
+
};
|
|
2628
|
+
}
|
|
2629
|
+
function createJobClaimAdapter(options) {
|
|
2630
|
+
const { actor, jobTypes } = options;
|
|
2631
|
+
const activeJob$ = new BehaviorSubject(null);
|
|
2632
|
+
const isProcessing$ = new BehaviorSubject(false);
|
|
2633
|
+
const jobsCompleted$ = new BehaviorSubject(0);
|
|
2634
|
+
const errors$ = new Subject();
|
|
2635
|
+
let jobSubscription = null;
|
|
2636
|
+
let started = false;
|
|
2637
|
+
const claimJob = async (assignment) => {
|
|
2638
|
+
try {
|
|
2639
|
+
const correlationId = crypto.randomUUID();
|
|
2640
|
+
const result$ = merge(
|
|
2641
|
+
actor.on$("job:claimed").pipe(
|
|
2642
|
+
filter$1((e) => e.correlationId === correlationId),
|
|
2643
|
+
map$1((e) => ({ ok: true, response: e.response }))
|
|
2644
|
+
),
|
|
2645
|
+
actor.on$("job:claim-failed").pipe(
|
|
2646
|
+
filter$1((e) => e.correlationId === correlationId),
|
|
2647
|
+
map$1(() => ({ ok: false }))
|
|
2648
|
+
)
|
|
2649
|
+
).pipe(take$1(1), timeout$1(1e4));
|
|
2650
|
+
const resultPromise = firstValueFrom(result$);
|
|
2651
|
+
await actor.emit("job:claim", { correlationId, jobId: assignment.jobId });
|
|
2652
|
+
const result = await resultPromise;
|
|
2653
|
+
if (!result.ok) return null;
|
|
2654
|
+
const job = result.response;
|
|
2655
|
+
return {
|
|
2656
|
+
jobId: assignment.jobId,
|
|
2657
|
+
type: assignment.type,
|
|
2658
|
+
resourceId: assignment.resourceId,
|
|
2659
|
+
userId: job.metadata?.userId ?? "",
|
|
2660
|
+
params: job.params ?? {}
|
|
2661
|
+
};
|
|
2662
|
+
} catch {
|
|
2663
|
+
return null;
|
|
2664
|
+
}
|
|
2665
|
+
};
|
|
2666
|
+
return {
|
|
2667
|
+
activeJob$: activeJob$.asObservable(),
|
|
2668
|
+
isProcessing$: isProcessing$.asObservable(),
|
|
2669
|
+
jobsCompleted$: jobsCompleted$.asObservable(),
|
|
2670
|
+
errors$: errors$.asObservable(),
|
|
2671
|
+
start: () => {
|
|
2672
|
+
if (started) return;
|
|
2673
|
+
started = true;
|
|
2674
|
+
actor.addChannels(["job:queued"]);
|
|
2675
|
+
jobSubscription = actor.on$("job:queued").subscribe((event) => {
|
|
2676
|
+
const jobType = event.jobType;
|
|
2677
|
+
if (jobTypes.length > 0 && !jobTypes.includes(jobType)) return;
|
|
2678
|
+
if (isProcessing$.getValue()) return;
|
|
2679
|
+
isProcessing$.next(true);
|
|
2680
|
+
claimJob({ jobId: event.jobId, type: jobType, resourceId: event.resourceId }).then((job) => {
|
|
2681
|
+
if (job) {
|
|
2682
|
+
activeJob$.next(job);
|
|
2683
|
+
} else {
|
|
2684
|
+
isProcessing$.next(false);
|
|
2685
|
+
}
|
|
2686
|
+
}).catch(() => {
|
|
2687
|
+
isProcessing$.next(false);
|
|
2688
|
+
});
|
|
2689
|
+
});
|
|
2690
|
+
},
|
|
2691
|
+
stop: () => {
|
|
2692
|
+
jobSubscription?.unsubscribe();
|
|
2693
|
+
jobSubscription = null;
|
|
2694
|
+
started = false;
|
|
2695
|
+
},
|
|
2696
|
+
completeJob: () => {
|
|
2697
|
+
activeJob$.next(null);
|
|
2698
|
+
isProcessing$.next(false);
|
|
2699
|
+
jobsCompleted$.next(jobsCompleted$.getValue() + 1);
|
|
2700
|
+
},
|
|
2701
|
+
failJob: (jid, error) => {
|
|
2702
|
+
activeJob$.next(null);
|
|
2703
|
+
isProcessing$.next(false);
|
|
2704
|
+
errors$.next({ jobId: jid, error });
|
|
2705
|
+
},
|
|
2706
|
+
dispose: () => {
|
|
2707
|
+
jobSubscription?.unsubscribe();
|
|
2708
|
+
jobSubscription = null;
|
|
2709
|
+
started = false;
|
|
2710
|
+
activeJob$.complete();
|
|
2711
|
+
isProcessing$.complete();
|
|
2712
|
+
jobsCompleted$.complete();
|
|
2713
|
+
errors$.complete();
|
|
2714
|
+
}
|
|
2715
|
+
};
|
|
2716
|
+
}
|
|
2717
|
+
function createJobQueueVM(client) {
|
|
2718
|
+
const jobs$ = new BehaviorSubject([]);
|
|
2719
|
+
const jobCreated$ = new Subject();
|
|
2720
|
+
const jobCompleted$ = new Subject();
|
|
2721
|
+
const jobFailed$ = new Subject();
|
|
2722
|
+
const pendingByType$ = jobs$.pipe(
|
|
2723
|
+
map$1((all) => {
|
|
2724
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2725
|
+
for (const j of all) {
|
|
2726
|
+
if (j.status === "pending") {
|
|
2727
|
+
counts.set(j.type, (counts.get(j.type) ?? 0) + 1);
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
return counts;
|
|
2731
|
+
})
|
|
2732
|
+
);
|
|
2733
|
+
const runningJobs$ = jobs$.pipe(
|
|
2734
|
+
map$1((all) => all.filter((j) => j.status === "running"))
|
|
2735
|
+
);
|
|
2736
|
+
const addOrUpdate = (job) => {
|
|
2737
|
+
const current = jobs$.getValue();
|
|
2738
|
+
const idx = current.findIndex((j) => j.jobId === job.jobId);
|
|
2739
|
+
if (idx >= 0) {
|
|
2740
|
+
const next = [...current];
|
|
2741
|
+
next[idx] = job;
|
|
2742
|
+
jobs$.next(next);
|
|
2743
|
+
} else {
|
|
2744
|
+
jobs$.next([...current, job]);
|
|
2745
|
+
}
|
|
2746
|
+
};
|
|
2747
|
+
const subs = [
|
|
2748
|
+
client.bus.get("job:queued").subscribe((event) => {
|
|
2749
|
+
const job = {
|
|
2750
|
+
jobId: event.jobId,
|
|
2751
|
+
type: event.jobType,
|
|
2752
|
+
status: "pending",
|
|
2753
|
+
resourceId: event.resourceId,
|
|
2754
|
+
userId: event.userId,
|
|
2755
|
+
created: (/* @__PURE__ */ new Date()).toISOString()
|
|
2756
|
+
};
|
|
2757
|
+
addOrUpdate(job);
|
|
2758
|
+
jobCreated$.next(job);
|
|
2759
|
+
}),
|
|
2760
|
+
client.bus.get("job:complete").subscribe((event) => {
|
|
2761
|
+
if (!event._userId) {
|
|
2762
|
+
throw new Error("job:complete missing _userId (gateway injection)");
|
|
2763
|
+
}
|
|
2764
|
+
const job = {
|
|
2765
|
+
jobId: event.jobId,
|
|
2766
|
+
type: event.jobType,
|
|
2767
|
+
status: "complete",
|
|
2768
|
+
resourceId: event.resourceId,
|
|
2769
|
+
userId: event._userId,
|
|
2770
|
+
created: "",
|
|
2771
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2772
|
+
result: event.result
|
|
2773
|
+
};
|
|
2774
|
+
addOrUpdate(job);
|
|
2775
|
+
jobCompleted$.next(job);
|
|
2776
|
+
}),
|
|
2777
|
+
client.bus.get("job:fail").subscribe((event) => {
|
|
2778
|
+
if (!event._userId) {
|
|
2779
|
+
throw new Error("job:fail missing _userId (gateway injection)");
|
|
2780
|
+
}
|
|
2781
|
+
const job = {
|
|
2782
|
+
jobId: event.jobId,
|
|
2783
|
+
type: event.jobType,
|
|
2784
|
+
status: "failed",
|
|
2785
|
+
resourceId: event.resourceId,
|
|
2786
|
+
userId: event._userId,
|
|
2787
|
+
created: "",
|
|
2788
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2789
|
+
error: event.error
|
|
2790
|
+
};
|
|
2791
|
+
addOrUpdate(job);
|
|
2792
|
+
jobFailed$.next(job);
|
|
2793
|
+
})
|
|
2794
|
+
];
|
|
2795
|
+
return {
|
|
2796
|
+
jobs$: jobs$.asObservable(),
|
|
2797
|
+
pendingByType$,
|
|
2798
|
+
runningJobs$,
|
|
2799
|
+
jobCreated$: jobCreated$.asObservable(),
|
|
2800
|
+
jobCompleted$: jobCompleted$.asObservable(),
|
|
2801
|
+
jobFailed$: jobFailed$.asObservable(),
|
|
2802
|
+
dispose: () => {
|
|
2803
|
+
subs.forEach((s) => s.unsubscribe());
|
|
2804
|
+
jobs$.complete();
|
|
2805
|
+
jobCreated$.complete();
|
|
2806
|
+
jobCompleted$.complete();
|
|
2807
|
+
jobFailed$.complete();
|
|
2808
|
+
}
|
|
2809
|
+
};
|
|
2810
|
+
}
|
|
2811
|
+
var WIZARD_CLOSED = {
|
|
2812
|
+
open: false,
|
|
2813
|
+
annotationId: null,
|
|
2814
|
+
resourceId: null,
|
|
2815
|
+
defaultTitle: "",
|
|
2816
|
+
entityTypes: []
|
|
2817
|
+
};
|
|
2818
|
+
function createResourceViewerPageVM(client, resourceId, locale, browse, options) {
|
|
2819
|
+
const disposer = createDisposer();
|
|
2820
|
+
const beckon = createBeckonVM(client);
|
|
2821
|
+
const mark = createMarkVM(client, resourceId);
|
|
2822
|
+
const gather = createGatherVM(client, resourceId);
|
|
2823
|
+
const matchVM = createMatchVM(client);
|
|
2824
|
+
const yieldVM = createYieldVM(client, resourceId, locale);
|
|
2825
|
+
disposer.add(beckon);
|
|
2826
|
+
disposer.add(browse);
|
|
2827
|
+
disposer.add(mark);
|
|
2828
|
+
disposer.add(gather);
|
|
2829
|
+
disposer.add(matchVM);
|
|
2830
|
+
disposer.add(yieldVM);
|
|
2831
|
+
const annotations$ = client.browse.annotations(resourceId).pipe(
|
|
2832
|
+
map$1((a) => a ?? [])
|
|
2833
|
+
);
|
|
2834
|
+
const annotationGroups$ = annotations$.pipe(
|
|
2835
|
+
map$1((anns) => {
|
|
2836
|
+
const groups = { highlights: [], comments: [], assessments: [], references: [], tags: [] };
|
|
2837
|
+
for (const ann of anns) {
|
|
2838
|
+
if (isHighlight(ann)) groups.highlights.push(ann);
|
|
2839
|
+
else if (isComment(ann)) groups.comments.push(ann);
|
|
2840
|
+
else if (isAssessment(ann)) groups.assessments.push(ann);
|
|
2841
|
+
else if (isReference(ann)) groups.references.push(ann);
|
|
2842
|
+
else if (isTag(ann)) groups.tags.push(ann);
|
|
2843
|
+
}
|
|
2844
|
+
return groups;
|
|
2845
|
+
})
|
|
2846
|
+
);
|
|
2847
|
+
const entityTypes$ = client.browse.entityTypes().pipe(
|
|
2848
|
+
map$1((e) => e ?? [])
|
|
2849
|
+
);
|
|
2850
|
+
const events$ = client.browse.events(resourceId).pipe(
|
|
2851
|
+
map$1((e) => e ?? [])
|
|
2852
|
+
);
|
|
2853
|
+
const referencedBy$ = client.browse.referencedBy(resourceId).pipe(
|
|
2854
|
+
map$1((r) => r ?? [])
|
|
2855
|
+
);
|
|
2856
|
+
const content$ = new BehaviorSubject("");
|
|
2857
|
+
const contentLoading$ = new BehaviorSubject(false);
|
|
2858
|
+
const mediaToken$ = new BehaviorSubject(null);
|
|
2859
|
+
const mediaType = options?.mediaType || "text/plain";
|
|
2860
|
+
const isBinaryType = mediaType.startsWith("image/") || mediaType === "application/pdf";
|
|
2861
|
+
if (!isBinaryType && mediaType) {
|
|
2862
|
+
contentLoading$.next(true);
|
|
2863
|
+
client.browse.resourceRepresentation(resourceId, { accept: mediaType }).then(({ data }) => {
|
|
2864
|
+
content$.next(decodeWithCharset(data, mediaType));
|
|
2865
|
+
contentLoading$.next(false);
|
|
2866
|
+
}).catch(() => {
|
|
2867
|
+
contentLoading$.next(false);
|
|
2868
|
+
});
|
|
2869
|
+
}
|
|
2870
|
+
if (isBinaryType) {
|
|
2871
|
+
client.auth.mediaToken(resourceId).then(({ token }) => mediaToken$.next(token)).catch(() => {
|
|
2872
|
+
});
|
|
2873
|
+
}
|
|
2874
|
+
const wizard$ = new BehaviorSubject(WIZARD_CLOSED);
|
|
2875
|
+
const unsubscribeResource = client.subscribeToResource(resourceId);
|
|
2876
|
+
disposer.add(unsubscribeResource);
|
|
2877
|
+
const bindInitiateSub = client.bus.get("bind:initiate").subscribe((event) => {
|
|
2878
|
+
wizard$.next({
|
|
2879
|
+
open: true,
|
|
2880
|
+
annotationId: event.annotationId,
|
|
2881
|
+
resourceId: event.resourceId,
|
|
2882
|
+
defaultTitle: event.defaultTitle,
|
|
2883
|
+
entityTypes: event.entityTypes
|
|
2884
|
+
});
|
|
2885
|
+
client.bus.get("gather:requested").next({
|
|
2886
|
+
correlationId: crypto.randomUUID(),
|
|
2887
|
+
annotationId: event.annotationId,
|
|
2888
|
+
resourceId: event.resourceId,
|
|
2889
|
+
options: { contextWindow: 2e3 }
|
|
2890
|
+
});
|
|
2891
|
+
});
|
|
2892
|
+
disposer.add(() => bindInitiateSub.unsubscribe());
|
|
2893
|
+
return {
|
|
2894
|
+
beckon,
|
|
2895
|
+
browse,
|
|
2896
|
+
mark,
|
|
2897
|
+
gather,
|
|
2898
|
+
yield: yieldVM,
|
|
2899
|
+
annotations$,
|
|
2900
|
+
annotationGroups$,
|
|
2901
|
+
entityTypes$,
|
|
2902
|
+
events$,
|
|
2903
|
+
referencedBy$,
|
|
2904
|
+
content$: content$.asObservable(),
|
|
2905
|
+
contentLoading$: contentLoading$.asObservable(),
|
|
2906
|
+
mediaToken$: mediaToken$.asObservable(),
|
|
2907
|
+
wizard$: wizard$.asObservable(),
|
|
2908
|
+
closeWizard: () => wizard$.next(WIZARD_CLOSED),
|
|
2909
|
+
dispose: () => {
|
|
2910
|
+
wizard$.complete();
|
|
2911
|
+
content$.complete();
|
|
2912
|
+
contentLoading$.complete();
|
|
2913
|
+
mediaToken$.complete();
|
|
2914
|
+
disposer.dispose();
|
|
2915
|
+
}
|
|
2916
|
+
};
|
|
2917
|
+
}
|
|
2918
|
+
function createComposePageVM(client, browse, params, auth) {
|
|
2919
|
+
const disposer = createDisposer();
|
|
2920
|
+
disposer.add(browse);
|
|
2921
|
+
const isReferenceMode = Boolean(params.annotationUri && params.sourceDocumentId && params.name);
|
|
2922
|
+
const isCloneMode = params.mode === "clone" && Boolean(params.token);
|
|
2923
|
+
const pageMode = isCloneMode ? "clone" : isReferenceMode ? "reference" : "new";
|
|
2924
|
+
const mode$ = new BehaviorSubject(pageMode);
|
|
2925
|
+
const loading$ = new BehaviorSubject(true);
|
|
2926
|
+
const cloneData$ = new BehaviorSubject(null);
|
|
2927
|
+
const referenceData$ = new BehaviorSubject(null);
|
|
2928
|
+
const gatheredContext$ = new BehaviorSubject(null);
|
|
2929
|
+
const entityTypes$ = client.browse.entityTypes().pipe(
|
|
2930
|
+
map$1((e) => e ?? [])
|
|
2931
|
+
);
|
|
2932
|
+
if (isReferenceMode) {
|
|
2933
|
+
const entityTypes = params.entityTypes ? params.entityTypes.split(",") : [];
|
|
2934
|
+
referenceData$.next({
|
|
2935
|
+
annotationUri: params.annotationUri,
|
|
2936
|
+
sourceDocumentId: params.sourceDocumentId,
|
|
2937
|
+
name: params.name,
|
|
2938
|
+
entityTypes
|
|
2939
|
+
});
|
|
2940
|
+
if (params.storedContext) {
|
|
2941
|
+
try {
|
|
2942
|
+
gatheredContext$.next(JSON.parse(params.storedContext));
|
|
2943
|
+
} catch {
|
|
2944
|
+
}
|
|
2945
|
+
}
|
|
2946
|
+
loading$.next(false);
|
|
2947
|
+
} else if (isCloneMode) {
|
|
2948
|
+
void (async () => {
|
|
2949
|
+
try {
|
|
2950
|
+
const tokenResult = await client.yield.fromToken(params.token);
|
|
2951
|
+
if (tokenResult && auth) {
|
|
2952
|
+
const rId = resourceId(tokenResult["@id"]);
|
|
2953
|
+
const mediaType = getPrimaryMediaType(tokenResult) || "text/plain";
|
|
2954
|
+
const { data } = await client.browse.resourceRepresentation(rId, {
|
|
2955
|
+
accept: mediaType
|
|
2956
|
+
});
|
|
2957
|
+
const content = decodeWithCharset(data, mediaType);
|
|
2958
|
+
cloneData$.next({ sourceResource: tokenResult, sourceContent: content });
|
|
2959
|
+
}
|
|
2960
|
+
} catch {
|
|
2961
|
+
}
|
|
2962
|
+
loading$.next(false);
|
|
2963
|
+
})();
|
|
2964
|
+
} else {
|
|
2965
|
+
loading$.next(false);
|
|
2966
|
+
}
|
|
2967
|
+
const save = async (saveParams) => {
|
|
2968
|
+
if (saveParams.mode === "clone") {
|
|
2969
|
+
const response2 = await client.yield.createFromToken({
|
|
2970
|
+
token: params.token,
|
|
2971
|
+
name: saveParams.name,
|
|
2972
|
+
content: saveParams.content,
|
|
2973
|
+
archiveOriginal: saveParams.archiveOriginal ?? true
|
|
2974
|
+
});
|
|
2975
|
+
return response2.resourceId;
|
|
2976
|
+
}
|
|
2977
|
+
let fileToUpload;
|
|
2978
|
+
let mimeType;
|
|
2979
|
+
if (saveParams.file) {
|
|
2980
|
+
fileToUpload = saveParams.file;
|
|
2981
|
+
mimeType = saveParams.format ?? "application/octet-stream";
|
|
2982
|
+
} else {
|
|
2983
|
+
const blob = new Blob([saveParams.content || ""], { type: saveParams.format ?? "application/octet-stream" });
|
|
2984
|
+
const extension = saveParams.format === "text/plain" ? ".txt" : saveParams.format === "text/html" ? ".html" : ".md";
|
|
2985
|
+
fileToUpload = new File([blob], saveParams.name + extension, { type: saveParams.format ?? "application/octet-stream" });
|
|
2986
|
+
mimeType = saveParams.format ?? "application/octet-stream";
|
|
2987
|
+
}
|
|
2988
|
+
const format = saveParams.charset && !saveParams.file ? `${mimeType}; charset=${saveParams.charset}` : mimeType;
|
|
2989
|
+
const response = await client.yield.resource({
|
|
2990
|
+
name: saveParams.name,
|
|
2991
|
+
file: fileToUpload,
|
|
2992
|
+
format,
|
|
2993
|
+
entityTypes: saveParams.entityTypes || [],
|
|
2994
|
+
language: saveParams.language,
|
|
2995
|
+
creationMethod: "ui",
|
|
2996
|
+
storageUri: saveParams.storageUri
|
|
2997
|
+
});
|
|
2998
|
+
const newResourceId = response.resourceId;
|
|
2999
|
+
if (saveParams.mode === "reference" && saveParams.annotationUri && saveParams.sourceDocumentId) {
|
|
3000
|
+
await client.bind.body(
|
|
3001
|
+
resourceId(saveParams.sourceDocumentId),
|
|
3002
|
+
annotationId(saveParams.annotationUri),
|
|
3003
|
+
[{ op: "add", item: { type: "SpecificResource", source: newResourceId, purpose: "linking" } }]
|
|
3004
|
+
);
|
|
3005
|
+
}
|
|
3006
|
+
return newResourceId;
|
|
3007
|
+
};
|
|
3008
|
+
return {
|
|
3009
|
+
browse,
|
|
3010
|
+
mode$: mode$.asObservable(),
|
|
3011
|
+
loading$: loading$.asObservable(),
|
|
3012
|
+
cloneData$: cloneData$.asObservable(),
|
|
3013
|
+
referenceData$: referenceData$.asObservable(),
|
|
3014
|
+
gatheredContext$: gatheredContext$.asObservable(),
|
|
3015
|
+
entityTypes$,
|
|
3016
|
+
save,
|
|
3017
|
+
dispose: () => {
|
|
3018
|
+
mode$.complete();
|
|
3019
|
+
loading$.complete();
|
|
3020
|
+
cloneData$.complete();
|
|
3021
|
+
referenceData$.complete();
|
|
3022
|
+
gatheredContext$.complete();
|
|
3023
|
+
disposer.dispose();
|
|
3024
|
+
}
|
|
3025
|
+
};
|
|
3026
|
+
}
|
|
3027
|
+
|
|
3028
|
+
export { AdminNamespace, AuthNamespace, BeckonNamespace, BindNamespace, BrowseNamespace, BusRequestError, COMMON_PANELS, FrontendSessionSignals, GatherNamespace, HOVER_DELAY_MS, InMemorySessionStorage, JobNamespace, MarkNamespace, MatchNamespace, RESOURCE_PANELS, SemiontBrowser, SemiontClient, SemiontError, SemiontSession, YieldNamespace, busRequest, createAdminSecurityVM, createAdminUsersVM, createBeckonVM, createCache, createComposePageVM, createDiscoverVM, createDisposer, createEntityTagsVM, createExchangeVM, createGatherVM, createHoverHandlers, createJobClaimAdapter, createJobQueueVM, createMarkVM, createMatchVM, createResourceLoaderVM, createResourceViewerPageVM, createSearchPipeline, createSessionVM, createShellVM, createSmelterActorVM, createWelcomeVM, createYieldVM, defaultProtocol, getBrowser, isValidHostname, kbBackendUrl, notifyPermissionDenied, notifySessionExpired, setStoredSession };
|
|
3029
|
+
//# sourceMappingURL=index.js.map
|
|
3030
|
+
//# sourceMappingURL=index.js.map
|