feat: extract @uncaged/pulse-local package, remove createStore from core exports
CI / test (push) Has been cancelled
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:
@@ -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"],
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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';
|
||||||
@@ -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();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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"]
|
||||||
|
}
|
||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
],
|
],
|
||||||
|
|||||||
Vendored
-2
@@ -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,
|
||||||
|
|||||||
@@ -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 ─────────────────────────────────────────────
|
||||||
|
|||||||
@@ -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('.');
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
Reference in New Issue
Block a user