MongoDB store
@inkanjs/mongo: an inkan Store on MongoDB, on your own mongodb client. Atomic commands, the server's clock, change streams or polling for jobs.
New with inkan 0.8
@inkanjs/mongo 0.1 needs inkan 0.8.0 or newer, below 1.0. It is a Store:
what it guarantees, and how it compares with the other adapters, is on the Storage page.
A Store for inkan on MongoDB: background jobs, rate
limits, sessions, logins and idempotency keys kept where every process of your app sees
them. You pass your own mongodb client; the package has no dependencies.
npm install @vxnsin/inkan @inkanjs/mongo mongodbimport { MongoClient } from "mongodb";
import { inkan, rateLimit } from "@vxnsin/inkan";
import { mongoStore } from "@inkanjs/mongo";
const client = await new MongoClient(process.env.MONGO_URL!).connect();
const store = mongoStore(client.db("app"));
const app = inkan({ workers: 4 })
.register(rateLimit({ max: 100, window: 60_000, store })) // counted across all workers
.job("/exports", { store }, async (job) => buildExport(job)); // progress and cancels reach every workerThe same store works for the middleware packages that take a Store:
app.register(session({ secret, store })); // @inkanjs/session
app.register(auth({ secret, users, session: { store } })); // @inkanjs/auth
app.register(idempotency({ store })); // @inkanjs/idempotencyGive two uses that might pick the same keys a prefix of their own:
mongoStore(db, { prefix: "sessions:" }).
Options
mongoStore(dbOrCollection, options) takes a Db (the keys go into options.collection) or
a Collection (the keys go there, messages into options.events of its database).
| option | default | |
|---|---|---|
collection | "inkan_store" | the collection for the keys, when a Db is given |
events | "inkan_events" | the collection for publish/subscribe messages |
prefix | "inkan:" | put in front of every key and channel; ends in : (added when missing) |
create | true | create the TTL indexes on first use; false when your migrations do it |
changeStreams | "auto" | true, false, or "auto": change streams when the server is a replica set or mongos |
poll | 1000 | ms between two reads of the messages without change streams |
store.close() stops listening for messages. The client is yours: close it yourself.
Give the keys a collection of their own. The TTL index on e deletes every document
whose e is a past date, the store's or not. So with create (the default) the store
refuses a collection that already holds documents but has no inkan_ttl index (one it did
not make before): the first call rejects with an error and nothing is changed. A collection
the store made itself is taken again after a restart. To use a collection of yours anyway,
pass create: false and make the index yourself, knowing what it deletes.
How it keeps things
One document a key: { _id: key, v: "<JSON text>", e: Date | null }, or { _id, n, e } for
a counter. Values are kept as JSON text, so objects whose fields start with $ or hold a
dot come back as they were. A TTL index on e (expireAfterSeconds: 0) deletes what is
over.
- Atomic.
addis onefindOneAndUpdatewith an upsert whose pipeline keeps the document when it is live and replaces it when it is missing or expired;takeis onefindOneAndDelete;incrementonefindOneAndUpdatethat adds to a live count, or starts over atbywith a new TTL when the key is missing or expired. Fifty at once on one key, from any number of processes: oneaddwins, onetakegets the value, the counts go 1 to 50. - One clock. Expiry is computed and checked with
$$NOW, the server's clock, so app servers whose clocks differ agree on what is over. - Messages.
publishinserts into theeventscollection. On a replica set or mongos, a change stream delivers them within milliseconds. On a standalone server, change streams do not exist: the collection is read everypollms while someone listens, so a message arrives up to about a second later. Messages are kept a minute (a TTL index onat).
Security
- Publish/subscribe has no authentication. A message is whatever arrives on the channel:
anyone who can insert into the
eventscollection can send one, and inkan's jobs act on them (a forged message can cancel a running job or report false progress). Use a MongoDB user of its own for the app with a role limited to the two collections, and no other user with write access to them. Do not share the database with tenants you do not trust. - Writing the store is trusting it. Whoever can write the keys can forge sessions,
idempotent replies, job records and rate-limit counts, and whoever can read them reads
every session. Give the app's database user access to the store's two collections (
find, insert, update, remove,createIndexwithcreate: true, andchangeStreamon the events) and nothing else, and nobody else write access to them. - Values are stored as JSON text, as they are given: not encrypted, not signed. Keep secrets out of them or encrypt them yourself; keys are stored as given too (under the prefix), so do not put secrets in keys.
- Errors carry no keys or values. A failing command rejects with a new
Errorthat keepscodeandcodeName, its message without the "dup key" part and with every quoted part replaced by"…"; the driver'skeyValue,keyPatternanderrorResponseare not passed on. Log them freely.
Limits
- Keys and the prefix. The prefix (default
"inkan:") ends in:; one without gets it ("app"is"app:"), so prefix"app"with key"1x"and prefix"app1"with key"x"stay apart.""puts nothing in front. Do not give one use a prefix that starts another's ("app:"and"app:sessions:"can meet). A key or channel holding NUL (\0) or a lone UTF-16 surrogate is refused with aTypeError, in all four inkan stores alike. - TTL monitor. MongoDB deletes expired documents about once a minute, not at
e. Nothing returns them in the meantime (every read filters one > $$NOW, andaddandincrementtreat them as absent), but they take space until then. - Polling. Without a replica set, messages arrive up to
pollms late, and a message whose write takes more than five seconds to land after a newer one can be missed. - Keys. Any string; MongoDB 4.2 and later have no index-key size limit, so long keys are kept as they are. A value must fit a 16 MB document.
- Versions. MongoDB 4.4 or later (
$isNumber, pipeline updates); tested on MongoDB 7 and 8 with themongodbdriver 7. Change streams need a replica set: a single-node one (mongod --replSet rs0andrs.initiate()) is enough. publishreaches subscribers in the order one process published, when it awaits eachpublish. A message sent while nobody listens is lost, as theStorecontract allows.
PostgreSQL store
@inkanjs/postgres: an inkan Store in one PostgreSQL table, on your pg Pool or postgres.js. Expiry by the database's clock, LISTEN/NOTIFY for jobs.
MySQL store
@inkanjs/mysql: an inkan Store on MySQL 8, on your own mysql2 pool. Exact binary keys, the database's clock, prepared statements only.