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