@bivy/bivy 0.17.2-staging.4 → 0.17.2-staging.5
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/dist/server.js +3 -1
- package/dist/session/event-log.js +74 -18
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -1744,8 +1744,10 @@ function referencedAttachmentHashes() {
|
|
|
1744
1744
|
const ids = new Set(metadata.listSessions().map((session) => session.id));
|
|
1745
1745
|
for (const record of new Set(openSessions.values()))
|
|
1746
1746
|
ids.add(record.id);
|
|
1747
|
+
// `scan`, not `entries`: this visits every session ever created, and must not pin
|
|
1748
|
+
// each one's full log in the event-log cache.
|
|
1747
1749
|
for (const id of ids) {
|
|
1748
|
-
for (const entry of eventLog.
|
|
1750
|
+
for (const entry of eventLog.scan(id, ["attachment", "outbound-attachment", "inline-image"])) {
|
|
1749
1751
|
if (entry.bivyKind === "attachment")
|
|
1750
1752
|
for (const ref of entry.refs)
|
|
1751
1753
|
hashes.add(ref.hash);
|
|
@@ -397,12 +397,12 @@ export function baseReplay(entries) {
|
|
|
397
397
|
}
|
|
398
398
|
return base;
|
|
399
399
|
}
|
|
400
|
-
function parseLogDetailed(body) {
|
|
400
|
+
function parseLogDetailed(body, keep = () => true) {
|
|
401
401
|
const records = [];
|
|
402
402
|
let malformedLines = 0;
|
|
403
403
|
for (const line of body.split("\n")) {
|
|
404
404
|
const trimmed = line.trim();
|
|
405
|
-
if (!trimmed)
|
|
405
|
+
if (!trimmed || !keep(trimmed))
|
|
406
406
|
continue;
|
|
407
407
|
try {
|
|
408
408
|
const value = JSON.parse(trimmed);
|
|
@@ -429,6 +429,10 @@ export function parseLog(body) {
|
|
|
429
429
|
* `throttleMs` per session (a burst of same-id deltas within a window collapses to
|
|
430
430
|
* one appended line). Durability at boundaries is explicit `flush()` (turn-end,
|
|
431
431
|
* close, delete, shutdown), mirroring `SidecarStore`.
|
|
432
|
+
*
|
|
433
|
+
* The in-memory copy of flushed records is an LRU capped at `maxCachedBytes`
|
|
434
|
+
* (measured as log-file size): an evicted session simply re-reads its file on next
|
|
435
|
+
* use, so memory tracks the sessions in use, not the node's whole history.
|
|
432
436
|
*/
|
|
433
437
|
export class EventLog {
|
|
434
438
|
dir;
|
|
@@ -436,7 +440,10 @@ export class EventLog {
|
|
|
436
440
|
redact;
|
|
437
441
|
throttleMs;
|
|
438
442
|
onIssue;
|
|
443
|
+
maxCachedBytes;
|
|
439
444
|
disk = new Map();
|
|
445
|
+
diskBytes = new Map();
|
|
446
|
+
cachedBytes = 0;
|
|
440
447
|
pending = new Map();
|
|
441
448
|
timers = new Map();
|
|
442
449
|
lastFlush = new Map();
|
|
@@ -446,12 +453,13 @@ export class EventLog {
|
|
|
446
453
|
// on first use of a session after a restart.
|
|
447
454
|
baseKeys = new Map();
|
|
448
455
|
lastIssue;
|
|
449
|
-
constructor(dir, pathFor, redact = (t) => t, throttleMs = 500, onIssue = (issue) => console.error(`[event-log] ${issue.operation} failed for ${issue.sessionId}: ${issue.message}`)) {
|
|
456
|
+
constructor(dir, pathFor, redact = (t) => t, throttleMs = 500, onIssue = (issue) => console.error(`[event-log] ${issue.operation} failed for ${issue.sessionId}: ${issue.message}`), maxCachedBytes = 64 * 1024 * 1024) {
|
|
450
457
|
this.dir = dir;
|
|
451
458
|
this.pathFor = pathFor;
|
|
452
459
|
this.redact = redact;
|
|
453
460
|
this.throttleMs = throttleMs;
|
|
454
461
|
this.onIssue = onIssue;
|
|
462
|
+
this.maxCachedBytes = maxCachedBytes;
|
|
455
463
|
}
|
|
456
464
|
report(id, operation, error) {
|
|
457
465
|
const issue = {
|
|
@@ -493,23 +501,64 @@ export class EventLog {
|
|
|
493
501
|
}
|
|
494
502
|
return { files, bytes };
|
|
495
503
|
}
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
if (cached)
|
|
499
|
-
return cached;
|
|
500
|
-
let data = [];
|
|
504
|
+
/** Read + parse a session's log file (lines passing `keep`), reporting problems. */
|
|
505
|
+
readLog(id, keep) {
|
|
501
506
|
try {
|
|
502
|
-
const
|
|
503
|
-
|
|
507
|
+
const body = fs.readFileSync(this.pathFor(id), "utf8");
|
|
508
|
+
const parsed = parseLogDetailed(body, keep);
|
|
504
509
|
if (parsed.malformedLines > 0)
|
|
505
510
|
this.report(id, "parse", new Error(`${parsed.malformedLines} malformed record(s); valid history was recovered`));
|
|
511
|
+
return { records: parsed.records, bytes: body.length };
|
|
506
512
|
}
|
|
507
513
|
catch (error) {
|
|
508
514
|
if (error?.code !== "ENOENT")
|
|
509
515
|
this.report(id, "read", error);
|
|
516
|
+
return { records: [], bytes: 0 };
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
load(id) {
|
|
520
|
+
const cached = this.disk.get(id);
|
|
521
|
+
if (cached) {
|
|
522
|
+
// Re-insert to mark most-recently-used (Map iterates in insertion order).
|
|
523
|
+
this.disk.delete(id);
|
|
524
|
+
this.disk.set(id, cached);
|
|
525
|
+
return cached;
|
|
510
526
|
}
|
|
511
|
-
this.
|
|
512
|
-
|
|
527
|
+
const { records, bytes } = this.readLog(id);
|
|
528
|
+
this.cache(id, records, bytes);
|
|
529
|
+
return records;
|
|
530
|
+
}
|
|
531
|
+
/** Store a session's flushed records, then evict least-recently-used others past the cap. */
|
|
532
|
+
cache(id, records, bytes) {
|
|
533
|
+
this.uncache(id);
|
|
534
|
+
this.disk.set(id, records);
|
|
535
|
+
this.diskBytes.set(id, bytes);
|
|
536
|
+
this.cachedBytes += bytes;
|
|
537
|
+
for (const oldest of this.disk.keys()) {
|
|
538
|
+
if (this.cachedBytes <= this.maxCachedBytes || oldest === id)
|
|
539
|
+
break;
|
|
540
|
+
this.uncache(oldest);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
uncache(id) {
|
|
544
|
+
this.cachedBytes -= this.diskBytes.get(id) ?? 0;
|
|
545
|
+
this.diskBytes.delete(id);
|
|
546
|
+
this.disk.delete(id);
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* The session's records of the given kinds, read WITHOUT caching the log. For
|
|
550
|
+
* whole-store sweeps (attachment GC) that visit every session, where `load` would
|
|
551
|
+
* pin every log in memory. Uses the cached copy when there is one, and always
|
|
552
|
+
* includes pending records so unflushed writes are never missed. Off the cache only
|
|
553
|
+
* candidate lines are parsed: those naming a wanted kind, plus torn lines (not
|
|
554
|
+
* ending in `}`) so a truncated write is still reported and callers can fail closed.
|
|
555
|
+
*/
|
|
556
|
+
scan(id, kinds) {
|
|
557
|
+
const wanted = new Set(kinds);
|
|
558
|
+
const markers = kinds.map((kind) => `"bivyKind":${JSON.stringify(kind)}`);
|
|
559
|
+
const flushed = this.disk.get(id) ?? this.readLog(id, (line) => !line.endsWith("}") || markers.some((m) => line.includes(m))).records;
|
|
560
|
+
const pending = this.pending.get(id)?.values() ?? [];
|
|
561
|
+
return [...flushed, ...pending].filter((record) => wanted.has(record.bivyKind));
|
|
513
562
|
}
|
|
514
563
|
/** Allocate a unique synthetic coalescing key (for id-less records). */
|
|
515
564
|
syntheticKey(id) {
|
|
@@ -681,9 +730,16 @@ export class EventLog {
|
|
|
681
730
|
const lines = [...batch.values()];
|
|
682
731
|
try {
|
|
683
732
|
fs.mkdirSync(this.dir, { recursive: true });
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
733
|
+
const body = this.redact(lines.map((e) => JSON.stringify(e)).join("\n") + "\n");
|
|
734
|
+
fs.appendFileSync(this.pathFor(id), body);
|
|
735
|
+
// Extend the cached copy only if it is still cached: an evicted session re-reads
|
|
736
|
+
// the file (which now holds these lines) on next use, so loading here would
|
|
737
|
+
// double-count them.
|
|
738
|
+
const cached = this.disk.get(id);
|
|
739
|
+
if (cached) {
|
|
740
|
+
cached.push(...lines);
|
|
741
|
+
this.cache(id, cached, (this.diskBytes.get(id) ?? 0) + body.length);
|
|
742
|
+
}
|
|
687
743
|
batch.clear();
|
|
688
744
|
this.lastFlush.set(id, Date.now());
|
|
689
745
|
}
|
|
@@ -708,10 +764,10 @@ export class EventLog {
|
|
|
708
764
|
}
|
|
709
765
|
this.pending.delete(id);
|
|
710
766
|
this.baseKeys.delete(id);
|
|
711
|
-
|
|
767
|
+
const body = copy.length ? copy.map((e) => JSON.stringify(e)).join("\n") + "\n" : "";
|
|
768
|
+
this.cache(id, copy, body.length);
|
|
712
769
|
try {
|
|
713
770
|
fs.mkdirSync(this.dir, { recursive: true });
|
|
714
|
-
const body = copy.length ? copy.map((e) => JSON.stringify(e)).join("\n") + "\n" : "";
|
|
715
771
|
fs.writeFileSync(this.pathFor(id), this.redact(body));
|
|
716
772
|
this.lastFlush.set(id, Date.now());
|
|
717
773
|
}
|
|
@@ -726,7 +782,7 @@ export class EventLog {
|
|
|
726
782
|
clearTimeout(timer);
|
|
727
783
|
this.timers.delete(id);
|
|
728
784
|
}
|
|
729
|
-
this.
|
|
785
|
+
this.uncache(id);
|
|
730
786
|
this.pending.delete(id);
|
|
731
787
|
this.lastFlush.delete(id);
|
|
732
788
|
this.counters.delete(id);
|
package/package.json
CHANGED