slotlock 0.2.0 → 0.5.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.
Files changed (66) hide show
  1. package/CHANGELOG.md +582 -0
  2. package/CONTRIBUTING.md +35 -8
  3. package/GOVERNANCE.md +4 -2
  4. package/MAINTAINERS.md +3 -2
  5. package/NOTICE +4 -4
  6. package/README.md +498 -65
  7. package/SECURITY.md +31 -2
  8. package/SPEC.md +142 -15
  9. package/agent-server.d.ts +62 -10
  10. package/agent-server.js +577 -105
  11. package/agent-store-backend.d.ts +12 -0
  12. package/agent-store-backend.js +1115 -55
  13. package/api-keys.d.ts +11 -3
  14. package/api-keys.js +29 -6
  15. package/application-api.d.ts +35 -1
  16. package/application-api.js +683 -12
  17. package/booking-rules.d.ts +84 -0
  18. package/booking-rules.js +149 -0
  19. package/config.d.ts +112 -0
  20. package/config.js +225 -0
  21. package/dashboard.d.ts +67 -5
  22. package/dashboard.js +1012 -140
  23. package/ddl.d.ts +45 -3
  24. package/ddl.js +1124 -13
  25. package/engine.d.ts +5 -0
  26. package/engine.js +23 -6
  27. package/event-feed.d.ts +178 -0
  28. package/event-feed.js +435 -0
  29. package/event-schema.d.ts +59 -0
  30. package/event-schema.js +85 -0
  31. package/ical-worker-thread.js +4 -2
  32. package/ical-worker.d.ts +8 -5
  33. package/ical-worker.js +9 -2
  34. package/index.d.ts +12 -6
  35. package/index.js +9 -4
  36. package/interop.d.ts +117 -0
  37. package/interop.js +356 -0
  38. package/maintenance.d.ts +27 -5
  39. package/maintenance.js +92 -28
  40. package/mcp-app.d.ts +7 -1
  41. package/mcp-app.js +259 -40
  42. package/metrics.d.ts +24 -0
  43. package/metrics.js +167 -0
  44. package/node-server.d.ts +9 -1
  45. package/node-server.js +7 -2
  46. package/openapi.json +10970 -0
  47. package/package.json +3 -2
  48. package/rate-limit.d.ts +80 -0
  49. package/rate-limit.js +741 -0
  50. package/rest-api.d.ts +22 -0
  51. package/rest-api.js +166 -11
  52. package/rules.js +6 -3
  53. package/self-host.d.ts +45 -8
  54. package/self-host.js +616 -46
  55. package/slotlock-manifest.schema.json +7 -1
  56. package/slotlock.manifest.json +8 -2
  57. package/store.d.ts +647 -28
  58. package/store.js +2982 -327
  59. package/sync.d.ts +43 -2
  60. package/sync.js +1515 -323
  61. package/timezone.d.ts +58 -3
  62. package/timezone.js +249 -20
  63. package/types.d.ts +300 -9
  64. package/types.js +0 -2
  65. package/webhooks.d.ts +91 -0
  66. package/webhooks.js +300 -0
package/types.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { SlotlockBookingRule, SlotlockBookingRules } from './booking-rules.js';
1
2
  /** Half-open time interval `[start, end)`. */
2
3
  export interface Interval {
3
4
  start: Date;
@@ -21,14 +22,35 @@ export interface SlotlockResource {
21
22
  * uses the default (`SLOTLOCK_AVAILABILITY`, or the backend's `availabilityRules`).
22
23
  */
23
24
  availabilityRules?: WeeklyAvailabilityRule[];
25
+ /**
26
+ * `shared` resources are listed to agents and booked through them; `private` ones are invisible
27
+ * to agents and reached only by the tenant's own code. Fixed when the resource is created. The
28
+ * store always says which; a resource built elsewhere without it is shared.
29
+ */
30
+ visibility?: 'shared' | 'private';
31
+ /** When the tenant archived it: no longer listed or booked, its history kept. Absent in use. */
32
+ archivedAt?: Date;
33
+ /** What a booking on it must satisfy (`SlotlockBookingRules`). Absent when it has none. */
34
+ bookingRules?: SlotlockBookingRules;
35
+ /**
36
+ * The pool it belongs to: interchangeable resources sharing a name, such as a vehicle class or a
37
+ * room type, booked by the class (`slotlock_book_pool`). Absent when it is in none.
38
+ */
39
+ pool?: string;
24
40
  }
25
41
  /** A confirmed occupation of a resource. The DB EXCLUDE constraint is the overlap arbiter. */
26
42
  export interface SlotlockReservation {
27
43
  id: string;
28
44
  resourceId: string;
29
- /** Null only for legacy, hold, and parity-shadow rows. */
45
+ /**
46
+ * Null only for live holds, rows on a resource that has no tenant (legacy), and parity-shadow rows.
47
+ * A booking made by confirming a hold, or by createReservation, on a tenant's resource has one.
48
+ */
30
49
  tenantRef: string | null;
31
- /** Stable booking identity inside tenantRef; null for legacy, hold, and shadow rows. */
50
+ /**
51
+ * Stable booking identity inside tenantRef; null exactly when tenantRef is. A confirmed hold, and
52
+ * a createReservation booking, use their own id.
53
+ */
32
54
  externalRef: string | null;
33
55
  start: Date;
34
56
  end: Date;
@@ -82,6 +104,31 @@ export type ExternalReservationResult = {
82
104
  } | {
83
105
  ok: false;
84
106
  code: 'idempotency_conflict';
107
+ }
108
+ /** The resource is archived: it takes no new booking (cancel and release still work). */
109
+ | {
110
+ ok: false;
111
+ code: 'resource_archived';
112
+ }
113
+ /**
114
+ * A recurring event on the resource is not stored as far as this write reaches, and cannot be
115
+ * extended that far (past the rolling horizon, or blocked): the time cannot be proven free.
116
+ */
117
+ | {
118
+ ok: false;
119
+ code: 'recurrence_not_materialized';
120
+ }
121
+ /** The tenant's bookings and retired references are at its `reservations` limit. */
122
+ | {
123
+ ok: false;
124
+ code: 'reservation_limit_reached';
125
+ limit: number;
126
+ }
127
+ /** The booking breaks one of the resource's booking rules (`SlotlockResource.bookingRules`). */
128
+ | {
129
+ ok: false;
130
+ code: 'booking_rule_violated';
131
+ rule: SlotlockBookingRule;
85
132
  } | {
86
133
  ok: false;
87
134
  code: 'overlap';
@@ -106,6 +153,15 @@ export type CancelExternalReservationResult = {
106
153
  } | {
107
154
  ok: false;
108
155
  code: 'revision_conflict';
156
+ }
157
+ /**
158
+ * Retiring a reference no booking holds would add a row past the tenant's `reservations` limit
159
+ * (cancelling a booking it has never does).
160
+ */
161
+ | {
162
+ ok: false;
163
+ code: 'reservation_limit_reached';
164
+ limit: number;
109
165
  };
110
166
  /**
111
167
  * VAVAILABILITY-style bookable window (RFC 7953 semantics, weekly subset): a weekly-cadence RRULE
@@ -127,6 +183,36 @@ export type CreateReservationResult = {
127
183
  } | {
128
184
  ok: false;
129
185
  code: 'invalid_window';
186
+ }
187
+ /** No resource with that id, or (under tenant RLS or with `tenantRef`) not one of the tenant's. */
188
+ | {
189
+ ok: false;
190
+ code: 'resource_not_found';
191
+ }
192
+ /** The resource is archived: it takes no new booking (cancel and release still work). */
193
+ | {
194
+ ok: false;
195
+ code: 'resource_archived';
196
+ }
197
+ /**
198
+ * A recurring event on the resource is not stored as far as this write reaches, and cannot be
199
+ * extended that far (past the rolling horizon, or blocked): the time cannot be proven free.
200
+ */
201
+ | {
202
+ ok: false;
203
+ code: 'recurrence_not_materialized';
204
+ }
205
+ /** The booking's tenant is at its `reservations` limit. */
206
+ | {
207
+ ok: false;
208
+ code: 'reservation_limit_reached';
209
+ limit: number;
210
+ }
211
+ /** The booking breaks one of the resource's booking rules (`SlotlockResource.bookingRules`). */
212
+ | {
213
+ ok: false;
214
+ code: 'booking_rule_violated';
215
+ rule: SlotlockBookingRule;
130
216
  } | {
131
217
  ok: false;
132
218
  code: 'overlap';
@@ -138,18 +224,93 @@ export interface SlotlockHold {
138
224
  resourceId: string;
139
225
  start: Date;
140
226
  end: Date;
227
+ /** Turnaround kept busy after `end`; the booking the hold is confirmed into keeps it. */
228
+ bufferAfterMs: number;
141
229
  expiresAt: Date;
142
230
  source: string | null;
231
+ /** The alternatives it was held with (`groupId` when acquired); absent when held alone. */
232
+ groupId?: string;
233
+ }
234
+ /** One entry of a tenant's agenda (`listAgenda`): what occupies a resource, and from when. */
235
+ export interface SlotlockAgendaEntry {
236
+ id: string;
237
+ resourceId: string;
238
+ start: Date;
239
+ end: Date;
240
+ /** A booking, a calendar event's occurrence, or a live hold. */
241
+ kind: 'booking' | 'event' | 'hold';
242
+ /** The booking's reference; null for an event's occurrence and a hold. */
243
+ reference: string | null;
244
+ /** The calendar event's summary; null for a booking and a hold. */
245
+ summary: string | null;
246
+ /** When the hold lapses; null for a booking and an event. */
247
+ expiresAt: Date | null;
248
+ source: string | null;
143
249
  }
144
- export type AcquireHoldResult = {
250
+ export type AcquireHoldResult =
251
+ /** `idempotent`: a retry of a keyed hold, answered with the hold the first request made. */
252
+ {
145
253
  ok: true;
146
254
  hold: SlotlockHold;
255
+ idempotent: boolean;
147
256
  } | {
148
257
  ok: false;
149
258
  code: 'invalid_window';
150
259
  } | {
151
260
  ok: false;
152
261
  code: 'invalid_ttl';
262
+ } | {
263
+ ok: false;
264
+ code: 'invalid_buffer';
265
+ }
266
+ /** A tenant, principal or idempotency key that is malformed, or a key without both of the others. */
267
+ | {
268
+ ok: false;
269
+ code: 'invalid_identity';
270
+ }
271
+ /** No resource with that id, or (under tenant RLS or with `tenantRef`) not one of the tenant's. */
272
+ | {
273
+ ok: false;
274
+ code: 'resource_not_found';
275
+ }
276
+ /** The resource is archived: it takes no new booking (cancel and release still work). */
277
+ | {
278
+ ok: false;
279
+ code: 'resource_archived';
280
+ }
281
+ /** The idempotency key already made a hold with another resource, window, ttl, buffer or source. */
282
+ | {
283
+ ok: false;
284
+ code: 'idempotency_conflict';
285
+ }
286
+ /**
287
+ * A recurring event on the resource is not stored as far as this write reaches, and cannot be
288
+ * extended that far (past the rolling horizon, or blocked): the time cannot be proven free.
289
+ */
290
+ | {
291
+ ok: false;
292
+ code: 'recurrence_not_materialized';
293
+ }
294
+ /** The resource's tenant has as many holds as its `holds` limit, expired ones deleted. */
295
+ | {
296
+ ok: false;
297
+ code: 'hold_limit_reached';
298
+ limit: number;
299
+ }
300
+ /**
301
+ * The holder's live holds would together hold more hours than `maxHeldHoursPerHolder` allows
302
+ * (`limit`, in hours): one agent cannot keep a resource's month from everyone, a hold at a time.
303
+ */
304
+ | {
305
+ ok: false;
306
+ code: 'held_time_limit_reached';
307
+ limit: number;
308
+ }
309
+ /** The booking breaks one of the resource's booking rules (`SlotlockResource.bookingRules`). */
310
+ | {
311
+ ok: false;
312
+ code: 'booking_rule_violated';
313
+ rule: SlotlockBookingRule;
153
314
  } | {
154
315
  ok: false;
155
316
  code: 'overlap';
@@ -164,6 +325,27 @@ export type ConfirmHoldResult = {
164
325
  } | {
165
326
  ok: false;
166
327
  code: 'hold_expired';
328
+ }
329
+ /** A cancellation of the hold's reference (its id) arrived first and retired it. */
330
+ | {
331
+ ok: false;
332
+ code: 'reservation_cancelled';
333
+ }
334
+ /** Another booking of the tenant already uses the hold's id as its reference. */
335
+ | {
336
+ ok: false;
337
+ code: 'idempotency_conflict';
338
+ }
339
+ /** The hold's resource was archived since: it takes no new booking. */
340
+ | {
341
+ ok: false;
342
+ code: 'resource_archived';
343
+ }
344
+ /** The tenant is at its `reservations` limit: the hold stays a hold. */
345
+ | {
346
+ ok: false;
347
+ code: 'reservation_limit_reached';
348
+ limit: number;
167
349
  };
168
350
  export type ReleaseHoldResult = {
169
351
  ok: true;
@@ -241,6 +423,11 @@ export interface SlotlockCalendarEvent extends CalendarEventContent {
241
423
  updatedAt: Date;
242
424
  }
243
425
  export interface PutCalendarEventCommand extends CalendarEventContent {
426
+ /**
427
+ * The resource the writer read the event on: a revision is refused (`resource_tenant_mismatch`)
428
+ * when the stored event is on another, checked under the event's lock.
429
+ */
430
+ expectedResourceId?: string;
244
431
  tenantRef: string;
245
432
  /** Defaults to the trusted `internal` namespace. Agent callers must supply their owner digest. */
246
433
  ownerRef?: string;
@@ -253,6 +440,24 @@ export interface PutCalendarEventCommand extends CalendarEventContent {
253
440
  /** Mandatory for recurrence; bounded to 367 days. Defaults to the event interval otherwise. */
254
441
  materializationWindow?: Interval;
255
442
  source?: string;
443
+ /**
444
+ * Judge the event as a booking made now against the resource's booking rules (its first
445
+ * occurrence for notice and advance, every occurrence for step and hours), and keep the
446
+ * resource's turnaround after each occurrence. Off by default: an imported event is what happened
447
+ * elsewhere. The agent tools set it.
448
+ */
449
+ enforceBookingRules?: boolean;
450
+ /**
451
+ * Book the time a live hold keeps: the hold must be `holderRef`'s (`acquireHold`'s
452
+ * `principalRef`), on this resource, and cover the event, a single occurrence. In one transaction
453
+ * the hold and the rest of its group are released and the event takes the time, so no rival can
454
+ * come between. Its notice and advance were judged when it was held; step and hours are judged
455
+ * again.
456
+ */
457
+ fromHold?: {
458
+ holdId: string;
459
+ holderRef: string;
460
+ };
256
461
  }
257
462
  export type PutCalendarEventResult = {
258
463
  ok: true;
@@ -281,9 +486,20 @@ export type PutCalendarEventResult = {
281
486
  ok: false;
282
487
  code: 'owner_command_quota_exceeded';
283
488
  limit: number;
489
+ }
490
+ /** A new event would pass the tenant's `events` limit (revising one it has never does). */
491
+ | {
492
+ ok: false;
493
+ code: 'event_limit_reached';
494
+ limit: number;
284
495
  } | {
285
496
  ok: false;
286
497
  code: 'resource_tenant_mismatch';
498
+ }
499
+ /** The resource is archived: it takes no new booking (cancel and release still work). */
500
+ | {
501
+ ok: false;
502
+ code: 'resource_archived';
287
503
  } | {
288
504
  ok: false;
289
505
  code: 'event_cancelled';
@@ -292,11 +508,46 @@ export type PutCalendarEventResult = {
292
508
  ok: false;
293
509
  code: 'revision_conflict';
294
510
  currentRevision: number;
511
+ }
512
+ /** `fromHold` names no live hold of that holder's: released, booked, expired, or another's. */
513
+ | {
514
+ ok: false;
515
+ code: 'hold_not_found';
516
+ }
517
+ /** `fromHold`'s hold is on another resource, or does not cover the event. */
518
+ | {
519
+ ok: false;
520
+ code: 'hold_mismatch';
521
+ }
522
+ /**
523
+ * A recurring event on the resource is not stored as far as this write reaches, and cannot be
524
+ * extended that far (past the rolling horizon, or blocked): the time cannot be proven free.
525
+ */
526
+ | {
527
+ ok: false;
528
+ code: 'recurrence_not_materialized';
529
+ }
530
+ /** The booking breaks one of the resource's booking rules (`SlotlockResource.bookingRules`). */
531
+ | {
532
+ ok: false;
533
+ code: 'booking_rule_violated';
534
+ rule: SlotlockBookingRule;
295
535
  } | {
296
536
  ok: false;
297
537
  code: 'overlap';
298
538
  conflictingReservationId: string | null;
299
539
  };
540
+ export type DeleteResourceResult =
541
+ /** `deleted`: false when the tenant has no resource with that id (deleted already, say). */
542
+ {
543
+ ok: true;
544
+ deleted: boolean;
545
+ }
546
+ /** A future booking, a live hold, or a calendar event is still on it. */
547
+ | {
548
+ ok: false;
549
+ code: 'resource_in_use';
550
+ };
300
551
  export type CancelCalendarEventResult = {
301
552
  ok: true;
302
553
  eventId: string;
@@ -324,8 +575,16 @@ export type CancelCalendarEventResult = {
324
575
  code: 'revision_conflict';
325
576
  currentRevision: number;
326
577
  };
327
- /** A place in the roll's order of due masters: the last one a batch examined. */
578
+ /**
579
+ * A place in the roll's order of due masters: the last one a batch examined. The roll takes the
580
+ * masters it has never put back first, then those it has, longest waiting first.
581
+ */
328
582
  export interface CalendarHorizonRollCursor {
583
+ /**
584
+ * When the master was due to be examined again, if the roll had put it back; null (or, from a
585
+ * cursor of an earlier release, missing) when it had not.
586
+ */
587
+ rollRetryAt?: Date | null;
329
588
  materializedEndsAt: Date;
330
589
  eventId: string;
331
590
  }
@@ -338,11 +597,18 @@ export interface CalendarHorizonRollResult {
338
597
  * stored end and the window's: a roll never moves a series' end back.
339
598
  */
340
599
  extended: number;
341
- /** Masters whose new occurrence collided; their previous coverage remains intact. */
600
+ /**
601
+ * Masters whose new occurrence collided; their previous coverage remains intact, and they wait an
602
+ * hour before a roll examines them again, behind every master it has not put back.
603
+ */
342
604
  conflicts: number;
605
+ /** The ids of those masters: what blocks each is a booking or hold on its new occurrences. */
606
+ conflictEventIds: string[];
343
607
  /**
344
608
  * Masters whose rule takes more than the expansion budget to reach the window (one that never
345
- * occurs again, or almost never); their previous coverage remains intact.
609
+ * occurs again, or almost never), or whose roll failed for any other reason; their previous
610
+ * coverage remains intact, and they wait an hour
611
+ * before a roll examines them again, behind every master it has not put back.
346
612
  */
347
613
  refused: number;
348
614
  /** The ids of those masters, to find and fix them. */
@@ -350,9 +616,8 @@ export interface CalendarHorizonRollResult {
350
616
  /** True when more due masters remain beyond this bounded batch. */
351
617
  hasMore: boolean;
352
618
  /**
353
- * Pass as `after` for the next batch of this pass, so the masters this batch left behind (in
354
- * conflict or refused, which keep their place at the front of the order) are not examined again
355
- * by every later batch. `null` when none remain.
619
+ * Pass as `after` for the next batch of this pass, so the masters this batch examined are not
620
+ * examined again by every later batch. `null` when none remain.
356
621
  */
357
622
  next: CalendarHorizonRollCursor | null;
358
623
  }
@@ -364,6 +629,26 @@ export interface CalendarEventRetentionResult {
364
629
  /** True when either class filled its batch, so more rows may already be eligible. */
365
630
  hasMore: boolean;
366
631
  }
632
+ /** What erasing a tenant deleted. */
633
+ export interface SlotlockTenantErasure {
634
+ /** Rows deleted, by table (every table the tenant owns rows in is named, with 0 for none). */
635
+ rows: Record<string, number>;
636
+ /** Rows deleted in all; 0 when the tenant had already been erased. */
637
+ total: number;
638
+ }
639
+ export interface BookingRetentionResult {
640
+ /** Holds past their expiry: they occupy nothing, and only a rival's write would reap them. */
641
+ expiredHoldsDeleted: number;
642
+ /** Keyed hold answers whose hold has expired: a retry with that key makes a new hold. */
643
+ holdCommandsDeleted: number;
644
+ /**
645
+ * References a cancel retired with no booking, older than the window: a create with one makes a
646
+ * new booking. A cancelled booking's reference stays retired.
647
+ */
648
+ tombstonesDeleted: number;
649
+ /** True when any class filled its batch, so more rows may already be eligible. */
650
+ hasMore: boolean;
651
+ }
367
652
  export interface CalendarCoverageCommand {
368
653
  tenantRef: string;
369
654
  resourceId: string;
@@ -385,6 +670,12 @@ export type CalendarCoverageResult = {
385
670
  } | {
386
671
  ok: false;
387
672
  code: 'stale_revision' | 'revision_conflict' | 'idempotency_conflict';
673
+ }
674
+ /** A new source would pass the tenant's `coverage` limit (advancing one it has never does). */
675
+ | {
676
+ ok: false;
677
+ code: 'coverage_limit_reached';
678
+ limit: number;
388
679
  };
389
680
  export interface CalendarCoverageAssessment {
390
681
  state: 'complete' | 'partial' | 'unknown';
package/types.js CHANGED
@@ -1,3 +1 @@
1
- // Slotlock core types. Every interval is half-open `[start, end)`: back-to-back reservations do not
2
- // conflict, matching RFC 5545 interval semantics and PostgreSQL range boundaries.
3
1
  export {};
package/webhooks.d.ts ADDED
@@ -0,0 +1,91 @@
1
+ import type { SlotlockChange, SlotlockEventFeed } from './event-feed.js';
2
+ /** The CloudEvents envelope a change is delivered and listed as. */
3
+ export interface SlotlockCloudEvent {
4
+ specversion: '1.0';
5
+ id: string;
6
+ source: string;
7
+ type: string;
8
+ subject: string;
9
+ time: string;
10
+ datacontenttype: 'application/json';
11
+ data: Record<string, unknown>;
12
+ }
13
+ /** A change as CloudEvents 1.0 (structured mode): `type` is `io.pylota.slotlock.<change type>`. */
14
+ export declare function slotlockCloudEvent(change: SlotlockChange): SlotlockCloudEvent;
15
+ /**
16
+ * The Standard Webhooks signature of one attempt: `v1,` and the base64 HMAC-SHA256, keyed by the
17
+ * secret's bytes, of `id.timestamp.body`.
18
+ */
19
+ export declare function signSlotlockWebhook(params: {
20
+ secret: string;
21
+ id: string;
22
+ timestamp: number;
23
+ body: string;
24
+ }): string;
25
+ /**
26
+ * When to try a failed endpoint again, by how many attempts have failed in a row: the Standard
27
+ * Webhooks example schedule (5 s, 5 min, 30 min, 2 h, 5 h, then 10 h), with ±10% jitter, and never
28
+ * sooner than the endpoint's Retry-After (capped at a day).
29
+ */
30
+ export declare function slotlockWebhookRetryDelayMs(attempts: number, retryAfterMs?: number, random?: () => number): number;
31
+ /** Whether `address` is one a webhook may not reach: loopback, private, link-local, and the like. */
32
+ export declare function isPrivateAddress(address: string): boolean;
33
+ export interface SlotlockWebhookResponse {
34
+ status: number;
35
+ /** From the response's Retry-After, when it gave one in seconds. */
36
+ retryAfterMs?: number;
37
+ }
38
+ export type SlotlockWebhookSender = (request: {
39
+ url: string;
40
+ headers: Record<string, string>;
41
+ body: string;
42
+ }) => Promise<SlotlockWebhookResponse>;
43
+ /**
44
+ * POST with Node's http client: no redirect is followed, the answer's body is never read beyond
45
+ * 64 KiB, and every address the host resolves to is checked when the socket connects, so a name
46
+ * that resolves to a private network (or is rebound to one) is refused unless `allowPrivateNetworks`.
47
+ */
48
+ export declare function createSlotlockWebhookSender(options?: {
49
+ timeoutMs?: number;
50
+ allowPrivateNetworks?: boolean;
51
+ }): SlotlockWebhookSender;
52
+ export interface SlotlockWebhookPassResult {
53
+ /** Changes an endpoint accepted. */
54
+ delivered: number;
55
+ /** Endpoints whose next change failed, to be tried again later. */
56
+ failed: number;
57
+ }
58
+ /**
59
+ * One delivery pass for a tenant: claim each endpoint with changes due, send them in order until
60
+ * one fails, and record how far each got. A 2xx is delivered; 410 disables the endpoint; anything
61
+ * else, or no answer, is retried on the schedule. The send happens outside any transaction.
62
+ */
63
+ export declare function deliverSlotlockWebhooks(params: {
64
+ feed: Pick<SlotlockEventFeed, 'claimWebhookDeliveries' | 'settleWebhookDelivery'>;
65
+ tenantRef: string;
66
+ send: SlotlockWebhookSender;
67
+ batch?: number;
68
+ leaseMs?: number;
69
+ /** After this long (default 30,000 ms) no further change is sent this pass; the rest wait. */
70
+ budgetMs?: number;
71
+ now?: () => number;
72
+ }): Promise<SlotlockWebhookPassResult>;
73
+ export interface SlotlockWebhookScheduler {
74
+ /** Deliver for this tenant too (a server calls it for each tenant it serves). */
75
+ touch(tenantRef: string): void;
76
+ close(): Promise<void>;
77
+ }
78
+ /**
79
+ * Delivery passes every `intervalMs` (250-60,000, default 2,000) for the tenants `tenants()` lists
80
+ * (the store's webhook_tenants(), say) and those touched, one tenant at a time. A pass that fails
81
+ * is reported and the next one runs as usual.
82
+ */
83
+ export declare function startSlotlockWebhookDelivery(options: {
84
+ tenants: () => Promise<readonly string[]>;
85
+ /** Tenants delivered at once (1-64, default 8), so one slow tenant's endpoints hold up none other. */
86
+ concurrency?: number;
87
+ deliver: (tenantRef: string) => Promise<SlotlockWebhookPassResult>;
88
+ intervalMs?: number;
89
+ onPass?: (tenantRef: string, result: SlotlockWebhookPassResult) => void;
90
+ onError?: (error: unknown) => void;
91
+ }): SlotlockWebhookScheduler;