loqkit
JavaScript运行时的类型记忆和分层锁定。
Loqkit发布匹配的ESM和CommonJS入口点。
锁定
import { withLock } from "loqkit";
const result = await withLock(
{ resource: "cache" },
async ({ resource, lock }) => {
console.log(`locked ${resource} with ${lock.backends.length} layers`);
},
{
key: "cache-refresh",
predicate: async ({ resource, state }) => {
return state === "predicate-after"
? await stillNeedsRefresh(resource)
: true;
},
onCheckFailed: async ({ resource, state }) => {
console.log(`${resource} skipped in ${state}`);
},
},
);
console.log(result.value, result.context.state, result.context.lock.waitedMs);
默认情况下, withLock 需要一个内存锁,使用工作线程和本机 flock 在可用时锁定,并在跳过本机群集时使用可移植轮询。动作后的参数可以是谓词函数或options对象,其中包含 predicate. 谓词在等待之前运行一次,在处理每个启用的层之后再运行一次。只有当两个检查都通过时,操作才会运行。
每个回调都会收到 ctx.state: "predicate-before" 对于第一张支票, "predicate-after" 用于收购后的支票,以及 "executed" 为了行动。当其中一个检查失败时, onCheckFailed(ctx) 在所有获得的层被释放后运行。它可以检查 ctx.state、记录、重读数据或启动其他锁工作而不持有此锁。
withLock 返回 { value, context }。 value 是行动的结果,还是 undefined 操作未运行时。返回的上下文具有与回调上下文相同的字段和状态值 "timeout" 添加,因为非抛出超时只能在结果中观察到。锁元数据(包括等待时间)在 result.context.lock。
省略 timeout 无限期地等待。一个数值超时抛出 LockTimeoutError 当它到期时。使用对象形式返回超时结果。零超时只进行一次非阻塞的获取尝试,并且不会休眠或轮询第二次:
const attempt = await withLock(context, action, {
timeout: { ms: 0, error: false },
});
if (attempt.context.state === "timeout") {
console.log("Lock is busy");
}
scope 是部分配置映射。省略映射或属性使用其默认值; false 禁用该层。工人和羊群另外接受 "auto",它在可用时使用该层,否则将其记录为跳过:
await withLock(context, action, {
scope: { worker: false, flock: false },
});
await withLock(context, action, {
scope: { memory: false, worker: true, flock: false, poll: false },
});
| 范围 | 值 | 默认的 | 行为 |
|---|---|---|---|
memory | true, false | true | 当前JavaScript隔离中的锁。 |
worker | true, false, "auto" | "auto" | 协调工作线程。 true 抛出 LockUnavailableError 当运行时API不可用时。 |
flock | true, false, "auto" | "auto" | 使用本机咨询文件锁定。 true 抛出 LockUnavailableError 本机支持不可用或失败时。 |
poll | true, false, "auto" | "auto" | 使用可移植轮询文件锁。 "auto" 当获得本地羊群或其他需要时跳过它。 |
启用的层总是运行 memory, worker, flock, poll 顺序和释放顺序相反。将所有四个设置为 false 允许的,并且运行谓词和操作时没有独占性边界。
上的回调和结果公开已解析的结果 ctx.lock.scope. 它包含一个布尔值for memory, true, false,或 "skipped" 为 worker, flock, poll; "auto" 从来没有出现过。自动层可以从 true 在第一个谓词上下文中to "skipped" 收购后确定它不受支持或冗余。 ctx.lock.backends 保留低级实现和回退细节。
记忆有关
memoize 将零参数工厂转换为计算一次并重用其成功结果的getter。同步工厂保持同步。异步或可启用的工厂返回原生承诺,并发调用者共享相同的动态工作。
import { memoize } from "loqkit";
const getConfig = memoize(() => ({ region: "us-east-1" }));
const config = getConfig();
const getCatalog = memoize(async () => {
const response = await fetch("https://example.com/catalog.json");
return response.json();
});
const catalog = await getCatalog();
成功的结果将在记忆getter的生命周期内保持缓存。抛出的错误和拒绝的承诺不会被缓存,因此下一个调用将重试。没有TTL或过期行为。
flowchart TD
call["Call memoized getter"] --> cached{"Successful result cached?"}
cached -- "Yes" --> reuse["Return cached value or promise"]
cached -- "No" --> factory["Run factory"]
factory --> kind{"Thenable result?"}
kind -- "No" --> storeValue["Cache and return value"]
kind -- "Yes" --> storePromise["Cache and return in-flight promise"]
storePromise --> outcome{"Promise outcome"}
outcome -- "Resolved" --> keep["Keep successful promise"]
outcome -- "Rejected" --> evict["Evict so the next call retries"]
锁是如何工作的
withLock 是一个键作用域锁。具有相同名称空间和键的调用一次只运行一个;具有不同键的调用可以并发运行。如果没有提供键,loqkit将上下文散列到一个稳定的键中。
当 scope 省略时,loqkit按顺序处理这些层,并反向释放获得的层:
- 一个内存中的
async-mutexlock序列化当前JavaScript隔离中的调用者。 - Node的工作锁管理器在运行时提供时协调工作线程。默认的
"auto"mode在API不可用时将该层记录为跳过。 - 在macOS和Linux上,可选的
fs-ext图层使用nativeflock. 默认的"auto"当本机锁定不可用或获取失败(包括拒绝的文件系统)时,Mode将该层记录为跳过flock。 - 一个
proper-lockfile轮询锁协调共享配置的锁目录的进程。它的默认"auto"Mode在flock被跳过或禁用时获取它,在flock成功后跳过它。
只有在不可用或非争用获取失败时才绕过自动群层。普通的争用继续等待羊群,这样排他性得以保留。集 scope.poll 来 true 需要两种机制;由于flock已经序列化了loqkit调用者,因此在该状态下的轮询争用立即失败 LockUnavailableError 而不是永远等待。集 scope.flock 来 false 对于已知具有不可靠群语义的网络挂载。 lockDirectory 或 LOQKIT_LOCK_DIR 两个基于文件的层存储其锁文件的控件。
flowchart TD
call["Call withLock"] --> options["Validate options and choose a key"]
options --> checking["Create predicate-before context"]
checking --> before{"Predicate passes before waiting?"}
before -- "No" --> failedBefore["Run onCheckFailed without a lock"]
failedBefore --> skippedBefore["Return predicate-before context"]
before -- "Yes or absent" --> memory["Acquire memory lock"]
memory --> worker["Acquire worker-thread lock"]
worker --> flock["Acquire or bypass native flock"]
flock --> poll["Acquire or bypass portable polling lock"]
memory -. "Timeout" .-> timeout{"timeout.error?"}
worker -. "Timeout" .-> timeout
flock -. "Timeout" .-> timeout
poll -. "Timeout" .-> timeout
timeout -- "Yes" --> timeoutError["Throw LockTimeoutError"]
timeout -- "No" --> timeoutResult["Return timeout context"]
poll --> acquired["Create predicate-after context with enabled layers handled"]
acquired --> after{"Predicate still passes?"}
after -- "No" --> releaseFailed["Release all layers"]
releaseFailed --> failedAfter["Run onCheckFailed without a lock"]
failedAfter --> skippedAfter["Return predicate-after context"]
after -- "Yes or absent" --> action["Run action with executed context"]
action --> release["Release poll, flock, worker, then memory"]
release --> result["Return value and executed context"]
当另一个调用者在此调用者等待时更改底层状态时,第二个谓词检查很重要:
sequenceDiagram
participant A as Caller A
participant B as Caller B
participant L as Layered lock for one key
A->>L: Acquire enabled memory, worker, flock, poll layers
L-->>A: Granted
B->>B: Check predicate
B->>L: Acquire same layers
Note over B,L: Caller B waits
A->>A: Run action
A-->>L: Release all layers
L-->>B: Granted
B->>B: Check predicate again
B->>B: Run action if still needed
B-->>L: Release all layers
默认的文件目录在主机本地。容器只有在共享配置的锁目录时才会协调,而单独的主机仍然需要共享锁服务或数据库支持的锁。
释放
运行 bun run release 从干净,同步 main 分支。Patch是默认增量; minor, major, --dry-run 也支持。现有的未标记的npm 0.0.1 需要一次性引导表单 bun run release -- --initial。
本地命令验证Git标识,从远程GitHub标记派生下一个版本,运行完整的套件,并在自动推送发布提交和标记之前打包包。 标签工作流 重复验证,因为发布标签不受保护,然后通过npm Trusted Publishing发布生成的存档,并注明出处。它不使用npm令牌。