arcane-os 0.3.0 → 0.3.2
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 +27 -0
- package/README.md +86 -117
- package/bin/arcane-test.mjs +170 -46
- package/browser-runtime/ai/browser-speech-artifacts.mjs +887 -909
- package/browser-runtime/ai/browser-speech-providers.mjs +96 -152
- package/browser-runtime/ai/browser-wasm-llm-provider.mjs +627 -819
- package/browser-runtime/ai/browser-wasm.mjs +24 -35
- package/browser-runtime/ai/browser-wllama-runtime.mjs +64 -316
- package/browser-runtime/ai/model-controller.mjs +584 -181
- package/browser-runtime/ai/speech-worker-client.mjs +8 -146
- package/browser-runtime/ai/speech-worker-runtime.mjs +643 -363
- package/browser-runtime/dom-event-instrumentation.mjs +55 -147
- package/browser-runtime/event-manager.mjs +239 -624
- package/package.json +5 -6
- package/runtime/arcane/components/app-bar.html +3 -15
- package/runtime/arcane/components/assistant-panel.html +10 -10
- package/runtime/arcane/components/calculator.html +1 -1
- package/runtime/arcane/components/chat.html +1359 -135
- package/runtime/arcane/components/conversation-view.html +2 -2
- package/runtime/arcane/components/document-inspector.html +11 -17
- package/runtime/arcane/components/file-manager.html +13 -56
- package/runtime/arcane/components/markdown-document.html +82 -281
- package/runtime/arcane/components/markdown-editor.html +7 -10
- package/runtime/arcane/components/media-embed.html +6 -6
- package/runtime/arcane/components/screen-capture.html +4 -4
- package/runtime/arcane/components/source-explanation.html +2 -2
- package/runtime/arcane/components/speech.html +112 -68
- package/runtime/arcane/components/terminal-workspace.html +4 -4
- package/runtime/arcane/components/theme-editor.html +1 -1
- package/runtime/arcane/components/unified-inbox.html +2 -2
- package/runtime/arcane/components/voice-transcription.html +31 -21
- package/runtime/arcane/entities/Calculation.js +2 -3
- package/runtime/arcane/entities/Chat.js +228 -43
- package/runtime/arcane/entities/Preference.js +3 -5
- package/runtime/arcane/entities/Weather.js +5 -5
- package/runtime/arcane/modules/AI.js +1050 -427
- package/runtime/arcane/modules/AIProviderRuntime.js +658 -363
- package/runtime/arcane/modules/AIResponseLength.js +9 -19
- package/runtime/arcane/modules/AIRuntimeState.js +109 -72
- package/runtime/arcane/modules/ArcaneNavigationPolicy.js +45 -32
- package/runtime/arcane/modules/BrowserTestSuite.js +78 -122
- package/runtime/arcane/modules/CalculatorEngine.js +9 -9
- package/runtime/arcane/modules/CommunicationAppController.js +3 -7
- package/runtime/arcane/modules/ComponentContracts.js +30 -32
- package/runtime/arcane/modules/ConfiguredAIChatSession.js +281 -230
- package/runtime/arcane/modules/ConversationActionItems.js +26 -59
- package/runtime/arcane/modules/ConversationClosingReport.js +34 -61
- package/runtime/arcane/modules/ConversationTimebox.js +27 -15
- package/runtime/arcane/modules/DBOPFSDocumentLibrary.js +152 -344
- package/runtime/arcane/modules/DocumentLexicalSearch.js +25 -91
- package/runtime/arcane/modules/HTMLImport.js +54 -1
- package/runtime/arcane/modules/IsolatedModelQuestionRunner.js +40 -203
- package/runtime/arcane/modules/LocalAIReadiness.js +40 -60
- package/runtime/arcane/modules/LocalAIReadinessController.js +15 -13
- package/runtime/arcane/modules/MD.js +1 -45
- package/runtime/arcane/modules/Mail.js +51 -103
- package/runtime/arcane/modules/MailOutbox.mjs +95 -193
- package/runtime/arcane/modules/MailTransport.mjs +36 -57
- package/runtime/arcane/modules/ModelDefinition.js +22 -106
- package/runtime/arcane/modules/OpenMeteoWeatherProvider.js +39 -101
- package/runtime/arcane/modules/PersistentAIChatSession.js +281 -18
- package/runtime/arcane/modules/PreferenceStore.js +102 -30
- package/runtime/arcane/modules/RiskSignalAnalyzer.js +8 -9
- package/runtime/arcane/modules/ScopedOPFSCache.js +7 -42
- package/runtime/arcane/modules/ScreenCapture.js +175 -128
- package/runtime/arcane/modules/SpeechPlayback.js +46 -149
- package/runtime/arcane/modules/StaticDocumentCatalog.js +173 -407
- package/runtime/arcane/modules/ToolCallRouter.js +25 -12
- package/runtime/arcane/modules/YouTubeMedia.js +6 -5
- package/schemas/arcane-app-bundle.schema.json +13 -78
- package/schemas/arcane-app.schema.json +9 -25
- package/schemas/arcane-lock.schema.json +18 -151
- package/schemas/arcane-package.schema.json +2 -16
- package/schemas/native-build-plan.schema.json +119 -122
- package/src/app-descriptor.mjs +75 -132
- package/src/application-tests.mjs +200 -0
- package/src/cli/main.mjs +27 -46
- package/src/constants.mjs +3 -4
- package/src/dev-server.mjs +30 -324
- package/src/doctor.mjs +92 -154
- package/src/dom-event-instrumentation.mjs +55 -147
- package/src/errors.mjs +2 -3
- package/src/event-manager.mjs +239 -624
- package/src/event-queue.mjs +3 -3
- package/src/import-map.mjs +273 -1028
- package/src/index.mjs +14 -16
- package/src/installed-sdk-runtime.mjs +40 -62
- package/src/integrated-provider-loader.mjs +53 -382
- package/src/mail-api.mjs +0 -2
- package/src/mail-server.mjs +224 -580
- package/src/mail.mjs +4 -10
- package/src/native-plan.mjs +163 -598
- package/src/native-provider-loader.mjs +104 -1063
- package/src/packager/core.mjs +485 -3229
- package/src/process.mjs +5 -10
- package/src/release-bundle.mjs +292 -2405
- package/src/runtime.mjs +76 -396
- package/src/scaffold.mjs +30 -80
- package/src/sdk-browser-runtime.mjs +70 -626
- package/src/source-server.mjs +588 -0
- package/src/targets/index.mjs +78 -188
- package/src/templates/workspace-template.mjs +19 -135
- package/src/testing-loader.mjs +164 -0
- package/src/testing.mjs +1 -1
- package/src/toolchain.mjs +131 -544
- package/src/update-check.mjs +26 -64
- package/src/workspace-operation-lock.mjs +139 -430
- package/src/workspace-runtime.mjs +112 -779
- package/src/workspace.mjs +40 -302
- package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +0 -218
- package/browser-runtime/ai/ARCANE_AI_BROWSER_SPEECH_COMPONENTS.json +0 -203
- package/browser-runtime/ai/ARCANE_AI_BROWSER_WASM_COMPONENTS.json +0 -80
- package/browser-runtime/ai/internal/sha256.mjs +0 -166
- package/docs/architecture.md +0 -344
- package/docs/compatibility.md +0 -36
- package/docs/event-manager.md +0 -294
- package/docs/platform-targets.md +0 -108
- package/docs/publishing.md +0 -201
- package/docs/reference/README.md +0 -187
- package/docs/reference/ai/browser-speech-package-authority.json +0 -835
- package/docs/reference/ai/browser-speech.md +0 -1252
- package/docs/reference/ai/browser-wasm.md +0 -530
- package/docs/reference/arcane-ollama.md +0 -288
- package/docs/reference/availability-and-normalization.md +0 -183
- package/docs/reference/behavioral-testing.md +0 -133
- package/docs/reference/cli.md +0 -779
- package/docs/reference/core/README.md +0 -62
- package/docs/reference/core/arcane-ai-contracts.md +0 -907
- package/docs/reference/core/arcane-api.md +0 -601
- package/docs/reference/core/arcane-entities.md +0 -65
- package/docs/reference/core/arcane-events.md +0 -134
- package/docs/reference/core/ollama-module.md +0 -181
- package/docs/reference/core/reference/arcane-api/ai-and-ollama.md +0 -1909
- package/docs/reference/core/reference/arcane-api/applications-terminal-capabilities.md +0 -1057
- package/docs/reference/core/reference/arcane-api/core-and-events.md +0 -320
- package/docs/reference/core/reference/arcane-api/filesystem-storage-preferences-appearance.md +0 -610
- package/docs/reference/core/reference/arcane-api/namespaces.md +0 -1157
- package/docs/reference/core/reference/arcane-api/platform-installation-users-system.md +0 -1423
- package/docs/reference/core/reference/arcane-api/session-provisioning-diagnostics-development.md +0 -315
- package/docs/reference/event-manager.md +0 -1511
- package/docs/reference/inventory/package-api.json +0 -3284
- package/docs/reference/inventory/runtime-components.json +0 -1011
- package/docs/reference/inventory/runtime-entities.json +0 -26
- package/docs/reference/inventory/runtime-modules.json +0 -1431
- package/docs/reference/mail.md +0 -316
- package/docs/reference/protocols.md +0 -677
- package/docs/reference/runtime-components.md +0 -1366
- package/docs/reference/runtime-entities.md +0 -303
- package/docs/reference/runtime-modules.md +0 -2960
- package/docs/reference/sdk-api.md +0 -6694
- package/docs/roadmap.md +0 -79
- package/docs/work-amplification.md +0 -129
- package/runtime/ARCANE_RUNTIME_RELEASE.json +0 -826
|
@@ -1,34 +1,22 @@
|
|
|
1
1
|
export const MAIL_OUTBOX_PROTOCOL='arcane-mail-outbox/1';
|
|
2
2
|
export const MAIL_OUTBOX_TABLE='mail_outbox';
|
|
3
3
|
export const MAIL_OUTBOX_IDEMPOTENCY_WINDOW_MS=24*60*60*1000;
|
|
4
|
-
export const MAIL_OUTBOX_STATES=
|
|
4
|
+
export const MAIL_OUTBOX_STATES=[
|
|
5
5
|
'queued',
|
|
6
6
|
'sending',
|
|
7
7
|
'retry_wait',
|
|
8
8
|
'accepted',
|
|
9
9
|
'failed',
|
|
10
10
|
'reconciliation_required'
|
|
11
|
-
]
|
|
12
|
-
export const MAIL_OUTBOX_ACCEPTANCE_AUTHORITIES=Object.freeze([
|
|
13
|
-
'arcane-core-mail-send-v1'
|
|
14
|
-
]);
|
|
15
|
-
|
|
11
|
+
];
|
|
16
12
|
const STATE_SET=new Set(MAIL_OUTBOX_STATES);
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
const TABLE_PATTERN=/^[a-z][a-z0-9_]{0,63}$/;
|
|
13
|
+
const REPORT_KEY_PATTERN=/^[A-Za-z0-9._:-]+$/;
|
|
14
|
+
const SAFE_CODE_PATTERN=/^[A-Za-z0-9._:-]+$/;
|
|
15
|
+
const SAFE_ID_PATTERN=/^[A-Za-z0-9._:-]+$/;
|
|
16
|
+
const TABLE_PATTERN=/^[a-z][a-z0-9_]*$/;
|
|
22
17
|
const FILE_SUFFIX='.mail-outbox.json';
|
|
23
18
|
const QUARANTINE_PROTOCOL='arcane-mail-outbox-quarantine/1';
|
|
24
19
|
const QUARANTINE_TABLE='mail_outbox_quarantine';
|
|
25
|
-
const DEFAULT_MAX_RECORDS=512;
|
|
26
|
-
const DEFAULT_MAX_INVALID_RECORDS=128;
|
|
27
|
-
const DEFAULT_MAX_ATTEMPTS_PER_DRAIN=16;
|
|
28
|
-
const DEFAULT_MAX_REPORT_BYTES=786_432;
|
|
29
|
-
const MAX_MAINTENANCE_BATCH=64;
|
|
30
|
-
const MAX_RETRY_AFTER_SECONDS=24*60*60;
|
|
31
|
-
const TABLE_LOCK_OPTIONS=Object.freeze({mode:'exclusive'});
|
|
32
20
|
const STRUCTURAL_RECORD_ERROR_CODES=new Set([
|
|
33
21
|
'MAIL_OUTBOX_RECORD_INVALID',
|
|
34
22
|
'MAIL_OUTBOX_REPORT_KEY_INVALID'
|
|
@@ -61,14 +49,6 @@ function isPlainRecord(value){
|
|
|
61
49
|
&&Object.getPrototypeOf(value)===Object.prototype;
|
|
62
50
|
}
|
|
63
51
|
|
|
64
|
-
function positiveInteger(value,label,defaultValue,maximum){
|
|
65
|
-
const resolved=value===undefined?defaultValue:value;
|
|
66
|
-
if(!Number.isSafeInteger(resolved)||resolved<1||resolved>maximum){
|
|
67
|
-
fail(`${label} must be an integer from 1 through ${maximum}.`);
|
|
68
|
-
}
|
|
69
|
-
return resolved;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
52
|
function timestamp(value,label,{nullable=false}={}){
|
|
73
53
|
if(nullable&&value===null) return null;
|
|
74
54
|
if(!Number.isSafeInteger(value)||value<0){
|
|
@@ -97,21 +77,12 @@ function safeStatusCode(value){
|
|
|
97
77
|
function retryAfterSeconds(value){
|
|
98
78
|
if(value===undefined||value===null) return null;
|
|
99
79
|
const seconds=Number(value);
|
|
100
|
-
if(!Number.isSafeInteger(seconds)||seconds<0
|
|
80
|
+
if(!Number.isSafeInteger(seconds)||seconds<0){
|
|
101
81
|
return null;
|
|
102
82
|
}
|
|
103
83
|
return seconds;
|
|
104
84
|
}
|
|
105
85
|
|
|
106
|
-
function deepFreeze(value,seen=new Set()){
|
|
107
|
-
if(value===null||(typeof value!=='object'&&typeof value!=='function')||seen.has(value)){
|
|
108
|
-
return value;
|
|
109
|
-
}
|
|
110
|
-
seen.add(value);
|
|
111
|
-
for(const key of Reflect.ownKeys(value)) deepFreeze(value[key],seen);
|
|
112
|
-
return Object.freeze(value);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
86
|
function parseSerializedReport(value){
|
|
116
87
|
if(typeof value!=='string'||!value){
|
|
117
88
|
fail('Mail outbox record contains no serialized report.','MAIL_OUTBOX_RECORD_INVALID');
|
|
@@ -133,7 +104,7 @@ function parseSerializedReport(value){
|
|
|
133
104
|
return parsed;
|
|
134
105
|
}
|
|
135
106
|
|
|
136
|
-
function serializeReport(report
|
|
107
|
+
function serializeReport(report){
|
|
137
108
|
if(!isPlainRecord(report)){
|
|
138
109
|
fail('Mail outbox report must be a plain object.','MAIL_OUTBOX_REPORT_INVALID',TypeError);
|
|
139
110
|
}
|
|
@@ -146,23 +117,13 @@ function serializeReport(report,maxReportBytes){
|
|
|
146
117
|
if(typeof serialized!=='string'||!serialized){
|
|
147
118
|
fail('Mail outbox report must be JSON serializable.','MAIL_OUTBOX_REPORT_INVALID',TypeError);
|
|
148
119
|
}
|
|
149
|
-
if(new TextEncoder().encode(serialized).byteLength>maxReportBytes){
|
|
150
|
-
fail(
|
|
151
|
-
`Mail outbox report cannot exceed ${maxReportBytes} serialized bytes.`,
|
|
152
|
-
'MAIL_OUTBOX_REPORT_TOO_LARGE',
|
|
153
|
-
RangeError
|
|
154
|
-
);
|
|
155
|
-
}
|
|
156
120
|
return serialized;
|
|
157
121
|
}
|
|
158
122
|
|
|
159
|
-
function quarantineSnapshot(value
|
|
123
|
+
function quarantineSnapshot(value){
|
|
160
124
|
try{
|
|
161
125
|
const serialized=JSON.stringify(value);
|
|
162
|
-
if(typeof serialized==='string'
|
|
163
|
-
&&new TextEncoder().encode(serialized).byteLength<=maxReportBytes){
|
|
164
|
-
return serialized;
|
|
165
|
-
}
|
|
126
|
+
if(typeof serialized==='string')return serialized;
|
|
166
127
|
}catch{}
|
|
167
128
|
return null;
|
|
168
129
|
}
|
|
@@ -170,7 +131,7 @@ function quarantineSnapshot(value,maxReportBytes){
|
|
|
170
131
|
function reportKey(value){
|
|
171
132
|
if(typeof value!=='string'||!REPORT_KEY_PATTERN.test(value)){
|
|
172
133
|
fail(
|
|
173
|
-
'Mail outbox reportKey must contain
|
|
134
|
+
'Mail outbox reportKey must contain safe characters.',
|
|
174
135
|
'MAIL_OUTBOX_REPORT_KEY_INVALID',
|
|
175
136
|
TypeError
|
|
176
137
|
);
|
|
@@ -195,11 +156,11 @@ function invalidRecordMetadata(fileName,error){
|
|
|
195
156
|
reportKeyFromFileName(fileName);
|
|
196
157
|
repairable=true;
|
|
197
158
|
}catch{}
|
|
198
|
-
return
|
|
159
|
+
return {
|
|
199
160
|
fileName:typeof fileName==='string'?fileName:'',
|
|
200
161
|
code:safeString(error?.code,SAFE_CODE_PATTERN)||'MAIL_OUTBOX_RECORD_UNREADABLE',
|
|
201
162
|
repairable
|
|
202
|
-
}
|
|
163
|
+
};
|
|
203
164
|
}
|
|
204
165
|
|
|
205
166
|
function structuralRecordError(error){
|
|
@@ -215,13 +176,14 @@ function normalizedFailure(value){
|
|
|
215
176
|
if(!code||typeof value.retryable!=='boolean'||typeof value.uncertain!=='boolean'){
|
|
216
177
|
fail('Mail outbox failure record is invalid.','MAIL_OUTBOX_RECORD_INVALID');
|
|
217
178
|
}
|
|
218
|
-
return
|
|
179
|
+
return {
|
|
180
|
+
...value,
|
|
219
181
|
code,
|
|
220
182
|
statusCode:safeStatusCode(value.statusCode),
|
|
221
183
|
retryable:value.retryable,
|
|
222
184
|
uncertain:value.uncertain,
|
|
223
185
|
retryAfterSeconds:retryAfterSeconds(value.retryAfterSeconds)
|
|
224
|
-
}
|
|
186
|
+
};
|
|
225
187
|
}
|
|
226
188
|
|
|
227
189
|
function normalizedResult(value,{invalidCode='MAIL_OUTBOX_RECORD_INVALID'}={}){
|
|
@@ -239,23 +201,21 @@ function normalizedResult(value,{invalidCode='MAIL_OUTBOX_RECORD_INVALID'}={}){
|
|
|
239
201
|
&&acceptanceAuthority===null){
|
|
240
202
|
fail('Mail outbox delivery result has a malformed acceptance authority.',invalidCode);
|
|
241
203
|
}
|
|
242
|
-
return
|
|
204
|
+
return {
|
|
205
|
+
...value,
|
|
243
206
|
status,
|
|
244
207
|
classification:safeString(value.classification,SAFE_CODE_PATTERN),
|
|
245
208
|
acceptanceAuthority,
|
|
246
209
|
requestId:safeString(value.requestId,SAFE_ID_PATTERN),
|
|
247
210
|
providerId:safeString(value.providerId,SAFE_ID_PATTERN),
|
|
248
|
-
providerStatus:
|
|
211
|
+
providerStatus:typeof value.providerStatus==='string'
|
|
212
|
+
||Number.isSafeInteger(value.providerStatus)
|
|
213
|
+
?value.providerStatus
|
|
214
|
+
:null,
|
|
249
215
|
providerCode:safeString(value.providerCode,SAFE_CODE_PATTERN),
|
|
250
216
|
statusCode:safeStatusCode(value.statusCode),
|
|
251
217
|
retryAfterSeconds:retryAfterSeconds(value.retryAfterSeconds)
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
function hasAcceptedEvidence(result){
|
|
256
|
-
return Boolean(result?.requestId&&(
|
|
257
|
-
result.providerId||ACCEPTANCE_AUTHORITY_SET.has(result.acceptanceAuthority)
|
|
258
|
-
));
|
|
218
|
+
};
|
|
259
219
|
}
|
|
260
220
|
|
|
261
221
|
function validateRecordState(record){
|
|
@@ -270,10 +230,7 @@ function validateRecordState(record){
|
|
|
270
230
|
)){
|
|
271
231
|
fail('Mail outbox record contains an invalid retry time.','MAIL_OUTBOX_RECORD_INVALID');
|
|
272
232
|
}
|
|
273
|
-
if(record.result?.acceptanceAuthority!=null&&
|
|
274
|
-
record.state!=='accepted'
|
|
275
|
-
||!ACCEPTANCE_AUTHORITY_SET.has(record.result.acceptanceAuthority)
|
|
276
|
-
)){
|
|
233
|
+
if(record.result?.acceptanceAuthority!=null&&record.state!=='accepted'){
|
|
277
234
|
fail('Mail outbox record contains an invalid acceptance authority.','MAIL_OUTBOX_RECORD_INVALID');
|
|
278
235
|
}
|
|
279
236
|
switch(record.state){
|
|
@@ -307,8 +264,8 @@ function validateRecordState(record){
|
|
|
307
264
|
||record.nextAttemptAt!==null
|
|
308
265
|
||record.failure!==null
|
|
309
266
|
||record.result?.status!=='accepted'
|
|
310
|
-
||!
|
|
311
|
-
fail('Accepted mail outbox record lacks
|
|
267
|
+
||!record.result.requestId){
|
|
268
|
+
fail('Accepted mail outbox record lacks a request identifier.','MAIL_OUTBOX_RECORD_INVALID');
|
|
312
269
|
}
|
|
313
270
|
return;
|
|
314
271
|
case 'failed':
|
|
@@ -369,7 +326,7 @@ function normalizedRecord(value,expectedReportKey=null){
|
|
|
369
326
|
fail('Mail outbox record contains inconsistent timestamps.','MAIL_OUTBOX_RECORD_INVALID');
|
|
370
327
|
}
|
|
371
328
|
validateRecordState(record);
|
|
372
|
-
return
|
|
329
|
+
return record;
|
|
373
330
|
}
|
|
374
331
|
|
|
375
332
|
function updatedRecord(record,patch){
|
|
@@ -377,13 +334,13 @@ function updatedRecord(record,patch){
|
|
|
377
334
|
}
|
|
378
335
|
|
|
379
336
|
function deliveryRequest(record,signal){
|
|
380
|
-
const report=
|
|
381
|
-
return
|
|
337
|
+
const report=parseSerializedReport(record.serializedReport);
|
|
338
|
+
return {
|
|
382
339
|
report,
|
|
383
340
|
reportKey:record.reportKey,
|
|
384
341
|
serializedReport:record.serializedReport,
|
|
385
342
|
signal
|
|
386
|
-
}
|
|
343
|
+
};
|
|
387
344
|
}
|
|
388
345
|
|
|
389
346
|
function deliveryResult(value){
|
|
@@ -397,10 +354,7 @@ function deliveryResult(value){
|
|
|
397
354
|
if(!result.requestId){
|
|
398
355
|
fail('Mail delivery result has no valid requestId.','MAIL_OUTBOX_DELIVERY_RESULT_INVALID');
|
|
399
356
|
}
|
|
400
|
-
if(result.acceptanceAuthority!==null&&
|
|
401
|
-
result.status!=='accepted'
|
|
402
|
-
||!ACCEPTANCE_AUTHORITY_SET.has(result.acceptanceAuthority)
|
|
403
|
-
)){
|
|
357
|
+
if(result.acceptanceAuthority!==null&&result.status!=='accepted'){
|
|
404
358
|
fail('Mail delivery result has an invalid acceptance authority.','MAIL_OUTBOX_DELIVERY_RESULT_INVALID');
|
|
405
359
|
}
|
|
406
360
|
switch(result.status){
|
|
@@ -408,52 +362,49 @@ function deliveryResult(value){
|
|
|
408
362
|
if(result.classification!==null&&result.classification!=='accepted'){
|
|
409
363
|
fail('Accepted mail delivery has an invalid classification.','MAIL_OUTBOX_DELIVERY_RESULT_INVALID');
|
|
410
364
|
}
|
|
411
|
-
|
|
412
|
-
fail(
|
|
413
|
-
'Accepted mail delivery has neither a valid providerId nor an admitted acceptance authority.',
|
|
414
|
-
'MAIL_OUTBOX_DELIVERY_RESULT_INVALID'
|
|
415
|
-
);
|
|
416
|
-
}
|
|
417
|
-
return Object.freeze({kind:'accepted',result,failure:null});
|
|
365
|
+
return {kind:'accepted',result,failure:null};
|
|
418
366
|
case 'delivery_uncertain':
|
|
419
367
|
if(result.classification!==null&&result.classification!=='ambiguous'){
|
|
420
368
|
fail('Uncertain mail delivery has an invalid classification.','MAIL_OUTBOX_DELIVERY_RESULT_INVALID');
|
|
421
369
|
}
|
|
422
|
-
return
|
|
370
|
+
return {
|
|
423
371
|
kind:'retry',
|
|
424
372
|
result,
|
|
425
|
-
failure:
|
|
373
|
+
failure:{
|
|
374
|
+
...result,
|
|
426
375
|
code:result.providerCode||'MAIL_DELIVERY_UNCERTAIN',
|
|
427
376
|
statusCode:result.statusCode,
|
|
428
377
|
retryable:true,
|
|
429
378
|
uncertain:true,
|
|
430
379
|
retryAfterSeconds:result.retryAfterSeconds
|
|
431
|
-
}
|
|
432
|
-
}
|
|
380
|
+
}
|
|
381
|
+
};
|
|
433
382
|
case 'retryable':
|
|
434
383
|
if(result.classification!==null&&result.classification!=='retryable'){
|
|
435
384
|
fail('Retryable mail delivery has an invalid classification.','MAIL_OUTBOX_DELIVERY_RESULT_INVALID');
|
|
436
385
|
}
|
|
437
|
-
return
|
|
386
|
+
return {
|
|
438
387
|
kind:'retry',
|
|
439
388
|
result,
|
|
440
|
-
failure:
|
|
389
|
+
failure:{
|
|
390
|
+
...result,
|
|
441
391
|
code:result.providerCode||'MAIL_DELIVERY_RETRYABLE',
|
|
442
392
|
statusCode:result.statusCode,
|
|
443
393
|
retryable:true,
|
|
444
394
|
uncertain:false,
|
|
445
395
|
retryAfterSeconds:result.retryAfterSeconds
|
|
446
|
-
}
|
|
447
|
-
}
|
|
396
|
+
}
|
|
397
|
+
};
|
|
448
398
|
case 'permanently_rejected':
|
|
449
399
|
case 'partially_accepted':
|
|
450
400
|
if(result.classification!==null&&result.classification!=='permanent'){
|
|
451
401
|
fail('Rejected mail delivery has an invalid classification.','MAIL_OUTBOX_DELIVERY_RESULT_INVALID');
|
|
452
402
|
}
|
|
453
|
-
return
|
|
403
|
+
return {
|
|
454
404
|
kind:'failed',
|
|
455
405
|
result,
|
|
456
|
-
failure:
|
|
406
|
+
failure:{
|
|
407
|
+
...result,
|
|
457
408
|
code:result.providerCode||(
|
|
458
409
|
result.status==='partially_accepted'
|
|
459
410
|
?'MAIL_DELIVERY_PARTIALLY_ACCEPTED'
|
|
@@ -463,8 +414,8 @@ function deliveryResult(value){
|
|
|
463
414
|
retryable:false,
|
|
464
415
|
uncertain:false,
|
|
465
416
|
retryAfterSeconds:null
|
|
466
|
-
}
|
|
467
|
-
}
|
|
417
|
+
}
|
|
418
|
+
};
|
|
468
419
|
default:
|
|
469
420
|
fail('Mail delivery returned an unsupported status.','MAIL_OUTBOX_DELIVERY_RESULT_INVALID');
|
|
470
421
|
}
|
|
@@ -485,17 +436,24 @@ function deliveryFailure(error){
|
|
|
485
436
|
:null
|
|
486
437
|
)
|
|
487
438
|
);
|
|
488
|
-
|
|
439
|
+
const errorRecord=error&&typeof error==='object'&&!Array.isArray(error)
|
|
440
|
+
?{...error}
|
|
441
|
+
:{};
|
|
442
|
+
return {
|
|
489
443
|
kind:retryable?'retry':'failed',
|
|
490
444
|
result:null,
|
|
491
|
-
failure:
|
|
445
|
+
failure:{
|
|
446
|
+
...errorRecord,
|
|
447
|
+
name:typeof error?.name==='string'?error.name:'Error',
|
|
448
|
+
message:typeof error?.message==='string'?error.message:String(error??''),
|
|
449
|
+
...(typeof error?.stack==='string'?{stack:error.stack}:{}),
|
|
492
450
|
code:safeString(error?.code,SAFE_CODE_PATTERN)||'MAIL_DELIVERY_FAILED',
|
|
493
451
|
statusCode:safeStatusCode(error?.statusCode),
|
|
494
452
|
retryable,
|
|
495
453
|
uncertain,
|
|
496
454
|
retryAfterSeconds:seconds
|
|
497
|
-
}
|
|
498
|
-
}
|
|
455
|
+
}
|
|
456
|
+
};
|
|
499
457
|
}
|
|
500
458
|
|
|
501
459
|
function abortError(signal){
|
|
@@ -563,7 +521,7 @@ function waitForOutboxOperation(operation,signal){
|
|
|
563
521
|
}
|
|
564
522
|
|
|
565
523
|
function drainReason(value){
|
|
566
|
-
if(typeof value!=='string'||!/^[a-z][a-z0-9_-]
|
|
524
|
+
if(typeof value!=='string'||!/^[a-z][a-z0-9_-]*$/.test(value)){
|
|
567
525
|
fail('Mail outbox drain reason is invalid.','MAIL_OUTBOX_INVALID',TypeError);
|
|
568
526
|
}
|
|
569
527
|
return value;
|
|
@@ -573,7 +531,7 @@ function terminalState(state){
|
|
|
573
531
|
return state==='accepted'||state==='failed'||state==='reconciliation_required';
|
|
574
532
|
}
|
|
575
533
|
|
|
576
|
-
function summary(reason,online,records,attempted,invalidRecords
|
|
534
|
+
function summary(reason,online,records,attempted,invalidRecords){
|
|
577
535
|
const counts={
|
|
578
536
|
queued:0,
|
|
579
537
|
sending:0,
|
|
@@ -583,16 +541,17 @@ function summary(reason,online,records,attempted,invalidRecords,bounded){
|
|
|
583
541
|
reconciliation_required:0
|
|
584
542
|
};
|
|
585
543
|
for(const record of records) counts[record.state]+=1;
|
|
586
|
-
return
|
|
544
|
+
return {
|
|
587
545
|
reason,
|
|
588
546
|
online,
|
|
547
|
+
records:[...records],
|
|
589
548
|
considered:records.length,
|
|
590
549
|
attempted,
|
|
591
|
-
invalidRecords,
|
|
592
|
-
|
|
550
|
+
invalidRecords:[...invalidRecords],
|
|
551
|
+
invalidRecordCount:invalidRecords.length,
|
|
593
552
|
pending:counts.queued+counts.sending+counts.retry_wait,
|
|
594
553
|
states:counts
|
|
595
|
-
}
|
|
554
|
+
};
|
|
596
555
|
}
|
|
597
556
|
|
|
598
557
|
class MailOutbox{
|
|
@@ -602,14 +561,10 @@ class MailOutbox{
|
|
|
602
561
|
#drainPromise=null;
|
|
603
562
|
#enqueuePromise=null;
|
|
604
563
|
#isOnline;
|
|
605
|
-
#invalidRecords=
|
|
564
|
+
#invalidRecords=[];
|
|
606
565
|
#lastBackgroundError=null;
|
|
607
566
|
#lockManager;
|
|
608
567
|
#lockName;
|
|
609
|
-
#maxAttemptsPerDrain;
|
|
610
|
-
#maxInvalidRecords;
|
|
611
|
-
#maxRecords;
|
|
612
|
-
#maxReportBytes;
|
|
613
568
|
#onlineHandler;
|
|
614
569
|
#onlineController=null;
|
|
615
570
|
#onlineTarget;
|
|
@@ -629,10 +584,6 @@ class MailOutbox{
|
|
|
629
584
|
lockManager=undefined,
|
|
630
585
|
onlineTarget=typeof globalThis.addEventListener==='function'?globalThis:null,
|
|
631
586
|
onRecordCommitted=null,
|
|
632
|
-
maxAttemptsPerDrain=DEFAULT_MAX_ATTEMPTS_PER_DRAIN,
|
|
633
|
-
maxInvalidRecords=DEFAULT_MAX_INVALID_RECORDS,
|
|
634
|
-
maxRecords=DEFAULT_MAX_RECORDS,
|
|
635
|
-
maxReportBytes=DEFAULT_MAX_REPORT_BYTES,
|
|
636
587
|
quarantineTable=QUARANTINE_TABLE,
|
|
637
588
|
table=MAIL_OUTBOX_TABLE
|
|
638
589
|
}={}){
|
|
@@ -691,25 +642,6 @@ class MailOutbox{
|
|
|
691
642
|
this.#lockManager=resolvedLockManager;
|
|
692
643
|
this.#onlineTarget=onlineTarget;
|
|
693
644
|
this.#onRecordCommitted=onRecordCommitted;
|
|
694
|
-
this.#maxAttemptsPerDrain=positiveInteger(
|
|
695
|
-
maxAttemptsPerDrain,
|
|
696
|
-
'maxAttemptsPerDrain',
|
|
697
|
-
DEFAULT_MAX_ATTEMPTS_PER_DRAIN,
|
|
698
|
-
100
|
|
699
|
-
);
|
|
700
|
-
this.#maxInvalidRecords=positiveInteger(
|
|
701
|
-
maxInvalidRecords,
|
|
702
|
-
'maxInvalidRecords',
|
|
703
|
-
DEFAULT_MAX_INVALID_RECORDS,
|
|
704
|
-
10_000
|
|
705
|
-
);
|
|
706
|
-
this.#maxRecords=positiveInteger(maxRecords,'maxRecords',DEFAULT_MAX_RECORDS,10_000);
|
|
707
|
-
this.#maxReportBytes=positiveInteger(
|
|
708
|
-
maxReportBytes,
|
|
709
|
-
'maxReportBytes',
|
|
710
|
-
DEFAULT_MAX_REPORT_BYTES,
|
|
711
|
-
25*1024*1024
|
|
712
|
-
);
|
|
713
645
|
this.#quarantineTable=quarantineTable;
|
|
714
646
|
this.#table=table;
|
|
715
647
|
this.#lockName=`arcane-mail-outbox:${encodeURIComponent(table)}`;
|
|
@@ -762,8 +694,8 @@ class MailOutbox{
|
|
|
762
694
|
async #withExclusiveLock(lockName,operation,signal=null){
|
|
763
695
|
let acquired=false;
|
|
764
696
|
const options=signal
|
|
765
|
-
?
|
|
766
|
-
:
|
|
697
|
+
?{mode:'exclusive',signal}
|
|
698
|
+
:{mode:'exclusive'};
|
|
767
699
|
try{
|
|
768
700
|
return await this.#lockManager.request(
|
|
769
701
|
lockName,
|
|
@@ -848,11 +780,9 @@ class MailOutbox{
|
|
|
848
780
|
|
|
849
781
|
async #inventory(){
|
|
850
782
|
const keys=await this.#keys();
|
|
851
|
-
const scanLimit=this.#maxRecords+this.#maxInvalidRecords;
|
|
852
|
-
const scannedKeys=keys.slice(0,scanLimit);
|
|
853
783
|
const records=[];
|
|
854
784
|
const invalidRecords=[];
|
|
855
|
-
for(const fileName of
|
|
785
|
+
for(const fileName of keys){
|
|
856
786
|
try{
|
|
857
787
|
const key=reportKeyFromFileName(fileName);
|
|
858
788
|
const value=await this.#readRawFile(fileName);
|
|
@@ -865,14 +795,12 @@ class MailOutbox{
|
|
|
865
795
|
records.sort(function sortMailOutboxRecords(left,right){
|
|
866
796
|
return left.createdAt-right.createdAt||left.reportKey.localeCompare(right.reportKey,'en');
|
|
867
797
|
});
|
|
868
|
-
const inventory=
|
|
798
|
+
const inventory={
|
|
869
799
|
records,
|
|
870
800
|
invalidRecords,
|
|
871
801
|
totalFiles:keys.length,
|
|
872
|
-
scannedFiles:
|
|
873
|
-
|
|
874
|
-
overCapacity:records.length>this.#maxRecords
|
|
875
|
-
});
|
|
802
|
+
scannedFiles:keys.length
|
|
803
|
+
};
|
|
876
804
|
this.#invalidRecords=inventory.invalidRecords;
|
|
877
805
|
return inventory;
|
|
878
806
|
}
|
|
@@ -886,7 +814,7 @@ class MailOutbox{
|
|
|
886
814
|
);
|
|
887
815
|
if(!target){
|
|
888
816
|
fail(
|
|
889
|
-
'Mail outbox invalid record was not found in the
|
|
817
|
+
'Mail outbox invalid record was not found in the inventory.',
|
|
890
818
|
'MAIL_OUTBOX_INVALID_RECORD_NOT_FOUND'
|
|
891
819
|
);
|
|
892
820
|
}
|
|
@@ -907,10 +835,10 @@ class MailOutbox{
|
|
|
907
835
|
normalizedRecord(value,key);
|
|
908
836
|
}catch(error){
|
|
909
837
|
if(!structuralRecordError(error)) throw error;
|
|
910
|
-
return
|
|
838
|
+
return {
|
|
911
839
|
metadata:invalidRecordMetadata(fileName,error),
|
|
912
840
|
value
|
|
913
|
-
}
|
|
841
|
+
};
|
|
914
842
|
}
|
|
915
843
|
fail(
|
|
916
844
|
'Mail outbox invalid record changed before maintenance.',
|
|
@@ -945,7 +873,7 @@ class MailOutbox{
|
|
|
945
873
|
const current=await this.#inspectInvalidFile(target.fileName);
|
|
946
874
|
await this.#deleteInvalidFile(current.metadata.fileName);
|
|
947
875
|
await this.#inventory();
|
|
948
|
-
return
|
|
876
|
+
return {...current.metadata,deleted:true};
|
|
949
877
|
}
|
|
950
878
|
|
|
951
879
|
async #repairConfirmedInvalid(target,replacement){
|
|
@@ -965,21 +893,21 @@ class MailOutbox{
|
|
|
965
893
|
|
|
966
894
|
async #quarantineConfirmedInvalid(target){
|
|
967
895
|
const current=await this.#inspectInvalidFile(target.fileName);
|
|
968
|
-
const snapshot=quarantineSnapshot(current.value
|
|
896
|
+
const snapshot=quarantineSnapshot(current.value);
|
|
969
897
|
if(snapshot===null){
|
|
970
898
|
fail(
|
|
971
899
|
'Mail outbox cannot capture the invalid record for quarantine.',
|
|
972
900
|
'MAIL_OUTBOX_QUARANTINE_SNAPSHOT_UNAVAILABLE'
|
|
973
901
|
);
|
|
974
902
|
}
|
|
975
|
-
const entry=
|
|
903
|
+
const entry={
|
|
976
904
|
protocol:QUARANTINE_PROTOCOL,
|
|
977
905
|
sourceTable:this.#table,
|
|
978
906
|
sourceFileName:current.metadata.fileName,
|
|
979
907
|
quarantinedAt:this.#time(),
|
|
980
908
|
reasonCode:current.metadata.code,
|
|
981
909
|
serializedValue:snapshot
|
|
982
|
-
}
|
|
910
|
+
};
|
|
983
911
|
try{
|
|
984
912
|
await this.#storage.set(
|
|
985
913
|
this.#quarantineTable,
|
|
@@ -995,7 +923,7 @@ class MailOutbox{
|
|
|
995
923
|
);
|
|
996
924
|
}
|
|
997
925
|
await this.#deleteInvalidFile(current.metadata.fileName);
|
|
998
|
-
return
|
|
926
|
+
return {...current.metadata,quarantined:true};
|
|
999
927
|
}
|
|
1000
928
|
|
|
1001
929
|
async get(key){
|
|
@@ -1037,17 +965,11 @@ class MailOutbox{
|
|
|
1037
965
|
);
|
|
1038
966
|
}
|
|
1039
967
|
|
|
1040
|
-
async quarantineInvalid(
|
|
968
|
+
async quarantineInvalid(){
|
|
1041
969
|
await this.#ready();
|
|
1042
970
|
this.#assertDeleteAvailable();
|
|
1043
|
-
const batchLimit=positiveInteger(
|
|
1044
|
-
limit,
|
|
1045
|
-
'Mail outbox quarantine limit',
|
|
1046
|
-
MAX_MAINTENANCE_BATCH,
|
|
1047
|
-
MAX_MAINTENANCE_BATCH
|
|
1048
|
-
);
|
|
1049
971
|
const inventory=await this.#inventory();
|
|
1050
|
-
const targets=inventory.invalidRecords
|
|
972
|
+
const targets=inventory.invalidRecords;
|
|
1051
973
|
const quarantined=[];
|
|
1052
974
|
for(const target of targets){
|
|
1053
975
|
const result=await this.#withTableMutation(
|
|
@@ -1056,11 +978,10 @@ class MailOutbox{
|
|
|
1056
978
|
quarantined.push(result);
|
|
1057
979
|
}
|
|
1058
980
|
const remaining=await this.#inventory();
|
|
1059
|
-
return
|
|
981
|
+
return {
|
|
1060
982
|
quarantined,
|
|
1061
|
-
remainingInvalidRecords:remaining.invalidRecords.length
|
|
1062
|
-
|
|
1063
|
-
});
|
|
983
|
+
remainingInvalidRecords:remaining.invalidRecords.length
|
|
984
|
+
};
|
|
1064
985
|
}
|
|
1065
986
|
|
|
1066
987
|
async #persistEnqueueLocked({serializedReport,key,signal}){
|
|
@@ -1075,21 +996,7 @@ class MailOutbox{
|
|
|
1075
996
|
}
|
|
1076
997
|
return existing;
|
|
1077
998
|
}
|
|
1078
|
-
|
|
1079
|
-
if(inventory.records.length>=this.#maxRecords){
|
|
1080
|
-
fail(
|
|
1081
|
-
`Mail outbox cannot exceed ${this.#maxRecords} records.`,
|
|
1082
|
-
'MAIL_OUTBOX_CAPACITY_EXCEEDED',
|
|
1083
|
-
RangeError
|
|
1084
|
-
);
|
|
1085
|
-
}
|
|
1086
|
-
const scanLimit=this.#maxRecords+this.#maxInvalidRecords;
|
|
1087
|
-
if(inventory.truncated||inventory.totalFiles>=scanLimit){
|
|
1088
|
-
fail(
|
|
1089
|
-
'Mail outbox inventory requires bounded invalid-record maintenance before enqueue.',
|
|
1090
|
-
'MAIL_OUTBOX_MAINTENANCE_REQUIRED'
|
|
1091
|
-
);
|
|
1092
|
-
}
|
|
999
|
+
await this.#inventory();
|
|
1093
1000
|
const now=this.#time();
|
|
1094
1001
|
const queued=normalizedRecord({
|
|
1095
1002
|
protocol:MAIL_OUTBOX_PROTOCOL,
|
|
@@ -1124,7 +1031,7 @@ class MailOutbox{
|
|
|
1124
1031
|
const resolvedSignal=validateSignal(signal);
|
|
1125
1032
|
throwIfAborted(resolvedSignal);
|
|
1126
1033
|
const normalizedKey=reportKey(key);
|
|
1127
|
-
const serializedReport=serializeReport(report
|
|
1034
|
+
const serializedReport=serializeReport(report);
|
|
1128
1035
|
const previous=this.#enqueuePromise;
|
|
1129
1036
|
const operation=(previous??Promise.resolve()).catch(
|
|
1130
1037
|
function ignorePriorEnqueueFailure(){}
|
|
@@ -1155,13 +1062,13 @@ class MailOutbox{
|
|
|
1155
1062
|
|
|
1156
1063
|
async #recoverSending(record,now){
|
|
1157
1064
|
const expiresAt=record.firstAttemptAt+MAIL_OUTBOX_IDEMPOTENCY_WINDOW_MS;
|
|
1158
|
-
const failure=
|
|
1065
|
+
const failure={
|
|
1159
1066
|
code:'MAIL_OUTBOX_INTERRUPTED_ATTEMPT',
|
|
1160
1067
|
statusCode:null,
|
|
1161
1068
|
retryable:true,
|
|
1162
1069
|
uncertain:true,
|
|
1163
1070
|
retryAfterSeconds:null
|
|
1164
|
-
}
|
|
1071
|
+
};
|
|
1165
1072
|
if(now>=expiresAt){
|
|
1166
1073
|
return this.#write(updatedRecord(record,{
|
|
1167
1074
|
state:'reconciliation_required',
|
|
@@ -1189,7 +1096,7 @@ class MailOutbox{
|
|
|
1189
1096
|
state:uncertain?'reconciliation_required':'failed',
|
|
1190
1097
|
updatedAt:Math.max(now,record.updatedAt),
|
|
1191
1098
|
nextAttemptAt:null,
|
|
1192
|
-
failure:
|
|
1099
|
+
failure:{
|
|
1193
1100
|
code:uncertain
|
|
1194
1101
|
?'MAIL_OUTBOX_RECONCILIATION_REQUIRED'
|
|
1195
1102
|
:'MAIL_OUTBOX_RETRY_WINDOW_EXPIRED',
|
|
@@ -1197,7 +1104,7 @@ class MailOutbox{
|
|
|
1197
1104
|
retryable:false,
|
|
1198
1105
|
uncertain,
|
|
1199
1106
|
retryAfterSeconds:null
|
|
1200
|
-
}
|
|
1107
|
+
}
|
|
1201
1108
|
}));
|
|
1202
1109
|
}
|
|
1203
1110
|
|
|
@@ -1213,14 +1120,14 @@ class MailOutbox{
|
|
|
1213
1120
|
updatedAt:Math.max(now,record.updatedAt),
|
|
1214
1121
|
nextAttemptAt:null,
|
|
1215
1122
|
result:outcome.result,
|
|
1216
|
-
failure:
|
|
1123
|
+
failure:{
|
|
1217
1124
|
...outcome.failure,
|
|
1218
1125
|
code:uncertain
|
|
1219
1126
|
?'MAIL_OUTBOX_RECONCILIATION_REQUIRED'
|
|
1220
1127
|
:'MAIL_OUTBOX_RETRY_WINDOW_EXPIRED',
|
|
1221
1128
|
retryable:false,
|
|
1222
1129
|
retryAfterSeconds:null
|
|
1223
|
-
}
|
|
1130
|
+
}
|
|
1224
1131
|
}));
|
|
1225
1132
|
}
|
|
1226
1133
|
return this.#write(updatedRecord(record,{
|
|
@@ -1297,9 +1204,8 @@ class MailOutbox{
|
|
|
1297
1204
|
throwIfAborted(signal);
|
|
1298
1205
|
const inventory=await this.#inventory();
|
|
1299
1206
|
const records=[...inventory.records];
|
|
1300
|
-
const invalidRecords=inventory.invalidRecords
|
|
1207
|
+
const invalidRecords=inventory.invalidRecords;
|
|
1301
1208
|
let attempted=0;
|
|
1302
|
-
let bounded=inventory.truncated||inventory.overCapacity;
|
|
1303
1209
|
const online=this.#online();
|
|
1304
1210
|
for(let index=0;index<records.length;index+=1){
|
|
1305
1211
|
throwIfAborted(signal);
|
|
@@ -1318,15 +1224,11 @@ class MailOutbox{
|
|
|
1318
1224
|
continue;
|
|
1319
1225
|
}
|
|
1320
1226
|
if(!online||!this.#online()) continue;
|
|
1321
|
-
if(attempted>=this.#maxAttemptsPerDrain){
|
|
1322
|
-
bounded=true;
|
|
1323
|
-
break;
|
|
1324
|
-
}
|
|
1325
1227
|
record=await this.#attempt(record,signal);
|
|
1326
1228
|
records[index]=record;
|
|
1327
1229
|
attempted+=1;
|
|
1328
1230
|
}
|
|
1329
|
-
return summary(reason,online,records,attempted,invalidRecords
|
|
1231
|
+
return summary(reason,online,records,attempted,invalidRecords);
|
|
1330
1232
|
}
|
|
1331
1233
|
|
|
1332
1234
|
async drain({reason='manual',signal=null}={}){
|