arcane-os 0.3.1 → 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.
Files changed (153) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +86 -117
  3. package/bin/arcane-test.mjs +170 -46
  4. package/browser-runtime/ai/browser-speech-artifacts.mjs +855 -895
  5. package/browser-runtime/ai/browser-speech-providers.mjs +80 -204
  6. package/browser-runtime/ai/browser-wasm-llm-provider.mjs +627 -819
  7. package/browser-runtime/ai/browser-wasm.mjs +24 -35
  8. package/browser-runtime/ai/browser-wllama-runtime.mjs +64 -316
  9. package/browser-runtime/ai/model-controller.mjs +584 -181
  10. package/browser-runtime/ai/speech-worker-client.mjs +8 -148
  11. package/browser-runtime/ai/speech-worker-runtime.mjs +642 -374
  12. package/browser-runtime/dom-event-instrumentation.mjs +55 -147
  13. package/browser-runtime/event-manager.mjs +239 -624
  14. package/package.json +5 -6
  15. package/runtime/arcane/components/app-bar.html +3 -15
  16. package/runtime/arcane/components/assistant-panel.html +10 -10
  17. package/runtime/arcane/components/calculator.html +1 -1
  18. package/runtime/arcane/components/chat.html +1359 -135
  19. package/runtime/arcane/components/conversation-view.html +2 -2
  20. package/runtime/arcane/components/document-inspector.html +11 -17
  21. package/runtime/arcane/components/file-manager.html +13 -56
  22. package/runtime/arcane/components/markdown-document.html +82 -281
  23. package/runtime/arcane/components/markdown-editor.html +7 -10
  24. package/runtime/arcane/components/media-embed.html +6 -6
  25. package/runtime/arcane/components/screen-capture.html +4 -4
  26. package/runtime/arcane/components/source-explanation.html +2 -2
  27. package/runtime/arcane/components/speech.html +112 -68
  28. package/runtime/arcane/components/terminal-workspace.html +4 -4
  29. package/runtime/arcane/components/theme-editor.html +1 -1
  30. package/runtime/arcane/components/unified-inbox.html +2 -2
  31. package/runtime/arcane/components/voice-transcription.html +31 -21
  32. package/runtime/arcane/entities/Calculation.js +2 -3
  33. package/runtime/arcane/entities/Chat.js +228 -43
  34. package/runtime/arcane/entities/Preference.js +3 -5
  35. package/runtime/arcane/entities/Weather.js +5 -5
  36. package/runtime/arcane/modules/AI.js +1042 -427
  37. package/runtime/arcane/modules/AIProviderRuntime.js +618 -359
  38. package/runtime/arcane/modules/AIResponseLength.js +9 -19
  39. package/runtime/arcane/modules/AIRuntimeState.js +109 -72
  40. package/runtime/arcane/modules/ArcaneNavigationPolicy.js +45 -32
  41. package/runtime/arcane/modules/BrowserTestSuite.js +78 -122
  42. package/runtime/arcane/modules/CalculatorEngine.js +9 -9
  43. package/runtime/arcane/modules/CommunicationAppController.js +3 -7
  44. package/runtime/arcane/modules/ComponentContracts.js +30 -32
  45. package/runtime/arcane/modules/ConfiguredAIChatSession.js +281 -230
  46. package/runtime/arcane/modules/ConversationActionItems.js +26 -59
  47. package/runtime/arcane/modules/ConversationClosingReport.js +34 -61
  48. package/runtime/arcane/modules/ConversationTimebox.js +27 -15
  49. package/runtime/arcane/modules/DBOPFSDocumentLibrary.js +152 -344
  50. package/runtime/arcane/modules/DocumentLexicalSearch.js +25 -91
  51. package/runtime/arcane/modules/HTMLImport.js +54 -1
  52. package/runtime/arcane/modules/IsolatedModelQuestionRunner.js +40 -203
  53. package/runtime/arcane/modules/LocalAIReadiness.js +40 -60
  54. package/runtime/arcane/modules/LocalAIReadinessController.js +15 -13
  55. package/runtime/arcane/modules/MD.js +1 -45
  56. package/runtime/arcane/modules/Mail.js +51 -103
  57. package/runtime/arcane/modules/MailOutbox.mjs +95 -193
  58. package/runtime/arcane/modules/MailTransport.mjs +36 -57
  59. package/runtime/arcane/modules/ModelDefinition.js +22 -106
  60. package/runtime/arcane/modules/OpenMeteoWeatherProvider.js +39 -101
  61. package/runtime/arcane/modules/PersistentAIChatSession.js +281 -18
  62. package/runtime/arcane/modules/PreferenceStore.js +102 -30
  63. package/runtime/arcane/modules/RiskSignalAnalyzer.js +8 -9
  64. package/runtime/arcane/modules/ScopedOPFSCache.js +7 -42
  65. package/runtime/arcane/modules/ScreenCapture.js +175 -128
  66. package/runtime/arcane/modules/SpeechPlayback.js +46 -149
  67. package/runtime/arcane/modules/StaticDocumentCatalog.js +173 -407
  68. package/runtime/arcane/modules/ToolCallRouter.js +25 -12
  69. package/runtime/arcane/modules/YouTubeMedia.js +6 -5
  70. package/schemas/arcane-app-bundle.schema.json +13 -78
  71. package/schemas/arcane-app.schema.json +9 -25
  72. package/schemas/arcane-lock.schema.json +18 -151
  73. package/schemas/arcane-package.schema.json +2 -16
  74. package/schemas/native-build-plan.schema.json +119 -122
  75. package/src/app-descriptor.mjs +75 -132
  76. package/src/application-tests.mjs +200 -0
  77. package/src/cli/main.mjs +27 -46
  78. package/src/constants.mjs +3 -4
  79. package/src/dev-server.mjs +30 -324
  80. package/src/doctor.mjs +92 -154
  81. package/src/dom-event-instrumentation.mjs +55 -147
  82. package/src/errors.mjs +2 -3
  83. package/src/event-manager.mjs +239 -624
  84. package/src/event-queue.mjs +3 -3
  85. package/src/import-map.mjs +273 -1028
  86. package/src/index.mjs +14 -16
  87. package/src/installed-sdk-runtime.mjs +27 -67
  88. package/src/integrated-provider-loader.mjs +53 -382
  89. package/src/mail-api.mjs +0 -2
  90. package/src/mail-server.mjs +224 -580
  91. package/src/mail.mjs +4 -10
  92. package/src/native-plan.mjs +163 -598
  93. package/src/native-provider-loader.mjs +104 -1063
  94. package/src/packager/core.mjs +485 -3229
  95. package/src/process.mjs +5 -10
  96. package/src/release-bundle.mjs +292 -2405
  97. package/src/runtime.mjs +76 -396
  98. package/src/scaffold.mjs +30 -80
  99. package/src/sdk-browser-runtime.mjs +70 -626
  100. package/src/source-server.mjs +588 -0
  101. package/src/targets/index.mjs +78 -188
  102. package/src/templates/workspace-template.mjs +19 -135
  103. package/src/testing-loader.mjs +164 -0
  104. package/src/testing.mjs +1 -1
  105. package/src/toolchain.mjs +131 -544
  106. package/src/update-check.mjs +26 -64
  107. package/src/workspace-operation-lock.mjs +139 -430
  108. package/src/workspace-runtime.mjs +109 -1558
  109. package/src/workspace.mjs +40 -302
  110. package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +0 -218
  111. package/browser-runtime/ai/ARCANE_AI_BROWSER_SPEECH_COMPONENTS.json +0 -203
  112. package/browser-runtime/ai/ARCANE_AI_BROWSER_WASM_COMPONENTS.json +0 -80
  113. package/browser-runtime/ai/internal/sha256.mjs +0 -166
  114. package/docs/architecture.md +0 -344
  115. package/docs/compatibility.md +0 -36
  116. package/docs/event-manager.md +0 -294
  117. package/docs/platform-targets.md +0 -108
  118. package/docs/publishing.md +0 -201
  119. package/docs/reference/README.md +0 -185
  120. package/docs/reference/ai/browser-speech-package-authority.json +0 -835
  121. package/docs/reference/ai/browser-speech.md +0 -1295
  122. package/docs/reference/ai/browser-wasm.md +0 -530
  123. package/docs/reference/arcane-ollama.md +0 -288
  124. package/docs/reference/availability-and-normalization.md +0 -183
  125. package/docs/reference/behavioral-testing.md +0 -133
  126. package/docs/reference/cli.md +0 -779
  127. package/docs/reference/core/README.md +0 -62
  128. package/docs/reference/core/arcane-ai-contracts.md +0 -907
  129. package/docs/reference/core/arcane-api.md +0 -601
  130. package/docs/reference/core/arcane-entities.md +0 -65
  131. package/docs/reference/core/arcane-events.md +0 -134
  132. package/docs/reference/core/ollama-module.md +0 -181
  133. package/docs/reference/core/reference/arcane-api/ai-and-ollama.md +0 -1909
  134. package/docs/reference/core/reference/arcane-api/applications-terminal-capabilities.md +0 -1057
  135. package/docs/reference/core/reference/arcane-api/core-and-events.md +0 -320
  136. package/docs/reference/core/reference/arcane-api/filesystem-storage-preferences-appearance.md +0 -610
  137. package/docs/reference/core/reference/arcane-api/namespaces.md +0 -1157
  138. package/docs/reference/core/reference/arcane-api/platform-installation-users-system.md +0 -1423
  139. package/docs/reference/core/reference/arcane-api/session-provisioning-diagnostics-development.md +0 -315
  140. package/docs/reference/event-manager.md +0 -1511
  141. package/docs/reference/inventory/package-api.json +0 -3284
  142. package/docs/reference/inventory/runtime-components.json +0 -1011
  143. package/docs/reference/inventory/runtime-entities.json +0 -26
  144. package/docs/reference/inventory/runtime-modules.json +0 -1431
  145. package/docs/reference/mail.md +0 -316
  146. package/docs/reference/protocols.md +0 -719
  147. package/docs/reference/runtime-components.md +0 -1366
  148. package/docs/reference/runtime-entities.md +0 -303
  149. package/docs/reference/runtime-modules.md +0 -2965
  150. package/docs/reference/sdk-api.md +0 -6698
  151. package/docs/roadmap.md +0 -79
  152. package/docs/work-amplification.md +0 -129
  153. 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=Object.freeze([
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 ACCEPTANCE_AUTHORITY_SET=new Set(MAIL_OUTBOX_ACCEPTANCE_AUTHORITIES);
18
- const REPORT_KEY_PATTERN=/^[A-Za-z0-9._:-]{8,128}$/;
19
- const SAFE_CODE_PATTERN=/^[A-Za-z0-9._:-]{1,128}$/;
20
- const SAFE_ID_PATTERN=/^[A-Za-z0-9._:-]{1,256}$/;
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||seconds>MAX_RETRY_AFTER_SECONDS){
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,maxReportBytes){
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,maxReportBytes){
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 8-128 safe characters.',
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 Object.freeze({
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 Object.freeze({
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 Object.freeze({
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:safeString(value.providerStatus,SAFE_CODE_PATTERN),
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
- ||!hasAcceptedEvidence(record.result)){
311
- fail('Accepted mail outbox record lacks provider acceptance evidence.','MAIL_OUTBOX_RECORD_INVALID');
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 deepFreeze(record);
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=deepFreeze(parseSerializedReport(record.serializedReport));
381
- return Object.freeze({
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
- if(!hasAcceptedEvidence(result)){
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 Object.freeze({
370
+ return {
423
371
  kind:'retry',
424
372
  result,
425
- failure:Object.freeze({
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 Object.freeze({
386
+ return {
438
387
  kind:'retry',
439
388
  result,
440
- failure:Object.freeze({
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 Object.freeze({
403
+ return {
454
404
  kind:'failed',
455
405
  result,
456
- failure:Object.freeze({
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
- return Object.freeze({
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:Object.freeze({
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_-]{0,63}$/.test(value)){
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,bounded){
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 deepFreeze({
544
+ return {
587
545
  reason,
588
546
  online,
547
+ records:[...records],
589
548
  considered:records.length,
590
549
  attempted,
591
- invalidRecords,
592
- bounded,
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=Object.freeze([]);
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
- ?Object.freeze({mode:'exclusive',signal})
766
- :TABLE_LOCK_OPTIONS;
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 scannedKeys){
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=deepFreeze({
798
+ const inventory={
869
799
  records,
870
800
  invalidRecords,
871
801
  totalFiles:keys.length,
872
- scannedFiles:scannedKeys.length,
873
- truncated:keys.length>scannedKeys.length,
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 bounded inventory.',
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 Object.freeze({
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 Object.freeze({...current.metadata,deleted:true});
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,this.#maxReportBytes);
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=Object.freeze({
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 Object.freeze({...current.metadata,quarantined:true});
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({limit=MAX_MAINTENANCE_BATCH}={}){
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.slice(0,batchLimit);
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 deepFreeze({
981
+ return {
1060
982
  quarantined,
1061
- remainingInvalidRecords:remaining.invalidRecords.length,
1062
- truncated:remaining.truncated
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
- const inventory=await this.#inventory();
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,this.#maxReportBytes);
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=Object.freeze({
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:Object.freeze({
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:Object.freeze({
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.length;
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,bounded);
1231
+ return summary(reason,online,records,attempted,invalidRecords);
1330
1232
  }
1331
1233
 
1332
1234
  async drain({reason='manual',signal=null}={}){