@roj-ai/kompjutr-platform 0.0.0 → 0.2.10
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/README.md +114 -1
- package/dist/event-store.d.ts +48 -0
- package/dist/event-store.d.ts.map +1 -0
- package/dist/event-store.js +206 -0
- package/dist/event-store.js.map +1 -0
- package/dist/fs.d.ts +19 -0
- package/dist/fs.d.ts.map +1 -0
- package/dist/fs.js +200 -0
- package/dist/fs.js.map +1 -0
- package/dist/git.d.ts +12 -0
- package/dist/git.d.ts.map +1 -0
- package/dist/git.js +70 -0
- package/dist/git.js.map +1 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +57 -0
- package/dist/index.js.map +1 -0
- package/dist/llm-call-log.d.ts +23 -0
- package/dist/llm-call-log.d.ts.map +1 -0
- package/dist/llm-call-log.js +135 -0
- package/dist/llm-call-log.js.map +1 -0
- package/dist/llm-call-payloads.d.ts +12 -0
- package/dist/llm-call-payloads.d.ts.map +1 -0
- package/dist/llm-call-payloads.js +77 -0
- package/dist/llm-call-payloads.js.map +1 -0
- package/dist/process.d.ts +14 -0
- package/dist/process.d.ts.map +1 -0
- package/dist/process.js +25 -0
- package/dist/process.js.map +1 -0
- package/dist/session-log.d.ts +22 -0
- package/dist/session-log.d.ts.map +1 -0
- package/dist/session-log.js +65 -0
- package/dist/session-log.js.map +1 -0
- package/dist/shell.d.ts +11 -0
- package/dist/shell.d.ts.map +1 -0
- package/dist/shell.js +36 -0
- package/dist/shell.js.map +1 -0
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +2 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/storage.d.ts +29 -0
- package/dist/testing/storage.d.ts.map +1 -0
- package/dist/testing/storage.js +132 -0
- package/dist/testing/storage.js.map +1 -0
- package/package.json +44 -4
- package/src/event-store.ts +250 -0
- package/src/fs.ts +232 -0
- package/src/git.ts +74 -0
- package/src/index.ts +83 -0
- package/src/llm-call-log.ts +204 -0
- package/src/llm-call-payloads.ts +95 -0
- package/src/process.ts +28 -0
- package/src/session-log.ts +79 -0
- package/src/shell.ts +49 -0
- package/src/testing/index.ts +1 -0
- package/src/testing/storage.ts +141 -0
package/dist/shell.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { createGitCommand } from '@kompjutr/do/git-shell';
|
|
2
|
+
import { createShell } from '@kompjutr/do/shell';
|
|
3
|
+
function quoteShellWord(value) {
|
|
4
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
5
|
+
}
|
|
6
|
+
export function createKompjutrShellRunner(options) {
|
|
7
|
+
const commands = options.git === undefined ? undefined : new Map([['git', createGitCommand(options.git)]]);
|
|
8
|
+
return {
|
|
9
|
+
confinement: options.confinement ?? 'none',
|
|
10
|
+
supportsTimeout: false,
|
|
11
|
+
async run(runOptions) {
|
|
12
|
+
if (!runOptions.cwd.startsWith('/'))
|
|
13
|
+
throw new Error('kompjutr shell requires an absolute cwd');
|
|
14
|
+
if ((runOptions.grants?.length ?? 0) > 0) {
|
|
15
|
+
throw new Error('kompjutr shell does not support path grants');
|
|
16
|
+
}
|
|
17
|
+
// ShellSession caches cwd per instance, even though instances share the default SQL row.
|
|
18
|
+
const shell = createShell({ fs: options.filesystem, commands });
|
|
19
|
+
const cwdResult = await shell.run(`cd ${quoteShellWord(runOptions.cwd)}`);
|
|
20
|
+
const result = cwdResult.exitCode === 0
|
|
21
|
+
? await shell.run(runOptions.command, {
|
|
22
|
+
env: runOptions.env,
|
|
23
|
+
stdin: runOptions.stdin,
|
|
24
|
+
})
|
|
25
|
+
: cwdResult;
|
|
26
|
+
return {
|
|
27
|
+
stdout: result.stdout,
|
|
28
|
+
stderr: result.stderr,
|
|
29
|
+
exitCode: result.exitCode,
|
|
30
|
+
timedOut: false,
|
|
31
|
+
truncated: result.truncated,
|
|
32
|
+
};
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=shell.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shell.js","sourceRoot":"","sources":["../src/shell.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AACzD,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAUhD,SAAS,cAAc,CAAC,KAAa;IACpC,OAAO,IAAI,KAAK,CAAC,UAAU,CAAC,GAAG,EAAE,OAAO,CAAC,GAAG,CAAA;AAC7C,CAAC;AAED,MAAM,UAAU,yBAAyB,CAAC,OAAmC;IAC5E,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,EAAE,gBAAgB,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAA;IAE1G,OAAO;QACN,WAAW,EAAE,OAAO,CAAC,WAAW,IAAI,MAAM;QAC1C,eAAe,EAAE,KAAK;QACtB,KAAK,CAAC,GAAG,CAAC,UAAU;YACnB,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC;gBAAE,MAAM,IAAI,KAAK,CAAC,yCAAyC,CAAC,CAAA;YAC/F,IAAI,CAAC,UAAU,CAAC,MAAM,EAAE,MAAM,IAAI,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC;gBAC1C,MAAM,IAAI,KAAK,CAAC,6CAA6C,CAAC,CAAA;YAC/D,CAAC;YAED,yFAAyF;YACzF,MAAM,KAAK,GAAG,WAAW,CAAC,EAAE,EAAE,EAAE,OAAO,CAAC,UAAU,EAAE,QAAQ,EAAE,CAAC,CAAA;YAC/D,MAAM,SAAS,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,MAAM,cAAc,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;YACzE,MAAM,MAAM,GACX,SAAS,CAAC,QAAQ,KAAK,CAAC;gBACvB,CAAC,CAAC,MAAM,KAAK,CAAC,GAAG,CAAC,UAAU,CAAC,OAAO,EAAE;oBACpC,GAAG,EAAE,UAAU,CAAC,GAAG;oBACnB,KAAK,EAAE,UAAU,CAAC,KAAK;iBACvB,CAAC;gBACH,CAAC,CAAC,SAAS,CAAA;YACb,OAAO;gBACN,MAAM,EAAE,MAAM,CAAC,MAAM;gBACrB,MAAM,EAAE,MAAM,CAAC,MAAM;gBACrB,QAAQ,EAAE,MAAM,CAAC,QAAQ;gBACzB,QAAQ,EAAE,KAAK;gBACf,SAAS,EAAE,MAAM,CAAC,SAAS;aAC3B,CAAA;QACF,CAAC;KACD,CAAA;AACF,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAA"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `bun:sqlite` standing in for Durable Object SQL storage, so the adapter can
|
|
3
|
+
* be driven by `bun test` without a Worker runtime.
|
|
4
|
+
*
|
|
5
|
+
* It is deliberately *stricter* than its backing engine on one point. A Durable
|
|
6
|
+
* Object binds at most 100 parameters per statement; bun:sqlite binds far more,
|
|
7
|
+
* and an earlier attempt at this port shipped a batch insert that passed every
|
|
8
|
+
* test and failed in production for exactly that reason. A fake that is more
|
|
9
|
+
* permissive than the thing it stands for hides the bugs worth catching, so the
|
|
10
|
+
* limit is enforced here rather than discovered on a deploy.
|
|
11
|
+
*/
|
|
12
|
+
import { Database as BunDatabase } from 'bun:sqlite';
|
|
13
|
+
import type { DurableObjectStorageLike, SQLStorageLike } from '@kompjutr/do';
|
|
14
|
+
/** What a Durable Object binds per statement. The 101st raises, as workerd does. */
|
|
15
|
+
export declare const MAX_BIND_PARAMETERS = 100;
|
|
16
|
+
export declare class BunSqliteStorage implements DurableObjectStorageLike {
|
|
17
|
+
#private;
|
|
18
|
+
readonly db: BunDatabase;
|
|
19
|
+
readonly sql: SQLStorageLike;
|
|
20
|
+
/** Statements executed since the last reset — what a cost test asserts on. */
|
|
21
|
+
statementCount: number;
|
|
22
|
+
/** Rows the caller actually pulled. Approximate: a cursor abandoned early stops counting. */
|
|
23
|
+
rowCount: number;
|
|
24
|
+
constructor(path?: string);
|
|
25
|
+
transactionSync<T>(closure: () => T): T;
|
|
26
|
+
resetCounters(): void;
|
|
27
|
+
close(): void;
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=storage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../src/testing/storage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,QAAQ,IAAI,WAAW,EAAyB,MAAM,YAAY,CAAA;AAC3E,OAAO,KAAK,EAAE,wBAAwB,EAAiB,cAAc,EAAE,MAAM,cAAc,CAAA;AAE3F,oFAAoF;AACpF,eAAO,MAAM,mBAAmB,MAAM,CAAA;AAuEtC,qBAAa,gBAAiB,YAAW,wBAAwB;;IAChE,QAAQ,CAAC,EAAE,EAAE,WAAW,CAAA;IACxB,QAAQ,CAAC,GAAG,EAAE,cAAc,CAAA;IAE5B,8EAA8E;IAC9E,cAAc,SAAI;IAClB,6FAA6F;IAC7F,QAAQ,SAAI;gBAIA,IAAI,SAAa;IAkB7B,eAAe,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,GAAG,CAAC;IAgBvC,aAAa,IAAI,IAAI;IAKrB,KAAK,IAAI,IAAI;CAGb"}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `bun:sqlite` standing in for Durable Object SQL storage, so the adapter can
|
|
3
|
+
* be driven by `bun test` without a Worker runtime.
|
|
4
|
+
*
|
|
5
|
+
* It is deliberately *stricter* than its backing engine on one point. A Durable
|
|
6
|
+
* Object binds at most 100 parameters per statement; bun:sqlite binds far more,
|
|
7
|
+
* and an earlier attempt at this port shipped a batch insert that passed every
|
|
8
|
+
* test and failed in production for exactly that reason. A fake that is more
|
|
9
|
+
* permissive than the thing it stands for hides the bugs worth catching, so the
|
|
10
|
+
* limit is enforced here rather than discovered on a deploy.
|
|
11
|
+
*/
|
|
12
|
+
import { Database as BunDatabase } from 'bun:sqlite';
|
|
13
|
+
/** What a Durable Object binds per statement. The 101st raises, as workerd does. */
|
|
14
|
+
export const MAX_BIND_PARAMETERS = 100;
|
|
15
|
+
/** SQLite takes bytes, numbers, strings and null; everything else is converted or refused. */
|
|
16
|
+
function toBinding(value) {
|
|
17
|
+
if (value === undefined || value === null)
|
|
18
|
+
return null;
|
|
19
|
+
if (typeof value === 'boolean')
|
|
20
|
+
return value ? 1 : 0;
|
|
21
|
+
if (value instanceof ArrayBuffer)
|
|
22
|
+
return new Uint8Array(value);
|
|
23
|
+
if (ArrayBuffer.isView(value) && !(value instanceof Uint8Array)) {
|
|
24
|
+
return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
|
|
25
|
+
}
|
|
26
|
+
if (typeof value === 'number' || typeof value === 'bigint' || typeof value === 'string' || value instanceof Uint8Array) {
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
throw new TypeError(`unsupported SQLite binding: ${typeof value}`);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Rows arrive lazily, because `Filesystem.scan` pages a range scan and a cursor
|
|
33
|
+
* that materialised would turn every page into the whole table.
|
|
34
|
+
*
|
|
35
|
+
* One step is taken up front all the same: a statement nobody iterates is a
|
|
36
|
+
* statement bun:sqlite never runs, and a `CREATE TABLE` has no rows to pull.
|
|
37
|
+
*/
|
|
38
|
+
class Cursor {
|
|
39
|
+
onRow;
|
|
40
|
+
#rows;
|
|
41
|
+
#prefetched;
|
|
42
|
+
#done = false;
|
|
43
|
+
constructor(rows, onRow) {
|
|
44
|
+
this.onRow = onRow;
|
|
45
|
+
this.#rows = rows;
|
|
46
|
+
this.#prefetched = this.#pull();
|
|
47
|
+
}
|
|
48
|
+
#pull() {
|
|
49
|
+
const step = this.#rows.next();
|
|
50
|
+
if (step.done === true)
|
|
51
|
+
this.#done = true;
|
|
52
|
+
else
|
|
53
|
+
this.onRow();
|
|
54
|
+
return step;
|
|
55
|
+
}
|
|
56
|
+
next() {
|
|
57
|
+
if (this.#prefetched !== null) {
|
|
58
|
+
const first = this.#prefetched;
|
|
59
|
+
this.#prefetched = null;
|
|
60
|
+
return first;
|
|
61
|
+
}
|
|
62
|
+
if (this.#done)
|
|
63
|
+
return { done: true, value: undefined };
|
|
64
|
+
return this.#pull();
|
|
65
|
+
}
|
|
66
|
+
return() {
|
|
67
|
+
if (!this.#done) {
|
|
68
|
+
this.#rows.return?.();
|
|
69
|
+
this.#done = true;
|
|
70
|
+
}
|
|
71
|
+
this.#prefetched = null;
|
|
72
|
+
return { done: true, value: undefined };
|
|
73
|
+
}
|
|
74
|
+
[Symbol.iterator]() {
|
|
75
|
+
return this;
|
|
76
|
+
}
|
|
77
|
+
toArray() {
|
|
78
|
+
return Array.from(this);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
export class BunSqliteStorage {
|
|
82
|
+
db;
|
|
83
|
+
sql;
|
|
84
|
+
/** Statements executed since the last reset — what a cost test asserts on. */
|
|
85
|
+
statementCount = 0;
|
|
86
|
+
/** Rows the caller actually pulled. Approximate: a cursor abandoned early stops counting. */
|
|
87
|
+
rowCount = 0;
|
|
88
|
+
#depth = 0;
|
|
89
|
+
constructor(path = ':memory:') {
|
|
90
|
+
this.db = new BunDatabase(path, { create: true });
|
|
91
|
+
this.db.run('PRAGMA journal_mode = WAL');
|
|
92
|
+
this.sql = {
|
|
93
|
+
exec: (query, ...bindings) => {
|
|
94
|
+
if (bindings.length > MAX_BIND_PARAMETERS) {
|
|
95
|
+
throw new Error(`too many SQL variables: ${bindings.length} exceeds the ${MAX_BIND_PARAMETERS} a Durable Object binds`);
|
|
96
|
+
}
|
|
97
|
+
this.statementCount++;
|
|
98
|
+
const statement = this.db.query(query);
|
|
99
|
+
const rows = statement.iterate(...bindings.map(toBinding));
|
|
100
|
+
return new Cursor(rows, () => {
|
|
101
|
+
this.rowCount++;
|
|
102
|
+
});
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
transactionSync(closure) {
|
|
107
|
+
if (this.#depth > 0)
|
|
108
|
+
return closure();
|
|
109
|
+
this.#depth++;
|
|
110
|
+
this.db.run('BEGIN');
|
|
111
|
+
try {
|
|
112
|
+
const result = closure();
|
|
113
|
+
this.db.run('COMMIT');
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
this.db.run('ROLLBACK');
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
this.#depth--;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
resetCounters() {
|
|
125
|
+
this.statementCount = 0;
|
|
126
|
+
this.rowCount = 0;
|
|
127
|
+
}
|
|
128
|
+
close() {
|
|
129
|
+
this.db.close();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
//# sourceMappingURL=storage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storage.js","sourceRoot":"","sources":["../../src/testing/storage.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,13 +1,53 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@roj-ai/kompjutr-platform",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
3
|
+
"version": "0.2.10",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"roj": {
|
|
8
|
+
"bunOnlyExports": [
|
|
9
|
+
"./testing"
|
|
10
|
+
]
|
|
11
|
+
},
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/index.js",
|
|
16
|
+
"default": "./dist/index.js"
|
|
17
|
+
},
|
|
18
|
+
"./testing": {
|
|
19
|
+
"types": "./dist/testing/index.d.ts",
|
|
20
|
+
"import": "./dist/testing/index.js",
|
|
21
|
+
"default": "./dist/testing/index.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"src",
|
|
26
|
+
"dist",
|
|
27
|
+
"!src/**/*.test.ts",
|
|
28
|
+
"!dist/**/*.test.*"
|
|
29
|
+
],
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
},
|
|
5
33
|
"repository": {
|
|
6
34
|
"type": "git",
|
|
7
35
|
"url": "git+https://github.com/contember/roj.git",
|
|
8
36
|
"directory": "packages/kompjutr-platform"
|
|
9
37
|
},
|
|
10
|
-
"
|
|
11
|
-
|
|
38
|
+
"homepage": "https://github.com/contember/roj/tree/main/packages/kompjutr-platform#readme",
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/contember/roj/issues"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"type-check": "tsc --noEmit"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@kompjutr/do": "0.1.2",
|
|
47
|
+
"@roj-ai/sdk": "^0.2.10"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@types/bun": "^1.3.3",
|
|
51
|
+
"typescript": "^5.9.3"
|
|
12
52
|
}
|
|
13
53
|
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `EventStore` over the workspace's own SQLite rather than a JSONL file in it.
|
|
3
|
+
*
|
|
4
|
+
* The file store rewrites a blob through the filesystem on every append and
|
|
5
|
+
* parses the whole file on every load. One row per event turns an append into
|
|
6
|
+
* an insert and `loadRange` into a seek down the clustered `(session_id, seq)`
|
|
7
|
+
* key. This is the write path an event-sourced session spends its day on.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { BaseEventStore, domainEventSchema, EventAppendError, EventStoreError, isDomainEvent, SessionId, sessionMetadataSchema } from '@roj-ai/sdk'
|
|
11
|
+
import type { DomainEvent, LoadRangeOptions, LoadRangeResult, Logger, SessionMetadata } from '@roj-ai/sdk'
|
|
12
|
+
import type { SqlDatabase } from '@kompjutr/do'
|
|
13
|
+
|
|
14
|
+
const EVENTS_TABLE = 'roj_events'
|
|
15
|
+
const METADATA_TABLE = 'roj_session_metadata'
|
|
16
|
+
|
|
17
|
+
/** A Durable Object binds at most 100 parameters per statement, and a row costs three. */
|
|
18
|
+
const EVENTS_PER_INSERT = 33
|
|
19
|
+
|
|
20
|
+
/** `seq` is the 0-based event index the contract reports as fromIndex/toIndex. */
|
|
21
|
+
interface EventRow {
|
|
22
|
+
seq: number
|
|
23
|
+
payload: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class KompjutrEventStore extends BaseEventStore {
|
|
27
|
+
/** Tail of the append chain per session; see #serialized. */
|
|
28
|
+
readonly #appendTails = new Map<SessionId, Promise<void>>()
|
|
29
|
+
|
|
30
|
+
constructor(
|
|
31
|
+
private readonly db: SqlDatabase,
|
|
32
|
+
private readonly logger?: Logger,
|
|
33
|
+
) {
|
|
34
|
+
super()
|
|
35
|
+
|
|
36
|
+
// WITHOUT ROWID stores rows in primary-key order, so a session's events are
|
|
37
|
+
// contiguous and loadRange never leaves the index.
|
|
38
|
+
this.db.run(
|
|
39
|
+
`CREATE TABLE IF NOT EXISTS ${EVENTS_TABLE} (
|
|
40
|
+
session_id TEXT NOT NULL,
|
|
41
|
+
seq INTEGER NOT NULL,
|
|
42
|
+
payload TEXT NOT NULL,
|
|
43
|
+
PRIMARY KEY (session_id, seq)
|
|
44
|
+
) WITHOUT ROWID`,
|
|
45
|
+
)
|
|
46
|
+
this.db.run(
|
|
47
|
+
`CREATE TABLE IF NOT EXISTS ${METADATA_TABLE} (
|
|
48
|
+
session_id TEXT PRIMARY KEY,
|
|
49
|
+
metadata TEXT NOT NULL
|
|
50
|
+
)`,
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
protected async doAppend(sessionId: SessionId, event: DomainEvent): Promise<void> {
|
|
55
|
+
await this.doAppendBatch(sessionId, [event])
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
protected async doAppendBatch(sessionId: SessionId, events: DomainEvent[]): Promise<void> {
|
|
59
|
+
if (events.length === 0) return
|
|
60
|
+
|
|
61
|
+
await this.#serialized(sessionId, async () => {
|
|
62
|
+
try {
|
|
63
|
+
// One transaction, so a batch too wide for a single statement still
|
|
64
|
+
// lands whole. The old shape relied on the host committing a
|
|
65
|
+
// synchronous run together, which only a Durable Object promises.
|
|
66
|
+
this.db.transactionSync(() => {
|
|
67
|
+
const firstSeq = this.#lastSeq(sessionId) + 1
|
|
68
|
+
for (let offset = 0; offset < events.length; offset += EVENTS_PER_INSERT) {
|
|
69
|
+
const chunk = events.slice(offset, offset + EVENTS_PER_INSERT)
|
|
70
|
+
const tuples = chunk.map(() => '(?, ?, ?)').join(', ')
|
|
71
|
+
const bindings = chunk.flatMap((event, index) => [sessionId, firstSeq + offset + index, JSON.stringify(event)])
|
|
72
|
+
this.db.run(`INSERT INTO ${EVENTS_TABLE} (session_id, seq, payload) VALUES ${tuples}`, ...bindings)
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
await this.updateMetadataFromEvents(sessionId, events)
|
|
77
|
+
} catch (error) {
|
|
78
|
+
throw new EventAppendError(sessionId, error)
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async load(sessionId: SessionId): Promise<DomainEvent[]> {
|
|
84
|
+
const rows = this.db.all<EventRow>(`SELECT seq, payload FROM ${EVENTS_TABLE} WHERE session_id = ? ORDER BY seq`, sessionId)
|
|
85
|
+
return rows.map((row) => this.#decodeEvent(sessionId, row))
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async loadRange(sessionId: SessionId, options?: LoadRangeOptions): Promise<LoadRangeResult> {
|
|
89
|
+
const since = options?.since ?? -1
|
|
90
|
+
const limit = options?.limit
|
|
91
|
+
|
|
92
|
+
const rows = this.db.all<EventRow>(
|
|
93
|
+
`SELECT seq, payload FROM ${EVENTS_TABLE} WHERE session_id = ? AND seq > ? ORDER BY seq LIMIT ?`,
|
|
94
|
+
sessionId,
|
|
95
|
+
since,
|
|
96
|
+
// SQLite reads a negative limit as unlimited; clamp rather than inherit that.
|
|
97
|
+
limit === undefined ? -1 : Math.max(limit, 0),
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
const first = rows[0]
|
|
101
|
+
const last = rows[rows.length - 1]
|
|
102
|
+
if (first === undefined || last === undefined) {
|
|
103
|
+
// Report the store's last index instead, so a poller keeps its cursor.
|
|
104
|
+
return { events: [], fromIndex: -1, toIndex: this.#lastSeq(sessionId) }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
events: rows.map((row) => this.#decodeEvent(sessionId, row)),
|
|
109
|
+
fromIndex: first.seq,
|
|
110
|
+
toIndex: last.seq,
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async exists(sessionId: SessionId): Promise<boolean> {
|
|
115
|
+
return this.db.one(`SELECT 1 FROM ${EVENTS_TABLE} WHERE session_id = ? LIMIT 1`, sessionId) !== undefined
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Every session this store holds, from either side of the pair.
|
|
120
|
+
*
|
|
121
|
+
* Neither side implies the other. A session created by `updateMetadata` alone
|
|
122
|
+
* has no events yet and the file store still lists it; a session whose
|
|
123
|
+
* metadata write failed after its rows landed has events and no metadata, and
|
|
124
|
+
* then the log is the only record it ran. So the answer is a union — taken
|
|
125
|
+
* without reading the log, because a status poll counts these.
|
|
126
|
+
*
|
|
127
|
+
* The recursive term seeks from one distinct id straight to the next down the
|
|
128
|
+
* `(session_id, seq)` primary key, so the cost follows sessions rather than
|
|
129
|
+
* events. `SELECT DISTINCT session_id` reads every row instead.
|
|
130
|
+
*/
|
|
131
|
+
async listSessions(): Promise<SessionId[]> {
|
|
132
|
+
const rows = this.db.all<{ session_id: string }>(
|
|
133
|
+
`WITH RECURSIVE event_sessions(session_id) AS (
|
|
134
|
+
SELECT MIN(session_id) FROM ${EVENTS_TABLE}
|
|
135
|
+
UNION ALL
|
|
136
|
+
SELECT (SELECT MIN(session_id) FROM ${EVENTS_TABLE} WHERE session_id > previous.session_id)
|
|
137
|
+
FROM event_sessions AS previous
|
|
138
|
+
WHERE previous.session_id IS NOT NULL
|
|
139
|
+
)
|
|
140
|
+
SELECT session_id FROM event_sessions WHERE session_id IS NOT NULL
|
|
141
|
+
UNION
|
|
142
|
+
SELECT session_id FROM ${METADATA_TABLE}
|
|
143
|
+
ORDER BY session_id`,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
return rows.map((row) => SessionId(row.session_id))
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Drop every row this store holds for one session, and report how many events went.
|
|
151
|
+
*
|
|
152
|
+
* Deliberately outside the `EventStore` contract: the log is the only record
|
|
153
|
+
* that a session existed, so nothing in the SDK can reach this by accident.
|
|
154
|
+
* Whether a session may be dropped is the caller's policy.
|
|
155
|
+
*/
|
|
156
|
+
async deleteSession(sessionId: SessionId): Promise<number> {
|
|
157
|
+
let events = 0
|
|
158
|
+
|
|
159
|
+
// Serialized with appends, so a delete cannot land between a batch's
|
|
160
|
+
// sequence claim and its inserts and leave the tail of that batch behind.
|
|
161
|
+
await this.#serialized(sessionId, async () => {
|
|
162
|
+
this.db.transactionSync(() => {
|
|
163
|
+
events = this.db.scalar<number>(`SELECT COUNT(*) FROM ${EVENTS_TABLE} WHERE session_id = ?`, sessionId) ?? 0
|
|
164
|
+
this.db.run(`DELETE FROM ${EVENTS_TABLE} WHERE session_id = ?`, sessionId)
|
|
165
|
+
this.db.run(`DELETE FROM ${METADATA_TABLE} WHERE session_id = ?`, sessionId)
|
|
166
|
+
})
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
return events
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
protected async readMetadata(sessionId: SessionId): Promise<SessionMetadata | null> {
|
|
173
|
+
const row = this.db.one<{ metadata: string }>(`SELECT metadata FROM ${METADATA_TABLE} WHERE session_id = ?`, sessionId)
|
|
174
|
+
return row === undefined ? null : this.#decodeMetadata(sessionId, row.metadata)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** A session whose metadata does not parse is skipped, never thrown over. */
|
|
178
|
+
#skip(sessionId: SessionId, reason: string): null {
|
|
179
|
+
this.logger?.warn('Skipping invalid session metadata', {
|
|
180
|
+
sessionId,
|
|
181
|
+
reason,
|
|
182
|
+
})
|
|
183
|
+
return null
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
protected async writeMetadata(sessionId: SessionId, metadata: SessionMetadata): Promise<void> {
|
|
187
|
+
this.db.run(
|
|
188
|
+
`INSERT INTO ${METADATA_TABLE} (session_id, metadata) VALUES (?, ?)
|
|
189
|
+
ON CONFLICT(session_id) DO UPDATE SET metadata = excluded.metadata`,
|
|
190
|
+
sessionId,
|
|
191
|
+
JSON.stringify(metadata),
|
|
192
|
+
)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
protected async getAllSessionMetadata(): Promise<SessionMetadata[]> {
|
|
196
|
+
const rows = this.db.all<{ session_id: string; metadata: string }>(`SELECT session_id, metadata FROM ${METADATA_TABLE} ORDER BY session_id`)
|
|
197
|
+
return rows.map((row) => this.#decodeMetadata(SessionId(row.session_id), row.metadata)).filter((metadata): metadata is SessionMetadata => metadata !== null)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Index of the last stored event, -1 when the session has none. */
|
|
201
|
+
#lastSeq(sessionId: SessionId): number {
|
|
202
|
+
return this.db.scalar<number | null>(`SELECT MAX(seq) FROM ${EVENTS_TABLE} WHERE session_id = ?`, sessionId) ?? -1
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Run appends for one session one at a time.
|
|
207
|
+
*
|
|
208
|
+
* The SQL is synchronous but the metadata update that follows it is not, so
|
|
209
|
+
* concurrent appends would otherwise read-modify-write the same metrics.
|
|
210
|
+
*/
|
|
211
|
+
#serialized(sessionId: SessionId, task: () => Promise<void>): Promise<void> {
|
|
212
|
+
const previous = this.#appendTails.get(sessionId) ?? Promise.resolve()
|
|
213
|
+
const next = previous.then(task)
|
|
214
|
+
// The stored tail must never reject, or the next append inherits this failure.
|
|
215
|
+
const tail = next
|
|
216
|
+
.catch(() => undefined)
|
|
217
|
+
.then(() => {
|
|
218
|
+
if (this.#appendTails.get(sessionId) === tail) this.#appendTails.delete(sessionId)
|
|
219
|
+
})
|
|
220
|
+
this.#appendTails.set(sessionId, tail)
|
|
221
|
+
return next
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
#decodeEvent(sessionId: SessionId, row: EventRow): DomainEvent {
|
|
225
|
+
try {
|
|
226
|
+
const parsed = domainEventSchema.parse(JSON.parse(row.payload))
|
|
227
|
+
// The schema validates fields; the SDK guard supplies the domain event brand.
|
|
228
|
+
if (!isDomainEvent(parsed)) throw new Error('Invalid domain event')
|
|
229
|
+
return parsed
|
|
230
|
+
} catch (error) {
|
|
231
|
+
throw new EventStoreError(`Failed to parse event at index ${row.seq}`, sessionId, error)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Null rather than a throw, matching `FileEventStore`: one unreadable row must
|
|
237
|
+
* not take out the listing every other session appears in.
|
|
238
|
+
*/
|
|
239
|
+
#decodeMetadata(sessionId: SessionId, raw: string): SessionMetadata | null {
|
|
240
|
+
let parsed: unknown
|
|
241
|
+
try {
|
|
242
|
+
parsed = JSON.parse(raw)
|
|
243
|
+
} catch (error) {
|
|
244
|
+
return this.#skip(sessionId, String(error))
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const result = sessionMetadataSchema.safeParse(parsed)
|
|
248
|
+
return result.success ? result.data : this.#skip(sessionId, result.error.message)
|
|
249
|
+
}
|
|
250
|
+
}
|