JoSk Custom Adapter API
August 11, 2026 · View on GitHub
JoSk supports 3rd party storage adapters. Built-in adapters cover MongoDB, Redis, PostgreSQL. Custom adapters should follow same contract.
Create Adapter
Start from blank-example.js.
Design Rules
- Keep second-layer scheduler lock. Use owner-bound lease token. Never release foreign lease.
- Derive lock lifetime from the lock object itself — prefer the relative
lock.leaseMs; uselock.expireAt/lock.expiresAtMsonly as a fallback for locks minted without it. Never substitutezombieTime(a substituted long TTL freezes the whole prefix for up tozombieTimewhen a holder dies uncleanly), and never re-derive a duration asexpiresAtMs - Date.now()whenleaseMsis present — that second app-clock read is distorted by any clock step between mint and acquire. - Claim due tasks atomically in storage. Do not
find all due -> update later. iterate()should claim and executeoneorbatchdepending onexecuteMode.ready()optional but recommended. Use it to finish schema/index/init work before first storage op.- Prefer storage-server time over client time when comparing lease expirations. Mixed client clocks across a cluster will cause incorrect lock ownership otherwise. See
adapters/postgres.js(CURRENT_TIMESTAMPinacquireLock) for a reference pattern. - Call
joskInstance.__execute(task)fire-and-forget (do notawait). JoSk handles internal concurrency and error wrapping.
Adapter Class API
new Adapter(opts){object} opts{string} [opts.prefix]scope isolation{boolean} [opts.resetOnInit]clear previous scoped state on init{mix} [opts.other]storage-specific options
- async
Adapter#ready() - {Promise<void>}optional - async
Adapter#ping() - {Promise<object>} - async
Adapter#acquireLock(lock) - {Promise<boolean>}{object} lock{string} lock.ownerId{string} lock.leaseId{Date} lock.expireAt{number} lock.expiresAtMs{number} [lock.leaseMs]— relative lease duration; prefer over re-deriving fromexpiresAtMs
- async
Adapter#releaseLock(lock) - {Promise<void>}- same
lockobject
- same
- async
Adapter#remove(uid) - {Promise<boolean>}{string} uid
- async
Adapter#add(uid, isInterval, delay) - {Promise<boolean|void>}{string} uid{boolean} isInterval{number} delay
- async
Adapter#update(task, nextExecuteAt) - {Promise<boolean>}{object} task{Date} nextExecuteAt
- async
Adapter#iterate(nextExecuteAt, lock, executeMode) - {Promise<number|void>}{Date} nextExecuteAtzombie retry timestamp{object} lockactive scheduler lease{'one'|'batch'} executeMode
Task Object
Inside Adapter#iterate() call this.joskInstance.__execute(task) with:
({
uid: String,
delay: Number,
executeAt: Number, // or Date — see "executeAt convention" below
isInterval: Boolean,
isDeleted: Boolean
})
executeAt convention
executeAt carries the pre-claim value — the moment the task was due to fire. Storage is updated to a post-claim park time (nextExecuteAt, typically now + zombieTime), but the task object handed back to JoSk reports the original due time. This lets handlers reason about scheduling drift and matches the semantics of all built-in adapters.
Recommended Storage Pattern
- Acquire scheduler lease with owner-bound token.
- Atomically claim next due task by moving
executeAttonextExecuteAt. - Return pre-claim task payload.
- Call
this.joskInstance.__execute(task). - Release scheduler lease only if owner token still matches.
Global lock alone is not enough for duplicate prevention. Atomic task claim is required.