| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import { backupInProgressError } from './backup.js'; |
| import { encodeFrame, encodeBatchOps, scanBatchOpRefs, HEADER_SIZE, TYPE_SET, TYPE_DEL, TYPE_BATCH } from './codec.js'; |
| import type { BatchOp as EncodedBatchOp, FrameRef } from './codec.js'; |
| import { frameToOps } from './recovery.js'; |
| import type { ValueMode, RecoveredOp } from './recovery.js'; |
| import { yieldToLoop } from './text-index/tokenize.js'; |
| import { toBuf, toKStr, normDt, MAX_KEY_LEN } from './value-codec.js'; |
| import type { Store, StoreRecord, ValueLoc } from './store.js'; |
| import type { WAL } from './wal.js'; |
| import type { IndexManager } from './index-manager.js'; |
| import type { DtIndex } from './dt-index.js'; |
| import type { CompoundIndexManager } from './compound-index.js'; |
| import type { OpTracker } from './op-tracker.js'; |
| import type { WalGroupTracker } from './wal-group.js'; |
| import type { MemoryGuard } from './memory-guard.js'; |
| import type { TextRegistry } from './text-registry.js'; |
| import type { GenerationBuilder } from './generation-builder.js'; |
| import type { TextIndex } from './text-index/index.js'; |
| import type { SetOptions, BatchInputOp, PreparedOp, ValueCodecName } from './types.js'; |
|
|
| |
| |
| export interface WritePathStats { |
| evictions: number; |
| compactionRotationPauseMs: number; |
| } |
|
|
| |
| export interface WritePathDeps<V> { |
| store: () => Store; |
| wal: () => WAL; |
| valueMode: () => ValueMode; |
| codecName: () => ValueCodecName; |
| |
| rotateLock: () => Promise<void> | null; |
| dt: DtIndex; |
| indexes: IndexManager; |
| compound: CompoundIndexManager; |
| textRegistry: TextRegistry<V>; |
| walGroups: WalGroupTracker; |
| memoryGuard: MemoryGuard<V>; |
| generationBuilder: GenerationBuilder<V>; |
| writeOps: OpTracker; |
| serializeUniqueWrites: <T>(fn: () => Promise<T>) => Promise<T>; |
| stats: WritePathStats; |
| encode: (v: V) => Buffer; |
| decode: (b: Buffer | undefined) => V | undefined; |
| indexable: (v: unknown) => v is Record<string, unknown>; |
| ensureOpen: () => void; |
| ensureWritable: () => void; |
| maybeAutoCompact: () => void; |
| } |
|
|
| export class WritePath<V> { |
| |
| |
| |
| |
| private readonly applyBox: { prev: StoreRecord | undefined } = { prev: undefined }; |
|
|
| constructor(private readonly deps: WritePathDeps<V>) {} |
|
|
| |
| |
| |
| private async awaitRotation(): Promise<void> { |
| const rl = this.deps.rotateLock(); |
| if (!rl) return; |
| const t0 = performance.now(); |
| await rl; |
| this.deps.stats.compactionRotationPauseMs += performance.now() - t0; |
| } |
|
|
| private hasUniqueIndexes(): boolean { |
| |
| |
| |
| return this.deps.indexes.hasUnique(); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| private async retryOnWalSeal(commit: () => Promise<void>): Promise<void> { |
| try { |
| await commit(); |
| } catch (e) { |
| const sealed = (e as { code?: string }).code === 'WAL_SEALED'; |
| const closedMidRotation = |
| this.deps.rotateLock() !== null && e instanceof Error && e.message === 'WAL is closed'; |
| if (!sealed && !closedMidRotation) throw e; |
| await this.awaitRotation(); |
| await commit(); |
| } |
| } |
|
|
| async evictKey(pk: string): Promise<void> { |
| const bytes = this.deps.store().recordBytes(pk); |
| if (!bytes) return; |
| const op = this.prepareDel(Buffer.from(pk, 'binary')); |
| |
| |
| |
| |
| |
| |
| const commit = async (): Promise<void> => { |
| const recoveryGate = this.deps.walGroups.walRecoveryGate(); |
| if (recoveryGate) await recoveryGate; |
| const wal = this.deps.wal(); |
| const appended = wal.appendLoc(encodeFrame({ type: TYPE_DEL, key: op.key })); |
| const group = this.deps.walGroups.groupFor(wal, appended.batchId); |
| const applied = this.applyBox; |
| let prev: StoreRecord | undefined; |
| let seq: number | undefined; |
| try { |
| this.applyOp(op, applied); |
| prev = applied.prev; |
| seq = this.deps.store().map.get(op.pk)?.seq; |
| } catch (err) { |
| |
| void appended.done.catch(() => {}); |
| if (group) { |
| wal.poisonPending(err); |
| this.deps.walGroups.groupNoteKey(group, op.pk, applied.prev); |
| this.deps.walGroups.rollbackGroup(group, wal, appended.batchId); |
| this.deps.walGroups.kickWalRecovery(wal); |
| } else { |
| this.restoreGroupKey(op.pk, applied.prev); |
| } |
| throw this.deps.walGroups.markAmbiguous(err); |
| } |
| this.deps.walGroups.groupNoteKey(group, op.pk, prev); |
| try { |
| await appended.done; |
| this.deps.stats.evictions++; |
| } catch (e) { |
| if (group) this.deps.walGroups.rollbackGroup(group, wal, appended.batchId); |
| else this.restoreKey(op.pk, prev, seq); |
| this.deps.walGroups.kickWalRecovery(wal); |
| throw this.deps.walGroups.markAmbiguous(e); |
| } |
| this.deps.walGroups.settleGroup(group, wal, appended.batchId); |
| }; |
| await this.retryOnWalSeal(commit); |
| } |
|
|
| private checkKey(key: string | Buffer): void { |
| const len = typeof key === 'string' ? key.length : Buffer.from(key).length; |
| if (len > MAX_KEY_LEN) throw new RangeError(`key too long (>${MAX_KEY_LEN})`); |
| if ((typeof key === 'string' && key.length === 0) || (Buffer.isBuffer(key) && key.length === 0)) { |
| throw new RangeError('key must be non-empty'); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| private publishWalRef( |
| pk: string, |
| wal: WAL, |
| seq: number | undefined, |
| loc: ValueLoc, |
| expireAt: number, |
| dt: Record<string, number> | null, |
| ): void { |
| if (this.deps.wal() !== wal || seq === undefined) return; |
| const cur = this.deps.store().map.get(pk); |
| if (!cur || cur.seq !== seq) return; |
| this.deps.store().setRef(pk, { kind: 'disk', loc }, expireAt, dt); |
| } |
|
|
| async set(key: string | Buffer, value: V, { ttl, dt }: SetOptions = {}): Promise<void> { |
| this.deps.ensureOpen(); |
| this.deps.ensureWritable(); |
| this.checkKey(key); |
| if (!this.deps.writeOps.enter()) throw backupInProgressError(); |
| try { |
| await this.awaitRotation(); |
| |
| |
| |
| |
| |
| |
| |
| |
| const run = async (): Promise<void> => { |
| const op = this.prepareSet(key, value, { ttl, dt }); |
| if (this.deps.indexes.size && this.deps.indexable(op.canonical)) this.deps.indexes.checkUnique(op.pk, op.canonical); |
| await this.deps.memoryGuard.ensureMemoryFor([op]); |
| await this.retryOnWalSeal(() => this.commitSetOp(op)); |
| }; |
| if (this.hasUniqueIndexes()) await this.deps.serializeUniqueWrites(run); |
| else await run(); |
| } finally { |
| this.deps.writeOps.leave(); |
| } |
| } |
|
|
| |
| |
| private async commitSetOp(op: PreparedOp<V>): Promise<void> { |
| |
| |
| |
| const recoveryGate = this.deps.walGroups.walRecoveryGate(); |
| if (recoveryGate) await recoveryGate; |
| const frame = encodeFrame({ type: TYPE_SET, key: op.key, value: op.value, meta: op.meta, expireAt: op.expireAt }); |
| const wal = this.deps.wal(); |
| const appended = wal.appendLoc(frame); |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const group = this.deps.walGroups.groupFor(wal, appended.batchId); |
| const applied = this.applyBox; |
| let prev: StoreRecord | undefined; |
| let seq: number | undefined; |
| try { |
| this.applyOp(op, applied); |
| |
| |
| prev = applied.prev; |
| seq = this.deps.store().map.get(op.pk)?.seq; |
| } catch (err) { |
| |
| |
| |
| |
| |
| |
| void appended.done.catch(() => {}); |
| if (group) { |
| wal.poisonPending(err); |
| this.deps.walGroups.groupNoteKey(group, op.pk, applied.prev); |
| this.deps.walGroups.rollbackGroup(group, wal, appended.batchId); |
| this.deps.walGroups.kickWalRecovery(wal); |
| } else { |
| this.restoreGroupKey(op.pk, applied.prev); |
| } |
| throw this.deps.walGroups.markAmbiguous(err); |
| } |
| this.deps.walGroups.groupNoteKey(group, op.pk, prev); |
| try { |
| await appended.done; |
| } catch (e) { |
| if (group) this.deps.walGroups.rollbackGroup(group, wal, appended.batchId); |
| else this.restoreKey(op.pk, prev, seq); |
| this.deps.walGroups.kickWalRecovery(wal); |
| throw this.deps.walGroups.markAmbiguous(e); |
| } |
| this.deps.walGroups.settleGroup(group, wal, appended.batchId); |
| if (this.deps.valueMode() === 'disk') { |
| this.publishWalRef( |
| op.pk, |
| wal, |
| seq, |
| { file: 'wal', off: appended.offset + HEADER_SIZE + op.key.length, len: op.value!.length }, |
| op.expireAt, |
| op.dtNorm, |
| ); |
| } |
| this.deps.maybeAutoCompact(); |
| } |
|
|
| async del(key: string | Buffer): Promise<boolean> { |
| this.deps.ensureOpen(); |
| this.deps.ensureWritable(); |
| if (!this.deps.writeOps.enter()) throw backupInProgressError(); |
| try { |
| await this.awaitRotation(); |
| const existed = this.deps.store().has(toKStr(key)); |
| if (!existed) return false; |
| const op = this.prepareDel(key); |
| await this.deps.memoryGuard.ensureMemoryFor([op]); |
| const commit = async (): Promise<void> => { |
| const recoveryGate = this.deps.walGroups.walRecoveryGate(); |
| if (recoveryGate) await recoveryGate; |
| const wal = this.deps.wal(); |
| const appended = wal.appendLoc(encodeFrame({ type: TYPE_DEL, key: op.key })); |
| const group = this.deps.walGroups.groupFor(wal, appended.batchId); |
| const applied = this.applyBox; |
| let prev: StoreRecord | undefined; |
| let seq: number | undefined; |
| try { |
| this.applyOp(op, applied); |
| prev = applied.prev; |
| seq = this.deps.store().map.get(op.pk)?.seq; |
| } catch (err) { |
| |
| void appended.done.catch(() => {}); |
| if (group) { |
| wal.poisonPending(err); |
| this.deps.walGroups.groupNoteKey(group, op.pk, applied.prev); |
| this.deps.walGroups.rollbackGroup(group, wal, appended.batchId); |
| this.deps.walGroups.kickWalRecovery(wal); |
| } else { |
| this.restoreGroupKey(op.pk, applied.prev); |
| } |
| throw this.deps.walGroups.markAmbiguous(err); |
| } |
| this.deps.walGroups.groupNoteKey(group, op.pk, prev); |
| try { |
| await appended.done; |
| } catch (e) { |
| if (group) this.deps.walGroups.rollbackGroup(group, wal, appended.batchId); |
| else this.restoreKey(op.pk, prev, seq); |
| this.deps.walGroups.kickWalRecovery(wal); |
| throw this.deps.walGroups.markAmbiguous(e); |
| } |
| this.deps.walGroups.settleGroup(group, wal, appended.batchId); |
| this.deps.maybeAutoCompact(); |
| }; |
| await this.retryOnWalSeal(commit); |
| return true; |
| } finally { |
| this.deps.writeOps.leave(); |
| } |
| } |
|
|
| |
| async batch(ops: readonly BatchInputOp<V>[]): Promise<void> { |
| this.deps.ensureOpen(); |
| this.deps.ensureWritable(); |
| if (!this.deps.writeOps.enter()) throw backupInProgressError(); |
| try { |
| await this.awaitRotation(); |
| if (!ops || ops.length === 0) return; |
| |
| |
| |
| |
| |
| const run = async (): Promise<void> => { |
| const prepared = ops.map((o) => this.prepareOp(o)); |
| if (this.deps.indexes.size) { |
| this.deps.indexes.checkUniqueBatch( |
| prepared.map((o) => ({ |
| pk: o.pk, |
| op: o.type === TYPE_DEL ? ('del' as const) : ('set' as const), |
| doc: o.canonical, |
| })), |
| ); |
| } |
| await this.deps.memoryGuard.ensureMemoryFor(prepared); |
| await this.retryOnWalSeal(() => this.commitBatchOps(prepared)); |
| }; |
| if (this.hasUniqueIndexes()) await this.deps.serializeUniqueWrites(run); |
| else await run(); |
| } finally { |
| this.deps.writeOps.leave(); |
| } |
| } |
|
|
| |
| |
| private async commitBatchOps(prepared: readonly PreparedOp<V>[]): Promise<void> { |
| const recoveryGate = this.deps.walGroups.walRecoveryGate(); |
| if (recoveryGate) await recoveryGate; |
| const body = encodeBatchOps( |
| prepared.map<EncodedBatchOp>((op) => ({ type: op.type, key: op.key, value: op.value, meta: op.meta, expireAt: op.expireAt })), |
| ); |
| const frame = encodeFrame({ type: TYPE_BATCH, key: Buffer.alloc(0), value: body }); |
| const wal = this.deps.wal(); |
| const appended = wal.appendLoc(frame); |
| const group = this.deps.walGroups.groupFor(wal, appended.batchId); |
| |
| |
| const prevs = new Map<string, StoreRecord | undefined>(); |
| const applied = this.applyBox; |
| let cur: PreparedOp<V> | null = null; |
| try { |
| for (const op of prepared) { |
| cur = op; |
| this.applyOp(op, applied); |
| if (!prevs.has(op.pk)) prevs.set(op.pk, applied.prev); |
| } |
| } catch (err) { |
| |
| |
| if (cur && !prevs.has(cur.pk)) prevs.set(cur.pk, applied.prev); |
| void appended.done.catch(() => {}); |
| if (group) { |
| wal.poisonPending(err); |
| for (const [pk, p] of prevs) this.deps.walGroups.groupNoteKey(group, pk, p); |
| this.deps.walGroups.rollbackGroup(group, wal, appended.batchId); |
| this.deps.walGroups.kickWalRecovery(wal); |
| } else { |
| for (const [pk, p] of prevs) this.restoreGroupKey(pk, p); |
| } |
| throw this.deps.walGroups.markAmbiguous(err); |
| } |
| for (const [pk, p] of prevs) this.deps.walGroups.groupNoteKey(group, pk, p); |
| |
| |
| |
| |
| const seqs = new Map<string, number | undefined>(); |
| for (const pk of prevs.keys()) seqs.set(pk, this.deps.store().map.get(pk)?.seq); |
| |
| |
| |
| |
| const lastSet = new Map<string, { op: PreparedOp<V>; loc: ValueLoc; seq: number | undefined }>(); |
| if (this.deps.valueMode() === 'disk') { |
| const bodyOff = appended.offset + HEADER_SIZE; |
| const opRefs = scanBatchOpRefs(body, 0); |
| for (let i = 0; i < prepared.length; i++) { |
| const op = prepared[i]!; |
| const ref = opRefs[i]; |
| if (op.type === TYPE_SET && ref) { |
| lastSet.set(op.pk, { op, loc: { file: 'wal', off: bodyOff + ref.valueOff, len: ref.valLen }, seq: seqs.get(op.pk) }); |
| } |
| } |
| } |
| try { |
| await appended.done; |
| } catch (e) { |
| if (group) this.deps.walGroups.rollbackGroup(group, wal, appended.batchId); |
| else for (const [pk, prev] of prevs) this.restoreKey(pk, prev, seqs.get(pk)); |
| this.deps.walGroups.kickWalRecovery(wal); |
| throw this.deps.walGroups.markAmbiguous(e); |
| } |
| this.deps.walGroups.settleGroup(group, wal, appended.batchId); |
| for (const [pk, { op, loc, seq }] of lastSet) { |
| this.publishWalRef(pk, wal, seq, loc, op.expireAt, op.dtNorm); |
| } |
| this.deps.maybeAutoCompact(); |
| } |
|
|
| private prepareOp(o: BatchInputOp<V>): PreparedOp<V> { |
| if (o.op === 'set') return this.prepareSet(o.key, o.value, { ttl: o.ttl, dt: o.dt }); |
| if (o.op === 'del') return this.prepareDel(o.key); |
| throw new TypeError(`unknown batch op: ${(o as { op: string }).op}`); |
| } |
|
|
| private prepareSet(key: string | Buffer, value: V, { ttl, dt }: SetOptions = {}): PreparedOp<V> { |
| this.checkKey(key); |
| const pk = toKStr(key); |
| const dtNorm = normDt(dt); |
| |
| |
| |
| |
| |
| if (ttl !== undefined && !Number.isFinite(ttl)) throw new RangeError('ttl must be a finite number of milliseconds'); |
| const expireAt = ttl ? Date.now() + Math.floor(ttl) : 0; |
| const vbuf = this.deps.encode(value); |
| |
| |
| |
| |
| |
| const canonical = this.deps.codecName() === 'json' ? (this.deps.decode(vbuf) as V) : value; |
| |
| |
| |
| let textTokens: Map<TextIndex, readonly string[] | null> | null = null; |
| if (this.deps.textRegistry.text.size) { |
| textTokens = new Map(); |
| for (const ti of this.deps.textRegistry.text.values()) { |
| textTokens.set(ti, this.deps.indexable(canonical) ? ti.prepareAdd(canonical) : null); |
| } |
| } |
| const meta = dtNorm ? Buffer.from(JSON.stringify({ dt: dtNorm })) : null; |
| return { type: TYPE_SET, key: toBuf(key), value: vbuf, meta, expireAt, dtNorm, pk, canonical, textTokens }; |
| } |
|
|
| private prepareDel(key: string | Buffer): PreparedOp<V> { |
| this.checkKey(key); |
| return { |
| type: TYPE_DEL, |
| key: toBuf(key), |
| value: null, |
| meta: null, |
| expireAt: 0, |
| dtNorm: null, |
| pk: toKStr(key), |
| canonical: undefined, |
| textTokens: null, |
| }; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| private applyOp(op: PreparedOp<V>, out: { prev: StoreRecord | undefined }): void { |
| const oldBuf = this.deps.store().get(op.pk); |
| out.prev = oldBuf !== undefined ? this.deps.store().map.get(op.pk) : undefined; |
| const oldDoc = oldBuf !== undefined ? this.deps.decode(oldBuf) : undefined; |
| if (op.type === TYPE_SET) { |
| |
| |
| |
| this.deps.store().set(op.key, op.value!, op.expireAt, op.dtNorm); |
| this.deps.dt.set(op.pk, op.dtNorm); |
| this.deps.compound.add(op.pk, op.canonical, op.dtNorm); |
| if (this.deps.indexes.size) { |
| if (this.deps.indexable(oldDoc)) this.deps.indexes.remove(op.pk, oldDoc); |
| if (this.deps.indexable(op.canonical)) this.deps.indexes.add(op.pk, op.canonical); |
| } |
| for (const ti of this.deps.textRegistry.text.values()) { |
| const tokens = op.textTokens?.get(ti); |
| if (tokens !== undefined) { |
| |
| |
| if (tokens === null) ti.remove(op.pk); |
| else ti.addPrepared(op.pk, tokens); |
| } else if (this.deps.indexable(op.canonical)) { |
| |
| |
| |
| |
| |
| ti.add(op.pk, op.canonical); |
| } else { |
| ti.remove(op.pk); |
| } |
| } |
| } else if (op.type === TYPE_DEL) { |
| const existed = this.deps.store().del(op.key); |
| if (existed) { |
| this.deps.memoryGuard.access.delete(op.pk); |
| this.deps.dt.del(op.pk); |
| this.deps.compound.remove(op.pk); |
| if (this.deps.indexes.size && this.deps.indexable(oldDoc)) this.deps.indexes.remove(op.pk, oldDoc); |
| for (const ti of this.deps.textRegistry.text.values()) ti.remove(op.pk); |
| } |
| } |
| |
| |
| |
| |
| const gb = this.deps.generationBuilder.genBuild; |
| if (gb) { |
| gb.queue.push({ |
| type: op.type, |
| pk: op.pk, |
| value: op.value, |
| expireAt: op.expireAt, |
| dtNorm: op.dtNorm, |
| canonical: op.canonical, |
| }); |
| gb.bytes += (op.value ? op.value.length : 0) + 64; |
| } |
| if (op.type === TYPE_SET) this.deps.memoryGuard.touchAccess(op.pk); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| private restoreKey(pk: string, prev: StoreRecord | undefined, appliedSeq: number | undefined): void { |
| const cur = this.deps.store().map.get(pk); |
| if (appliedSeq === undefined ? cur !== undefined : cur?.seq !== appliedSeq) return; |
| this.restoreGroupKey(pk, prev); |
| } |
|
|
| |
| |
| |
| restoreGroupKey(pk: string, prev: StoreRecord | undefined): void { |
| |
| |
| |
| const gb = this.deps.generationBuilder.genBuild; |
| if (gb) gb.aborted = true; |
| if (this.deps.indexes.size) this.deps.indexes.remove(pk, undefined); |
| for (const ti of this.deps.textRegistry.text.values()) ti.remove(pk); |
| this.deps.dt.del(pk); |
| this.deps.compound.remove(pk); |
| if (prev === undefined) { |
| this.deps.store().del(pk); |
| this.deps.memoryGuard.access.delete(pk); |
| return; |
| } |
| this.deps.store().setRef(pk, prev.ref, prev.expireAt, prev.dt); |
| this.deps.memoryGuard.touchAccess(pk); |
| const doc = this.deps.decode(this.deps.store().get(pk)); |
| this.deps.dt.set(pk, prev.dt); |
| this.deps.compound.add(pk, doc, prev.dt); |
| if (this.deps.indexable(doc)) this.deps.indexes.add(pk, doc); |
| for (const ti of this.deps.textRegistry.text.values()) { |
| if (this.deps.indexable(doc)) ti.add(pk, doc); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| async applyRecoveredFrameAsync(f: FrameRef, fd: number, slice: () => boolean): Promise<void> { |
| for (const op of frameToOps(f, 'wal', fd, this.deps.valueMode())) { |
| this.applyRecoveredOp(op); |
| if (slice()) await yieldToLoop(); |
| } |
| } |
|
|
| applyRecoveredOp(op: RecoveredOp): void { |
| const pk = toKStr(op.key); |
| |
| |
| |
| const oldDoc = this.deps.indexes.size ? this.deps.decode(this.deps.store().get(pk)) : undefined; |
| if (op.type === TYPE_DEL) { |
| if (!this.deps.store().del(pk)) return; |
| this.deps.memoryGuard.access.delete(pk); |
| this.deps.dt.del(pk); |
| this.deps.compound.remove(pk); |
| if (this.deps.indexes.size && this.deps.indexable(oldDoc)) this.deps.indexes.remove(pk, oldDoc); |
| for (const ti of this.deps.textRegistry.text.values()) ti.remove(pk); |
| return; |
| } |
| this.deps.store().setRef(op.key, op.ref!, op.expireAt, op.dt); |
| |
| |
| |
| |
| |
| const buf = this.deps.store().get(pk); |
| if (buf === undefined) return; |
| this.deps.dt.set(pk, op.dt); |
| |
| |
| if (this.deps.indexes.size || this.deps.textRegistry.text.size || this.deps.compound.size) { |
| const doc = this.deps.decode(buf)!; |
| this.deps.compound.add(pk, doc, op.dt); |
| if (this.deps.indexes.size) { |
| if (this.deps.indexable(oldDoc)) this.deps.indexes.remove(pk, oldDoc); |
| if (this.deps.indexable(doc)) this.deps.indexes.add(pk, doc); |
| } |
| for (const ti of this.deps.textRegistry.text.values()) { |
| if (this.deps.indexable(doc)) ti.add(pk, doc); |
| else ti.remove(pk); |
| } |
| } |
| this.deps.memoryGuard.touchAccess(pk); |
| } |
|
|
| async expire(key: string | Buffer, ttlMs: number): Promise<boolean> { |
| this.deps.ensureOpen(); |
| this.deps.ensureWritable(); |
| if (!this.deps.writeOps.enter()) throw backupInProgressError(); |
| try { |
| await this.awaitRotation(); |
| const k = toKStr(key); |
| const cur = this.deps.store().getRecord(k); |
| if (cur === undefined) return false; |
| |
| |
| if (!Number.isFinite(ttlMs)) throw new RangeError('ttl must be a finite number of milliseconds'); |
| const expireAt = Date.now() + Math.floor(ttlMs); |
| const curValue = this.deps.store().get(k); |
| if (curValue === undefined) return false; |
| const meta = cur.dt ? Buffer.from(JSON.stringify({ dt: cur.dt })) : null; |
| const keyBuf = toBuf(key); |
| const frame = encodeFrame({ type: TYPE_SET, key: keyBuf, value: curValue, meta, expireAt }); |
| const commit = async (): Promise<void> => { |
| const recoveryGate = this.deps.walGroups.walRecoveryGate(); |
| if (recoveryGate) await recoveryGate; |
| const wal = this.deps.wal(); |
| const appended = wal.appendLoc(frame); |
| const group = this.deps.walGroups.groupFor(wal, appended.batchId); |
| |
| |
| |
| |
| const prev = this.deps.store().map.get(k); |
| let seq: number | undefined; |
| try { |
| this.deps.store().set(k, curValue, expireAt, cur.dt); |
| |
| |
| |
| const gb = this.deps.generationBuilder.genBuild; |
| if (gb) { |
| gb.queue.push({ |
| type: TYPE_SET, |
| pk: k, |
| value: curValue, |
| expireAt, |
| dtNorm: cur.dt, |
| canonical: undefined, |
| storeOnly: true, |
| }); |
| gb.bytes += curValue.length + 64; |
| } |
| seq = this.deps.store().map.get(k)?.seq; |
| } catch (err) { |
| |
| |
| |
| void appended.done.catch(() => {}); |
| if (group) { |
| wal.poisonPending(err); |
| this.deps.walGroups.groupNoteKey(group, k, prev); |
| this.deps.walGroups.rollbackGroup(group, wal, appended.batchId); |
| this.deps.walGroups.kickWalRecovery(wal); |
| } else { |
| this.restoreGroupKey(k, prev); |
| } |
| throw this.deps.walGroups.markAmbiguous(err); |
| } |
| this.deps.walGroups.groupNoteKey(group, k, prev); |
| try { |
| await appended.done; |
| } catch (e) { |
| if (group) this.deps.walGroups.rollbackGroup(group, wal, appended.batchId); |
| else this.restoreKey(k, prev, seq); |
| this.deps.walGroups.kickWalRecovery(wal); |
| throw this.deps.walGroups.markAmbiguous(e); |
| } |
| this.deps.walGroups.settleGroup(group, wal, appended.batchId); |
| if (this.deps.valueMode() === 'disk') { |
| this.publishWalRef( |
| k, |
| wal, |
| seq, |
| { file: 'wal', off: appended.offset + HEADER_SIZE + keyBuf.length, len: curValue.length }, |
| expireAt, |
| cur.dt, |
| ); |
| } |
| this.deps.maybeAutoCompact(); |
| }; |
| await this.retryOnWalSeal(commit); |
| return true; |
| } finally { |
| this.deps.writeOps.leave(); |
| } |
| } |
| } |
|
|