cultcache-ts: export StoreLinkedError from the package entry; pin lstat errors other than ENOENT are rethrown; changelog
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
b13f5f0ba0
commit
d86f2158f9
3 changed files with 36 additions and 1 deletions
|
|
@ -7,7 +7,7 @@ All notable changes to `@gamecult/cultcache-ts` are recorded here, following
|
|||
|
||||
### Breaking
|
||||
|
||||
- `SingleFileMessagePackBackingStore.pushAll` through a dangling symbolic link at the store path is refused with `ENOENT`, as `push` and `pullAll` are, and writes nothing; it used to replace the link with a new store. The reader owns the verdict on what is at the path.
|
||||
- A write at a single-file store path that is a symbolic link, live or dangling, is refused with the new `StoreLinkedError` (exported from the package entry; `error.path` names the path) before the lock and the replace, and writes nothing: `push`, `pushAll`, `delete` and a soft flush alike. Before, a write at a live link replaced the link with a regular file, a dangling link failed with `ENOENT` (or, for `pushAll`, was replaced), and a soft flush at a link skipped. A caller catching `ENOENT` around a write at a dangling link now catches `StoreLinkedError`; configure the link's target as the store path. Reads through a live link work. An `lstat` failure other than `ENOENT` (`ENOTDIR`, `EACCES`, `ELOOP`) is rethrown as it is.
|
||||
- `SingleFileMessagePackBackingStore.pushAll` writes the given entries onto the store as it is under the lock, as `push` does: a record under the same key is replaced and no other is removed. It used to replace the whole file with the entries, so `CultMesh.flush()` from a cache that held less than the store (a node created with `pullOnStart: false`, or one whose image was stale) wiped the records it never read. A caller that relied on `pushAll` to reset a store removes the file first. A soft flush is unchanged. `CultCache.storedSnapshot()` is the image as the stores hold it (a global loaded from a legacy key under that key) and is what `CultMesh.flush` persists, so a flush no longer leaves such a global under two keys; `snapshot()` is unchanged.
|
||||
- A single-file store marked `cultcache.store.v3` (it holds an element id, written by the C# runtime) is read, and a rewrite keeps the mark and the ids; this runtime does not decide when a document carries ids. A runtime from before this change refuses a v3 store by name, so rolling back to an older release after a store has been written as v3 is one-way: there is no carry-back, so restore from a copy taken before upgrading.
|
||||
- `indexes` are unique, as C# `[CultIndex]`: a write or a load that leaves two records of a type holding one non-blank
|
||||
|
|
|
|||
|
|
@ -1,2 +1,3 @@
|
|||
export * from "./browser";
|
||||
export { SingleFileMessagePackBackingStore, readSingleFileStore, type SingleFileStoreRead } from "./single-file-messagepack-backing-store";
|
||||
export { StoreLinkedError } from "./store-file";
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { promisify } from "node:util";
|
|||
|
||||
import { decode, encode } from "@msgpack/msgpack";
|
||||
|
||||
import { StoreLinkedError as PackageStoreLinkedError } from "../src/index";
|
||||
import { documentPayloadBytes, documentPayloadFromBytes } from "../src/document";
|
||||
import { SingleFileMessagePackBackingStore } from "../src/single-file-messagepack-backing-store";
|
||||
import { SchemaConflictError, StoreUnreadableError } from "../src/store-format";
|
||||
|
|
@ -23,6 +24,7 @@ import {
|
|||
replaceStoreFile,
|
||||
replaceStoreFileWindows,
|
||||
windowsKernel,
|
||||
refuseLinkedStore,
|
||||
withStoreWriteLock,
|
||||
} from "../src/store-file";
|
||||
import type { ReplaceKernel, StoreFileOps } from "../src/store-file";
|
||||
|
|
@ -818,3 +820,35 @@ test("opening writes nothing: a missing store leaves no lock file, a legacy stor
|
|||
await rm(directory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("StoreLinkedError is importable from the package entry and is what a write at a link throws", { skip: process.platform === "win32" }, async () => {
|
||||
const directory = await scratch();
|
||||
try {
|
||||
const target = join(directory, "target.cc");
|
||||
await writeFile(target, "x");
|
||||
const link = join(directory, "link.cc");
|
||||
await fsp.symlink(target, link);
|
||||
await assert.rejects(
|
||||
() => refuseLinkedStore(link),
|
||||
(error: unknown) => error instanceof PackageStoreLinkedError && error.path === link && error.name === "StoreLinkedError",
|
||||
);
|
||||
await assert.rejects(() => new SingleFileMessagePackBackingStore(link).pushAll([]), (error: unknown) => error instanceof PackageStoreLinkedError);
|
||||
} finally {
|
||||
await rm(directory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("refuseLinkedStore lets a path that is absent pass and rethrows any other lstat failure", { skip: process.platform === "win32" }, async () => {
|
||||
const directory = await scratch();
|
||||
try {
|
||||
await refuseLinkedStore(join(directory, "absent.cc"));
|
||||
const file = join(directory, "file");
|
||||
await writeFile(file, "x");
|
||||
await assert.rejects(
|
||||
() => refuseLinkedStore(join(file, "child.cc")),
|
||||
(error: unknown) => (error as NodeJS.ErrnoException).code === "ENOTDIR",
|
||||
);
|
||||
} finally {
|
||||
await rm(directory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue