arcane-os 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/NOTICE +5 -3
- package/README.md +73 -24
- package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +67 -18
- package/browser-runtime/ai/ARCANE_AI_BROWSER_WASM_COMPONENTS.json +16 -5
- package/browser-runtime/ai/browser-kokoro-worker.mjs +3 -0
- package/browser-runtime/ai/browser-speech-artifacts.mjs +1108 -0
- package/browser-runtime/ai/browser-speech-providers.mjs +475 -0
- package/browser-runtime/ai/browser-speech.mjs +9 -0
- package/browser-runtime/ai/browser-wasm-llm-provider.mjs +1537 -167
- package/browser-runtime/ai/browser-wasm.mjs +46 -1
- package/browser-runtime/ai/browser-whisper-worker.mjs +3 -0
- package/browser-runtime/ai/browser-wllama-runtime.mjs +677 -132
- package/browser-runtime/ai/model-controller.mjs +138 -12
- package/browser-runtime/ai/speech-worker-client.mjs +207 -0
- package/browser-runtime/ai/speech-worker-runtime.mjs +516 -0
- package/browser-runtime/ai/wllama/index.mjs +389 -0
- package/docs/architecture.md +132 -22
- package/docs/reference/README.md +1 -1
- package/docs/reference/ai/browser-wasm.md +101 -42
- package/docs/reference/availability-and-normalization.md +19 -5
- package/docs/reference/behavioral-testing.md +18 -5
- package/docs/reference/cli.md +2 -2
- package/docs/reference/inventory/package-api.json +14 -14
- package/docs/reference/protocols.md +4 -4
- package/docs/reference/sdk-api.md +68 -38
- package/docs/work-amplification.md +8 -4
- package/package.json +7 -3
- package/runtime/ARCANE_RUNTIME_RELEASE.json +50 -20
- package/runtime/arcane/components/chat.html +280 -62
- package/runtime/arcane/components/speech.html +1113 -265
- package/runtime/arcane/entities/Chat.js +246 -43
- package/runtime/arcane/modules/AI.js +713 -162
- package/runtime/arcane/modules/AIProviderRuntime.js +2289 -0
- package/runtime/arcane/modules/AIRuntimeState.js +872 -0
- package/runtime/arcane/modules/ConfiguredAIChatSession.js +293 -27
- package/runtime/arcane/modules/DBOPFSDocumentLibrary.js +682 -0
- package/runtime/arcane/modules/DocumentLexicalSearch.js +292 -0
- package/runtime/arcane/modules/PersistentAIChatSession.js +268 -0
- package/runtime/arcane/modules/StaticDocumentCatalog.js +25 -206
- package/schemas/arcane-lock.schema.json +6 -4
- package/src/cli/main.mjs +14 -2
- package/src/constants.mjs +1 -1
- package/src/dev-server.mjs +244 -13
- package/src/import-map.mjs +59 -1
- package/src/packager/core.mjs +2 -2
- package/src/runtime.mjs +14 -4
- package/src/sdk-browser-runtime.mjs +28 -75
- package/src/templates/workspace-template.mjs +4 -4
- package/src/toolchain.mjs +3 -0
- package/src/workspace-runtime.mjs +1 -1
- package/src/workspace.mjs +1 -1
|
@@ -6,6 +6,74 @@ import {normalizeMemoryContent} from '../modules/MemoryRecords.js';
|
|
|
6
6
|
|
|
7
7
|
const is = new Is(false);
|
|
8
8
|
|
|
9
|
+
function plainRecord(value){
|
|
10
|
+
return Boolean(value)
|
|
11
|
+
&&typeof value==='object'
|
|
12
|
+
&&!Array.isArray(value)
|
|
13
|
+
&&Object.getPrototypeOf(value)===Object.prototype;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function copyToolCalls(value){
|
|
17
|
+
if(value===undefined) return null;
|
|
18
|
+
if(!Array.isArray(value)||value.length!==1){
|
|
19
|
+
throw new TypeError('assistantMessage.tool_calls must contain exactly one structural tool call.');
|
|
20
|
+
}
|
|
21
|
+
const ids=new Set();
|
|
22
|
+
return value.map((call,index)=>{
|
|
23
|
+
if(!plainRecord(call)||!plainRecord(call.function)||call.type!=='function'){
|
|
24
|
+
throw new TypeError(`assistantMessage.tool_calls[${index}] is invalid.`);
|
|
25
|
+
}
|
|
26
|
+
const id=String(call.id??'').trim();
|
|
27
|
+
const name=String(call.function.name??'').trim();
|
|
28
|
+
const argumentValue=call.function.arguments;
|
|
29
|
+
if(!id||id.length>128||ids.has(id)||!name||name.length>128||typeof argumentValue!=='string'){
|
|
30
|
+
throw new TypeError(`assistantMessage.tool_calls[${index}] is invalid.`);
|
|
31
|
+
}
|
|
32
|
+
ids.add(id);
|
|
33
|
+
return {
|
|
34
|
+
function:{arguments:argumentValue,name},
|
|
35
|
+
id,
|
|
36
|
+
type:'function'
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function turnMessage(value,label){
|
|
42
|
+
if(!plainRecord(value)||!['tool','user'].includes(value.role)||typeof value.content!=='string'){
|
|
43
|
+
throw new TypeError(`${label} must be a user or tool message.`);
|
|
44
|
+
}
|
|
45
|
+
if(value.role==='tool'){
|
|
46
|
+
const toolCallId=String(value.tool_call_id??'').trim();
|
|
47
|
+
if(!toolCallId||toolCallId.length>128) throw new TypeError(`${label}.tool_call_id is invalid.`);
|
|
48
|
+
return {content:value.content,role:'tool',tool_call_id:toolCallId};
|
|
49
|
+
}
|
|
50
|
+
return {content:value.content,role:'user'};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function pendingToolCallId(messages){
|
|
54
|
+
let pending=null;
|
|
55
|
+
for(const [index,message] of messages.entries()){
|
|
56
|
+
if(message.role==='user'&&pending){
|
|
57
|
+
throw new TypeError(`Chat message ${index+1} starts a user turn before the pending tool result.`);
|
|
58
|
+
}
|
|
59
|
+
if(message.role==='assistant'&&message.tool_calls){
|
|
60
|
+
const calls=copyToolCalls(message.tool_calls);
|
|
61
|
+
if(pending){
|
|
62
|
+
throw new TypeError(`Chat message ${index+1} overlaps a pending tool call.`);
|
|
63
|
+
}
|
|
64
|
+
pending=calls[0].id;
|
|
65
|
+
}
|
|
66
|
+
if(message.role==='tool'){
|
|
67
|
+
const toolCallId=String(message.tool_call_id??'').trim();
|
|
68
|
+
if(!pending||pending!==toolCallId){
|
|
69
|
+
throw new TypeError(`Chat message ${index+1} does not match the pending tool call.`);
|
|
70
|
+
}
|
|
71
|
+
pending=null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return pending;
|
|
75
|
+
}
|
|
76
|
+
|
|
9
77
|
/**
|
|
10
78
|
* Represents a single chat message.
|
|
11
79
|
*
|
|
@@ -19,6 +87,8 @@ const is = new Is(false);
|
|
|
19
87
|
* Internal persistence marker omitted from model-facing messages.
|
|
20
88
|
* @property {boolean} [ui_hidden]
|
|
21
89
|
* Internal persistence marker for messages intentionally omitted from chat UI.
|
|
90
|
+
* @property {boolean} [persistence_excluded]
|
|
91
|
+
* Internal marker for session-only messages omitted from durable chat and memory.
|
|
22
92
|
* @property {Array<Object>} [tool_calls]
|
|
23
93
|
* Assistant tool calls retained in the saved conversation log.
|
|
24
94
|
* @property {string} [tool_call_id]
|
|
@@ -111,6 +181,9 @@ class ChatEntity{
|
|
|
111
181
|
/** Serializes snapshot and append writes for this chat instance. */
|
|
112
182
|
#persistenceQueue=Promise.resolve();
|
|
113
183
|
|
|
184
|
+
/** Serializes automatic memory updates without delaying the completed turn. */
|
|
185
|
+
#memoryQueue=Promise.resolve(false);
|
|
186
|
+
|
|
114
187
|
|
|
115
188
|
/**
|
|
116
189
|
* Creates a new chat session.
|
|
@@ -154,7 +227,15 @@ class ChatEntity{
|
|
|
154
227
|
get messages(){
|
|
155
228
|
return Object.freeze(this.#messages.map(function publicChatMessage(message){
|
|
156
229
|
const copy={...message};
|
|
230
|
+
if(copy.tool_calls){
|
|
231
|
+
copy.tool_calls=Object.freeze(copyToolCalls(copy.tool_calls).map(call=>Object.freeze({
|
|
232
|
+
function:Object.freeze({...call.function}),
|
|
233
|
+
id:call.id,
|
|
234
|
+
type:call.type,
|
|
235
|
+
})));
|
|
236
|
+
}
|
|
157
237
|
delete copy.memory_excluded;
|
|
238
|
+
delete copy.persistence_excluded;
|
|
158
239
|
delete copy.ui_hidden;
|
|
159
240
|
delete copy.timestamp;
|
|
160
241
|
return Object.freeze(copy);
|
|
@@ -205,16 +286,22 @@ class ChatEntity{
|
|
|
205
286
|
*
|
|
206
287
|
* @param {string} text
|
|
207
288
|
* Message content from the user.
|
|
208
|
-
* @param {{hidden?:boolean}} options
|
|
289
|
+
* @param {{hidden?:boolean,persist?:boolean}} options
|
|
209
290
|
* Hidden messages remain in the saved/model context but are not user-authored UI turns.
|
|
210
291
|
*/
|
|
211
|
-
addUserMessage(text='',{hidden=false}={}){
|
|
292
|
+
addUserMessage(text='',{hidden=false,persist=true}={}){
|
|
212
293
|
if(!is.string(text)){
|
|
213
294
|
throw new Error('user message must be string');
|
|
214
295
|
}
|
|
215
296
|
if(!is.boolean(hidden)){
|
|
216
297
|
throw new Error('hidden must be boolean');
|
|
217
298
|
}
|
|
299
|
+
if(!is.boolean(persist)){
|
|
300
|
+
throw new Error('persist must be boolean');
|
|
301
|
+
}
|
|
302
|
+
if(pendingToolCallId(this.#messages)){
|
|
303
|
+
throw new TypeError('The pending structural tool result must be supplied before a new user turn.');
|
|
304
|
+
}
|
|
218
305
|
|
|
219
306
|
const message={
|
|
220
307
|
role:'user',
|
|
@@ -227,7 +314,8 @@ class ChatEntity{
|
|
|
227
314
|
}
|
|
228
315
|
|
|
229
316
|
return this.#appendMessage(
|
|
230
|
-
message
|
|
317
|
+
message,
|
|
318
|
+
{persist}
|
|
231
319
|
);
|
|
232
320
|
}
|
|
233
321
|
|
|
@@ -239,10 +327,10 @@ class ChatEntity{
|
|
|
239
327
|
*
|
|
240
328
|
* @param {string|number} text
|
|
241
329
|
* Message content generated by the AI.
|
|
242
|
-
* @param {{extractMemory?:boolean}} options
|
|
330
|
+
* @param {{extractMemory?:boolean,persist?:boolean}} options
|
|
243
331
|
* Set extractMemory to false for deterministic application-authored messages.
|
|
244
332
|
*/
|
|
245
|
-
addAIMessage(text='',{extractMemory=true}={}){
|
|
333
|
+
addAIMessage(text='',{extractMemory=true,persist=true}={}){
|
|
246
334
|
|
|
247
335
|
if(!is.union(text,'string','number')){
|
|
248
336
|
throw new Error('assistant message must be string or number');
|
|
@@ -250,11 +338,8 @@ class ChatEntity{
|
|
|
250
338
|
if(!is.boolean(extractMemory)){
|
|
251
339
|
throw new Error('extractMemory must be boolean');
|
|
252
340
|
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
void this.getMemoriesAboutUser().catch(function reportMemoryFailure(error){
|
|
256
|
-
console.warn('Unable to update chat memory.',error);
|
|
257
|
-
});
|
|
341
|
+
if(!is.boolean(persist)){
|
|
342
|
+
throw new Error('persist must be boolean');
|
|
258
343
|
}
|
|
259
344
|
|
|
260
345
|
const message={
|
|
@@ -266,21 +351,32 @@ class ChatEntity{
|
|
|
266
351
|
message.memory_excluded=true;
|
|
267
352
|
}
|
|
268
353
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
354
|
+
const appended=this.#appendMessage(message,{persist});
|
|
355
|
+
if(extractMemory&&persist&&this.persist){
|
|
356
|
+
return Promise.resolve(appended).then(result=>{
|
|
357
|
+
this.#queueMemoryUpdate(messages=>ai.fetch(messages));
|
|
358
|
+
return result;
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
return appended;
|
|
272
362
|
}
|
|
273
363
|
|
|
274
364
|
/**
|
|
275
365
|
* Adds an assistant tool call and its result as one hidden, atomic log exchange.
|
|
276
366
|
* The host application decides whether anything is rendered in the chat UI.
|
|
277
367
|
*/
|
|
278
|
-
addToolExchange({id='',name='',arguments:argumentValue='',result=''}={}){
|
|
368
|
+
addToolExchange({id='',name='',arguments:argumentValue='',result='',persist=true}={}){
|
|
279
369
|
const toolCallId=String(id).trim();
|
|
280
370
|
const toolName=String(name).trim();
|
|
281
371
|
if(!toolCallId||!toolName){
|
|
282
372
|
throw new TypeError('Tool exchanges require an id and name.');
|
|
283
373
|
}
|
|
374
|
+
if(!is.boolean(persist)){
|
|
375
|
+
throw new TypeError('persist must be boolean.');
|
|
376
|
+
}
|
|
377
|
+
if(pendingToolCallId(this.#messages)){
|
|
378
|
+
throw new TypeError('The pending structural tool result must be supplied before another tool exchange.');
|
|
379
|
+
}
|
|
284
380
|
|
|
285
381
|
const serializedArguments=typeof argumentValue==='string'
|
|
286
382
|
?argumentValue
|
|
@@ -317,7 +413,68 @@ class ChatEntity{
|
|
|
317
413
|
timestamp,
|
|
318
414
|
memory_excluded:true
|
|
319
415
|
}
|
|
320
|
-
]);
|
|
416
|
+
],{persist});
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Atomically adds one model request and its assistant response. Persistence
|
|
421
|
+
* failures remove both in-memory records so the caller can roll back its
|
|
422
|
+
* recurring provider context without divergence.
|
|
423
|
+
*/
|
|
424
|
+
addTurn({
|
|
425
|
+
assistantMessage,
|
|
426
|
+
extractMemory=true,
|
|
427
|
+
memoryRequest=messages=>ai.fetch(messages),
|
|
428
|
+
messagePersist=true,
|
|
429
|
+
requestMessage,
|
|
430
|
+
responsePersist=messagePersist,
|
|
431
|
+
}={}){
|
|
432
|
+
const request=turnMessage(requestMessage,'requestMessage');
|
|
433
|
+
if(!plainRecord(assistantMessage)||assistantMessage.role!=='assistant'){
|
|
434
|
+
throw new TypeError('assistantMessage must be an assistant message.');
|
|
435
|
+
}
|
|
436
|
+
if(!is.boolean(messagePersist)||!is.boolean(responsePersist)||!is.boolean(extractMemory)){
|
|
437
|
+
throw new TypeError('Turn persistence and memory options must be boolean.');
|
|
438
|
+
}
|
|
439
|
+
if(typeof memoryRequest!=='function'){
|
|
440
|
+
throw new TypeError('memoryRequest must be a function.');
|
|
441
|
+
}
|
|
442
|
+
if(messagePersist!==responsePersist){
|
|
443
|
+
throw new TypeError('messagePersist and responsePersist must match for one coherent durable turn.');
|
|
444
|
+
}
|
|
445
|
+
const pendingTool=pendingToolCallId(this.#messages);
|
|
446
|
+
if(request.role==='user'&&pendingTool){
|
|
447
|
+
throw new TypeError('The pending structural tool result must be supplied before a new user turn.');
|
|
448
|
+
}
|
|
449
|
+
if(request.role==='tool'&&pendingTool!==request.tool_call_id){
|
|
450
|
+
throw new TypeError('requestMessage does not match the pending structural tool call.');
|
|
451
|
+
}
|
|
452
|
+
const toolCalls=copyToolCalls(assistantMessage.tool_calls);
|
|
453
|
+
const assistantContent=assistantMessage.content??'';
|
|
454
|
+
if(!is.union(assistantContent,'string','number')||(!String(assistantContent)&&!toolCalls)){
|
|
455
|
+
throw new TypeError('assistantMessage must contain text or structural tool calls.');
|
|
456
|
+
}
|
|
457
|
+
const timestamp=Date.now();
|
|
458
|
+
const requestRecord={...request,timestamp};
|
|
459
|
+
const assistantRecord={
|
|
460
|
+
content:assistantContent,
|
|
461
|
+
role:'assistant',
|
|
462
|
+
timestamp,
|
|
463
|
+
...(toolCalls?{tool_calls:toolCalls}:{})
|
|
464
|
+
};
|
|
465
|
+
if(request.role==='tool') requestRecord.memory_excluded=true;
|
|
466
|
+
if(!extractMemory||toolCalls) assistantRecord.memory_excluded=true;
|
|
467
|
+
if(!messagePersist) requestRecord.persistence_excluded=true;
|
|
468
|
+
if(!responsePersist) assistantRecord.persistence_excluded=true;
|
|
469
|
+
|
|
470
|
+
const appended=this.#appendMessages([requestRecord,assistantRecord],{prepared:true});
|
|
471
|
+
if(extractMemory&&messagePersist&&responsePersist&&!toolCalls&&this.persist){
|
|
472
|
+
return Promise.resolve(appended).then(result=>{
|
|
473
|
+
this.#queueMemoryUpdate(memoryRequest);
|
|
474
|
+
return result;
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
return appended;
|
|
321
478
|
}
|
|
322
479
|
|
|
323
480
|
/**
|
|
@@ -337,8 +494,9 @@ class ChatEntity{
|
|
|
337
494
|
this.fileName
|
|
338
495
|
);
|
|
339
496
|
|
|
340
|
-
if(!content){
|
|
341
|
-
this.#messages
|
|
497
|
+
if(!content||(Array.isArray(content)&&content.length===0)){
|
|
498
|
+
const systemMessage=this.#messages.find(message=>message.role==='system');
|
|
499
|
+
this.#messages=systemMessage?[systemMessage]:[];
|
|
342
500
|
this.#persistedMessageCount=0;
|
|
343
501
|
this.#saved=true;
|
|
344
502
|
return this.messages;
|
|
@@ -351,26 +509,44 @@ class ChatEntity{
|
|
|
351
509
|
.map(row=>row.trim())
|
|
352
510
|
.filter(Boolean)
|
|
353
511
|
.map(row=>JSON.parse(row));
|
|
354
|
-
this.#persistedMessageCount=this.#
|
|
512
|
+
this.#persistedMessageCount=this.#durableMessages().length;
|
|
355
513
|
this.#saved=true;
|
|
356
514
|
|
|
357
515
|
return this.messages;
|
|
358
516
|
}
|
|
359
517
|
|
|
360
|
-
async getMemoriesAboutUser(){
|
|
361
|
-
if(
|
|
518
|
+
async getMemoriesAboutUser({request=messages=>ai.fetch(messages)}={}){
|
|
519
|
+
if(typeof request!=='function'){
|
|
520
|
+
throw new TypeError('Memory request must be a function.');
|
|
521
|
+
}
|
|
522
|
+
return this.#writeMemory(
|
|
523
|
+
this.#durableMessages().map(message=>({...message})),
|
|
524
|
+
request
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/** Waits for all automatic memory work owned by this chat instance. */
|
|
529
|
+
async settleMemory(){
|
|
530
|
+
return this.#memoryQueue;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
async #writeMemory(snapshot,request){
|
|
534
|
+
if(!hasUserEntry(snapshot)){
|
|
362
535
|
return false;
|
|
363
536
|
}
|
|
364
537
|
|
|
365
|
-
const transcript=
|
|
366
|
-
.
|
|
367
|
-
|
|
538
|
+
const transcript=snapshot
|
|
539
|
+
.filter(message=>
|
|
540
|
+
['assistant','user'].includes(message.role)
|
|
541
|
+
&&message.memory_excluded!==true
|
|
542
|
+
)
|
|
368
543
|
.map(function publicMemoryMessage(message){
|
|
369
544
|
const copy={...message};
|
|
370
545
|
delete copy.memory_excluded;
|
|
546
|
+
delete copy.persistence_excluded;
|
|
371
547
|
return copy;
|
|
372
548
|
});
|
|
373
|
-
const summary=await
|
|
549
|
+
const summary=await request(
|
|
374
550
|
[
|
|
375
551
|
{
|
|
376
552
|
role:'system',
|
|
@@ -388,7 +564,7 @@ ${JSON.stringify(transcript)}`
|
|
|
388
564
|
);
|
|
389
565
|
|
|
390
566
|
const memory=normalizeMemoryContent(
|
|
391
|
-
summary
|
|
567
|
+
summary?.choices?.[0]?.message?.content??summary?.message?.content
|
|
392
568
|
);
|
|
393
569
|
|
|
394
570
|
if(!memory){
|
|
@@ -412,12 +588,12 @@ ${JSON.stringify(transcript)}`
|
|
|
412
588
|
* @returns {Promise<*>}
|
|
413
589
|
*/
|
|
414
590
|
async save(){
|
|
415
|
-
if(!hasUserEntry(this.#
|
|
591
|
+
if(!hasUserEntry(this.#durableMessages())){
|
|
416
592
|
this.#saved=false;
|
|
417
593
|
return false;
|
|
418
594
|
}
|
|
419
595
|
|
|
420
|
-
const snapshot=this.#
|
|
596
|
+
const snapshot=this.#durableMessages().map(message=>({...message}));
|
|
421
597
|
this.#saved=false;
|
|
422
598
|
|
|
423
599
|
return this.#queuePersistence(
|
|
@@ -425,6 +601,16 @@ ${JSON.stringify(transcript)}`
|
|
|
425
601
|
);
|
|
426
602
|
}
|
|
427
603
|
|
|
604
|
+
#queueMemoryUpdate(request){
|
|
605
|
+
const snapshot=this.#durableMessages().map(message=>({...message}));
|
|
606
|
+
const queued=this.#memoryQueue.then(()=>this.#writeMemory(snapshot,request));
|
|
607
|
+
this.#memoryQueue=queued.catch(()=>{
|
|
608
|
+
console.warn('Unable to update chat memory.');
|
|
609
|
+
return false;
|
|
610
|
+
});
|
|
611
|
+
return this.#memoryQueue;
|
|
612
|
+
}
|
|
613
|
+
|
|
428
614
|
#queuePersistence(operation){
|
|
429
615
|
const queued=this.#persistenceQueue.then(operation);
|
|
430
616
|
|
|
@@ -444,7 +630,7 @@ ${JSON.stringify(transcript)}`
|
|
|
444
630
|
content
|
|
445
631
|
);
|
|
446
632
|
this.#persistedMessageCount=snapshot.length;
|
|
447
|
-
this.#saved=this.#persistedMessageCount===this.#
|
|
633
|
+
this.#saved=this.#persistedMessageCount===this.#durableMessages().length;
|
|
448
634
|
return true;
|
|
449
635
|
}catch(error){
|
|
450
636
|
this.#saved=false;
|
|
@@ -515,24 +701,33 @@ ${JSON.stringify(transcript)}`
|
|
|
515
701
|
* );
|
|
516
702
|
* ```
|
|
517
703
|
*/
|
|
518
|
-
async #appendMessage(message){
|
|
519
|
-
return this.#appendMessages([message]);
|
|
704
|
+
async #appendMessage(message,options){
|
|
705
|
+
return this.#appendMessages([message],options);
|
|
520
706
|
}
|
|
521
707
|
|
|
522
|
-
|
|
523
|
-
|
|
708
|
+
#durableMessages(){
|
|
709
|
+
return this.#messages.filter(message=>message.persistence_excluded!==true);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
async #appendMessages(messages,{persist=true,prepared=false}={}){
|
|
713
|
+
const records=Array.from(messages||[]).map(message=>
|
|
714
|
+
prepared?message:(persist?message:{...message,persistence_excluded:true})
|
|
715
|
+
);
|
|
524
716
|
if(!records.length){
|
|
525
717
|
return false;
|
|
526
718
|
}
|
|
527
719
|
|
|
528
720
|
this.#messages.push(...records);
|
|
529
|
-
|
|
721
|
+
const durableRecords=records.filter(message=>message.persistence_excluded!==true);
|
|
722
|
+
if(durableRecords.length){
|
|
723
|
+
this.#saved=false;
|
|
724
|
+
}
|
|
530
725
|
|
|
531
|
-
if(!this.persist){
|
|
726
|
+
if(!this.persist||!durableRecords.length){
|
|
532
727
|
return;
|
|
533
728
|
}
|
|
534
729
|
|
|
535
|
-
if(!hasUserEntry(this.#
|
|
730
|
+
if(!hasUserEntry(this.#durableMessages())){
|
|
536
731
|
return false;
|
|
537
732
|
}
|
|
538
733
|
|
|
@@ -548,22 +743,30 @@ ${JSON.stringify(transcript)}`
|
|
|
548
743
|
if(!recordsAreContiguous){
|
|
549
744
|
throw new Error('Chat records changed before persistence completed.');
|
|
550
745
|
}
|
|
551
|
-
const
|
|
552
|
-
|
|
746
|
+
const durableSnapshot=this.#durableMessages();
|
|
747
|
+
const durableIndex=durableSnapshot.indexOf(durableRecords[0]);
|
|
748
|
+
const durableRecordsAreContiguous=durableRecords.every(
|
|
749
|
+
(record,index)=>durableSnapshot[durableIndex+index]===record
|
|
750
|
+
);
|
|
751
|
+
if(durableIndex<0||!durableRecordsAreContiguous){
|
|
752
|
+
throw new Error('Durable chat records changed before persistence completed.');
|
|
753
|
+
}
|
|
754
|
+
const lastDurableIndex=durableIndex+durableRecords.length-1;
|
|
755
|
+
if(this.#persistedMessageCount===durableIndex){
|
|
553
756
|
await dbopfs.set(
|
|
554
757
|
this.#tableName,
|
|
555
758
|
this.fileName,
|
|
556
|
-
|
|
759
|
+
durableRecords.map(record=>JSON.stringify(record)).join('\n')+'\n',
|
|
557
760
|
true
|
|
558
761
|
);
|
|
559
|
-
this.#persistedMessageCount=
|
|
762
|
+
this.#persistedMessageCount=lastDurableIndex+1;
|
|
560
763
|
}else{
|
|
561
|
-
const snapshot=
|
|
562
|
-
.slice(0,
|
|
764
|
+
const snapshot=durableSnapshot
|
|
765
|
+
.slice(0,lastDurableIndex+1)
|
|
563
766
|
.map(entry=>({...entry}));
|
|
564
767
|
await this.#writeSnapshot(snapshot);
|
|
565
768
|
}
|
|
566
|
-
this.#saved=this.#persistedMessageCount===this.#
|
|
769
|
+
this.#saved=this.#persistedMessageCount===this.#durableMessages().length;
|
|
567
770
|
return true;
|
|
568
771
|
}catch(error){
|
|
569
772
|
const failedIndex=this.#messages.indexOf(records[0]);
|
|
@@ -571,7 +774,7 @@ ${JSON.stringify(transcript)}`
|
|
|
571
774
|
this.#messages.splice(failedIndex,records.length);
|
|
572
775
|
}
|
|
573
776
|
this.#saved=
|
|
574
|
-
this.#persistedMessageCount===this.#
|
|
777
|
+
this.#persistedMessageCount===this.#durableMessages().length;
|
|
575
778
|
throw error;
|
|
576
779
|
}
|
|
577
780
|
});
|