arcane-os 0.86.1 → 0.88.0
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/CHANGELOG.md +34 -0
- package/README.md +1 -1
- package/browser-runtime/core/contracts.mjs +1 -0
- package/browser-runtime/document-acquisition.mjs +112 -0
- package/docs/reference/README.md +1 -0
- package/docs/reference/document-acquisition.md +159 -0
- package/docs/reference/inventory/package-api.json +30 -2
- package/docs/reference/local-ai.md +17 -0
- package/docs/reference/model-observation.md +7 -0
- package/docs/reference/protocols.md +15 -0
- package/docs/reference/runtime-components.md +6 -2
- package/docs/reference/runtime-modules.md +35 -4
- package/docs/reference/sdk-api.md +62 -0
- package/package.json +4 -2
- package/runtime/arcane/components/theme-switcher.html +10 -5
- package/runtime/arcane/modules/AI.js +306 -120
- package/runtime/arcane/modules/AIRuntimeState.js +5 -2
- package/src/core/services/document-acquisition.mjs +221 -0
- package/src/import-map.mjs +1 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import {Buffer} from 'node:buffer';
|
|
2
|
+
import Is from 'strong-type';
|
|
3
|
+
import {CoreError, serializeCoreError} from '../../../browser-runtime/core/contracts.mjs';
|
|
4
|
+
|
|
5
|
+
const is = new Is(false);
|
|
6
|
+
const redirectStatuses = new Set([301, 302, 303, 307, 308]);
|
|
7
|
+
|
|
8
|
+
/** The application supplies its optional destination decision in this native process. */
|
|
9
|
+
export function createDocumentAcquisitionService({destinationPredicate, signal} = {}) {
|
|
10
|
+
if (destinationPredicate !== undefined && !is.function(destinationPredicate)) {
|
|
11
|
+
throw new TypeError('Document acquisition destinationPredicate must be a function.');
|
|
12
|
+
}
|
|
13
|
+
const lifetime = new AbortController();
|
|
14
|
+
const lifetimeSignal = signal ? AbortSignal.any([signal, lifetime.signal]) : lifetime.signal;
|
|
15
|
+
const jobs = new Set();
|
|
16
|
+
let closing;
|
|
17
|
+
|
|
18
|
+
function acquire({url, signal: requestSignal, onProgress} = {}) {
|
|
19
|
+
if (!is.string(url) || url === '') {
|
|
20
|
+
throw new TypeError('Document acquisition url must be a nonempty string.');
|
|
21
|
+
}
|
|
22
|
+
if (onProgress !== undefined && !is.function(onProgress)) {
|
|
23
|
+
throw new TypeError('Document acquisition onProgress must be a function.');
|
|
24
|
+
}
|
|
25
|
+
const operationSignal = requestSignal
|
|
26
|
+
? AbortSignal.any([lifetimeSignal, requestSignal]) : lifetimeSignal;
|
|
27
|
+
const evidence = {requestedUrl: url, requestUrl: url, redirects: []};
|
|
28
|
+
|
|
29
|
+
async function progress(phase, completed = 0) {
|
|
30
|
+
operationSignal.throwIfAborted();
|
|
31
|
+
await onProgress?.({
|
|
32
|
+
phase, completed, total: 1, unit: 'documents',
|
|
33
|
+
requestedUrl: url, url: evidence.requestUrl
|
|
34
|
+
});
|
|
35
|
+
operationSignal.throwIfAborted();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const task = Promise.resolve().then(async function acquireDocument() {
|
|
39
|
+
try {
|
|
40
|
+
await progress('accepted');
|
|
41
|
+
let destination = new URL(url);
|
|
42
|
+
while (true) {
|
|
43
|
+
evidence.requestUrl = destination.href;
|
|
44
|
+
if (destination.protocol !== 'http:' && destination.protocol !== 'https:') {
|
|
45
|
+
throw new TypeError('Document acquisition requires an HTTP or HTTPS URL.');
|
|
46
|
+
}
|
|
47
|
+
await progress('destination');
|
|
48
|
+
if (destinationPredicate !== undefined) {
|
|
49
|
+
const accepted = await destinationPredicate(destination.href, {
|
|
50
|
+
requestedUrl: url,
|
|
51
|
+
previousUrl: evidence.redirects.at(-1)?.url ?? null,
|
|
52
|
+
signal: operationSignal
|
|
53
|
+
});
|
|
54
|
+
operationSignal.throwIfAborted();
|
|
55
|
+
if (accepted !== true) {
|
|
56
|
+
throw new CoreError({
|
|
57
|
+
code: 'DOCUMENT_DESTINATION_DECLINED',
|
|
58
|
+
message: `The application declined document destination ${destination.href}.`
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
await progress('request');
|
|
63
|
+
// Manual redirects keep the application decision ahead of every request.
|
|
64
|
+
const response = await fetch(destination.href, {
|
|
65
|
+
method: 'GET', redirect: 'manual', signal: operationSignal
|
|
66
|
+
});
|
|
67
|
+
const record = {
|
|
68
|
+
url: response.url,
|
|
69
|
+
status: response.status,
|
|
70
|
+
statusText: response.statusText,
|
|
71
|
+
ok: response.ok,
|
|
72
|
+
headers: Array.from(response.headers.entries()),
|
|
73
|
+
mediaType: response.headers.get('content-type'),
|
|
74
|
+
body: new Uint8Array(),
|
|
75
|
+
complete: false
|
|
76
|
+
};
|
|
77
|
+
evidence.response = record;
|
|
78
|
+
await readResponse(response, record, operationSignal, function reading() {
|
|
79
|
+
return progress('response');
|
|
80
|
+
});
|
|
81
|
+
const location = response.headers.get('location');
|
|
82
|
+
if (redirectStatuses.has(record.status) && location !== null) {
|
|
83
|
+
evidence.redirects.push(record);
|
|
84
|
+
delete evidence.response;
|
|
85
|
+
await progress('redirect');
|
|
86
|
+
destination = new URL(location, response.url || destination.href);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const result = {
|
|
90
|
+
requestedUrl: url, finalUrl: response.url,
|
|
91
|
+
...record, redirects: evidence.redirects
|
|
92
|
+
};
|
|
93
|
+
await progress('complete', 1);
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
} catch (cause) {
|
|
97
|
+
// Retain the original error as cause, including complete native properties.
|
|
98
|
+
throw new CoreError({
|
|
99
|
+
...serializeCoreError(cause), cause,
|
|
100
|
+
documentAcquisition: evidence
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
jobs.add(task);
|
|
105
|
+
function settled() { jobs.delete(task); }
|
|
106
|
+
task.then(settled, settled);
|
|
107
|
+
return task;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function acquireThroughCore(parameters, request) {
|
|
111
|
+
try {
|
|
112
|
+
const result = await acquire({
|
|
113
|
+
url: parameters.url,
|
|
114
|
+
signal: request.signal,
|
|
115
|
+
onProgress(progress) {
|
|
116
|
+
request.emit('documents.progress', {
|
|
117
|
+
requestId: request.clientRequestId ?? request.requestId,
|
|
118
|
+
...progress
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
return encodeResult(result);
|
|
123
|
+
} catch (error) {
|
|
124
|
+
if (!error.documentAcquisition) throw error;
|
|
125
|
+
throw new CoreError({
|
|
126
|
+
...serializeCoreError(error),
|
|
127
|
+
documentAcquisition: encodeEvidence(error.documentAcquisition)
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function dispose() {
|
|
133
|
+
closing ??= Promise.resolve().then(async function closeAcquisitionService() {
|
|
134
|
+
lifetime.abort(new CoreError({
|
|
135
|
+
code: 'DOCUMENT_ACQUISITION_CLOSED',
|
|
136
|
+
message: 'The document acquisition service is closing.'
|
|
137
|
+
}));
|
|
138
|
+
await Promise.allSettled([...jobs]);
|
|
139
|
+
});
|
|
140
|
+
return closing;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
name: 'documents',
|
|
145
|
+
methods: {'documents.acquire': acquireThroughCore},
|
|
146
|
+
acquire,
|
|
147
|
+
dispose
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function readResponse(response, record, signal, onReading) {
|
|
152
|
+
const chunks = [];
|
|
153
|
+
const cleanupErrors = [];
|
|
154
|
+
let reader;
|
|
155
|
+
let cancellation;
|
|
156
|
+
let failed = false;
|
|
157
|
+
let failure;
|
|
158
|
+
|
|
159
|
+
function cancelReader(reason) {
|
|
160
|
+
if (!reader || cancellation) return;
|
|
161
|
+
cancellation = Promise.resolve().then(function cancelResponseBody() {
|
|
162
|
+
return reader.cancel(reason);
|
|
163
|
+
}).catch(function retainCleanupError(error) { cleanupErrors.push(error); });
|
|
164
|
+
}
|
|
165
|
+
function aborted() { cancelReader(signal.reason); }
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
reader = response.body?.getReader();
|
|
169
|
+
signal.addEventListener('abort', aborted, {once: true});
|
|
170
|
+
if (signal.aborted) aborted();
|
|
171
|
+
await onReading();
|
|
172
|
+
while (reader) {
|
|
173
|
+
signal.throwIfAborted();
|
|
174
|
+
const {done, value} = await reader.read();
|
|
175
|
+
if (value !== undefined) chunks.push(Buffer.from(value));
|
|
176
|
+
if (done) {
|
|
177
|
+
signal.throwIfAborted();
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
signal.throwIfAborted();
|
|
182
|
+
record.complete = true;
|
|
183
|
+
} catch (error) {
|
|
184
|
+
failed = true;
|
|
185
|
+
failure = error;
|
|
186
|
+
cancelReader(error);
|
|
187
|
+
} finally {
|
|
188
|
+
signal.removeEventListener('abort', aborted);
|
|
189
|
+
if (cancellation) await cancellation;
|
|
190
|
+
try { reader?.releaseLock(); } catch (error) { cleanupErrors.push(error); }
|
|
191
|
+
record.body = Buffer.concat(chunks);
|
|
192
|
+
}
|
|
193
|
+
if (cleanupErrors.length) {
|
|
194
|
+
throw new AggregateError(
|
|
195
|
+
failed ? [failure, ...cleanupErrors] : cleanupErrors,
|
|
196
|
+
'Document response cleanup failed.',
|
|
197
|
+
failed ? {cause: failure} : undefined
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
if (failed) throw failure;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function encodeResponse(record) {
|
|
204
|
+
const {body, ...metadata} = record;
|
|
205
|
+
// Encoding belongs only to the existing JSON Core transport.
|
|
206
|
+
return {...metadata, body: {encoding: 'base64', data: Buffer.from(body).toString('base64')}};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function encodeResult(result) {
|
|
210
|
+
const {redirects, ...response} = result;
|
|
211
|
+
return {...encodeResponse(response), redirects: redirects.map(encodeResponse)};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function encodeEvidence(evidence) {
|
|
215
|
+
const {response, redirects, ...metadata} = evidence;
|
|
216
|
+
return {
|
|
217
|
+
...metadata,
|
|
218
|
+
redirects: redirects.map(encodeResponse),
|
|
219
|
+
...(response ? {response: encodeResponse(response)} : {})
|
|
220
|
+
};
|
|
221
|
+
}
|
package/src/import-map.mjs
CHANGED
|
@@ -44,6 +44,7 @@ const SDK_BROWSER_SELF_IMPORTS=new Map([
|
|
|
44
44
|
['arcane-os/websocket-client','sdk/websocket-client.mjs'],
|
|
45
45
|
['arcane-os/core/client','sdk/core/client.mjs'],
|
|
46
46
|
['arcane-os/core/contracts','sdk/core/contracts.mjs'],
|
|
47
|
+
['arcane-os/document-acquisition','sdk/document-acquisition.mjs'],
|
|
47
48
|
['arcane-os/logging','sdk/logging.mjs'],
|
|
48
49
|
['arcane-os/browser-device','sdk/browser-device.mjs'],
|
|
49
50
|
['arcane-os/pwa','sdk/pwa.mjs'],
|