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.
- package/CHANGELOG.md +582 -0
- package/CONTRIBUTING.md +35 -8
- package/GOVERNANCE.md +4 -2
- package/MAINTAINERS.md +3 -2
- package/NOTICE +4 -4
- package/README.md +498 -65
- package/SECURITY.md +31 -2
- package/SPEC.md +142 -15
- package/agent-server.d.ts +62 -10
- package/agent-server.js +577 -105
- package/agent-store-backend.d.ts +12 -0
- package/agent-store-backend.js +1115 -55
- package/api-keys.d.ts +11 -3
- package/api-keys.js +29 -6
- package/application-api.d.ts +35 -1
- package/application-api.js +683 -12
- package/booking-rules.d.ts +84 -0
- package/booking-rules.js +149 -0
- package/config.d.ts +112 -0
- package/config.js +225 -0
- package/dashboard.d.ts +67 -5
- package/dashboard.js +1012 -140
- package/ddl.d.ts +45 -3
- package/ddl.js +1124 -13
- package/engine.d.ts +5 -0
- package/engine.js +23 -6
- package/event-feed.d.ts +178 -0
- package/event-feed.js +435 -0
- package/event-schema.d.ts +59 -0
- package/event-schema.js +85 -0
- package/ical-worker-thread.js +4 -2
- package/ical-worker.d.ts +8 -5
- package/ical-worker.js +9 -2
- package/index.d.ts +12 -6
- package/index.js +9 -4
- package/interop.d.ts +117 -0
- package/interop.js +356 -0
- package/maintenance.d.ts +27 -5
- package/maintenance.js +92 -28
- package/mcp-app.d.ts +7 -1
- package/mcp-app.js +259 -40
- package/metrics.d.ts +24 -0
- package/metrics.js +167 -0
- package/node-server.d.ts +9 -1
- package/node-server.js +7 -2
- package/openapi.json +10970 -0
- package/package.json +3 -2
- package/rate-limit.d.ts +80 -0
- package/rate-limit.js +741 -0
- package/rest-api.d.ts +22 -0
- package/rest-api.js +166 -11
- package/rules.js +6 -3
- package/self-host.d.ts +45 -8
- package/self-host.js +616 -46
- package/slotlock-manifest.schema.json +7 -1
- package/slotlock.manifest.json +8 -2
- package/store.d.ts +647 -28
- package/store.js +2982 -327
- package/sync.d.ts +43 -2
- package/sync.js +1515 -323
- package/timezone.d.ts +58 -3
- package/timezone.js +249 -20
- package/types.d.ts +300 -9
- package/types.js +0 -2
- package/webhooks.d.ts +91 -0
- 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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
354
|
-
*
|
|
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
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;
|