@gregiteen/ssss-cli 0.10.1 → 0.10.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/VERSION +1 -1
- package/conformance/reference-bundle.ucw.json +1 -1
- package/docs/ssss-spec.md +28 -0
- package/package.json +2 -2
- package/registry/core.json +11 -3
- package/scripts/check-events-streaming.mjs +41 -0
- package/scripts/conformance.mjs +95 -1
- package/src/events.mjs +34 -19
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.10.3] - 2026-10-01
|
|
11
|
+
### Fixed
|
|
12
|
+
- Read event logs in 64 KiB chunks when building duplicate-ID indexes or
|
|
13
|
+
replaying events. Large histories no longer require one full-log string.
|
|
14
|
+
UTF-8 decoding, replay cursors, file locks and duplicate detection retain
|
|
15
|
+
their contracts. A regression check guards bounded reads and corrupt-log
|
|
16
|
+
rejection.
|
|
17
|
+
|
|
18
|
+
## [0.10.2] - 2026-09-28
|
|
19
|
+
### Fixed
|
|
20
|
+
- The 0.10.0 notes promised pointer fields for large-file assets, but the
|
|
21
|
+
`asset` registry entry never declared them. It now declares `storage_uri`
|
|
22
|
+
and `resource_ref`. An asset with `encoding: none` is a pointer: it must set
|
|
23
|
+
`storage_uri` and be marked `x_portability: resource_bound` (or
|
|
24
|
+
`tenant_private`). `resource_ref` must name a `resource` document. Template
|
|
25
|
+
and sale exports replace `storage_uri` with `REQUIREMENT` and keep `hash` and
|
|
26
|
+
`size_bytes`, so the buyer can check the file they bind.
|
|
27
|
+
|
|
28
|
+
### Behavior changes hosts should check
|
|
29
|
+
- An `asset` with `encoding: none` and no `storage_uri` or no
|
|
30
|
+
`x_portability: resource_bound|tenant_private` is now rejected.
|
|
31
|
+
- An `asset`'s `x_portability` may only be `resource_bound` or `tenant_private`.
|
|
32
|
+
- `resource_ref` on an `asset` must point at an existing `resource` document.
|
|
33
|
+
- Template/sale bundles containing pointer assets now declare a
|
|
34
|
+
`asset_storage_uri` parameter and a `bind-asset` provisioning step.
|
|
35
|
+
- New conformance section: asset pointers (§5.4 asset, §5.5).
|
|
36
|
+
|
|
10
37
|
## [0.10.1] - 2026-09-28
|
|
11
38
|
### Fixed
|
|
12
39
|
- A required list field set to an empty list (`deferred_steps: []`) was
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.10.
|
|
1
|
+
0.10.3
|
|
@@ -106,7 +106,7 @@
|
|
|
106
106
|
"file_count": 14,
|
|
107
107
|
"provenance": {
|
|
108
108
|
"content_hash": "sha256:7febd673938a3dc04c068e0bf08097a9783c09e612606d2df9a4db8dcb8907f5",
|
|
109
|
-
"exporter": "@gregiteen/ssss-cli@0.10.
|
|
109
|
+
"exporter": "@gregiteen/ssss-cli@0.10.3"
|
|
110
110
|
}
|
|
111
111
|
},
|
|
112
112
|
"files": [
|
package/docs/ssss-spec.md
CHANGED
|
@@ -354,6 +354,7 @@ timestamp: 2026-05-16T14:00:00Z
|
|
|
354
354
|
A text-encoded asset payload: source code, vector graphics, or base64-encoded binary content, or pointer metadata for large files.
|
|
355
355
|
|
|
356
356
|
REQUIRED: `type`, `name`, `mime_type`, `encoding` (`base64|utf-8|none`).
|
|
357
|
+
OPTIONAL: `slug`, `description`, `hash`, `size_bytes`, `storage_uri`, `resource_ref`.
|
|
357
358
|
|
|
358
359
|
```markdown
|
|
359
360
|
---
|
|
@@ -369,6 +370,33 @@ encoding: "base64"
|
|
|
369
370
|
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==
|
|
370
371
|
```
|
|
371
372
|
|
|
373
|
+
A file too large to carry in the vault is stored as a **pointer**: `encoding: none`
|
|
374
|
+
and an empty body. A pointer REQUIRES `storage_uri`, the URI of the stored object,
|
|
375
|
+
and `x_portability` set to `resource_bound` or `tenant_private` (§5.5), so a
|
|
376
|
+
template or sale export never ships the seller's storage location: the exporter
|
|
377
|
+
replaces `storage_uri` with `REQUIREMENT` and the buyer binds their own copy at
|
|
378
|
+
provision. `resource_ref`, when present, is the vault path of the `resource`
|
|
379
|
+
document (bucket, object store) that holds the object. `hash` (`sha256:<hex>`) and
|
|
380
|
+
`size_bytes` pin the object's content and survive export, so the buyer can verify
|
|
381
|
+
the file they supply.
|
|
382
|
+
|
|
383
|
+
```markdown
|
|
384
|
+
---
|
|
385
|
+
type: asset
|
|
386
|
+
name: "training-video"
|
|
387
|
+
title: "Onboarding Video"
|
|
388
|
+
description: "Staff onboarding video, stored in object storage."
|
|
389
|
+
timestamp: 2026-09-28T00:00:00Z
|
|
390
|
+
mime_type: "video/mp4"
|
|
391
|
+
encoding: "none"
|
|
392
|
+
storage_uri: "s3://media-bucket/training/onboarding.mp4"
|
|
393
|
+
resource_ref: "resources/media-bucket.md"
|
|
394
|
+
hash: "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
|
|
395
|
+
size_bytes: 734003200
|
|
396
|
+
x_portability: resource_bound
|
|
397
|
+
---
|
|
398
|
+
```
|
|
399
|
+
|
|
372
400
|
#### `resource`
|
|
373
401
|
|
|
374
402
|
An external infrastructure, cloud asset, domain, database, or API dependency bound to the workspace. Carries `portability: resource_bound`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gregiteen/ssss-cli",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.3",
|
|
4
4
|
"description": "CLI tools and the reference engine for the Structured Semantic Syntax System",
|
|
5
5
|
"main": "src/engine.mjs",
|
|
6
6
|
"exports": {
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
],
|
|
57
57
|
"scripts": {
|
|
58
58
|
"conformance": "node ./scripts/conformance.mjs conformance",
|
|
59
|
-
"test": "node ./scripts/conformance.mjs conformance --engine"
|
|
59
|
+
"test": "node ./scripts/conformance.mjs conformance --engine && node ./scripts/check-events-streaming.mjs"
|
|
60
60
|
},
|
|
61
61
|
"keywords": [
|
|
62
62
|
"ssss",
|
package/registry/core.json
CHANGED
|
@@ -40,11 +40,19 @@
|
|
|
40
40
|
"append_only": false,
|
|
41
41
|
"portability": "structural",
|
|
42
42
|
"required_fields": ["type", "name", "mime_type", "encoding"],
|
|
43
|
-
"optional_fields": ["slug", "hash", "size_bytes", "description"],
|
|
43
|
+
"optional_fields": ["slug", "hash", "size_bytes", "description", "storage_uri", "resource_ref", "x_portability"],
|
|
44
|
+
"required_when": {
|
|
45
|
+
"encoding==none": ["storage_uri", "x_portability"]
|
|
46
|
+
},
|
|
44
47
|
"enums": {
|
|
45
|
-
"encoding": ["base64", "utf-8", "none"]
|
|
48
|
+
"encoding": ["base64", "utf-8", "none"],
|
|
49
|
+
"x_portability": ["resource_bound", "tenant_private"]
|
|
50
|
+
},
|
|
51
|
+
"references": {
|
|
52
|
+
"resource_ref": { "allowed_types": ["resource"] }
|
|
46
53
|
},
|
|
47
|
-
"
|
|
54
|
+
"resource": { "kind": "storage", "binds": ["storage_uri"], "provision_relation": "stores_asset" },
|
|
55
|
+
"notes": "A text-encoded asset payload: source code, vector graphics, or base64-encoded binary content. With `encoding: none` the body is empty and the asset is a pointer to a large file: `storage_uri` locates the object, `resource_ref` optionally names the `resource` document (bucket, store) that holds it, and `hash`/`size_bytes` pin its content. A pointer MUST be marked `x_portability: resource_bound` (or `tenant_private`), so template/sale exports replace `storage_uri` with `REQUIREMENT` instead of shipping the seller's storage location.",
|
|
48
56
|
"spec_ref": "§5.4 asset"
|
|
49
57
|
},
|
|
50
58
|
"resource": {
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { JsonlEventStore, createCanonicalEvent } from '../src/events.mjs';
|
|
6
|
+
|
|
7
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ssss-event-stream-'));
|
|
8
|
+
const file = path.join(root, 'acme.jsonl');
|
|
9
|
+
const make = index => createCanonicalEvent({
|
|
10
|
+
workspace_id: 'acme', action: 'event', subject: 'stream-test',
|
|
11
|
+
principal: { id: 'test', kind: 'service' }, operation_id: `operation-${index}`,
|
|
12
|
+
idempotency_key: `key-${index}`, payload: { text: 'a'.repeat(65500) + '🧠é'.repeat(25000) }
|
|
13
|
+
});
|
|
14
|
+
const events = [make(1), make(2)];
|
|
15
|
+
const originals = { alloc: Buffer.alloc, readFileSync: fs.readFileSync, readSync: fs.readSync };
|
|
16
|
+
try {
|
|
17
|
+
fs.writeFileSync(file, events.map(event => JSON.stringify(event)).join('\n') + '\n');
|
|
18
|
+
Buffer.alloc = (size, ...rest) => { assert.ok(size <= 65536, 'Event reader allocated the whole log'); return originals.alloc(size, ...rest); };
|
|
19
|
+
fs.readFileSync = (target, ...rest) => { assert.notEqual(target, file, 'Event reader loaded the whole log'); return originals.readFileSync(target, ...rest); };
|
|
20
|
+
let largestRead = 0;
|
|
21
|
+
fs.readSync = (fd, buffer, offset, length, position) => {
|
|
22
|
+
largestRead = Math.max(largestRead, length);
|
|
23
|
+
assert.ok(length <= 65536, 'Event reader requested the whole log');
|
|
24
|
+
return originals.readSync(fd, buffer, offset, Math.min(length, 4093), position);
|
|
25
|
+
};
|
|
26
|
+
const store = new JsonlEventStore(root);
|
|
27
|
+
await assert.rejects(store.append(events[0]), /Duplicate event_id/);
|
|
28
|
+
const third = make(3);
|
|
29
|
+
await store.append(third);
|
|
30
|
+
const replayed = [];
|
|
31
|
+
for await (const entry of store.replay({ workspaceId: 'acme', cursor: 1 })) replayed.push(entry);
|
|
32
|
+
assert.deepEqual(replayed.map(entry => entry.cursor), [2, 3]);
|
|
33
|
+
assert.deepEqual(replayed.map(entry => entry.event), [events[1], third]);
|
|
34
|
+
await assert.rejects(new JsonlEventStore(root).append(third), /Duplicate event_id/);
|
|
35
|
+
fs.appendFileSync(file, '{invalid JSON}\n');
|
|
36
|
+
await assert.rejects(new JsonlEventStore(root).append(make(4)), SyntaxError);
|
|
37
|
+
console.log(JSON.stringify({ ok: true, boundedReads: true, utf8: true, cursor: true, duplicate: true, corruptFailClosed: true, largestRead }));
|
|
38
|
+
} finally {
|
|
39
|
+
Buffer.alloc = originals.alloc; fs.readFileSync = originals.readFileSync; fs.readSync = originals.readSync;
|
|
40
|
+
fs.rmSync(root, { recursive: true });
|
|
41
|
+
}
|
package/scripts/conformance.mjs
CHANGED
|
@@ -480,6 +480,98 @@ async function runOperationContractRegressionConformance() {
|
|
|
480
480
|
return pass === checks.length;
|
|
481
481
|
}
|
|
482
482
|
|
|
483
|
+
async function runAssetPointerConformance() {
|
|
484
|
+
const checks = [];
|
|
485
|
+
const check = (name, cond, detail = '') => { checks.push({ name, cond, detail }); };
|
|
486
|
+
|
|
487
|
+
const vault = fs.mkdtempSync(path.join(os.tmpdir(), 'ssss-asset-'));
|
|
488
|
+
const doc = (fields, body = '') => ['---', ...fields, '---', '', body, ''].join('\n');
|
|
489
|
+
const assetHead = [
|
|
490
|
+
'type: asset',
|
|
491
|
+
'name: training-video',
|
|
492
|
+
'title: Training Video',
|
|
493
|
+
'description: Onboarding video stored outside the vault.',
|
|
494
|
+
'timestamp: 2026-09-28T00:00:00Z',
|
|
495
|
+
'mime_type: video/mp4',
|
|
496
|
+
];
|
|
497
|
+
let n = 0;
|
|
498
|
+
const write = (filePath, content) => engine.processOperation({
|
|
499
|
+
type: 'operation',
|
|
500
|
+
idempotency_key: `asset-${++n}`,
|
|
501
|
+
workspace_id: 'ws-asset',
|
|
502
|
+
path: filePath,
|
|
503
|
+
actor: { role: 'system' },
|
|
504
|
+
content,
|
|
505
|
+
}, vault);
|
|
506
|
+
const errorsOf = (res) => (res.validation?.errors || []).join('; ');
|
|
507
|
+
const engine = createEngine({ registryDir: REGISTRY_DIR });
|
|
508
|
+
try {
|
|
509
|
+
const bucket = await write('resources/media-bucket.md', doc([
|
|
510
|
+
'type: resource',
|
|
511
|
+
'name: Media Bucket',
|
|
512
|
+
'title: Media Bucket',
|
|
513
|
+
'description: Object storage for large media.',
|
|
514
|
+
'timestamp: 2026-09-28T00:00:00Z',
|
|
515
|
+
'kind: s3_bucket',
|
|
516
|
+
'status: bound',
|
|
517
|
+
'binds: s3://seller-media',
|
|
518
|
+
]));
|
|
519
|
+
check('resource fixture commits', bucket.success === true, errorsOf(bucket));
|
|
520
|
+
|
|
521
|
+
const inline = await write('assets/logo.md', doc([
|
|
522
|
+
'type: asset', 'name: logo', 'title: Logo', 'description: Inline icon.',
|
|
523
|
+
'timestamp: 2026-09-28T00:00:00Z', 'mime_type: image/svg+xml', 'encoding: utf-8',
|
|
524
|
+
], '<svg xmlns="http://www.w3.org/2000/svg"/>'));
|
|
525
|
+
check('inline asset needs no pointer fields', inline.success === true, errorsOf(inline));
|
|
526
|
+
|
|
527
|
+
const noUri = await write('assets/no-uri.md', doc([...assetHead, 'encoding: none', 'x_portability: resource_bound']));
|
|
528
|
+
check('pointer asset without storage_uri is rejected',
|
|
529
|
+
noUri.success === false && errorsOf(noUri).includes('storage_uri'), errorsOf(noUri));
|
|
530
|
+
|
|
531
|
+
const noClass = await write('assets/no-class.md', doc([...assetHead, 'encoding: none', 'storage_uri: s3://seller-media/v.mp4']));
|
|
532
|
+
check('pointer asset without x_portability is rejected',
|
|
533
|
+
noClass.success === false && errorsOf(noClass).includes('x_portability'), errorsOf(noClass));
|
|
534
|
+
|
|
535
|
+
const structural = await write('assets/structural.md', doc([...assetHead, 'encoding: none',
|
|
536
|
+
'storage_uri: s3://seller-media/v.mp4', 'x_portability: structural']));
|
|
537
|
+
check('pointer asset cannot be marked structural',
|
|
538
|
+
structural.success === false && errorsOf(structural).includes('x_portability'), errorsOf(structural));
|
|
539
|
+
|
|
540
|
+
const wrongRef = await write('assets/wrong-ref.md', doc([...assetHead, 'encoding: none',
|
|
541
|
+
'storage_uri: s3://seller-media/v.mp4', 'x_portability: resource_bound', 'resource_ref: assets/logo.md']));
|
|
542
|
+
check('resource_ref must name a resource document',
|
|
543
|
+
wrongRef.success === false && errorsOf(wrongRef).includes('resource_ref'), errorsOf(wrongRef));
|
|
544
|
+
|
|
545
|
+
const hash = `sha256:${'a'.repeat(64)}`;
|
|
546
|
+
const pointer = await write('assets/training-video.md', doc([...assetHead, 'encoding: none',
|
|
547
|
+
'storage_uri: s3://seller-media/training/v.mp4', 'resource_ref: resources/media-bucket.md',
|
|
548
|
+
`hash: "${hash}"`, 'size_bytes: 734003200', 'x_portability: resource_bound']));
|
|
549
|
+
check('pointer asset with storage_uri, resource_ref, hash and size_bytes commits',
|
|
550
|
+
pointer.success === true, errorsOf(pointer));
|
|
551
|
+
|
|
552
|
+
const sale = exportBundle(vault, { registryDir: REGISTRY_DIR, profile: 'sale' });
|
|
553
|
+
const exported = sale.files.find((f) => f.path === 'assets/training-video.md');
|
|
554
|
+
const exportedData = exported ? parseDocument(exported.content).data : {};
|
|
555
|
+
check('sale export replaces storage_uri with REQUIREMENT and keeps the content pin',
|
|
556
|
+
exportedData.storage_uri === 'REQUIREMENT' && exportedData.hash === hash &&
|
|
557
|
+
exportedData.size_bytes === 734003200 && !exported.content.includes('seller-media/training'),
|
|
558
|
+
exported?.content || 'pointer asset missing from sale bundle');
|
|
559
|
+
check('sale bundle with a pointer asset validates',
|
|
560
|
+
validateBundle(sale, { registryDir: REGISTRY_DIR }).valid === true,
|
|
561
|
+
validateBundle(sale, { registryDir: REGISTRY_DIR }).errors.join('; '));
|
|
562
|
+
} finally {
|
|
563
|
+
fs.rmSync(vault, { recursive: true, force: true });
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
let pass = 0;
|
|
567
|
+
for (const c of checks) {
|
|
568
|
+
if (c.cond) { pass++; console.log(` ✅ ${c.name}`); }
|
|
569
|
+
else console.log(` ❌ ${c.name}${c.detail ? ` — ${c.detail}` : ''}`);
|
|
570
|
+
}
|
|
571
|
+
console.log(`\n ${pass}/${checks.length} asset pointer checks passed (§5.4 asset, §5.5)`);
|
|
572
|
+
return pass === checks.length;
|
|
573
|
+
}
|
|
574
|
+
|
|
483
575
|
async function runSemanticLocalizationConformance() {
|
|
484
576
|
const checks = [];
|
|
485
577
|
const check = (name, cond, detail = '') => { checks.push({ name, cond, detail }); };
|
|
@@ -996,6 +1088,8 @@ async function main() {
|
|
|
996
1088
|
const runtimeOk = await runRuntimeConformance();
|
|
997
1089
|
console.log('\nRunning operation regression conformance (src/engine.mjs, §6/§7) ...');
|
|
998
1090
|
const operationRegressionOk = await runOperationContractRegressionConformance();
|
|
1091
|
+
console.log('\nRunning asset pointer conformance (registry/core.json asset, §5.4/§5.5) ...');
|
|
1092
|
+
const assetPointerOk = await runAssetPointerConformance();
|
|
999
1093
|
console.log('\nRunning extension-registry conformance (src/registry.mjs) ...');
|
|
1000
1094
|
const extensionRegistryOk = runRegistryExtensionConformance();
|
|
1001
1095
|
console.log('\nRunning semantic/multilingual rendering conformance (src/semantic.mjs, §11.9) ...');
|
|
@@ -1007,7 +1101,7 @@ async function main() {
|
|
|
1007
1101
|
console.log('\nRunning CLI smoke conformance (scripts/ssss.mjs) ...');
|
|
1008
1102
|
const cliSmokeOk = await runCliSmokeConformance();
|
|
1009
1103
|
process.exit(
|
|
1010
|
-
engineOk && runtimeOk && operationRegressionOk && extensionRegistryOk &&
|
|
1104
|
+
engineOk && runtimeOk && operationRegressionOk && assetPointerOk && extensionRegistryOk &&
|
|
1011
1105
|
semanticOk && kernel09Ok && bundleOk && cliSmokeOk ? 0 : 1
|
|
1012
1106
|
);
|
|
1013
1107
|
} else {
|
package/src/events.mjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import crypto from 'node:crypto';
|
|
5
|
+
import { StringDecoder } from 'node:string_decoder';
|
|
5
6
|
import { withFileLock } from './file-lock.mjs';
|
|
6
7
|
|
|
7
8
|
// UUIDv5 namespace for event ids derived from idempotency keys.
|
|
@@ -83,22 +84,33 @@ function sameStat(a, b) {
|
|
|
83
84
|
return a.dev === b.dev && a.ino === b.ino && a.size === b.size && a.mtimeMs === b.mtimeMs && a.ctimeMs === b.ctimeMs;
|
|
84
85
|
}
|
|
85
86
|
|
|
86
|
-
/**
|
|
87
|
+
/** Decode one line at a time, never materializing the complete log as a string. */
|
|
88
|
+
function* logLines(fd, size) {
|
|
89
|
+
const buffer = Buffer.alloc(64 * 1024);
|
|
90
|
+
const decoder = new StringDecoder('utf8');
|
|
91
|
+
let offset = 0, pending = '';
|
|
92
|
+
while (offset < size) {
|
|
93
|
+
const read = fs.readSync(fd, buffer, 0, Math.min(buffer.length, size - offset), offset);
|
|
94
|
+
if (read === 0) break;
|
|
95
|
+
offset += read;
|
|
96
|
+
const text = pending + decoder.write(buffer.subarray(0, read));
|
|
97
|
+
let start = 0, newline;
|
|
98
|
+
while ((newline = text.indexOf('\n', start)) !== -1) {
|
|
99
|
+
yield text.slice(start, newline);
|
|
100
|
+
start = newline + 1;
|
|
101
|
+
}
|
|
102
|
+
pending = text.slice(start);
|
|
103
|
+
}
|
|
104
|
+
pending += decoder.end();
|
|
105
|
+
if (pending) yield pending;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Read every event_id, bounded to the bytes covered by the returned stat. */
|
|
87
109
|
function loadIndex(file) {
|
|
88
110
|
const fd = openLog(file, fs.constants.O_RDONLY | NOFOLLOW);
|
|
89
111
|
try {
|
|
90
|
-
const stat = fs.fstatSync(fd);
|
|
91
|
-
const
|
|
92
|
-
let offset = 0;
|
|
93
|
-
while (offset < bytes.length) {
|
|
94
|
-
const read = fs.readSync(fd, bytes, offset, bytes.length - offset, offset);
|
|
95
|
-
if (read === 0) break;
|
|
96
|
-
offset += read;
|
|
97
|
-
}
|
|
98
|
-
const ids = new Set();
|
|
99
|
-
for (const line of bytes.subarray(0, offset).toString('utf8').split('\n')) {
|
|
100
|
-
if (line.trim()) ids.add(JSON.parse(line).event_id);
|
|
101
|
-
}
|
|
112
|
+
const stat = fs.fstatSync(fd), ids = new Set();
|
|
113
|
+
for (const line of logLines(fd, stat.size)) if (line.trim()) ids.add(JSON.parse(line).event_id);
|
|
102
114
|
return { ids, stat };
|
|
103
115
|
} finally { fs.closeSync(fd); }
|
|
104
116
|
}
|
|
@@ -168,12 +180,15 @@ export class JsonlEventStore {
|
|
|
168
180
|
for (const file of files) {
|
|
169
181
|
if (!fs.existsSync(file)) continue;
|
|
170
182
|
if (fs.lstatSync(file).isSymbolicLink()) throw new Error('Symlinked event logs are forbidden.');
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
183
|
+
const fd = openLog(file, fs.constants.O_RDONLY | NOFOLLOW);
|
|
184
|
+
try {
|
|
185
|
+
for (const line of logLines(fd, fs.fstatSync(fd).size)) {
|
|
186
|
+
if (!line.trim()) continue;
|
|
187
|
+
cursor++;
|
|
188
|
+
if (cursor <= (options.cursor || 0)) continue;
|
|
189
|
+
yield { cursor, event: JSON.parse(line) };
|
|
190
|
+
}
|
|
191
|
+
} finally { fs.closeSync(fd); }
|
|
177
192
|
}
|
|
178
193
|
}
|
|
179
194
|
}
|