arcane-os 0.3.4 → 0.3.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.
Files changed (35) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +7 -7
  3. package/browser-runtime/ai/browser-wasm-llm-provider.mjs +315 -119
  4. package/browser-runtime/ai/model-controller.mjs +439 -95
  5. package/package.json +1 -1
  6. package/runtime/arcane/components/assistant-panel.html +2 -1
  7. package/runtime/arcane/components/chat.html +469 -220
  8. package/runtime/arcane/components/speech.html +33 -13
  9. package/runtime/arcane/components/voice-transcription.html +27 -3
  10. package/runtime/arcane/entities/Chat.js +165 -97
  11. package/runtime/arcane/entities/IntentEnvelope.js +52 -118
  12. package/runtime/arcane/entities/TWiNPolicyDecision.js +33 -130
  13. package/runtime/arcane/entities/User.js +38 -44
  14. package/runtime/arcane/modules/AI.js +569 -302
  15. package/runtime/arcane/modules/AIProviderRuntime.js +776 -151
  16. package/runtime/arcane/modules/AIRuntimeState.js +22 -24
  17. package/runtime/arcane/modules/ApiModelDatabase.js +54 -48
  18. package/runtime/arcane/modules/CaseEvidenceIndexer.js +6 -10
  19. package/runtime/arcane/modules/CommunicationHub.js +90 -94
  20. package/runtime/arcane/modules/ComponentContracts.js +23 -9
  21. package/runtime/arcane/modules/ConfiguredAIChatSession.js +77 -77
  22. package/runtime/arcane/modules/ConversationTimebox.js +76 -104
  23. package/runtime/arcane/modules/DBLS.js +14 -12
  24. package/runtime/arcane/modules/DBOPFS.js +20 -15
  25. package/runtime/arcane/modules/Errors.js +196 -436
  26. package/runtime/arcane/modules/HTMLImport.js +49 -32
  27. package/runtime/arcane/modules/Ollama.js +16 -14
  28. package/runtime/arcane/modules/PersistentAIChatSession.js +174 -93
  29. package/runtime/arcane/modules/RecordReviewStore.js +40 -35
  30. package/runtime/arcane/modules/TerminalClient.js +12 -14
  31. package/runtime/arcane/modules/ThemeBootstrap.js +7 -7
  32. package/runtime/arcane/modules/ThemeManager.js +5 -5
  33. package/runtime/arcane/modules/TimeGuard.js +5 -65
  34. package/runtime/arcane/modules/WaitForComponent.js +43 -40
  35. package/src/installed-sdk-runtime.mjs +11 -1
@@ -6,12 +6,20 @@ import ConfiguredAIChatSession,{
6
6
  const SESSION_MANAGED_REQUEST_FIELDS=new Set([
7
7
  'messages',
8
8
  'onChunk',
9
+ 'onDataChunk',
10
+ 'onDataResult',
9
11
  'onResponse',
10
12
  'onToolCall',
11
13
  'signal',
12
14
  'stream',
13
15
  ]);
14
- const PROVIDER_LIFECYCLE_FIELDS=new Set(['onChunk','onResponse','onToolCall']);
16
+ const PROVIDER_LIFECYCLE_FIELDS=new Set([
17
+ 'onChunk',
18
+ 'onDataChunk',
19
+ 'onDataResult',
20
+ 'onResponse',
21
+ 'onToolCall'
22
+ ]);
15
23
 
16
24
  function coded(error,code){
17
25
  if(!error.code) error.code=code;
@@ -19,10 +27,9 @@ function coded(error,code){
19
27
  }
20
28
 
21
29
  function isPlainRecord(value){
22
- return Boolean(value)
23
- &&typeof value==='object'
24
- &&!Array.isArray(value)
25
- &&Object.getPrototypeOf(value)===Object.prototype;
30
+ if(!value||typeof value!=='object'||Array.isArray(value))return false;
31
+ const prototype=Object.getPrototypeOf(value);
32
+ return prototype===Object.prototype||prototype===null;
26
33
  }
27
34
 
28
35
  function assertKnownKeys(value,allowed,label){
@@ -71,14 +78,20 @@ function configuredAIChat(ai){
71
78
  function normalizeStreamHandlers(value){
72
79
  if(value===undefined) return {};
73
80
  if(!isPlainRecord(value)) throw new TypeError('Persistent chat stream handlers must be a plain object.');
74
- assertKnownKeys(value,new Set(['onChunk','onToolCall']),'Persistent chat stream handlers');
75
- for(const key of ['onChunk','onToolCall']){
81
+ assertKnownKeys(
82
+ value,
83
+ new Set(['onChunk','onDataChunk','onDataResult','onToolCall']),
84
+ 'Persistent chat stream handlers',
85
+ );
86
+ for(const key of ['onChunk','onDataChunk','onDataResult','onToolCall']){
76
87
  if(value[key]!==undefined&&typeof value[key]!=='function'){
77
88
  throw new TypeError(`${key} must be a function when provided.`);
78
89
  }
79
90
  }
80
91
  return {
81
92
  onChunk:value.onChunk??function ignorePersistentChatChunk(){},
93
+ onDataChunk:value.onDataChunk??function ignorePersistentChatDataChunk(){},
94
+ onDataResult:value.onDataResult??function ignorePersistentChatDataResult(){},
82
95
  onToolCall:value.onToolCall??function ignorePersistentChatToolCall(){},
83
96
  };
84
97
  }
@@ -95,7 +108,7 @@ function normalizeStreamResponse(terminal,output){
95
108
  );
96
109
  }
97
110
 
98
- function terminalStructuralToolCall(response){
111
+ function terminalStructuralToolCalls(response){
99
112
  const hasMessage=Object.hasOwn(response,'message');
100
113
  const hasChoices=Object.hasOwn(response,'choices');
101
114
  if(hasMessage&&hasChoices){
@@ -120,49 +133,97 @@ function terminalStructuralToolCall(response){
120
133
  );
121
134
  }
122
135
  for(let callIndex=0;callIndex<value.length;callIndex++){
123
- calls.push(normalizeStructuralToolCall(
136
+ const normalized=normalizeStructuralToolCall(
124
137
  value[callIndex],
125
138
  `The terminal structural tool call ${messageIndex+1}.${callIndex+1}`,
126
- ));
139
+ );
140
+ if(messageIndex===0) calls.push(normalized);
127
141
  }
128
142
  }
129
- if(calls.length>1){
130
- throw coded(
131
- new TypeError('The AI stream terminal response contains parallel structural tool calls.'),
132
- 'AI_CHAT_PARALLEL_TOOLS_UNSUPPORTED',
133
- );
143
+ return calls;
144
+ }
145
+
146
+ function sameDataValue(left,right,seen=new Map()){
147
+ if(Object.is(left,right)) return true;
148
+ if(!left||!right||typeof left!=='object'||typeof right!=='object') return false;
149
+ if(Array.isArray(left)!==Array.isArray(right)) return false;
150
+ const matched=seen.get(left);
151
+ if(matched!==undefined) return matched===right;
152
+ seen.set(left,right);
153
+ if(Array.isArray(left)){
154
+ return left.length===right.length
155
+ &&left.every((value,index)=>sameDataValue(value,right[index],seen));
134
156
  }
135
- return calls[0]??null;
157
+ if(!isPlainRecord(left)||!isPlainRecord(right)) return false;
158
+ const leftKeys=Reflect.ownKeys(left).filter(key=>Object.prototype.propertyIsEnumerable.call(left,key));
159
+ const rightKeys=Reflect.ownKeys(right).filter(key=>Object.prototype.propertyIsEnumerable.call(right,key));
160
+ return leftKeys.length===rightKeys.length
161
+ &&leftKeys.every(key=>Object.prototype.propertyIsEnumerable.call(right,key)
162
+ &&sameDataValue(left[key],right[key],seen));
136
163
  }
137
164
 
138
165
  function sameStructuralToolCall(left,right){
139
- return Boolean(left&&right)
140
- &&left.id===right.id
141
- &&left.type===right.type
142
- &&left.function.name===right.function.name
143
- &&left.function.arguments===right.function.arguments;
166
+ return sameDataValue(left,right);
167
+ }
168
+
169
+ function sameStructuralToolCalls(left,right){
170
+ return Array.isArray(left)
171
+ &&Array.isArray(right)
172
+ &&left.length===right.length
173
+ &&left.every((call,index)=>sameStructuralToolCall(call,right[index]));
144
174
  }
145
175
 
146
176
  function normalizeSend(input){
147
177
  if(!isPlainRecord(input)) throw new TypeError('Persistent chat input must be a plain object.');
148
- assertKnownKeys(input,new Set(['message','request','response','signal']),'Persistent chat input');
149
- if(!isPlainRecord(input.message)) throw new TypeError('message must be a plain object.');
150
- assertKnownKeys(input.message,new Set(['content','persist','role','tool_call_id']),'message');
151
- if(typeof input.message.content!=='string'||!input.message.content.trim()){
152
- throw new TypeError('message.content must contain text.');
178
+ assertKnownKeys(input,new Set(['message','messages','request','response','signal']),'Persistent chat input');
179
+ const hasMessage=Object.hasOwn(input,'message');
180
+ const hasMessages=Object.hasOwn(input,'messages');
181
+ if(hasMessage===hasMessages){
182
+ throw new TypeError('Persistent chat input must contain exactly one message or messages field.');
183
+ }
184
+ const sourceMessages=hasMessages?input.messages:[input.message];
185
+ if(!Array.isArray(sourceMessages)||!sourceMessages.length){
186
+ throw new TypeError('messages must be a nonempty array of tool-result messages.');
153
187
  }
154
- const role=input.message.role??'user';
155
- if(!['tool','user'].includes(role)) throw new TypeError('message.role must be user or tool.');
156
- let toolCallId=null;
157
- if(role==='tool'){
158
- if(typeof input.message.tool_call_id!=='string'||!input.message.tool_call_id.trim()){
159
- throw new TypeError('message.tool_call_id is required for tool messages.');
188
+ const normalizedMessages=sourceMessages.map((value,index)=>{
189
+ const label=hasMessages?`messages[${index}]`:'message';
190
+ if(!isPlainRecord(value)) throw new TypeError(`${label} must be a plain object.`);
191
+ if(typeof value.content!=='string'||!value.content.trim()){
192
+ throw new TypeError(`${label}.content must contain text.`);
193
+ }
194
+ const role=value.role??'user';
195
+ if(!['tool','user'].includes(role)) throw new TypeError(`${label}.role must be user or tool.`);
196
+ if(hasMessages&&role!=='tool'){
197
+ throw new TypeError('messages accepts only tool-result messages.');
198
+ }
199
+ let toolCallId=null;
200
+ if(role==='tool'){
201
+ if(typeof value.tool_call_id!=='string'||!value.tool_call_id.trim()){
202
+ throw new TypeError(`${label}.tool_call_id is required for tool messages.`);
203
+ }
204
+ toolCallId=value.tool_call_id;
205
+ }else if(value.tool_call_id!==undefined){
206
+ throw new TypeError(`${label}.tool_call_id is supported only for tool messages.`);
160
207
  }
161
- toolCallId=input.message.tool_call_id;
162
- }else if(input.message.tool_call_id!==undefined){
163
- throw new TypeError('message.tool_call_id is supported only for tool messages.');
208
+ const completeMessage={...value};
209
+ delete completeMessage.persist;
210
+ return {
211
+ persist:boolean(value.persist,`${label}.persist`,true),
212
+ message:{
213
+ ...completeMessage,
214
+ content:value.content,
215
+ role,
216
+ ...(toolCallId?{tool_call_id:toolCallId}:{})
217
+ }
218
+ };
219
+ });
220
+ const messagePersist=normalizedMessages[0].persist;
221
+ if(normalizedMessages.some(item=>item.persist!==messagePersist)){
222
+ throw coded(
223
+ new TypeError('Every message in one tool-result batch must use the same persistence choice.'),
224
+ 'AI_CHAT_INCOHERENT_PERSISTENCE',
225
+ );
164
226
  }
165
- const messagePersist=boolean(input.message.persist,'message.persist',true);
166
227
  const response=input.response??{};
167
228
  if(!isPlainRecord(response)) throw new TypeError('response must be a plain object.');
168
229
  assertKnownKeys(response,new Set(['persist']),'response');
@@ -184,11 +245,7 @@ function normalizeSend(input){
184
245
  if(!signalLike(input.signal)) throw new TypeError('signal must be an AbortSignal.');
185
246
  return {
186
247
  messagePersist,
187
- requestMessage:{
188
- content:input.message.content,
189
- role,
190
- ...(toolCallId?{tool_call_id:toolCallId}:{}),
191
- },
248
+ requestMessages:normalizedMessages.map(item=>item.message),
192
249
  responsePersist,
193
250
  request:{...request},
194
251
  signal:input.signal??null,
@@ -196,14 +253,14 @@ function normalizeSend(input){
196
253
  }
197
254
 
198
255
  function fileName(value){
199
- if(typeof value!=='string'||!/^[A-Za-z0-9][A-Za-z0-9._-]{0,255}\.jsonl$/.test(value)){
200
- throw new TypeError('chatFileName must be a safe .jsonl file name.');
256
+ if(typeof value!=='string'||value.length===0){
257
+ throw new TypeError('chatFileName must be a nonempty string.');
201
258
  }
202
259
  return value;
203
260
  }
204
261
 
205
262
  /**
206
- * Composes the bounded configured chat session with one automatically selected
263
+ * Composes the configured chat session with one automatically selected
207
264
  * ChatEntity. Request-only context is delegated to ConfiguredAIChatSession;
208
265
  * per-turn persistence affects DBOPFS and memory, never the live model context.
209
266
  */
@@ -212,6 +269,7 @@ class PersistentAIChatSession{
212
269
  #configured=null;
213
270
  #entity;
214
271
  #fetchChat;
272
+ #historyError=null;
215
273
  #memory;
216
274
  #options;
217
275
  #pending=false;
@@ -258,20 +316,10 @@ class PersistentAIChatSession{
258
316
  if(managedRequestField){
259
317
  throw new TypeError(`request.${managedRequestField} is managed by the chat session.`);
260
318
  }
261
- if(
262
- request.parallelToolCalls===true
263
- ||request.parallel_tool_calls===true
264
- ){
265
- throw coded(
266
- new TypeError('Persistent chat supports one structural tool call at a time.'),
267
- 'AI_CHAT_PARALLEL_TOOLS_UNSUPPORTED',
268
- );
269
- }
270
- if(
271
- request.parallelToolCalls===undefined
272
- &&request.parallel_tool_calls===undefined
273
- ){
274
- request.parallelToolCalls=false;
319
+ for(const key of ['parallelToolCalls','parallel_tool_calls']){
320
+ if(Object.hasOwn(request,key)&&typeof request[key]!=='boolean'){
321
+ throw new TypeError(`request.${key} must be a boolean when provided.`);
322
+ }
275
323
  }
276
324
  const systemPrompt=options.systemPrompt??'';
277
325
  if(typeof systemPrompt!=='string') throw new TypeError('systemPrompt must be a string.');
@@ -307,56 +355,70 @@ class PersistentAIChatSession{
307
355
  const providerRequest=providerRequestWithoutLifecycleCallbacks(request);
308
356
  if(!this.#activeStream) return this.#fetchChat(providerRequest);
309
357
  if(typeof this.#options.ai?.streamRequest!=='function'){
310
- return this.#fetchChat(providerRequest);
358
+ const response=await this.#fetchChat(providerRequest);
359
+ await this.#activeStream.onDataResult(response,providerRequest.id??null);
360
+ return response;
311
361
  }
312
362
  const activeStream=this.#activeStream;
313
363
  let terminal=null;
314
- let streamedToolCall=null;
315
- let streamedToolDetails=[];
364
+ const streamedToolCalls=[];
365
+ const streamedToolDetails=[];
316
366
  const output=await this.#options.ai.streamRequest({
317
367
  ...providerRequest,
318
368
  onChunk:activeStream.onChunk,
369
+ onDataChunk:activeStream.onDataChunk,
370
+ onDataResult:activeStream.onDataResult,
319
371
  onToolCall:function retainStructuralToolCall(call,...details){
320
372
  const normalized=normalizeStructuralToolCall(
321
373
  call,
322
374
  'The streamed structural tool call',
323
375
  );
324
- if(streamedToolCall&&!sameStructuralToolCall(streamedToolCall,normalized)){
376
+ if(streamedToolCalls.some(call=>call.id===normalized.id)){
325
377
  throw coded(
326
- new TypeError('The AI stream emitted divergent structural tool calls.'),
378
+ new TypeError('The AI stream emitted a duplicate structural tool call.'),
327
379
  'AI_CHAT_STREAM_TOOL_CALL_MISMATCH',
328
380
  );
329
381
  }
330
- streamedToolCall=normalized;
331
- streamedToolDetails=details;
382
+ streamedToolCalls.push(normalized);
383
+ streamedToolDetails.push(details);
332
384
  },
333
385
  onResponse:async function retainPersistentChatTerminal(response){
334
386
  terminal=response;
335
387
  },
336
388
  });
337
389
  const response=normalizeStreamResponse(terminal,output);
338
- const terminalToolCall=terminalStructuralToolCall(response);
339
- if(streamedToolCall&&!sameStructuralToolCall(streamedToolCall,terminalToolCall)){
390
+ const terminalToolCalls=terminalStructuralToolCalls(response);
391
+ if(streamedToolCalls.length&&!sameStructuralToolCalls(streamedToolCalls,terminalToolCalls)){
340
392
  throw coded(
341
393
  new TypeError(
342
- 'The AI stream terminal response omitted or changed its structural tool call.'
394
+ 'The AI stream terminal response omitted, reordered, or changed its structural tool calls.'
343
395
  ),
344
396
  'AI_CHAT_STREAM_TOOL_CALL_MISMATCH',
345
397
  );
346
398
  }
347
- activeStream.streamedToolCall=streamedToolCall;
399
+ activeStream.streamedToolCalls=streamedToolCalls;
348
400
  activeStream.streamedToolDetails=streamedToolDetails;
349
- activeStream.terminalToolCall=terminalToolCall;
401
+ activeStream.terminalToolCalls=terminalToolCalls;
350
402
  return response;
351
403
  }
352
404
 
353
405
  async #initialize(){
354
- if(this.#options.loadExisting) await this.#entity.load();
355
- const storedMessages=this.#entity.messages;
406
+ let storedMessages=[];
407
+ try{
408
+ if(this.#options.loadExisting) await this.#entity.load();
409
+ storedMessages=this.#entity.messages;
410
+ }catch(error){
411
+ this.#historyError=coded(
412
+ error instanceof Error?error:new Error('The saved chat history is invalid.'),
413
+ 'AI_CHAT_INCOHERENT_PERSISTENCE',
414
+ );
415
+ console.error('Arcane saved chat history is readable but not actionable.',error);
416
+ }
356
417
  const storedSystem=storedMessages.find(message=>message.role==='system');
357
418
  const initialMessages=storedMessages
358
419
  .filter(message=>['user','assistant','tool'].includes(message.role))
359
420
  .map(message=>({
421
+ ...message,
360
422
  role:message.role,
361
423
  content:String(message.content??''),
362
424
  ...(message.role==='assistant'&&Object.hasOwn(message,'reasoning_content')
@@ -396,6 +458,7 @@ class PersistentAIChatSession{
396
458
 
397
459
  async history(){
398
460
  await this.ready();
461
+ if(this.#historyError)throw this.#historyError;
399
462
  return this.#configured.history();
400
463
  }
401
464
 
@@ -418,35 +481,49 @@ class PersistentAIChatSession{
418
481
  let prepared=null;
419
482
  try{
420
483
  await this.ready();
484
+ if(this.#historyError)throw this.#historyError;
421
485
  await this.#entity.settleMemory();
422
- if(settings.requestMessage.role==='user'&&this.#toolCallPersistence.size){
486
+ const requestMessage=settings.requestMessages[0];
487
+ if(requestMessage.role==='user'&&this.#toolCallPersistence.size){
423
488
  throw coded(
424
489
  new TypeError('The pending structural tool result must be supplied before a new user turn.'),
425
490
  'AI_CHAT_TOOL_RESULT_REQUIRED',
426
491
  );
427
492
  }
428
- if(settings.requestMessage.role==='tool'){
429
- const persistence=this.#toolCallPersistence.get(settings.requestMessage.tool_call_id);
430
- if(persistence===undefined){
431
- throw coded(new TypeError('The tool message has no pending structural tool call.'),'AI_CHAT_INVALID_TOOL_MESSAGE');
493
+ if(requestMessage.role==='tool'){
494
+ const submittedIds=new Set();
495
+ for(const message of settings.requestMessages){
496
+ const persistence=this.#toolCallPersistence.get(message.tool_call_id);
497
+ if(persistence===undefined||submittedIds.has(message.tool_call_id)){
498
+ throw coded(new TypeError('A tool message has no unique pending structural tool call.'),'AI_CHAT_INVALID_TOOL_MESSAGE');
499
+ }
500
+ if(persistence!==settings.messagePersist){
501
+ throw coded(
502
+ new TypeError('A tool result must use the persistence of its assistant tool call.'),
503
+ 'AI_CHAT_INCOHERENT_PERSISTENCE',
504
+ );
505
+ }
506
+ submittedIds.add(message.tool_call_id);
432
507
  }
433
- if(persistence!==settings.messagePersist){
508
+ if(submittedIds.size!==this.#toolCallPersistence.size){
434
509
  throw coded(
435
- new TypeError('A tool result must use the persistence of its assistant tool call.'),
436
- 'AI_CHAT_INCOHERENT_PERSISTENCE',
510
+ new TypeError('Every pending structural tool result must be supplied before provider continuation.'),
511
+ 'AI_CHAT_TOOL_RESULT_REQUIRED',
437
512
  );
438
513
  }
439
514
  }
440
515
  const streamState=streamHandlers?{
441
516
  ...streamHandlers,
442
- streamedToolCall:null,
517
+ streamedToolCalls:[],
443
518
  streamedToolDetails:[],
444
- terminalToolCall:null,
519
+ terminalToolCalls:[],
445
520
  }:null;
446
521
  if(streamState) this.#activeStream=streamState;
447
522
  try{
448
523
  prepared=await this.#configured.prepare(
449
- settings.requestMessage,
524
+ settings.requestMessages.length===1
525
+ ?settings.requestMessages[0]
526
+ :settings.requestMessages,
450
527
  {request:settings.request,signal:settings.signal},
451
528
  );
452
529
  }finally{
@@ -454,22 +531,22 @@ class PersistentAIChatSession{
454
531
  }
455
532
  const result=prepared.response;
456
533
  if(streamState){
457
- const validatedToolCall=result.message.tool_calls?.[0]??null;
534
+ const validatedToolCalls=result.message.tool_calls??[];
458
535
  if(
459
- streamState.terminalToolCall
460
- &&!sameStructuralToolCall(streamState.terminalToolCall,validatedToolCall)
536
+ streamState.terminalToolCalls.length
537
+ &&!sameStructuralToolCalls(streamState.terminalToolCalls,validatedToolCalls)
461
538
  ){
462
539
  throw coded(
463
540
  new TypeError(
464
- 'The validated AI response changed its terminal structural tool call.'
541
+ 'The validated AI response changed its terminal structural tool calls.'
465
542
  ),
466
543
  'AI_CHAT_STREAM_TOOL_CALL_MISMATCH',
467
544
  );
468
545
  }
469
- if(validatedToolCall){
546
+ for(let index=0;index<validatedToolCalls.length;index++){
470
547
  await streamState.onToolCall(
471
- validatedToolCall,
472
- ...streamState.streamedToolDetails,
548
+ validatedToolCalls[index],
549
+ ...(streamState.streamedToolDetails[index]??[]),
473
550
  );
474
551
  }
475
552
  }
@@ -483,12 +560,16 @@ class PersistentAIChatSession{
483
560
  })
484
561
  ),
485
562
  messagePersist:settings.messagePersist,
486
- requestMessage:settings.requestMessage,
563
+ ...(settings.requestMessages.length===1
564
+ ?{requestMessage:settings.requestMessages[0]}
565
+ :{requestMessages:settings.requestMessages}),
487
566
  responsePersist:settings.responsePersist,
488
567
  });
489
568
  const committed=prepared.commit();
490
- if(settings.requestMessage.role==='tool'){
491
- this.#toolCallPersistence.delete(settings.requestMessage.tool_call_id);
569
+ if(requestMessage.role==='tool'){
570
+ for(const message of settings.requestMessages){
571
+ this.#toolCallPersistence.delete(message.tool_call_id);
572
+ }
492
573
  }
493
574
  for(const call of result.message.tool_calls??[]){
494
575
  this.#toolCallPersistence.set(call.id,settings.responsePersist);
@@ -1,18 +1,18 @@
1
1
  import {createArcaneEventSource} from 'arcane-os/event-manager';
2
2
  import {resolveApplicationLocalStorageKey} from './AppDataScope.js';
3
3
 
4
- export const RECORD_REVIEW_STORE_ERROR_CODES=Object.freeze({
4
+ export const RECORD_REVIEW_STORE_ERROR_CODES={
5
5
  adapterInvalid:'ARCANE_RECORD_REVIEW_STORE_ADAPTER_INVALID',
6
6
  disposed:'ARCANE_RECORD_REVIEW_STORE_DISPOSED',
7
7
  operationAborted:'ARCANE_RECORD_REVIEW_STORE_OPERATION_ABORTED',
8
8
  operationOptionsInvalid:'ARCANE_RECORD_REVIEW_STORE_OPERATION_OPTIONS_INVALID',
9
9
  recordIdInvalid:'ARCANE_RECORD_REVIEW_ID_INVALID',
10
10
  storedRecordsInvalid:'ARCANE_RECORD_REVIEW_STORED_RECORDS_INVALID'
11
- });
11
+ };
12
12
 
13
- export const RECORD_REVIEW_STORE_EVENT_TYPES=Object.freeze({
13
+ export const RECORD_REVIEW_STORE_EVENT_TYPES={
14
14
  change:'record-review-change'
15
- });
15
+ };
16
16
 
17
17
  function recordReviewStoreError(code,reason,message,ErrorType=Error,cause){
18
18
  const error=new ErrorType(message);
@@ -85,7 +85,7 @@ function normalizeOperationOptions(value={}){
85
85
  TypeError
86
86
  );
87
87
  }
88
- return Object.freeze({signal});
88
+ return {signal};
89
89
  }
90
90
 
91
91
  function setDataProperty(target,key,value){
@@ -97,8 +97,8 @@ function setDataProperty(target,key,value){
97
97
  }
98
98
 
99
99
  function normalizeRecordId(value=''){
100
- const id=String(value).trim();
101
- if(!id||id.length>160||/[\x00-\x1f]/.test(id)){
100
+ const id=String(value);
101
+ if(!id.trim()||/[\x00-\x1f]/.test(id)){
102
102
  throw recordReviewStoreError(
103
103
  RECORD_REVIEW_STORE_ERROR_CODES.recordIdInvalid,
104
104
  'record-review-id-invalid',
@@ -113,39 +113,42 @@ function normalizeReview(value={}){
113
113
  const source=value&&typeof value==='object'?value:{};
114
114
  const attributes={};
115
115
  if(source.attributes&&typeof source.attributes==='object'&&!Array.isArray(source.attributes)){
116
- for(const [key,value] of Object.entries(source.attributes).slice(0,40)){
117
- const normalizedKey=String(key).replace(/[^a-z0-9._-]/gi,'-').slice(0,80);
118
- if(!normalizedKey) continue;
116
+ for(const [key,value] of Object.entries(source.attributes)){
117
+ const normalizedKey=String(key);
119
118
  setDataProperty(
120
119
  attributes,
121
120
  normalizedKey,
122
121
  Array.isArray(value)
123
- ?Object.freeze(value.slice(0,100).map(function normalizeReviewAttributeItem(item){
124
- return String(item).slice(0,500);
125
- }))
126
- :String(value??'').slice(0,10000)
122
+ ?value.map(function normalizeReviewAttributeItem(item){
123
+ return String(item);
124
+ })
125
+ :String(value??'')
127
126
  );
128
127
  }
129
128
  }
130
- return Object.freeze({
131
- status:String(source.status||'not-reviewed').trim().slice(0,80)||'not-reviewed',
132
- classification:String(source.classification||'unassigned').trim().slice(0,80)||'unassigned',
133
- attributes:Object.freeze(attributes),
134
- notes:String(source.notes||'').slice(0,10000),
129
+ return {
130
+ status:source.status===undefined||source.status===null||source.status===''
131
+ ?'not-reviewed'
132
+ :String(source.status),
133
+ classification:source.classification===undefined||source.classification===null||source.classification===''
134
+ ?'unassigned'
135
+ :String(source.classification),
136
+ attributes,
137
+ notes:String(source.notes??''),
135
138
  updatedAt:source.updatedAt?String(source.updatedAt):null
136
- });
139
+ };
137
140
  }
138
141
 
139
- function frozenRecordMap(records){
142
+ function recordMap(records){
140
143
  const copy={};
141
144
  for(const [id,review] of Object.entries(records)){
142
145
  setDataProperty(copy,id,normalizeReview(review));
143
146
  }
144
- return Object.freeze(copy);
147
+ return copy;
145
148
  }
146
149
 
147
150
  function normalizedStoredRecords(value){
148
- if(!isPlainRecord(value)) return Object.freeze({});
151
+ if(!isPlainRecord(value)) return {};
149
152
  const records={};
150
153
  for(const [recordId,review] of Object.entries(value)){
151
154
  let id;
@@ -170,7 +173,7 @@ function normalizedStoredRecords(value){
170
173
  }
171
174
  setDataProperty(records,id,normalizeReview(review));
172
175
  }
173
- return Object.freeze(records);
176
+ return records;
174
177
  }
175
178
 
176
179
  function validateAdapter(adapter){
@@ -229,15 +232,17 @@ class RecordReviewStore extends EventTarget{
229
232
 
230
233
  constructor({namespace='records',adapter=null}={}){
231
234
  super();
232
- this.namespace=String(namespace||'records').replace(/[^a-z0-9._-]/gi,'-').slice(0,120);
235
+ this.namespace=namespace===undefined||namespace===null||namespace===''
236
+ ?'records'
237
+ :String(namespace);
233
238
  this.adapter=validateAdapter(adapter||nativeAdapter(this.namespace)||localAdapter(this.namespace));
234
- this.records=Object.freeze({});
239
+ this.records={};
235
240
  this.loaded=false;
236
241
  this.#events=createArcaneEventSource(
237
242
  this,
238
243
  {
239
244
  source:'record-review-store',
240
- eventTypes:Object.freeze(Object.values(RECORD_REVIEW_STORE_EVENT_TYPES))
245
+ eventTypes:Object.values(RECORD_REVIEW_STORE_EVENT_TYPES)
241
246
  }
242
247
  );
243
248
  }
@@ -254,7 +259,7 @@ class RecordReviewStore extends EventTarget{
254
259
  return this.#enqueueOperation(
255
260
  async function loadRecordReviews(){
256
261
  const stored=await store.adapter.get(
257
- Object.freeze({operationId,signal:operation.signal})
262
+ {operationId,signal:operation.signal}
258
263
  );
259
264
  store.#assertOperationActive(operation.signal);
260
265
  store.records=normalizedStoredRecords(stored);
@@ -282,18 +287,18 @@ class RecordReviewStore extends EventTarget{
282
287
  ...value,
283
288
  updatedAt:new Date().toISOString()
284
289
  });
285
- const records=frozenRecordMap({...store.records,[id]:review});
290
+ const records=recordMap({...store.records,[id]:review});
286
291
  await store.adapter.set(
287
- records,
288
- Object.freeze({operationId,signal:operation.signal})
292
+ recordMap(records),
293
+ {operationId,signal:operation.signal}
289
294
  );
290
295
  store.#assertOperationActive(operation.signal);
291
- store.records=records;
292
- const detail=Object.freeze({
296
+ store.records=recordMap(records);
297
+ const detail={
293
298
  namespace:store.namespace,
294
299
  recordId:id,
295
300
  review:normalizeReview(review)
296
- });
301
+ };
297
302
  store.#events.dispatch(
298
303
  RECORD_REVIEW_STORE_EVENT_TYPES.change,
299
304
  detail,
@@ -306,7 +311,7 @@ class RecordReviewStore extends EventTarget{
306
311
  }
307
312
 
308
313
  snapshot(){
309
- return frozenRecordMap(this.records);
314
+ return recordMap(this.records);
310
315
  }
311
316
 
312
317
  dispose(){