fix(update-seam): the metadata crossing never carries BigInt endpoint ints
resolveVerbEndpointInts mirrors the resolved u64 endpoint ints onto the verb object itself as BigInt (verb.sourceInt/targetInt) for the graph legs' own params. The live verb path's delete legs then reused that same object as the metadata-index crossing — and the seam's metadata is JSON-safe by contract (a native provider serializes it; u64 as Number corrupts above 2^53), so JSON.stringify threw and the whole transaction aborted. Found by the first joint pair gate; four downstream suites red from one crossing. The crossing now routes through a JSON-safe view that drops BigInt-valued top-level keys — endpoint ints ride their own op params on the graph legs, exactly as designed, and never the metadata crossing. Applied at the retraction helper (cascade + unrelate + transact mirrors) and updateRelation's remove leg. Pinned by driving the exact shape (relate resolves ints, remove cascades the same object) through a provider shim enforcing the JSON contract — red-proved against the unfixed path (the joint gate's verbatim error), green with the fix.
This commit is contained in:
parent
f14da34b27
commit
f4780c8e88
2 changed files with 70 additions and 2 deletions
|
|
@ -3831,13 +3831,45 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
|||
* @returns The operation to add to the caller's batch, or `null` when
|
||||
* nothing could be done (already narrated + tracked as degraded).
|
||||
*/
|
||||
/**
|
||||
* @description A JSON-safe view of a record bound for the metadata-index
|
||||
* crossing. The seam's metadata is JSON-safe BY CONTRACT (a native provider
|
||||
* serializes it; u64 ints as Number corrupt above 2^53) — but
|
||||
* {@link resolveVerbEndpointInts} MIRRORS the resolved endpoint ints onto
|
||||
* the verb object itself as BigInt (`verb.sourceInt`/`targetInt`), so a
|
||||
* verb object reused as index metadata carried BigInts into
|
||||
* JSON.stringify, which throws, aborting the whole transaction (found by
|
||||
* the first joint pair gate). Endpoint ints ride their OWN op params on the
|
||||
* graph legs — the metadata crossing drops every BigInt-valued top-level
|
||||
* key instead of guessing at a lossy numeric encoding.
|
||||
* @param metadata - The candidate index-metadata record.
|
||||
* @returns The same object when already JSON-safe, else a shallow copy
|
||||
* without the BigInt-valued keys.
|
||||
*/
|
||||
private static jsonSafeIndexMetadata(metadata: unknown): unknown {
|
||||
if (metadata === null || typeof metadata !== 'object') return metadata
|
||||
const rec = metadata as Record<string, unknown>
|
||||
let hasBigint = false
|
||||
for (const k in rec) {
|
||||
if (typeof rec[k] === 'bigint') { hasBigint = true; break }
|
||||
}
|
||||
if (!hasBigint) return metadata
|
||||
const out: Record<string, unknown> = {}
|
||||
for (const k in rec) {
|
||||
if (typeof rec[k] !== 'bigint') out[k] = rec[k]
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
private metadataIndexRetractionOp(
|
||||
id: string,
|
||||
metadata: unknown,
|
||||
context: string
|
||||
): Operation | null {
|
||||
if (metadata) {
|
||||
return new RemoveFromMetadataIndexOperation(this.metadataIndex, id, metadata, this.indexWriteGeneration)
|
||||
return new RemoveFromMetadataIndexOperation(
|
||||
this.metadataIndex, id, Brainy.jsonSafeIndexMetadata(metadata), this.indexWriteGeneration
|
||||
)
|
||||
}
|
||||
const prov = this.metadataIndex as unknown as {
|
||||
removeEntityById?: (id: string) => Promise<void>
|
||||
|
|
@ -5114,7 +5146,7 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
|||
// read above); `updatedMetadata` is the raw stored record just
|
||||
// persisted — the same shape relate()/rebuild() use to add.
|
||||
tx.addOperation(
|
||||
new RemoveFromMetadataIndexOperation(this.metadataIndex, params.id, existing, this.indexWriteGeneration)
|
||||
new RemoveFromMetadataIndexOperation(this.metadataIndex, params.id, Brainy.jsonSafeIndexMetadata(existing), this.indexWriteGeneration)
|
||||
)
|
||||
tx.addOperation(
|
||||
new AddToMetadataIndexOperation(this.metadataIndex, params.id, updatedMetadata, this.indexWriteGeneration)
|
||||
|
|
|
|||
|
|
@ -187,4 +187,40 @@ describe('verb metadata rows — the live path matches the rebuild walk', () =>
|
|||
|
||||
expect(await index.getIds('tag', 'parity-f')).toEqual([])
|
||||
})
|
||||
|
||||
|
||||
it('the metadata crossing never carries BigInt endpoint ints — a cascade delete after graph resolution survives JSON', async () => {
|
||||
// resolveVerbEndpointInts MIRRORS the resolved u64 ints onto the verb
|
||||
// object as BigInt (verb.sourceInt/targetInt). A provider that JSON-
|
||||
// serializes the metadata crossing dies on BigInt — found by the first
|
||||
// joint pair gate. This pin drives the exact shape: relate (graph legs
|
||||
// resolve ints), then remove the source entity (the cascade passes the
|
||||
// SAME verb object to the retraction), through a provider shim that
|
||||
// enforces the JSON-safety contract the way a native provider does.
|
||||
const employee = await brain.add({ data: 'cascade employee', type: 'person' })
|
||||
const invoice = await brain.add({ data: 'cascade invoice', type: 'document' })
|
||||
await brain.relate({ from: employee, to: invoice, type: 'relatedTo' })
|
||||
const mgr: any = (brain as any).metadataIndex
|
||||
const origRemove = mgr.removeFromIndex.bind(mgr)
|
||||
const seen: unknown[] = []
|
||||
mgr.removeFromIndex = async (id: string, metadata?: unknown, generation?: bigint) => {
|
||||
seen.push(metadata)
|
||||
JSON.stringify(metadata) // the contract: throws on BigInt, exactly like a native crossing
|
||||
return origRemove(id, metadata, generation)
|
||||
}
|
||||
try {
|
||||
await brain.remove(employee) // cascades the relation's retraction
|
||||
} finally {
|
||||
mgr.removeFromIndex = origRemove
|
||||
}
|
||||
expect(seen.length).toBeGreaterThan(0)
|
||||
for (const m of seen) {
|
||||
if (m && typeof m === 'object') {
|
||||
for (const [k, v] of Object.entries(m as Record<string, unknown>)) {
|
||||
expect(typeof v, `metadata key ${k} must be JSON-safe`).not.toBe('bigint')
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue