@semiont/http-transport 0.5.6
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 +74 -0
- package/dist/index.d.ts +233 -0
- package/dist/index.js +948 -0
- package/dist/index.js.map +1 -0
- package/package.json +64 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,948 @@
|
|
|
1
|
+
import ky, { HTTPError } from 'ky';
|
|
2
|
+
import { Subject, BehaviorSubject, Observable } from 'rxjs';
|
|
3
|
+
import { RESOURCE_BROADCAST_TYPES, PERSISTED_EVENT_TYPES, SemiontError, BRIDGED_CHANNELS, busLog } from '@semiont/core';
|
|
4
|
+
import { getActiveTraceparent, recordBusEmit, withSpan, SpanKind, extractTraceparent, withTraceparent } from '@semiont/observability';
|
|
5
|
+
import { share, filter, map } from 'rxjs/operators';
|
|
6
|
+
|
|
7
|
+
// src/transport/http-transport.ts
|
|
8
|
+
var DEGRADED_THRESHOLD_MS = 3e3;
|
|
9
|
+
var ALLOWED_TRANSITIONS = {
|
|
10
|
+
initial: ["connecting", "closed"],
|
|
11
|
+
connecting: ["open", "reconnecting", "closed"],
|
|
12
|
+
open: ["reconnecting", "closed"],
|
|
13
|
+
reconnecting: ["connecting", "degraded", "closed"],
|
|
14
|
+
// `degraded → reconnecting` is a legitimate recovery edge: a channel-set
|
|
15
|
+
// change (`addChannels`/`removeChannels`) schedules a reconnect that can
|
|
16
|
+
// fire while the connection is degraded. Omitting it made `reconnect()`
|
|
17
|
+
// throw a fatal, uncaught exception from the reconnect timer (#844).
|
|
18
|
+
degraded: ["connecting", "reconnecting", "closed"],
|
|
19
|
+
closed: []
|
|
20
|
+
};
|
|
21
|
+
function createActorStateUnit(options) {
|
|
22
|
+
const { baseUrl, token: tokenOrGetter, channels: initialChannels, scope: initialScope, reconnectMs = 5e3 } = options;
|
|
23
|
+
const getToken = typeof tokenOrGetter === "function" ? tokenOrGetter : () => tokenOrGetter;
|
|
24
|
+
const globalChannels = new Set(initialChannels);
|
|
25
|
+
const scopedChannels = /* @__PURE__ */ new Set();
|
|
26
|
+
let activeScope = initialScope;
|
|
27
|
+
const events$ = new Subject();
|
|
28
|
+
const state$ = new BehaviorSubject("initial");
|
|
29
|
+
let currentState = "initial";
|
|
30
|
+
let degradedTimer = null;
|
|
31
|
+
const transition = (next) => {
|
|
32
|
+
if (currentState === next) return;
|
|
33
|
+
const allowed = ALLOWED_TRANSITIONS[currentState];
|
|
34
|
+
if (!allowed.includes(next)) {
|
|
35
|
+
console.warn(`[actor] ignoring invalid connection state transition: ${currentState} \u2192 ${next}`);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const prev = currentState;
|
|
39
|
+
currentState = next;
|
|
40
|
+
if (next === "reconnecting" && prev !== "reconnecting") {
|
|
41
|
+
if (degradedTimer) clearTimeout(degradedTimer);
|
|
42
|
+
degradedTimer = setTimeout(() => {
|
|
43
|
+
if (currentState === "reconnecting") transition("degraded");
|
|
44
|
+
}, DEGRADED_THRESHOLD_MS);
|
|
45
|
+
}
|
|
46
|
+
if (prev === "reconnecting" && next !== "reconnecting") {
|
|
47
|
+
if (degradedTimer) {
|
|
48
|
+
clearTimeout(degradedTimer);
|
|
49
|
+
degradedTimer = null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
state$.next(next);
|
|
53
|
+
};
|
|
54
|
+
let running = false;
|
|
55
|
+
const inflightControllers = /* @__PURE__ */ new Set();
|
|
56
|
+
let reconnectTimer = null;
|
|
57
|
+
let lastEventId = null;
|
|
58
|
+
const seenEventIds = /* @__PURE__ */ new Set();
|
|
59
|
+
const SEEN_EVENT_IDS_MAX = 512;
|
|
60
|
+
const rememberEventId = (id) => {
|
|
61
|
+
seenEventIds.add(id);
|
|
62
|
+
if (seenEventIds.size > SEEN_EVENT_IDS_MAX) {
|
|
63
|
+
const oldest = seenEventIds.values().next().value;
|
|
64
|
+
if (oldest !== void 0) seenEventIds.delete(oldest);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
const shared$ = events$.pipe(share());
|
|
68
|
+
const disconnect = () => {
|
|
69
|
+
for (const c of inflightControllers) {
|
|
70
|
+
try {
|
|
71
|
+
c.abort();
|
|
72
|
+
} catch {
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
inflightControllers.clear();
|
|
76
|
+
if (reconnectTimer) {
|
|
77
|
+
clearTimeout(reconnectTimer);
|
|
78
|
+
reconnectTimer = null;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
const connect = async (keepPrevious = false) => {
|
|
82
|
+
transition("connecting");
|
|
83
|
+
const previous = [...inflightControllers];
|
|
84
|
+
if (!keepPrevious) {
|
|
85
|
+
for (const c of previous) {
|
|
86
|
+
try {
|
|
87
|
+
c.abort();
|
|
88
|
+
} catch {
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
inflightControllers.clear();
|
|
92
|
+
}
|
|
93
|
+
const params = new URLSearchParams();
|
|
94
|
+
for (const ch of globalChannels) {
|
|
95
|
+
params.append("channel", ch);
|
|
96
|
+
}
|
|
97
|
+
if (activeScope && scopedChannels.size > 0) {
|
|
98
|
+
params.append("scope", activeScope);
|
|
99
|
+
for (const ch of scopedChannels) {
|
|
100
|
+
params.append("scoped", ch);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const url = `${baseUrl}/bus/subscribe?${params.toString()}`;
|
|
104
|
+
const controller = new AbortController();
|
|
105
|
+
inflightControllers.add(controller);
|
|
106
|
+
try {
|
|
107
|
+
const headers = { Authorization: `Bearer ${getToken()}` };
|
|
108
|
+
if (lastEventId) headers["Last-Event-ID"] = lastEventId;
|
|
109
|
+
const response = await fetch(url, { headers, signal: controller.signal });
|
|
110
|
+
if (!response.ok || !response.body) {
|
|
111
|
+
throw new Error(`SSE connect failed: ${response.status}`);
|
|
112
|
+
}
|
|
113
|
+
if (!running) return;
|
|
114
|
+
if (keepPrevious) {
|
|
115
|
+
for (const c of previous) {
|
|
116
|
+
try {
|
|
117
|
+
c.abort();
|
|
118
|
+
} catch {
|
|
119
|
+
}
|
|
120
|
+
inflightControllers.delete(c);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
transition("open");
|
|
124
|
+
const reader = response.body.getReader();
|
|
125
|
+
const decoder = new TextDecoder();
|
|
126
|
+
let buffer = "";
|
|
127
|
+
let currentEvent = "";
|
|
128
|
+
let currentData = "";
|
|
129
|
+
let currentId;
|
|
130
|
+
while (running && inflightControllers.has(controller)) {
|
|
131
|
+
const { done, value } = await reader.read();
|
|
132
|
+
if (done) break;
|
|
133
|
+
buffer += decoder.decode(value, { stream: true });
|
|
134
|
+
const lines = buffer.split("\n");
|
|
135
|
+
buffer = lines.pop() ?? "";
|
|
136
|
+
for (const line of lines) {
|
|
137
|
+
if (line.startsWith("event: ")) {
|
|
138
|
+
currentEvent = line.slice(7);
|
|
139
|
+
} else if (line.startsWith("data: ")) {
|
|
140
|
+
currentData = line.slice(6);
|
|
141
|
+
} else if (line.startsWith("id: ")) {
|
|
142
|
+
currentId = line.slice(4);
|
|
143
|
+
} else if (line === "") {
|
|
144
|
+
const isDuplicate = currentId !== void 0 && seenEventIds.has(currentId);
|
|
145
|
+
if (currentEvent === "bus-event" && currentData && !isDuplicate) {
|
|
146
|
+
if (currentId !== void 0) {
|
|
147
|
+
lastEventId = currentId;
|
|
148
|
+
rememberEventId(currentId);
|
|
149
|
+
}
|
|
150
|
+
const parsed = JSON.parse(currentData);
|
|
151
|
+
busLog("RECV", parsed.channel, parsed.payload, parsed.scope);
|
|
152
|
+
const carrier = extractTraceparent(
|
|
153
|
+
parsed.payload
|
|
154
|
+
);
|
|
155
|
+
await withTraceparent(
|
|
156
|
+
carrier,
|
|
157
|
+
() => withSpan(
|
|
158
|
+
`bus.recv:${parsed.channel}`,
|
|
159
|
+
() => {
|
|
160
|
+
events$.next(parsed);
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
kind: SpanKind.CONSUMER,
|
|
164
|
+
attrs: {
|
|
165
|
+
"bus.channel": parsed.channel,
|
|
166
|
+
...parsed.scope ? { "bus.scope": parsed.scope } : {}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
)
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
currentEvent = "";
|
|
173
|
+
currentData = "";
|
|
174
|
+
currentId = void 0;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
} catch (err) {
|
|
179
|
+
if (err.name === "AbortError") return;
|
|
180
|
+
} finally {
|
|
181
|
+
inflightControllers.delete(controller);
|
|
182
|
+
}
|
|
183
|
+
if (running) {
|
|
184
|
+
transition("reconnecting");
|
|
185
|
+
reconnectTimer = setTimeout(() => {
|
|
186
|
+
if (running) connect();
|
|
187
|
+
}, reconnectMs);
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
const reconnect = () => {
|
|
191
|
+
if (!running) return;
|
|
192
|
+
if (currentState === "open" || currentState === "connecting" || currentState === "degraded") {
|
|
193
|
+
transition("reconnecting");
|
|
194
|
+
}
|
|
195
|
+
if (reconnectTimer) {
|
|
196
|
+
clearTimeout(reconnectTimer);
|
|
197
|
+
reconnectTimer = null;
|
|
198
|
+
}
|
|
199
|
+
connect(true);
|
|
200
|
+
};
|
|
201
|
+
let reconnectTimer2 = null;
|
|
202
|
+
const RECONNECT_DEBOUNCE_MS = 100;
|
|
203
|
+
const scheduleReconnect = () => {
|
|
204
|
+
if (reconnectTimer2) clearTimeout(reconnectTimer2);
|
|
205
|
+
reconnectTimer2 = setTimeout(() => {
|
|
206
|
+
reconnectTimer2 = null;
|
|
207
|
+
reconnect();
|
|
208
|
+
}, RECONNECT_DEBOUNCE_MS);
|
|
209
|
+
};
|
|
210
|
+
return {
|
|
211
|
+
on$(channel) {
|
|
212
|
+
return shared$.pipe(
|
|
213
|
+
filter((e) => e.channel === channel),
|
|
214
|
+
map((e) => e.payload)
|
|
215
|
+
);
|
|
216
|
+
},
|
|
217
|
+
emit: async (channel, payload, emitScope) => {
|
|
218
|
+
const body = { channel, payload };
|
|
219
|
+
if (emitScope) body.scope = emitScope;
|
|
220
|
+
const headers = {
|
|
221
|
+
"Content-Type": "application/json",
|
|
222
|
+
Authorization: `Bearer ${getToken()}`
|
|
223
|
+
};
|
|
224
|
+
const trace = getActiveTraceparent();
|
|
225
|
+
if (trace) {
|
|
226
|
+
headers["traceparent"] = trace.traceparent;
|
|
227
|
+
if (trace.tracestate) headers["tracestate"] = trace.tracestate;
|
|
228
|
+
}
|
|
229
|
+
await fetch(`${baseUrl}/bus/emit`, {
|
|
230
|
+
method: "POST",
|
|
231
|
+
headers,
|
|
232
|
+
body: JSON.stringify(body)
|
|
233
|
+
});
|
|
234
|
+
},
|
|
235
|
+
state$: state$.asObservable(),
|
|
236
|
+
addChannels: (channels, scope) => {
|
|
237
|
+
let changed = false;
|
|
238
|
+
if (scope !== void 0) {
|
|
239
|
+
for (const ch of channels) {
|
|
240
|
+
if (!scopedChannels.has(ch)) {
|
|
241
|
+
scopedChannels.add(ch);
|
|
242
|
+
changed = true;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (scope !== activeScope) {
|
|
246
|
+
activeScope = scope;
|
|
247
|
+
changed = true;
|
|
248
|
+
}
|
|
249
|
+
} else {
|
|
250
|
+
for (const ch of channels) {
|
|
251
|
+
if (!globalChannels.has(ch)) {
|
|
252
|
+
globalChannels.add(ch);
|
|
253
|
+
changed = true;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (changed) scheduleReconnect();
|
|
258
|
+
},
|
|
259
|
+
removeChannels: (channels) => {
|
|
260
|
+
let changed = false;
|
|
261
|
+
for (const ch of channels) {
|
|
262
|
+
if (scopedChannels.delete(ch)) changed = true;
|
|
263
|
+
if (globalChannels.delete(ch)) changed = true;
|
|
264
|
+
}
|
|
265
|
+
if (scopedChannels.size === 0) activeScope = void 0;
|
|
266
|
+
if (changed) scheduleReconnect();
|
|
267
|
+
},
|
|
268
|
+
start: () => {
|
|
269
|
+
if (running) return;
|
|
270
|
+
running = true;
|
|
271
|
+
connect();
|
|
272
|
+
},
|
|
273
|
+
stop: () => {
|
|
274
|
+
running = false;
|
|
275
|
+
if (currentState !== "closed") transition("closed");
|
|
276
|
+
if (reconnectTimer2) {
|
|
277
|
+
clearTimeout(reconnectTimer2);
|
|
278
|
+
reconnectTimer2 = null;
|
|
279
|
+
}
|
|
280
|
+
if (degradedTimer) {
|
|
281
|
+
clearTimeout(degradedTimer);
|
|
282
|
+
degradedTimer = null;
|
|
283
|
+
}
|
|
284
|
+
disconnect();
|
|
285
|
+
},
|
|
286
|
+
dispose: () => {
|
|
287
|
+
running = false;
|
|
288
|
+
if (currentState !== "closed") transition("closed");
|
|
289
|
+
if (reconnectTimer2) {
|
|
290
|
+
clearTimeout(reconnectTimer2);
|
|
291
|
+
reconnectTimer2 = null;
|
|
292
|
+
}
|
|
293
|
+
if (degradedTimer) {
|
|
294
|
+
clearTimeout(degradedTimer);
|
|
295
|
+
degradedTimer = null;
|
|
296
|
+
}
|
|
297
|
+
disconnect();
|
|
298
|
+
events$.complete();
|
|
299
|
+
state$.complete();
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
var RESOURCE_SCOPED_CHANNELS = [
|
|
304
|
+
...PERSISTED_EVENT_TYPES.filter((t) => t !== "frame:entity-type-added"),
|
|
305
|
+
...RESOURCE_BROADCAST_TYPES
|
|
306
|
+
];
|
|
307
|
+
function responseToDownload(response) {
|
|
308
|
+
const contentType = response.headers.get("Content-Type") ?? "application/octet-stream";
|
|
309
|
+
const contentDisposition = response.headers.get("Content-Disposition");
|
|
310
|
+
const filename = contentDisposition?.match(/filename="(.+?)"/)?.[1];
|
|
311
|
+
return {
|
|
312
|
+
stream: response.body,
|
|
313
|
+
contentType,
|
|
314
|
+
...filename ? { filename } : {}
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
function classifyApiCode(status) {
|
|
318
|
+
if (status === 400) return "bad-request";
|
|
319
|
+
if (status === 401) return "unauthorized";
|
|
320
|
+
if (status === 403) return "forbidden";
|
|
321
|
+
if (status === 404) return "not-found";
|
|
322
|
+
if (status === 409) return "conflict";
|
|
323
|
+
if (status >= 500) return "unavailable";
|
|
324
|
+
return "error";
|
|
325
|
+
}
|
|
326
|
+
var APIError = class extends SemiontError {
|
|
327
|
+
status;
|
|
328
|
+
statusText;
|
|
329
|
+
constructor(message, status, statusText, body) {
|
|
330
|
+
super(message, classifyApiCode(status), { status, statusText, body });
|
|
331
|
+
this.name = "APIError";
|
|
332
|
+
this.status = status;
|
|
333
|
+
this.statusText = statusText;
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
var HttpTransport = class {
|
|
337
|
+
baseUrl;
|
|
338
|
+
http;
|
|
339
|
+
token$;
|
|
340
|
+
logger;
|
|
341
|
+
errorsSubject = new Subject();
|
|
342
|
+
/**
|
|
343
|
+
* Stream of `APIError` instances surfaced from any HTTP request just
|
|
344
|
+
* before the transport throws to the caller. Satisfies the `ITransport`
|
|
345
|
+
* `errors$` contract — see `@semiont/core/transport.ts`.
|
|
346
|
+
*/
|
|
347
|
+
errors$ = this.errorsSubject.asObservable();
|
|
348
|
+
_actor = null;
|
|
349
|
+
_actorStarted = false;
|
|
350
|
+
disposed = false;
|
|
351
|
+
activeResource = null;
|
|
352
|
+
/** Buses we've been asked to bridge wire events into. */
|
|
353
|
+
bridges = [];
|
|
354
|
+
constructor(config) {
|
|
355
|
+
const { baseUrl, timeout = 3e4, retry = 2, logger, tokenRefresher } = config;
|
|
356
|
+
this.baseUrl = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
|
|
357
|
+
this.token$ = config.token$ ?? new BehaviorSubject(null);
|
|
358
|
+
this.logger = logger;
|
|
359
|
+
const retryConfig = tokenRefresher ? {
|
|
360
|
+
limit: 1,
|
|
361
|
+
methods: ["get", "post", "put", "patch", "delete", "head", "options"],
|
|
362
|
+
statusCodes: [401, 408, 413, 429, 500, 502, 503, 504]
|
|
363
|
+
} : retry;
|
|
364
|
+
this.http = ky.create({
|
|
365
|
+
timeout,
|
|
366
|
+
retry: retryConfig,
|
|
367
|
+
credentials: "include",
|
|
368
|
+
hooks: {
|
|
369
|
+
beforeRequest: [
|
|
370
|
+
({ request }) => {
|
|
371
|
+
if (this.logger) {
|
|
372
|
+
this.logger.debug("HTTP Request", {
|
|
373
|
+
type: "http_request",
|
|
374
|
+
url: request.url,
|
|
375
|
+
method: request.method,
|
|
376
|
+
timestamp: Date.now(),
|
|
377
|
+
hasAuth: request.headers.has("Authorization")
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
],
|
|
382
|
+
beforeRetry: tokenRefresher ? [
|
|
383
|
+
async ({ request, error }) => {
|
|
384
|
+
if (!(error instanceof HTTPError) || error.response.status !== 401) {
|
|
385
|
+
return void 0;
|
|
386
|
+
}
|
|
387
|
+
try {
|
|
388
|
+
const newToken = await tokenRefresher();
|
|
389
|
+
if (!newToken) return ky.stop;
|
|
390
|
+
request.headers.set("Authorization", `Bearer ${newToken}`);
|
|
391
|
+
return void 0;
|
|
392
|
+
} catch {
|
|
393
|
+
return ky.stop;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
] : [],
|
|
397
|
+
afterResponse: [
|
|
398
|
+
({ request, response }) => {
|
|
399
|
+
if (this.logger) {
|
|
400
|
+
this.logger.debug("HTTP Response", {
|
|
401
|
+
type: "http_response",
|
|
402
|
+
url: request.url,
|
|
403
|
+
method: request.method,
|
|
404
|
+
status: response.status,
|
|
405
|
+
statusText: response.statusText
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
return response;
|
|
409
|
+
}
|
|
410
|
+
],
|
|
411
|
+
beforeError: [
|
|
412
|
+
async ({ request, error }) => {
|
|
413
|
+
const response = error instanceof HTTPError ? error.response : void 0;
|
|
414
|
+
if (response) {
|
|
415
|
+
const body = await response.json().catch(() => ({}));
|
|
416
|
+
if (this.logger) {
|
|
417
|
+
this.logger.error("HTTP Request Failed", {
|
|
418
|
+
type: "http_error",
|
|
419
|
+
url: request.url,
|
|
420
|
+
method: request.method,
|
|
421
|
+
status: response.status,
|
|
422
|
+
statusText: response.statusText,
|
|
423
|
+
error: body.message || `HTTP ${response.status}: ${response.statusText}`
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
const apiError = new APIError(
|
|
427
|
+
body.message || `HTTP ${response.status}: ${response.statusText}`,
|
|
428
|
+
response.status,
|
|
429
|
+
response.statusText,
|
|
430
|
+
body
|
|
431
|
+
);
|
|
432
|
+
this.errorsSubject.next(apiError);
|
|
433
|
+
throw apiError;
|
|
434
|
+
}
|
|
435
|
+
return error;
|
|
436
|
+
}
|
|
437
|
+
]
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
this.token$.subscribe((token) => {
|
|
441
|
+
if (token && !this._actorStarted && !this.disposed) {
|
|
442
|
+
this._actorStarted = true;
|
|
443
|
+
this.actor.start();
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
// ── Lazy actor construction + per-channel fan-in to bridges ───────────
|
|
448
|
+
//
|
|
449
|
+
// `actor` is exposed so the legacy `SemiontClient` can keep `.actor`
|
|
450
|
+
// pointing at the same ActorStateUnit during the transport-abstraction
|
|
451
|
+
// migration. Once SemiontClient is removed, this should be made
|
|
452
|
+
// private again — external callers should use emit/on/stream/state$.
|
|
453
|
+
get actor() {
|
|
454
|
+
if (!this._actor) {
|
|
455
|
+
this._actor = createActorStateUnit({
|
|
456
|
+
baseUrl: this.baseUrl,
|
|
457
|
+
token: () => this.token$.getValue() ?? "",
|
|
458
|
+
channels: [...BRIDGED_CHANNELS]
|
|
459
|
+
});
|
|
460
|
+
for (const channel of BRIDGED_CHANNELS) {
|
|
461
|
+
this._actor.on$(channel).subscribe((payload) => {
|
|
462
|
+
for (const bus of this.bridges) {
|
|
463
|
+
bus.get(channel).next(payload);
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
return this._actor;
|
|
469
|
+
}
|
|
470
|
+
// ── ITransport — bus primitives ───────────────────────────────────────
|
|
471
|
+
async emit(channel, payload, resourceScope) {
|
|
472
|
+
busLog("EMIT", channel, payload, resourceScope);
|
|
473
|
+
recordBusEmit(channel, resourceScope);
|
|
474
|
+
await withSpan(
|
|
475
|
+
`bus.emit:${channel}`,
|
|
476
|
+
async () => {
|
|
477
|
+
if (resourceScope !== void 0) {
|
|
478
|
+
await this.actor.emit(
|
|
479
|
+
channel,
|
|
480
|
+
payload,
|
|
481
|
+
resourceScope
|
|
482
|
+
);
|
|
483
|
+
} else {
|
|
484
|
+
await this.actor.emit(
|
|
485
|
+
channel,
|
|
486
|
+
payload
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
},
|
|
490
|
+
{
|
|
491
|
+
kind: SpanKind.PRODUCER,
|
|
492
|
+
attrs: {
|
|
493
|
+
"bus.channel": channel,
|
|
494
|
+
...resourceScope ? { "bus.scope": resourceScope } : {}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
on(channel, handler) {
|
|
500
|
+
const sub = this.actor.on$(channel).subscribe(handler);
|
|
501
|
+
return () => sub.unsubscribe();
|
|
502
|
+
}
|
|
503
|
+
stream(channel) {
|
|
504
|
+
return this.actor.on$(channel);
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* Wire this transport's SSE fan-in into the given bus. Every channel
|
|
508
|
+
* in `BRIDGED_CHANNELS` (and subsequently per-resource scoped channels
|
|
509
|
+
* opened by `subscribeToResource`) is published on the bus. Safe to
|
|
510
|
+
* call multiple times — each bus is added to the fan-out list.
|
|
511
|
+
*/
|
|
512
|
+
bridgeInto(bus) {
|
|
513
|
+
this.bridges.push(bus);
|
|
514
|
+
}
|
|
515
|
+
subscribeToResource(resourceId) {
|
|
516
|
+
if (this.activeResource) {
|
|
517
|
+
if (this.activeResource.resourceId !== resourceId) {
|
|
518
|
+
throw new Error(
|
|
519
|
+
`HttpTransport already subscribed to resource ${this.activeResource.resourceId}; call the unsubscribe returned from the previous subscribeToResource before subscribing to ${resourceId}.`
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
this.activeResource.refCount++;
|
|
523
|
+
return this.makeUnsubscriber();
|
|
524
|
+
}
|
|
525
|
+
this.actor.addChannels([...RESOURCE_SCOPED_CHANNELS], resourceId);
|
|
526
|
+
const bridgeSubs = [];
|
|
527
|
+
for (const channel of RESOURCE_SCOPED_CHANNELS) {
|
|
528
|
+
bridgeSubs.push(
|
|
529
|
+
this.actor.on$(channel).subscribe((payload) => {
|
|
530
|
+
for (const bus of this.bridges) {
|
|
531
|
+
bus.get(channel).next(payload);
|
|
532
|
+
}
|
|
533
|
+
})
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
this.activeResource = { resourceId, refCount: 1, bridgeSubs };
|
|
537
|
+
return this.makeUnsubscriber();
|
|
538
|
+
}
|
|
539
|
+
makeUnsubscriber() {
|
|
540
|
+
let called = false;
|
|
541
|
+
return () => {
|
|
542
|
+
if (called) return;
|
|
543
|
+
called = true;
|
|
544
|
+
if (!this.activeResource) return;
|
|
545
|
+
this.activeResource.refCount--;
|
|
546
|
+
if (this.activeResource.refCount > 0) return;
|
|
547
|
+
for (const sub of this.activeResource.bridgeSubs) sub.unsubscribe();
|
|
548
|
+
this.actor.removeChannels([...RESOURCE_SCOPED_CHANNELS]);
|
|
549
|
+
this.activeResource = null;
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
get state$() {
|
|
553
|
+
return this.actor.state$;
|
|
554
|
+
}
|
|
555
|
+
dispose() {
|
|
556
|
+
if (this.disposed) return;
|
|
557
|
+
this.disposed = true;
|
|
558
|
+
if (this.activeResource) {
|
|
559
|
+
for (const sub of this.activeResource.bridgeSubs) sub.unsubscribe();
|
|
560
|
+
this.activeResource = null;
|
|
561
|
+
}
|
|
562
|
+
if (this._actor) {
|
|
563
|
+
this._actor.dispose();
|
|
564
|
+
this._actor = null;
|
|
565
|
+
}
|
|
566
|
+
this.errorsSubject.complete();
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* Route a transport-level error onto `errors$`. Used by sibling adapters
|
|
570
|
+
* (e.g. `HttpContentTransport`'s XHR upload path) that don't go through
|
|
571
|
+
* the `ky` `beforeError` hook and need to surface failures on the same
|
|
572
|
+
* stream the rest of the transport publishes to.
|
|
573
|
+
*/
|
|
574
|
+
pushError(error) {
|
|
575
|
+
if (this.disposed) return;
|
|
576
|
+
this.errorsSubject.next(error);
|
|
577
|
+
}
|
|
578
|
+
// ── Auth ──────────────────────────────────────────────────────────────
|
|
579
|
+
authHeaders() {
|
|
580
|
+
const token = this.token$.getValue() ?? void 0;
|
|
581
|
+
return token ? { Authorization: `Bearer ${token}` } : {};
|
|
582
|
+
}
|
|
583
|
+
async authenticatePassword(email, password) {
|
|
584
|
+
return this.http.post(`${this.baseUrl}/api/tokens/password`, {
|
|
585
|
+
json: { email, password },
|
|
586
|
+
headers: this.authHeaders()
|
|
587
|
+
}).json();
|
|
588
|
+
}
|
|
589
|
+
async authenticateGoogle(credential) {
|
|
590
|
+
return this.http.post(`${this.baseUrl}/api/tokens/google`, {
|
|
591
|
+
json: { credential },
|
|
592
|
+
headers: this.authHeaders()
|
|
593
|
+
}).json();
|
|
594
|
+
}
|
|
595
|
+
async refreshAccessToken(token) {
|
|
596
|
+
return this.http.post(`${this.baseUrl}/api/tokens/refresh`, {
|
|
597
|
+
json: { refreshToken: token },
|
|
598
|
+
headers: this.authHeaders()
|
|
599
|
+
}).json();
|
|
600
|
+
}
|
|
601
|
+
async logout() {
|
|
602
|
+
await this.http.post(`${this.baseUrl}/api/users/logout`, {
|
|
603
|
+
headers: this.authHeaders()
|
|
604
|
+
}).json();
|
|
605
|
+
}
|
|
606
|
+
async acceptTerms() {
|
|
607
|
+
await this.http.post(`${this.baseUrl}/api/users/accept-terms`, {
|
|
608
|
+
headers: this.authHeaders()
|
|
609
|
+
}).json();
|
|
610
|
+
}
|
|
611
|
+
async getCurrentUser() {
|
|
612
|
+
return this.http.get(`${this.baseUrl}/api/users/me`, {
|
|
613
|
+
headers: this.authHeaders()
|
|
614
|
+
}).json();
|
|
615
|
+
}
|
|
616
|
+
async generateMcpToken() {
|
|
617
|
+
return this.http.post(`${this.baseUrl}/api/tokens/mcp-generate`, {
|
|
618
|
+
headers: this.authHeaders()
|
|
619
|
+
}).json();
|
|
620
|
+
}
|
|
621
|
+
async getMediaToken(resourceId) {
|
|
622
|
+
return this.http.post(`${this.baseUrl}/api/tokens/media`, {
|
|
623
|
+
json: { resourceId },
|
|
624
|
+
headers: this.authHeaders()
|
|
625
|
+
}).json();
|
|
626
|
+
}
|
|
627
|
+
// ── Admin ─────────────────────────────────────────────────────────────
|
|
628
|
+
async listUsers() {
|
|
629
|
+
return this.http.get(`${this.baseUrl}/api/admin/users`, {
|
|
630
|
+
headers: this.authHeaders()
|
|
631
|
+
}).json();
|
|
632
|
+
}
|
|
633
|
+
async getUserStats() {
|
|
634
|
+
return this.http.get(`${this.baseUrl}/api/admin/users/stats`, {
|
|
635
|
+
headers: this.authHeaders()
|
|
636
|
+
}).json();
|
|
637
|
+
}
|
|
638
|
+
async updateUser(id, data) {
|
|
639
|
+
return this.http.patch(`${this.baseUrl}/api/admin/users/${id}`, {
|
|
640
|
+
json: data,
|
|
641
|
+
headers: this.authHeaders()
|
|
642
|
+
}).json();
|
|
643
|
+
}
|
|
644
|
+
async getOAuthConfig() {
|
|
645
|
+
return this.http.get(`${this.baseUrl}/api/admin/oauth/config`, {
|
|
646
|
+
headers: this.authHeaders()
|
|
647
|
+
}).json();
|
|
648
|
+
}
|
|
649
|
+
// ── Exchange (backup/restore/export/import) ───────────────────────────
|
|
650
|
+
async backupKnowledgeBase() {
|
|
651
|
+
const response = await this.http.post(`${this.baseUrl}/api/admin/exchange/backup`, {
|
|
652
|
+
headers: this.authHeaders()
|
|
653
|
+
});
|
|
654
|
+
return responseToDownload(response);
|
|
655
|
+
}
|
|
656
|
+
restoreKnowledgeBase(file) {
|
|
657
|
+
return this.sseProgressStream(`${this.baseUrl}/api/admin/exchange/restore`, file);
|
|
658
|
+
}
|
|
659
|
+
async exportKnowledgeBase(params) {
|
|
660
|
+
const searchParams = params?.includeArchived ? new URLSearchParams({ includeArchived: "true" }) : void 0;
|
|
661
|
+
const response = await this.http.post(`${this.baseUrl}/api/moderate/exchange/export`, {
|
|
662
|
+
headers: this.authHeaders(),
|
|
663
|
+
...searchParams ? { searchParams } : {}
|
|
664
|
+
});
|
|
665
|
+
return responseToDownload(response);
|
|
666
|
+
}
|
|
667
|
+
importKnowledgeBase(file) {
|
|
668
|
+
return this.sseProgressStream(`${this.baseUrl}/api/moderate/exchange/import`, file);
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* POST a file to a server-sent-events endpoint and surface each `data:`
|
|
672
|
+
* frame as an Observable emission. Completes when the stream closes;
|
|
673
|
+
* errors if the request itself fails or the SSE stream is aborted.
|
|
674
|
+
* The returned Observable is cold — the POST happens on subscribe and
|
|
675
|
+
* is aborted via `AbortController` on unsubscribe.
|
|
676
|
+
*/
|
|
677
|
+
sseProgressStream(url, file) {
|
|
678
|
+
return new Observable((subscriber) => {
|
|
679
|
+
const ctrl = new AbortController();
|
|
680
|
+
const formData = new FormData();
|
|
681
|
+
formData.append("file", file);
|
|
682
|
+
(async () => {
|
|
683
|
+
try {
|
|
684
|
+
const response = await this.http.post(url, {
|
|
685
|
+
body: formData,
|
|
686
|
+
headers: this.authHeaders(),
|
|
687
|
+
signal: ctrl.signal
|
|
688
|
+
});
|
|
689
|
+
const reader = response.body.getReader();
|
|
690
|
+
const decoder = new TextDecoder();
|
|
691
|
+
let buffer = "";
|
|
692
|
+
while (!subscriber.closed) {
|
|
693
|
+
const { done, value } = await reader.read();
|
|
694
|
+
if (done) break;
|
|
695
|
+
buffer += decoder.decode(value, { stream: true });
|
|
696
|
+
const lines = buffer.split("\n");
|
|
697
|
+
buffer = lines.pop();
|
|
698
|
+
for (const line of lines) {
|
|
699
|
+
if (line.startsWith("data: ")) {
|
|
700
|
+
const event = JSON.parse(line.slice(6));
|
|
701
|
+
subscriber.next(event);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
subscriber.complete();
|
|
706
|
+
} catch (err) {
|
|
707
|
+
if (!subscriber.closed) subscriber.error(err);
|
|
708
|
+
}
|
|
709
|
+
})();
|
|
710
|
+
return () => ctrl.abort();
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
// ── System status ─────────────────────────────────────────────────────
|
|
714
|
+
async healthCheck() {
|
|
715
|
+
return this.http.get(`${this.baseUrl}/api/health`, {
|
|
716
|
+
headers: this.authHeaders()
|
|
717
|
+
}).json();
|
|
718
|
+
}
|
|
719
|
+
async getStatus() {
|
|
720
|
+
return this.http.get(`${this.baseUrl}/api/status`, {
|
|
721
|
+
headers: this.authHeaders()
|
|
722
|
+
}).json();
|
|
723
|
+
}
|
|
724
|
+
// ── Internal: ky accessor for legacy passthroughs (temporary) ─────────
|
|
725
|
+
/**
|
|
726
|
+
* Temporary escape hatch for the ongoing transport migration: namespaces
|
|
727
|
+
* that still need to issue ad-hoc HTTP calls (e.g. legacy browse/mark
|
|
728
|
+
* HTTP fallbacks) can borrow the configured `ky` instance here. Will be
|
|
729
|
+
* deleted once all namespaces route through bus channels or through
|
|
730
|
+
* typed methods on this transport.
|
|
731
|
+
*/
|
|
732
|
+
get rawHttp() {
|
|
733
|
+
return this.http;
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
736
|
+
* Current access token (synchronously read from the BehaviorSubject).
|
|
737
|
+
* Used by content-transport and legacy namespace HTTP fallbacks that
|
|
738
|
+
* need to pass `auth: token` through some code paths.
|
|
739
|
+
*/
|
|
740
|
+
getToken() {
|
|
741
|
+
return this.token$.getValue() ?? void 0;
|
|
742
|
+
}
|
|
743
|
+
};
|
|
744
|
+
var HttpContentTransport = class {
|
|
745
|
+
constructor(transport) {
|
|
746
|
+
this.transport = transport;
|
|
747
|
+
}
|
|
748
|
+
transport;
|
|
749
|
+
async putBinary(request, options) {
|
|
750
|
+
const sizeBytes = request.file instanceof File ? request.file.size : request.file.length;
|
|
751
|
+
busLog("PUT", "content", {
|
|
752
|
+
name: request.name,
|
|
753
|
+
format: request.format,
|
|
754
|
+
storageUri: request.storageUri,
|
|
755
|
+
sizeBytes
|
|
756
|
+
});
|
|
757
|
+
return withSpan(
|
|
758
|
+
"content.put",
|
|
759
|
+
async () => {
|
|
760
|
+
const formData = buildFormData(request);
|
|
761
|
+
const headers = this.requestHeaders(options?.auth);
|
|
762
|
+
const xhrAvailable = typeof XMLHttpRequest !== "undefined";
|
|
763
|
+
if (xhrAvailable && (options?.onProgress || options?.signal)) {
|
|
764
|
+
return uploadViaXhr({
|
|
765
|
+
url: `${this.transport.baseUrl}/resources`,
|
|
766
|
+
formData,
|
|
767
|
+
headers,
|
|
768
|
+
onProgress: options.onProgress,
|
|
769
|
+
signal: options.signal,
|
|
770
|
+
onApiError: (err) => this.transport.pushError(err)
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
const result = await this.transport.rawHttp.post(`${this.transport.baseUrl}/resources`, {
|
|
774
|
+
body: formData,
|
|
775
|
+
headers
|
|
776
|
+
}).json();
|
|
777
|
+
return { resourceId: result.resourceId };
|
|
778
|
+
},
|
|
779
|
+
{
|
|
780
|
+
kind: SpanKind.CLIENT,
|
|
781
|
+
attrs: {
|
|
782
|
+
"content.format": request.format,
|
|
783
|
+
"content.size_bytes": sizeBytes
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
async getBinary(resourceId, options) {
|
|
789
|
+
busLog("GET", "content", { resourceId });
|
|
790
|
+
return withSpan(
|
|
791
|
+
"content.get",
|
|
792
|
+
async () => {
|
|
793
|
+
const response = await this.transport.rawHttp.get(`${this.transport.baseUrl}/resources/${resourceId}`, {
|
|
794
|
+
headers: this.requestHeaders(options?.auth)
|
|
795
|
+
});
|
|
796
|
+
const contentType = response.headers.get("content-type") || "application/octet-stream";
|
|
797
|
+
const data = await response.arrayBuffer();
|
|
798
|
+
return { data, contentType };
|
|
799
|
+
},
|
|
800
|
+
{ kind: SpanKind.CLIENT, attrs: { "resource.id": resourceId } }
|
|
801
|
+
);
|
|
802
|
+
}
|
|
803
|
+
async getBinaryStream(resourceId, options) {
|
|
804
|
+
busLog("GET", "content", { resourceId, stream: true });
|
|
805
|
+
return withSpan(
|
|
806
|
+
"content.get",
|
|
807
|
+
async () => {
|
|
808
|
+
const response = await this.transport.rawHttp.get(`${this.transport.baseUrl}/resources/${resourceId}`, {
|
|
809
|
+
headers: this.requestHeaders(options?.auth)
|
|
810
|
+
});
|
|
811
|
+
const contentType = response.headers.get("content-type") || "application/octet-stream";
|
|
812
|
+
if (!response.body) {
|
|
813
|
+
throw new Error("Response body is null - cannot create stream");
|
|
814
|
+
}
|
|
815
|
+
return { stream: response.body, contentType };
|
|
816
|
+
},
|
|
817
|
+
{
|
|
818
|
+
kind: SpanKind.CLIENT,
|
|
819
|
+
attrs: { "resource.id": resourceId, "content.stream": true }
|
|
820
|
+
}
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
/**
|
|
824
|
+
* Dereference the resource's JSON-LD graph over HTTP — the LD face an
|
|
825
|
+
* external linked-data client sees. Deliberately HTTP, not the bus
|
|
826
|
+
* (SIMPLER-JSON-LD.md §5).
|
|
827
|
+
*/
|
|
828
|
+
async getResourceGraph(resourceId, options) {
|
|
829
|
+
busLog("GET", "content", { resourceId, graph: true });
|
|
830
|
+
return withSpan(
|
|
831
|
+
"content.get_graph",
|
|
832
|
+
() => this.transport.rawHttp.get(`${this.transport.baseUrl}/resources/${resourceId}/jsonld`, {
|
|
833
|
+
headers: this.requestHeaders(options?.auth)
|
|
834
|
+
}).json(),
|
|
835
|
+
{ kind: SpanKind.CLIENT, attrs: { "resource.id": resourceId, "content.graph": true } }
|
|
836
|
+
);
|
|
837
|
+
}
|
|
838
|
+
dispose() {
|
|
839
|
+
}
|
|
840
|
+
/** Auth header + W3C trace propagation for the active span. */
|
|
841
|
+
requestHeaders(override) {
|
|
842
|
+
const token = override ?? this.transport.getToken();
|
|
843
|
+
const headers = token ? { Authorization: `Bearer ${token}` } : {};
|
|
844
|
+
const trace = getActiveTraceparent();
|
|
845
|
+
if (trace) {
|
|
846
|
+
headers["traceparent"] = trace.traceparent;
|
|
847
|
+
if (trace.tracestate) headers["tracestate"] = trace.tracestate;
|
|
848
|
+
}
|
|
849
|
+
return headers;
|
|
850
|
+
}
|
|
851
|
+
};
|
|
852
|
+
function buildFormData(request) {
|
|
853
|
+
const formData = new FormData();
|
|
854
|
+
formData.append("name", request.name);
|
|
855
|
+
formData.append("format", request.format);
|
|
856
|
+
formData.append("storageUri", request.storageUri);
|
|
857
|
+
if (request.file instanceof File) {
|
|
858
|
+
formData.append("file", request.file);
|
|
859
|
+
} else if (typeof Buffer !== "undefined" && Buffer.isBuffer(request.file)) {
|
|
860
|
+
const blob = new Blob([new Uint8Array(request.file)], { type: request.format });
|
|
861
|
+
formData.append("file", blob, request.name);
|
|
862
|
+
} else {
|
|
863
|
+
throw new Error("file must be a File or Buffer");
|
|
864
|
+
}
|
|
865
|
+
if (request.entityTypes && request.entityTypes.length > 0) {
|
|
866
|
+
formData.append("entityTypes", JSON.stringify(request.entityTypes));
|
|
867
|
+
}
|
|
868
|
+
if (request.language) formData.append("language", request.language);
|
|
869
|
+
if (request.sourceAnnotationId) formData.append("sourceAnnotationId", String(request.sourceAnnotationId));
|
|
870
|
+
if (request.sourceResourceId) formData.append("sourceResourceId", String(request.sourceResourceId));
|
|
871
|
+
if (request.generationPrompt) formData.append("generationPrompt", request.generationPrompt);
|
|
872
|
+
if (request.generator) formData.append("generator", JSON.stringify(request.generator));
|
|
873
|
+
if (request.isDraft !== void 0) formData.append("isDraft", String(request.isDraft));
|
|
874
|
+
return formData;
|
|
875
|
+
}
|
|
876
|
+
function uploadViaXhr(opts) {
|
|
877
|
+
const { url, formData, headers, onProgress, signal, onApiError } = opts;
|
|
878
|
+
return new Promise((resolve, reject) => {
|
|
879
|
+
const xhr = new XMLHttpRequest();
|
|
880
|
+
if (signal?.aborted) {
|
|
881
|
+
const err = new APIError("Upload aborted", 0, "aborted");
|
|
882
|
+
onApiError(err);
|
|
883
|
+
reject(err);
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
xhr.open("POST", url);
|
|
887
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
888
|
+
xhr.setRequestHeader(name, value);
|
|
889
|
+
}
|
|
890
|
+
if (onProgress) {
|
|
891
|
+
xhr.upload.onprogress = (e) => {
|
|
892
|
+
const totalBytes = e.lengthComputable ? e.total : 0;
|
|
893
|
+
onProgress({ bytesUploaded: e.loaded, totalBytes });
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
xhr.onload = () => {
|
|
897
|
+
if (xhr.status >= 200 && xhr.status < 300) {
|
|
898
|
+
try {
|
|
899
|
+
const body2 = JSON.parse(xhr.responseText);
|
|
900
|
+
resolve({ resourceId: body2.resourceId });
|
|
901
|
+
} catch (parseErr) {
|
|
902
|
+
const err2 = new APIError(
|
|
903
|
+
`Upload succeeded but response was not valid JSON: ${parseErr.message}`,
|
|
904
|
+
xhr.status,
|
|
905
|
+
xhr.statusText,
|
|
906
|
+
xhr.responseText
|
|
907
|
+
);
|
|
908
|
+
onApiError(err2);
|
|
909
|
+
reject(err2);
|
|
910
|
+
}
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
let body = xhr.responseText;
|
|
914
|
+
try {
|
|
915
|
+
body = JSON.parse(xhr.responseText);
|
|
916
|
+
} catch {
|
|
917
|
+
}
|
|
918
|
+
const message = body && typeof body === "object" && "message" in body && typeof body.message === "string" ? body.message : `HTTP ${xhr.status}: ${xhr.statusText}`;
|
|
919
|
+
const err = new APIError(message, xhr.status, xhr.statusText, body);
|
|
920
|
+
onApiError(err);
|
|
921
|
+
reject(err);
|
|
922
|
+
};
|
|
923
|
+
xhr.onerror = () => {
|
|
924
|
+
const err = new APIError("Network error during upload", 0, "network-error");
|
|
925
|
+
onApiError(err);
|
|
926
|
+
reject(err);
|
|
927
|
+
};
|
|
928
|
+
xhr.ontimeout = () => {
|
|
929
|
+
const err = new APIError("Upload timed out", 0, "timeout");
|
|
930
|
+
onApiError(err);
|
|
931
|
+
reject(err);
|
|
932
|
+
};
|
|
933
|
+
xhr.onabort = () => {
|
|
934
|
+
const err = new APIError("Upload aborted", 0, "aborted");
|
|
935
|
+
onApiError(err);
|
|
936
|
+
reject(err);
|
|
937
|
+
};
|
|
938
|
+
if (signal) {
|
|
939
|
+
const onAbort = () => xhr.abort();
|
|
940
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
941
|
+
}
|
|
942
|
+
xhr.send(formData);
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
export { APIError, DEGRADED_THRESHOLD_MS, HttpContentTransport, HttpTransport, createActorStateUnit };
|
|
947
|
+
//# sourceMappingURL=index.js.map
|
|
948
|
+
//# sourceMappingURL=index.js.map
|