bunderstack 0.24.6 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/jobs/define.d.ts +43 -4
- package/dist/jobs/define.d.ts.map +1 -1
- package/dist/jobs/define.js +13 -0
- package/dist/jobs/define.js.map +1 -1
- package/dist/jobs/index.d.ts +2 -2
- package/dist/jobs/index.d.ts.map +1 -1
- package/dist/jobs/index.js +1 -1
- package/dist/jobs/index.js.map +1 -1
- package/dist/jobs/queue.d.ts +8 -0
- package/dist/jobs/queue.d.ts.map +1 -1
- package/dist/jobs/queue.js +25 -1
- package/dist/jobs/queue.js.map +1 -1
- package/dist/jobs/worker.d.ts.map +1 -1
- package/dist/jobs/worker.js +10 -2
- package/dist/jobs/worker.js.map +1 -1
- package/dist/runtime.d.ts +2 -2
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +3 -2
- package/dist/runtime.js.map +1 -1
- package/llms-full.txt +101 -0
- package/llms.txt +21 -0
- package/package.json +1 -1
package/llms-full.txt
CHANGED
|
@@ -299,6 +299,7 @@ Major optional groups:
|
|
|
299
299
|
|
|
300
300
|
- `access`: generated CRUD exposure, ownership, filters, sorting, and guards;
|
|
301
301
|
- `auth` / `authResolver`: Better Auth and custom session resolution;
|
|
302
|
+
- `session`: an explicit projection of extra authenticated-user fields;
|
|
302
303
|
- `env`: declared environment, validated before anything reads it;
|
|
303
304
|
- `storage`, `messaging`, and `jobs`: application facilities;
|
|
304
305
|
- `api`: your oRPC router, as an object or as `(o) => router`;
|
|
@@ -392,6 +393,30 @@ type ApiContext = {
|
|
|
392
393
|
`o.protected` adds non-null `context.user` and the active organization session
|
|
393
394
|
to handlers. Inputs and optional outputs accept Standard Schema.
|
|
394
395
|
|
|
396
|
+
`AccessUser` includes Better Auth's `emailVerified` field. Session resolution
|
|
397
|
+
normalizes it fail-closed: only the literal value `true` is treated as verified.
|
|
398
|
+
Use `defineSessionUser` to expose additional application fields without copying
|
|
399
|
+
the complete Better Auth user into every procedure:
|
|
400
|
+
|
|
401
|
+
```ts
|
|
402
|
+
const session = defineSessionUser({
|
|
403
|
+
mapUser: (user) => ({ clinicId: String(user.clinicId) }),
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
const o = defineApi({ schema, session })
|
|
407
|
+
|
|
408
|
+
const backend = bunderstack({
|
|
409
|
+
schema,
|
|
410
|
+
database,
|
|
411
|
+
session,
|
|
412
|
+
api,
|
|
413
|
+
})
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
The mapped fields are inferred on `getSession().user` and `o.protected`'s
|
|
417
|
+
`context.user`. `id`, `email`, `emailVerified`, `name`, and `role` remain
|
|
418
|
+
framework-owned and cannot be overridden by the mapper.
|
|
419
|
+
|
|
395
420
|
`peekSession()` exists for graph-wide middleware, which runs before
|
|
396
421
|
authentication. Use it for observability only, never for authorization — see
|
|
397
422
|
[Middleware](/docs/middleware#reading-the-caller).
|
|
@@ -401,6 +426,7 @@ authentication. Use it for observability only, never for authorization — see
|
|
|
401
426
|
| Export | Purpose |
|
|
402
427
|
| ---------------------------- | --------------------------------------------------------- |
|
|
403
428
|
| `defineApi({ schema, env })` | The procedure builder, with generics inferred from values |
|
|
429
|
+
| `defineSessionUser(config)` | A typed, explicit projection of extra session user fields |
|
|
404
430
|
| `listSpec(table, options)` | Input schema and handler for a list endpoint outside CRUD |
|
|
405
431
|
| `BunderstackError` | Typed error for code outside a handler |
|
|
406
432
|
| `BunderstackDb<TSchema>` | The database type for a helper parameter |
|
|
@@ -815,6 +841,80 @@ is best effort and can race; `concurrency` is not a strict provider-wide
|
|
|
815
841
|
semaphore. Use an application/provider limiter when several workers share one
|
|
816
842
|
external quota.
|
|
817
843
|
|
|
844
|
+
## Deduplication
|
|
845
|
+
|
|
846
|
+
A `dedupeKey` collapses enqueues of the same job type into one row. The second
|
|
847
|
+
call returns the existing row's id instead of inserting another:
|
|
848
|
+
|
|
849
|
+
```ts
|
|
850
|
+
await app.jobs.enqueue('syncPerson', { personId }, { dedupeKey: personId })
|
|
851
|
+
```
|
|
852
|
+
|
|
853
|
+
`dedupeUntil` on the job declaration decides how long the key collapses new
|
|
854
|
+
enqueues:
|
|
855
|
+
|
|
856
|
+
- **`'finish'` (default)** — the key is held until the job succeeds or finally
|
|
857
|
+
fails. An enqueue while the job runs returns the running row. Right for "at
|
|
858
|
+
most one of these at a time" work.
|
|
859
|
+
- **`'start'`** — the key is released when a worker claims the job. A burst of
|
|
860
|
+
enqueues before the claim still collapses into one row, but an enqueue during
|
|
861
|
+
the run creates one more row that runs after it and reads the newer state.
|
|
862
|
+
Right for debounced sync, reindexing, and cache rebuilds.
|
|
863
|
+
|
|
864
|
+
```ts
|
|
865
|
+
syncPerson: j.job({
|
|
866
|
+
input: v.object({ personId: v.string() }),
|
|
867
|
+
dedupeUntil: 'start',
|
|
868
|
+
retries: 5,
|
|
869
|
+
handler: async ({ personId }, ctx) => {
|
|
870
|
+
// Read the latest state, then push it.
|
|
871
|
+
},
|
|
872
|
+
})
|
|
873
|
+
```
|
|
874
|
+
|
|
875
|
+
With `'start'`, a job that fails and waits for a retry no longer holds its key,
|
|
876
|
+
so a newer row can exist beside it and both run. `'start'` also does not
|
|
877
|
+
serialize runs per key: two workers can run an older and a newer row at the
|
|
878
|
+
same time. Keep the default, or take a lock in the handler, when runs for one
|
|
879
|
+
key must never overlap.
|
|
880
|
+
|
|
881
|
+
Cron tasks reject `dedupeUntil`; their slot ownership depends on the retained
|
|
882
|
+
key.
|
|
883
|
+
|
|
884
|
+
## Transactional enqueue
|
|
885
|
+
|
|
886
|
+
Pass `tx` to insert the job row through your own Drizzle transaction. The job
|
|
887
|
+
becomes visible to workers only when the transaction commits, and a rollback
|
|
888
|
+
removes it. Your data and the work that follows from it are written atomically,
|
|
889
|
+
so you do not need an outbox table:
|
|
890
|
+
|
|
891
|
+
```ts
|
|
892
|
+
await context.db.transaction(async (tx) => {
|
|
893
|
+
const person = await mergePerson(tx, event)
|
|
894
|
+
await context.jobs.enqueue(
|
|
895
|
+
'syncPerson',
|
|
896
|
+
{ personId: person.id },
|
|
897
|
+
{ tx, dedupeKey: `sync:${person.id}` },
|
|
898
|
+
)
|
|
899
|
+
})
|
|
900
|
+
```
|
|
901
|
+
|
|
902
|
+
`tx` works on `app.jobs`, on `context.jobs` in procedures, on `ctx.jobs` in job
|
|
903
|
+
handlers, and on `fixture.app.jobs` in tests. It must be a transaction on the
|
|
904
|
+
app's own database. A transaction of the other dialect throws; a transaction
|
|
905
|
+
from an unrelated connection of the same dialect is not supported.
|
|
906
|
+
|
|
907
|
+
- **Postgres:** an enqueue whose `dedupeKey` conflicts with a row that another
|
|
908
|
+
open transaction inserted waits for that transaction to end. Two transactions
|
|
909
|
+
that enqueue the same key therefore run one after the other. Keep such
|
|
910
|
+
transactions short.
|
|
911
|
+
- **SQLite and libSQL:** a write transaction already holds the database write
|
|
912
|
+
lock, so `tx` adds no contention.
|
|
913
|
+
|
|
914
|
+
Delivery stays at-least-once. `tx` removes lost jobs (a crash between commit and
|
|
915
|
+
enqueue) and phantom jobs (an enqueue whose transaction rolled back). It does
|
|
916
|
+
not make handlers exactly-once.
|
|
917
|
+
|
|
818
918
|
## Cron
|
|
819
919
|
|
|
820
920
|
`j.cron()` uses a five-field UTC schedule. Each due minute is materialized as a
|
|
@@ -964,6 +1064,7 @@ const backend = bunderstack({
|
|
|
964
1064
|
access,
|
|
965
1065
|
auth,
|
|
966
1066
|
authResolver,
|
|
1067
|
+
session,
|
|
967
1068
|
storage,
|
|
968
1069
|
messaging,
|
|
969
1070
|
jobs: (j) => j.define({}),
|
package/llms.txt
CHANGED
|
@@ -325,6 +325,27 @@ One table, one loop. A cron is a job created on a schedule.
|
|
|
325
325
|
Enqueue with app.jobs.enqueue(name, input, { dedupeKey }). The background loop
|
|
326
326
|
starts on its own, gated by BUNDERSTACK_ROLE: all (default), web, or worker.
|
|
327
327
|
|
|
328
|
+
Pass { tx } to insert the job row through your own Drizzle transaction. The job
|
|
329
|
+
becomes visible to workers only when the transaction commits; a rollback
|
|
330
|
+
removes it. Use it instead of an application outbox table:
|
|
331
|
+
|
|
332
|
+
await context.db.transaction(async (tx) => {
|
|
333
|
+
const person = await mergePerson(tx, event)
|
|
334
|
+
await context.jobs.enqueue('syncPerson', { personId: person.id },
|
|
335
|
+
{ tx, dedupeKey: `sync:${person.id}` })
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
The tx must belong to the app's own database; a tx of the other dialect
|
|
339
|
+
throws. On Postgres, two open transactions that enqueue the same dedupeKey
|
|
340
|
+
serialize on it. Delivery stays at-least-once.
|
|
341
|
+
|
|
342
|
+
dedupeKey collapses enqueues until the job finishes (dedupeUntil: 'finish', the
|
|
343
|
+
default). Declare dedupeUntil: 'start' on j.job() to release the key when a
|
|
344
|
+
worker claims the job: a burst before the claim still collapses into one row,
|
|
345
|
+
and an enqueue during the run creates one more row that reads the newer state.
|
|
346
|
+
Use it for debounced sync, reindex, and cache rebuilds. 'start' does not
|
|
347
|
+
serialize runs per key. j.cron() rejects dedupeUntil.
|
|
348
|
+
|
|
328
349
|
STORAGE
|
|
329
350
|
|
|
330
351
|
storage: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bunderstack",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.0",
|
|
4
4
|
"description": "Batteries-included backend framework for Bun: type-safe oRPC APIs, auth, storage, realtime, jobs, messaging, and validated env from one declaration.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"backend",
|