cultcache-ts 0.14.2: version, CHANGELOG, README persistence model, store-composition contract

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-20 14:54:14 +02:00
commit ed432b59da
5 changed files with 37 additions and 9 deletions

2
package-lock.json generated
View file

@ -1184,7 +1184,7 @@
},
"packages/cultcache-ts": {
"name": "@gamecult/cultcache-ts",
"version": "0.14.1",
"version": "0.14.2",
"license": "MIT",
"dependencies": {
"@msgpack/msgpack": "^3.1.3",

View file

@ -3,6 +3,26 @@
All notable changes to `@gamecult/cultcache-ts` are recorded here, following
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
## [0.14.2]
### Added
- `SingleFileMessagePackBackingStore` takes a cross-process write lock on `<file>.lock`,
interoperable with the C# and Rust runtimes (`flock` on Unix, `LockFileEx` on Windows).
Each write reads the current file, merges and replaces it under the lock, so concurrent
writers no longer lose each other's records.
- The staged file is fsynced before the replace, and on Unix the directory after it. A
directory sync failure after the replace is reported as replaced but not durable.
### Changed
- The Windows replace is a POSIX-semantics rename first (it lands while C# and Rust readers
hold the store open), falling back to a write-through `MoveFileExW`, with 25 attempts 20 ms
apart on errors 5, 32 and 33. Writers wait up to 30 s for the lock.
- Opening a legacy store that needs healing rewrites it under the lock, from a read taken
under the lock.
### Dependencies
- `koffi` 3.3.0, loaded only on the first write.
## [0.14.1]
### Added

View file

@ -358,7 +358,8 @@ That is the TypeScript version of the original CultCache trick:
- each record is `[key, schemaId, storedAt, payload]`
- each catalog entry is `[schemaId, schemaName, schemaVersion, contentHash, canonicalSchemaJson, compatibleSchemaIds, members]`
- it still reads the older TS/Rust-style envelope array and rewrites repaired legacy payloads as v1 snapshots
- it queues local writes so one process does not step on its own shoelaces
- it queues local writes in order, then takes the shared `<file>.lock` (`flock` on Unix, `LockFileEx` on Windows, via koffi, loaded on the first write) for each one, so C#, Rust and TypeScript writers exclude each other; the read a write merges from is taken under that lock
- it fsyncs the staged file and, on Unix, the directory after the replace; on Windows it replaces with a POSIX-semantics rename and falls back to a write-through `MoveFileExW`, retrying briefly on sharing violations
- it persists raw MessagePack payload bytes inside schema-addressed records
It is a control-plane store, not a tiny god. If multiple processes hammer the same file, you still need a coordinator or an external lock instead of prayer.
It is a control-plane store, not a tiny god: every write rewrites the whole file.

View file

@ -1,6 +1,6 @@
{
"name": "@gamecult/cultcache-ts",
"version": "0.14.1",
"version": "0.14.2",
"description": "CultCache-style typed persistence for TypeScript with schema-backed polymorphic MessagePack storage.",
"main": "dist/index.js",
"types": "dist/index.d.ts",

View file

@ -240,8 +240,13 @@ replaces; writers bump a minted timestamp by one tick when it is not later.
wait bound (30 seconds).
- A conditional commit on a store with staged single-record writes throws:
flush first.
- Single-file stores lock a sidecar `<path>.lock` opened exclusively. Directory
stores use their existing commit lease.
- Single-file stores lock a sidecar `<path>.lock`: C# by an exclusive
(share-none) open, Rust and TypeScript by an exclusive `LockFileEx` on a
shared open on Windows, and every runtime by `flock(LOCK_EX)` on Unix. Each
excludes the others (TypeScript tests: "a TypeScript write waits while a C#
writer holds the store lock", "... a Rust writer ...", "a C# writer waits
while a TypeScript writer holds the store lock", "a Rust writer waits ...").
Directory stores use their existing commit lease.
- A directory store with no `.commit.lock` (no writer has created it yet, or
the store was copied without it) gives a pull no lease, and opening it
creates none. Such a pull re-reads the manifest after loading its pages and
@ -265,9 +270,11 @@ against another writer; processes sharing a store must all use it.
in that store, and leaves the store clean.
- A conditional commit lands the batch onto the file as it is under the lock.
A store is written by one runtime at a time. Whether a Rust `fs2` lock and a C#
exclusive open of the same lock file exclude each other is not established, and
another runtime need not obey the `storedAt` rule.
C#, Rust and TypeScript writers exclude each other on the shared lock file;
Python takes no cross-process lock yet (`variants:follow_up:py-shared-write-lock`),
and Rust's open fails rather than waits while a C# writer holds the lock
(`variants:follow_up:rust-lock-open-waits`). Another runtime need not obey the
`storedAt` rule.
## Serialization options