feat: extract @uncaged/pulse-local package, remove createStore from core exports
CI / test (push) Has been cancelled

Phase 2 of PulseDatabase abstraction:
- Create @uncaged/pulse-local with bun:sqlite implementation
- Core @uncaged/pulse now exports types only for store (no createStore/createScopedStore)
- Update pulse-workflows, upulse to import factories from @uncaged/pulse-local
- All tests passing (267 core, 39 pulse-workflows)
This commit is contained in:
2026-04-20 01:34:46 +00:00
parent 03ee151c72
commit 7993ecc6d6
18 changed files with 859 additions and 26 deletions
+15
View File
@@ -48,6 +48,17 @@
"@uncaged/pulse": ">=0.1.0", "@uncaged/pulse": ">=0.1.0",
}, },
}, },
"packages/pulse-local": {
"name": "@uncaged/pulse-local",
"version": "0.1.0",
"dependencies": {
"@uncaged/pulse": "workspace:*",
},
"devDependencies": {
"bun-types": "latest",
"typescript": "^6.0.2",
},
},
"packages/pulse-openclaw": { "packages/pulse-openclaw": {
"name": "@uncaged/pulse-openclaw", "name": "@uncaged/pulse-openclaw",
"version": "0.1.0", "version": "0.1.0",
@@ -66,6 +77,7 @@
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@uncaged/pulse": "workspace:*", "@uncaged/pulse": "workspace:*",
"@uncaged/pulse-local": "workspace:*",
}, },
"devDependencies": { "devDependencies": {
"bun-types": "^1.3.12", "bun-types": "^1.3.12",
@@ -90,6 +102,7 @@
}, },
"dependencies": { "dependencies": {
"@uncaged/pulse": "workspace:*", "@uncaged/pulse": "workspace:*",
"@uncaged/pulse-local": "workspace:*",
"commander": "^12.0.0", "commander": "^12.0.0",
}, },
"devDependencies": { "devDependencies": {
@@ -264,6 +277,8 @@
"@uncaged/pulse-hermes": ["@uncaged/pulse-hermes@workspace:packages/pulse-hermes"], "@uncaged/pulse-hermes": ["@uncaged/pulse-hermes@workspace:packages/pulse-hermes"],
"@uncaged/pulse-local": ["@uncaged/pulse-local@workspace:packages/pulse-local"],
"@uncaged/pulse-openclaw": ["@uncaged/pulse-openclaw@workspace:packages/pulse-openclaw"], "@uncaged/pulse-openclaw": ["@uncaged/pulse-openclaw@workspace:packages/pulse-openclaw"],
"@uncaged/pulseflare": ["@uncaged/pulseflare@workspace:packages/pulseflare"], "@uncaged/pulseflare": ["@uncaged/pulseflare@workspace:packages/pulseflare"],
+22
View File
@@ -0,0 +1,22 @@
{
"name": "@uncaged/pulse-local",
"version": "0.1.0",
"description": "Pulse local storage — bun:sqlite implementation of PulseStore",
"type": "module",
"main": "src/index.ts",
"types": "src/index.ts",
"scripts": {
"build": "tsc",
"test": "bun test"
},
"keywords": ["pulse", "sqlite", "local", "storage"],
"author": "oc-xiaoju",
"license": "MIT",
"dependencies": {
"@uncaged/pulse": "workspace:*"
},
"devDependencies": {
"bun-types": "latest",
"typescript": "^6.0.2"
}
}
+11
View File
@@ -0,0 +1,11 @@
/**
* @uncaged/pulse-local — Local (bun:sqlite + node:fs) implementation of Pulse storage.
*
* Re-exports all core types from @uncaged/pulse plus local factory functions.
*/
// Re-export everything from core
export * from '@uncaged/pulse';
// Export local factories (override core's type-only createStore/createScopedStore)
export { createStore, createScopedStore } from './store-local.js';
+752
View File
@@ -0,0 +1,752 @@
/**
* @uncaged/pulse-local — Local Storage Implementation (bun:sqlite + node:fs)
*
* Single `events` table with INTEGER AUTOINCREMENT primary keys +
* content-addressed object store (CAS) on disk via SHA-256 hashes.
*/
import { Database } from 'bun:sqlite';
import type { PulseDatabase } from '@uncaged/pulse';
import { createHash } from 'node:crypto';
import {
existsSync,
mkdirSync,
readdirSync,
readFileSync,
writeFileSync,
} from 'node:fs';
import { dirname, join } from 'node:path';
import { initDefsSchema } from '@uncaged/pulse/defs';
import {
applyGuardUpdates,
checkGuards,
initGuardSchema,
} from '@uncaged/pulse/guard-projection';
import { PROJECTIONS_SCHEMA } from '@uncaged/pulse/projection-engine';
import type {
EventRecord,
ObjectInstance,
PulseStore,
CreateStoreOptions,
CreateScopedStoreOptions,
ScopedStore,
} from '@uncaged/pulse';
// ── CAS Hashing ────────────────────────────────────────────────
function hashObject(data: unknown): string {
return createHash('sha256')
.update(JSON.stringify(data))
.digest('hex')
.slice(0, 32); // 32 hex chars = 128 bits, safe against birthday collisions
}
// ── Schema ─────────────────────────────────────────────────────
const EVENTS_SCHEMA = `
CREATE TABLE IF NOT EXISTS events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
occurred_at INTEGER NOT NULL,
kind TEXT NOT NULL,
key TEXT,
hash TEXT,
code_rev TEXT,
meta TEXT
);
CREATE INDEX IF NOT EXISTS idx_occurred ON events(occurred_at);
CREATE INDEX IF NOT EXISTS idx_kind_key ON events(kind, key, occurred_at);
CREATE INDEX IF NOT EXISTS idx_code_rev ON events(code_rev, occurred_at);
`;
const OBJECTS_SCHEMA = `
CREATE TABLE IF NOT EXISTS objects (
id INTEGER PRIMARY KEY AUTOINCREMENT,
object_type TEXT NOT NULL,
external_id TEXT,
created_at INTEGER NOT NULL,
code_rev TEXT NOT NULL,
UNIQUE(object_type, external_id)
);
`;
/** Safe migration: add object_id column to events if it doesn't exist yet. */
function migrateEventsObjectId(db: Database): void {
try {
db.run(
'ALTER TABLE events ADD COLUMN object_id INTEGER REFERENCES objects(id)',
);
} catch (_e) {
// Column already exists — ignore
}
}
// ── Row mapping helper ─────────────────────────────────────────
interface RawRow {
id: number;
occurred_at: number;
kind: string;
key: string | null;
hash: string | null;
code_rev: string | null;
meta: string | null;
object_id: number | null;
}
function rowToRecord(row: RawRow): EventRecord {
const rec: EventRecord = {
id: Number(row.id),
occurredAt: row.occurred_at,
kind: row.kind,
};
if (row.key != null) rec.key = row.key;
if (row.hash != null) rec.hash = row.hash;
if (row.code_rev != null) rec.codeRev = row.code_rev;
if (row.meta != null) rec.meta = row.meta;
if (row.object_id != null) rec.objectId = row.object_id;
return rec;
}
interface RawObjectRow {
id: number;
object_type: string;
external_id: string | null;
created_at: number;
code_rev: string;
}
function rowToObjectInstance(row: RawObjectRow): ObjectInstance {
return {
id: row.id,
objectType: row.object_type,
externalId: row.external_id,
createdAt: row.created_at,
codeRev: row.code_rev,
};
}
async function appendOneWithGuards(
db: Database,
insert: (event: Omit<EventRecord, 'id'>) => EventRecord,
event: Omit<EventRecord, 'id'>,
): Promise<EventRecord> {
const nested = db.inTransaction;
if (!nested) db.exec('BEGIN IMMEDIATE');
try {
const { updates } = await checkGuards(db, event, event.codeRev ?? '');
const written = insert(event);
applyGuardUpdates(
db,
updates.map((u) => ({ ...u, lastEventId: written.id })),
);
if (!nested) db.exec('COMMIT');
return written;
} catch (e) {
if (!nested) db.exec('ROLLBACK');
throw e;
}
}
async function appendManyWithGuards(
db: Database,
insert: (event: Omit<EventRecord, 'id'>) => EventRecord,
events: Omit<EventRecord, 'id'>[],
): Promise<EventRecord[]> {
if (events.length === 0) return [];
const nested = db.inTransaction;
if (!nested) db.exec('BEGIN IMMEDIATE');
try {
const results: EventRecord[] = [];
for (const event of events) {
const { updates } = await checkGuards(db, event, event.codeRev ?? '');
const written = insert(event);
applyGuardUpdates(
db,
updates.map((u) => ({ ...u, lastEventId: written.id })),
);
results.push(written);
}
if (!nested) db.exec('COMMIT');
return results;
} catch (e) {
if (!nested) db.exec('ROLLBACK');
throw e;
}
}
// ── Factory ────────────────────────────────────────────────────
export function createStore(options: CreateStoreOptions): PulseStore {
const { eventsDbPath, objectsDir } = options;
mkdirSync(objectsDir, { recursive: true });
const eventsDb = new Database(eventsDbPath, { create: true });
eventsDb.exec('PRAGMA journal_mode = WAL');
eventsDb.exec('PRAGMA busy_timeout = 5000');
eventsDb.exec(EVENTS_SCHEMA);
eventsDb.exec(OBJECTS_SCHEMA);
migrateEventsObjectId(eventsDb);
initGuardSchema(eventsDb);
const insertEvent = eventsDb.prepare(`
INSERT INTO events (occurred_at, kind, key, hash, code_rev, meta, object_id)
VALUES (?, ?, ?, ?, ?, ?, ?)
`);
const selectLatest = eventsDb.prepare(`
SELECT * FROM events
WHERE kind = ? AND (key = ? OR ? IS NULL)
ORDER BY occurred_at DESC, id DESC
LIMIT 1
`);
const selectHasEvents = eventsDb.prepare(`
SELECT 1 FROM events LIMIT 1
`);
function doAppendEvent(event: Omit<EventRecord, 'id'>): EventRecord {
const result = insertEvent.run(
event.occurredAt,
event.kind,
event.key ?? null,
event.hash ?? null,
event.codeRev ?? null,
event.meta ?? null,
event.objectId ?? null,
);
const id = Number(result.lastInsertRowid);
return { id, ...event };
}
return {
async appendEvent(event: Omit<EventRecord, 'id'>): Promise<EventRecord> {
return appendOneWithGuards(eventsDb, doAppendEvent, event);
},
async appendEvents(
events: Omit<EventRecord, 'id'>[],
): Promise<EventRecord[]> {
return appendManyWithGuards(eventsDb, doAppendEvent, events);
},
async getLatest(kind: string, key?: string): Promise<EventRecord | null> {
const row = selectLatest.get(
kind,
key ?? null,
key ?? null,
) as RawRow | null;
return row ? rowToRecord(row) : null;
},
async getLatestWhere(opts: {
kind: string;
key?: string;
codeRev?: string;
}): Promise<EventRecord | null> {
const conditions: string[] = ['kind = ?'];
const params: (string | number | null)[] = [opts.kind];
if (opts.key !== undefined) {
conditions.push('key = ?');
params.push(opts.key);
}
if (opts.codeRev !== undefined) {
conditions.push('code_rev = ?');
params.push(opts.codeRev);
}
const sql = `SELECT * FROM events WHERE ${conditions.join(' AND ')} ORDER BY occurred_at DESC, id DESC LIMIT 1`;
const row = eventsDb.prepare(sql).get(...params) as RawRow | null;
return row ? rowToRecord(row) : null;
},
async getRecent(limit: number = 20): Promise<EventRecord[]> {
const sql = `SELECT * FROM events ORDER BY occurred_at DESC, id DESC LIMIT ?`;
return (eventsDb.prepare(sql).all(limit) as RawRow[]).map(rowToRecord);
},
async queryByKind(
kind: string,
opts?: {
key?: string;
since?: number;
codeRev?: string;
limit?: number;
},
): Promise<EventRecord[]> {
const conditions: string[] = ['kind = ?'];
const params: (string | number | null)[] = [kind];
if (opts?.key !== undefined) {
conditions.push('key = ?');
params.push(opts.key);
}
if (opts?.since !== undefined) {
conditions.push('occurred_at >= ?');
params.push(opts.since);
}
if (opts?.codeRev !== undefined) {
conditions.push('code_rev = ?');
params.push(opts.codeRev);
}
let sql = `SELECT * FROM events WHERE ${conditions.join(' AND ')} ORDER BY occurred_at DESC, id DESC`;
if (opts?.limit !== undefined) {
sql += ' LIMIT ?';
params.push(opts.limit);
}
return (eventsDb.prepare(sql).all(...params) as RawRow[]).map(
rowToRecord,
);
},
async getAfter(
afterId: number,
opts?: {
kind?: string;
key?: string;
codeRev?: string;
},
): Promise<EventRecord[]> {
const conditions: string[] = ['id > ?'];
const params: (string | number | null)[] = [afterId];
if (opts?.kind !== undefined) {
conditions.push('kind = ?');
params.push(opts.kind);
}
if (opts?.key !== undefined) {
conditions.push('key = ?');
params.push(opts.key);
}
if (opts?.codeRev !== undefined) {
conditions.push('code_rev = ?');
params.push(opts.codeRev);
}
const sql = `SELECT * FROM events WHERE ${conditions.join(' AND ')} ORDER BY id ASC`;
return (eventsDb.prepare(sql).all(...params) as RawRow[]).map(
rowToRecord,
);
},
async hasEvents(): Promise<boolean> {
return selectHasEvents.get() !== null;
},
async createObject(opts: {
objectType: string;
externalId?: string;
codeRev: string;
}): Promise<number> {
const now = Date.now();
const extId = opts.externalId ?? null;
if (extId !== null) {
const existing = eventsDb
.prepare(
'SELECT id FROM objects WHERE object_type = ? AND external_id = ?',
)
.get(opts.objectType, extId) as { id: number } | null;
if (existing) return existing.id;
}
const result = eventsDb
.prepare(
'INSERT INTO objects (object_type, external_id, created_at, code_rev) VALUES (?, ?, ?, ?)',
)
.run(opts.objectType, extId, now, opts.codeRev);
return Number(result.lastInsertRowid);
},
async getObjectInstance(id: number): Promise<ObjectInstance | null> {
const row = eventsDb
.prepare('SELECT * FROM objects WHERE id = ?')
.get(id) as RawObjectRow | null;
return row ? rowToObjectInstance(row) : null;
},
async queryObjectsByType(objectType: string): Promise<ObjectInstance[]> {
return (
eventsDb
.prepare('SELECT * FROM objects WHERE object_type = ?')
.all(objectType) as RawObjectRow[]
).map(rowToObjectInstance);
},
async putObject(data: unknown): Promise<string> {
const hash = hashObject(data);
const filePath = join(objectsDir, `${hash}.json`);
if (!existsSync(filePath)) {
mkdirSync(objectsDir, { recursive: true });
writeFileSync(filePath, JSON.stringify(data), 'utf-8');
}
return hash;
},
async getObject(hash: string): Promise<unknown | null> {
const filePath = join(objectsDir, `${hash}.json`);
if (!existsSync(filePath)) return null;
return JSON.parse(readFileSync(filePath, 'utf-8'));
},
async close(): Promise<void> {
eventsDb.close();
},
getDatabase(): Database {
return eventsDb;
},
async archiveEvents(olderThan: number): Promise<number> {
const result = eventsDb
.prepare('DELETE FROM events WHERE occurred_at < ?')
.run(olderThan);
return result.changes;
},
async downsampleEvents(
kind: string,
key: string,
intervalMs: number,
olderThan: number,
): Promise<number> {
const safeInterval = Math.floor(Math.abs(intervalMs));
if (safeInterval <= 0) return 0;
const stmt = eventsDb.prepare(`
DELETE FROM events WHERE kind = ? AND key = ? AND occurred_at < ? AND id NOT IN (
SELECT id FROM (
SELECT id, ROW_NUMBER() OVER (
PARTITION BY (occurred_at / ${safeInterval}) ORDER BY occurred_at DESC
) as rn FROM events WHERE kind = ? AND key = ? AND occurred_at < ?
) WHERE rn = 1
)
`);
const result = stmt.run(kind, key, olderThan, kind, key, olderThan);
return result.changes;
},
};
}
// ── Scoped Store ──────────────────────────────────────────────
const SCOPE_NAME_RE = /^[a-z0-9_-]{1,64}$/;
function validateScopeName(name: string): void {
if (!SCOPE_NAME_RE.test(name)) {
throw new Error(
`Invalid scope name "${name}": must match [a-z0-9_-] and be 1-64 chars`,
);
}
}
/**
* Open (or create) a scope database at the given path.
* Sets WAL mode and creates the events table and projections table.
*/
function openScopeDb(path: string): Database {
mkdirSync(dirname(path), { recursive: true });
const db = new Database(path, { create: true });
db.exec('PRAGMA journal_mode = WAL');
db.exec('PRAGMA busy_timeout = 5000');
db.exec(EVENTS_SCHEMA);
db.exec(OBJECTS_SCHEMA);
migrateEventsObjectId(db);
db.exec(PROJECTIONS_SCHEMA);
void initDefsSchema(db);
initGuardSchema(db);
return db;
}
function createScopeStore(db: Database, objectsDir: string): PulseStore {
const insertEvent = db.prepare(`
INSERT INTO events (occurred_at, kind, key, hash, code_rev, meta, object_id)
VALUES (?, ?, ?, ?, ?, ?, ?)
`);
const selectLatest = db.prepare(`
SELECT * FROM events
WHERE kind = ? AND (key = ? OR ? IS NULL)
ORDER BY occurred_at DESC, id DESC
LIMIT 1
`);
const selectHasEvents = db.prepare(`
SELECT 1 FROM events LIMIT 1
`);
function doAppendEvent(event: Omit<EventRecord, 'id'>): EventRecord {
const result = insertEvent.run(
event.occurredAt,
event.kind,
event.key ?? null,
event.hash ?? null,
event.codeRev ?? null,
event.meta ?? null,
event.objectId ?? null,
);
const id = Number(result.lastInsertRowid);
return { id, ...event };
}
return {
async appendEvent(event) {
return appendOneWithGuards(db, doAppendEvent, event);
},
async appendEvents(events) {
return appendManyWithGuards(db, doAppendEvent, events);
},
async getLatest(kind, key?) {
const row = selectLatest.get(
kind,
key ?? null,
key ?? null,
) as RawRow | null;
return row ? rowToRecord(row) : null;
},
async getLatestWhere(opts) {
const conditions: string[] = ['kind = ?'];
const params: (string | number | null)[] = [opts.kind];
if (opts.key !== undefined) {
conditions.push('key = ?');
params.push(opts.key);
}
if (opts.codeRev !== undefined) {
conditions.push('code_rev = ?');
params.push(opts.codeRev);
}
const sql = `SELECT * FROM events WHERE ${conditions.join(' AND ')} ORDER BY occurred_at DESC, id DESC LIMIT 1`;
const row = db.prepare(sql).get(...params) as RawRow | null;
return row ? rowToRecord(row) : null;
},
async getRecent(limit = 20) {
const sql = `SELECT * FROM events ORDER BY occurred_at DESC, id DESC LIMIT ?`;
return (db.prepare(sql).all(limit) as RawRow[]).map(rowToRecord);
},
async queryByKind(kind, opts?) {
const conditions: string[] = ['kind = ?'];
const params: (string | number | null)[] = [kind];
if (opts?.key !== undefined) {
conditions.push('key = ?');
params.push(opts.key);
}
if (opts?.since !== undefined) {
conditions.push('occurred_at >= ?');
params.push(opts.since);
}
if (opts?.codeRev !== undefined) {
conditions.push('code_rev = ?');
params.push(opts.codeRev);
}
let sql = `SELECT * FROM events WHERE ${conditions.join(' AND ')} ORDER BY occurred_at DESC, id DESC`;
if (opts?.limit !== undefined) {
sql += ' LIMIT ?';
params.push(opts.limit);
}
return (db.prepare(sql).all(...params) as RawRow[]).map(rowToRecord);
},
async getAfter(afterId: number, opts?) {
const conditions: string[] = ['id > ?'];
const params: (string | number | null)[] = [afterId];
if (opts?.kind !== undefined) {
conditions.push('kind = ?');
params.push(opts.kind);
}
if (opts?.key !== undefined) {
conditions.push('key = ?');
params.push(opts.key);
}
if (opts?.codeRev !== undefined) {
conditions.push('code_rev = ?');
params.push(opts.codeRev);
}
const sql = `SELECT * FROM events WHERE ${conditions.join(' AND ')} ORDER BY id ASC`;
return (db.prepare(sql).all(...params) as RawRow[]).map(rowToRecord);
},
async hasEvents() {
return selectHasEvents.get() !== null;
},
async createObject(opts: {
objectType: string;
externalId?: string;
codeRev: string;
}): Promise<number> {
const now = Date.now();
const extId = opts.externalId ?? null;
if (extId !== null) {
const existing = db
.prepare(
'SELECT id FROM objects WHERE object_type = ? AND external_id = ?',
)
.get(opts.objectType, extId) as { id: number } | null;
if (existing) return existing.id;
}
const result = db
.prepare(
'INSERT INTO objects (object_type, external_id, created_at, code_rev) VALUES (?, ?, ?, ?)',
)
.run(opts.objectType, extId, now, opts.codeRev);
return Number(result.lastInsertRowid);
},
async getObjectInstance(id: number): Promise<ObjectInstance | null> {
const row = db
.prepare('SELECT * FROM objects WHERE id = ?')
.get(id) as RawObjectRow | null;
return row ? rowToObjectInstance(row) : null;
},
async queryObjectsByType(objectType: string): Promise<ObjectInstance[]> {
return (
db
.prepare('SELECT * FROM objects WHERE object_type = ?')
.all(objectType) as RawObjectRow[]
).map(rowToObjectInstance);
},
async putObject(data) {
const hash = hashObject(data);
const filePath = join(objectsDir, `${hash}.json`);
if (!existsSync(filePath)) {
mkdirSync(objectsDir, { recursive: true });
writeFileSync(filePath, JSON.stringify(data), 'utf-8');
}
return hash;
},
async getObject(hash) {
const filePath = join(objectsDir, `${hash}.json`);
if (!existsSync(filePath)) return null;
return JSON.parse(readFileSync(filePath, 'utf-8'));
},
async close() {
db.close();
},
getDatabase(): Database {
return db;
},
async archiveEvents(olderThan: number): Promise<number> {
const result = db
.prepare('DELETE FROM events WHERE occurred_at < ?')
.run(olderThan);
return result.changes;
},
async downsampleEvents(
kind: string,
key: string,
intervalMs: number,
olderThan: number,
): Promise<number> {
const safeInterval = Math.floor(Math.abs(intervalMs));
if (safeInterval <= 0) return 0;
const stmt = db.prepare(`
DELETE FROM events WHERE kind = ? AND key = ? AND occurred_at < ? AND id NOT IN (
SELECT id FROM (
SELECT id, ROW_NUMBER() OVER (
PARTITION BY (occurred_at / ${safeInterval}) ORDER BY occurred_at DESC
) as rn FROM events WHERE kind = ? AND key = ? AND occurred_at < ?
) WHERE rn = 1
)
`);
const result = stmt.run(kind, key, olderThan, kind, key, olderThan);
return result.changes;
},
};
}
export function createScopedStore(
options: CreateScopedStoreOptions,
): ScopedStore {
const { basePath, objectsDir } = options;
mkdirSync(basePath, { recursive: true });
mkdirSync(objectsDir, { recursive: true });
const openStores = new Map<string, PulseStore>();
const openDatabases = new Map<string, Database>();
return {
scope(name: string): PulseStore {
validateScopeName(name);
const existing = openStores.get(name);
if (existing) return existing;
const dbPath = join(basePath, `${name}.db`);
const db = openScopeDb(dbPath);
const store = createScopeStore(db, objectsDir);
openStores.set(name, store);
openDatabases.set(name, db);
return store;
},
scopeDatabase(name: string): Database {
validateScopeName(name);
const existing = openDatabases.get(name);
if (existing) return existing;
const dbPath = join(basePath, `${name}.db`);
const db = openScopeDb(dbPath);
openDatabases.set(name, db);
if (!openStores.has(name)) {
const store = createScopeStore(db, objectsDir);
openStores.set(name, store);
}
return db;
},
listScopes(): string[] {
if (!existsSync(basePath)) return [];
return readdirSync(basePath)
.filter((f) => f.endsWith('.db'))
.map((f) => f.slice(0, -3))
.sort();
},
async putObject(data: unknown): Promise<string> {
const hash = hashObject(data);
const filePath = join(objectsDir, `${hash}.json`);
if (!existsSync(filePath)) {
mkdirSync(objectsDir, { recursive: true });
writeFileSync(filePath, JSON.stringify(data), 'utf-8');
}
return hash;
},
async getObject(hash: string): Promise<unknown | null> {
const filePath = join(objectsDir, `${hash}.json`);
if (!existsSync(filePath)) return null;
return JSON.parse(readFileSync(filePath, 'utf-8'));
},
async close(): Promise<void> {
for (const store of openStores.values()) {
await store.close();
}
for (const db of openDatabases.values()) {
db.close();
}
openStores.clear();
openDatabases.clear();
},
};
}
+17
View File
@@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"types": ["bun-types"],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"outDir": "dist",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"rootDir": "src"
},
"include": ["src"]
}
+2 -1
View File
@@ -11,7 +11,8 @@
"test": "bun test" "test": "bun test"
}, },
"dependencies": { "dependencies": {
"@uncaged/pulse": "workspace:*" "@uncaged/pulse": "workspace:*",
"@uncaged/pulse-local": "workspace:*"
}, },
"devDependencies": { "devDependencies": {
"bun-types": "^1.3.12" "bun-types": "^1.3.12"
@@ -8,7 +8,7 @@ import { afterEach, describe, expect, it } from 'bun:test';
import { mkdtempSync, rmSync } from 'node:fs'; import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os'; import { tmpdir } from 'node:os';
import { join } from 'node:path'; import { join } from 'node:path';
import { createStore, type PulseStore } from '@uncaged/pulse'; import { createStore, type PulseStore } from '@uncaged/pulse-local';
import type { WorkflowMessage } from '@uncaged/pulse'; import type { WorkflowMessage } from '@uncaged/pulse';
describe('coder-cursor role', () => { describe('coder-cursor role', () => {
@@ -8,7 +8,7 @@ import { afterEach, describe, expect, it } from 'bun:test';
import { mkdtempSync, rmSync } from 'node:fs'; import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os'; import { tmpdir } from 'node:os';
import { join } from 'node:path'; import { join } from 'node:path';
import { createStore, type PulseStore } from '@uncaged/pulse'; import { createStore, type PulseStore } from '@uncaged/pulse-local';
import type { WorkflowMessage } from '@uncaged/pulse'; import type { WorkflowMessage } from '@uncaged/pulse';
describe('reviewer-cursor role', () => { describe('reviewer-cursor role', () => {
@@ -8,7 +8,7 @@ import { describe, expect, it } from 'bun:test';
import { mkdtempSync, rmSync } from 'node:fs'; import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os'; import { tmpdir } from 'node:os';
import { join } from 'node:path'; import { join } from 'node:path';
import { createStore, type PulseStore } from '@uncaged/pulse'; import { createStore, type PulseStore } from '@uncaged/pulse-local';
import { createTddCodingWorkflow } from './coding-tdd.js'; import { createTddCodingWorkflow } from './coding-tdd.js';
import { createWorkflowRule } from '@uncaged/pulse'; import { createWorkflowRule } from '@uncaged/pulse';
import { END, START } from '@uncaged/pulse'; import { END, START } from '@uncaged/pulse';
@@ -8,7 +8,7 @@ import { afterEach, describe, expect, it } from 'bun:test';
import { mkdtempSync, rmSync } from 'node:fs'; import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os'; import { tmpdir } from 'node:os';
import { join } from 'node:path'; import { join } from 'node:path';
import { createStore, type PulseStore } from '@uncaged/pulse'; import { createStore, type PulseStore } from '@uncaged/pulse-local';
import { createCodingWorkflow } from './coding.js'; import { createCodingWorkflow } from './coding.js';
import { createWorkflowRule, END } from '@uncaged/pulse'; import { createWorkflowRule, END } from '@uncaged/pulse';
@@ -5,7 +5,7 @@
*/ */
import { describe, expect, test } from 'bun:test'; import { describe, expect, test } from 'bun:test';
import { createStore, type PulseStore } from '@uncaged/pulse'; import { createStore, type PulseStore } from '@uncaged/pulse-local';
import { createReportWorkflow } from './report.js'; import { createReportWorkflow } from './report.js';
import { createWorkflowRule } from '@uncaged/pulse'; import { createWorkflowRule } from '@uncaged/pulse';
+22
View File
@@ -5,6 +5,28 @@
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./defs": {
"types": "./dist/defs.d.ts",
"default": "./dist/defs.js"
},
"./guard-projection": {
"types": "./dist/guard-projection.d.ts",
"default": "./dist/guard-projection.js"
},
"./projection-engine": {
"types": "./dist/projection-engine.d.ts",
"default": "./dist/projection-engine.js"
},
"./store": {
"types": "./dist/store.d.ts",
"default": "./dist/store.js"
}
},
"files": [ "files": [
"dist" "dist"
], ],
-2
View File
@@ -251,8 +251,6 @@ export declare function createRule<S, E, T>(
export { export {
type CreateScopedStoreOptions, type CreateScopedStoreOptions,
type CreateStoreOptions, type CreateStoreOptions,
createScopedStore,
createStore,
type EventRecord, type EventRecord,
type ObjectInstance, type ObjectInstance,
type PulseStore, type PulseStore,
+7 -9
View File
@@ -905,15 +905,13 @@ export type { PulseDatabase, PulseStatement } from './database.js';
// ── Storage ──────────────────────────────────────────────────── // ── Storage ────────────────────────────────────────────────────
export { export type {
type CreateScopedStoreOptions, CreateStoreOptions,
type CreateStoreOptions, CreateScopedStoreOptions,
createScopedStore, EventRecord,
createStore, ObjectInstance,
type EventRecord, PulseStore,
type ObjectInstance, ScopedStore,
type PulseStore,
type ScopedStore,
} from './store.js'; } from './store.js';
// ── Built-in Rules ───────────────────────────────────────────── // ── Built-in Rules ─────────────────────────────────────────────
+1
View File
@@ -17,6 +17,7 @@
}, },
"dependencies": { "dependencies": {
"@uncaged/pulse": "workspace:*", "@uncaged/pulse": "workspace:*",
"@uncaged/pulse-local": "workspace:*",
"commander": "^12.0.0" "commander": "^12.0.0"
}, },
"devDependencies": { "devDependencies": {
@@ -6,7 +6,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
import { mkdirSync, rmSync } from 'node:fs'; import { mkdirSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os'; import { tmpdir } from 'node:os';
import { join } from 'node:path'; import { join } from 'node:path';
import { createStore, type PulseStore } from '@uncaged/pulse'; import { createStore, type PulseStore } from '@uncaged/pulse-local';
function parseWorkflowKind(kind: string): { wf: string; role: string } | null { function parseWorkflowKind(kind: string): { wf: string; role: string } | null {
const i = kind.lastIndexOf('.'); const i = kind.lastIndexOf('.');
+2 -1
View File
@@ -442,7 +442,8 @@ export default collectSystemRule;
} }
function writePulseConfig(enginePath: string): void { function writePulseConfig(enginePath: string): void {
const content = `import { runPulse, createScopedStore } from '@uncaged/pulse'; const content = `import { runPulse } from '@uncaged/pulse';
import { createScopedStore } from '@uncaged/pulse-local';
import type { Snapshot, Effect } from './types.js'; import type { Snapshot, Effect } from './types.js';
import { collectSystem } from './executors/system.js'; import { collectSystem } from './executors/system.js';
import { join } from 'node:path'; import { join } from 'node:path';
+2 -7
View File
@@ -7,13 +7,8 @@
import { existsSync, mkdirSync } from 'node:fs'; import { existsSync, mkdirSync } from 'node:fs';
import { dirname, join } from 'node:path'; import { dirname, join } from 'node:path';
import { import type { EventRecord, PulseStore, ScopedStore } from '@uncaged/pulse';
createScopedStore, import { createScopedStore, createStore } from '@uncaged/pulse-local';
createStore,
type EventRecord,
type PulseStore,
type ScopedStore,
} from '@uncaged/pulse';
import type { UpulseConfig } from './config.js'; import type { UpulseConfig } from './config.js';
export type { EventRecord, PulseStore, ScopedStore }; export type { EventRecord, PulseStore, ScopedStore };