MCPcopy Create free account
hub / github.com/TraderAlice/OpenAlice / createEntityStore

Function createEntityStore

src/core/entity-store.ts:97–205  ·  view source on GitHub ↗
(opts: EntityStoreOptions = {})

Source from the content-addressed store, hash-verified

95}
96
97export function createEntityStore(opts: EntityStoreOptions = {}): IEntityStore {
98 const filePath = opts.filePath ?? ENTITY_FILE
99 const emitter = new EventEmitter()
100 emitter.setMaxListeners(50)
101
102 // Serialize mutating ops so each read-modify-write-rename cycle is atomic for
103 // this store instance. Without it, concurrent upserts — which Pi triggers by
104 // running tool calls in PARALLEL — race on the shared `${filePath}.tmp`,
105 // interleaving bytes and corrupting the file (observed in the wild as
106 // "Unexpected token ','" on the next read), and clobber each other's writes.
107 let writeTail: Promise<unknown> = Promise.resolve()
108 function serialize<T>(op: () => Promise<T>): Promise<T> {
109 const result = writeTail.then(op, op)
110 writeTail = result.then(() => undefined, () => undefined)
111 return result
112 }
113
114 async function readAll(): Promise<Entity[]> {
115 let raw: string
116 try {
117 raw = await readFile(filePath, 'utf-8')
118 } catch (err: unknown) {
119 if (err instanceof Error && 'code' in err && (err as NodeJS.ErrnoException).code === 'ENOENT') {
120 return []
121 }
122 throw err
123 }
124 const out: Entity[] = []
125 for (const line of raw.split('\n')) {
126 if (!line.trim()) continue
127 try {
128 out.push(JSON.parse(line) as Entity)
129 } catch {
130 // Tolerate a malformed line instead of bricking every entity op. A
131 // corrupted line (e.g. left by a pre-fix concurrent-write interleave)
132 // is skipped here and dropped on the next atomic rewrite — self-healing.
133 console.warn('entity-store: skipping malformed line in', filePath)
134 }
135 }
136 return out
137 }
138
139 async function writeAll(entities: Entity[]): Promise<void> {
140 await mkdir(dirname(filePath), { recursive: true })
141 const body = entities.length > 0 ? entities.map((e) => JSON.stringify(e)).join('\n') + '\n' : ''
142 // Atomic rewrite — tmp + rename, same as InboxStore.delete. Current-state
143 // file (deduped by name), not an append log, so upsert is a read-modify-write.
144 const tmp = `${filePath}.tmp`
145 await writeFile(tmp, body, 'utf-8')
146 await rename(tmp, filePath)
147 }
148
149 async function upsert(input: EntityInput): Promise<Entity> {
150 validateInput(input) // sync — fail fast, outside the write queue
151 return serialize(async () => {
152 const all = await readAll()
153 const k = keyOf(input.name)
154 const idx = all.findIndex((e) => keyOf(e.name) === k)

Callers 2

mainFunction · 0.85

Calls 1

resolveMethod · 0.45

Tested by

no test coverage detected