slotlock 0.1.1 → 0.3.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 +455 -0
- package/CONTRIBUTING.md +55 -19
- package/GOVERNANCE.md +4 -2
- package/MAINTAINERS.md +3 -2
- package/NOTICE +4 -4
- package/README.md +403 -103
- package/SECURITY.md +70 -5
- package/SPEC.md +161 -21
- package/agent-server.d.ts +76 -10
- package/agent-server.js +381 -146
- package/agent-store-backend.d.ts +22 -0
- package/agent-store-backend.js +598 -24
- package/api-keys.d.ts +22 -7
- package/api-keys.js +38 -8
- package/application-api.d.ts +67 -0
- package/application-api.js +552 -0
- package/config.d.ts +93 -0
- package/config.js +192 -0
- package/dashboard.d.ts +10 -1
- package/dashboard.js +23 -5
- package/ddl.d.ts +47 -4
- package/ddl.js +679 -25
- package/engine.js +20 -6
- package/event-schema.d.ts +59 -0
- package/event-schema.js +85 -0
- package/hooks.d.ts +7 -0
- package/hooks.js +13 -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 -8
- package/index.js +7 -5
- package/maintenance.d.ts +67 -0
- package/maintenance.js +213 -0
- package/node-server.d.ts +9 -1
- package/node-server.js +127 -6
- package/openapi.json +6024 -0
- package/package.json +3 -2
- package/rate-limit.d.ts +71 -0
- package/rate-limit.js +718 -0
- package/rest-api.d.ts +33 -18
- package/rest-api.js +141 -63
- package/rules.d.ts +19 -0
- package/rules.js +38 -7
- package/self-host.d.ts +26 -9
- package/self-host.js +427 -47
- package/slotlock-manifest.schema.json +6 -1
- package/slotlock.manifest.json +7 -2
- package/store.d.ts +359 -27
- package/store.js +2077 -296
- package/sync.d.ts +43 -2
- package/sync.js +1950 -277
- package/timezone.d.ts +77 -3
- package/timezone.js +265 -21
- package/types.d.ts +217 -10
- package/weekly-hours.js +2 -2
package/types.d.ts
CHANGED
|
@@ -21,14 +21,28 @@ export interface SlotlockResource {
|
|
|
21
21
|
* uses the default (`SLOTLOCK_AVAILABILITY`, or the backend's `availabilityRules`).
|
|
22
22
|
*/
|
|
23
23
|
availabilityRules?: WeeklyAvailabilityRule[];
|
|
24
|
+
/**
|
|
25
|
+
* `shared` resources are listed to agents and booked through them; `private` ones are invisible
|
|
26
|
+
* to agents and reached only by the tenant's own code. Fixed when the resource is created. The
|
|
27
|
+
* store always says which; a resource built elsewhere without it is shared.
|
|
28
|
+
*/
|
|
29
|
+
visibility?: 'shared' | 'private';
|
|
30
|
+
/** When the tenant archived it: no longer listed or booked, its history kept. Absent in use. */
|
|
31
|
+
archivedAt?: Date;
|
|
24
32
|
}
|
|
25
33
|
/** A confirmed occupation of a resource. The DB EXCLUDE constraint is the overlap arbiter. */
|
|
26
34
|
export interface SlotlockReservation {
|
|
27
35
|
id: string;
|
|
28
36
|
resourceId: string;
|
|
29
|
-
/**
|
|
37
|
+
/**
|
|
38
|
+
* Null only for live holds, rows on a resource that has no tenant (legacy), and parity-shadow rows.
|
|
39
|
+
* A booking made by confirming a hold, or by createReservation, on a tenant's resource has one.
|
|
40
|
+
*/
|
|
30
41
|
tenantRef: string | null;
|
|
31
|
-
/**
|
|
42
|
+
/**
|
|
43
|
+
* Stable booking identity inside tenantRef; null exactly when tenantRef is. A confirmed hold, and
|
|
44
|
+
* a createReservation booking, use their own id.
|
|
45
|
+
*/
|
|
32
46
|
externalRef: string | null;
|
|
33
47
|
start: Date;
|
|
34
48
|
end: Date;
|
|
@@ -82,6 +96,25 @@ export type ExternalReservationResult = {
|
|
|
82
96
|
} | {
|
|
83
97
|
ok: false;
|
|
84
98
|
code: 'idempotency_conflict';
|
|
99
|
+
}
|
|
100
|
+
/** The resource is archived: it takes no new booking (cancel and release still work). */
|
|
101
|
+
| {
|
|
102
|
+
ok: false;
|
|
103
|
+
code: 'resource_archived';
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* A recurring event on the resource is not stored as far as this write reaches, and cannot be
|
|
107
|
+
* extended that far (past the rolling horizon, or blocked): the time cannot be proven free.
|
|
108
|
+
*/
|
|
109
|
+
| {
|
|
110
|
+
ok: false;
|
|
111
|
+
code: 'recurrence_not_materialized';
|
|
112
|
+
}
|
|
113
|
+
/** The tenant's bookings and retired references are at its `reservations` limit. */
|
|
114
|
+
| {
|
|
115
|
+
ok: false;
|
|
116
|
+
code: 'reservation_limit_reached';
|
|
117
|
+
limit: number;
|
|
85
118
|
} | {
|
|
86
119
|
ok: false;
|
|
87
120
|
code: 'overlap';
|
|
@@ -106,6 +139,15 @@ export type CancelExternalReservationResult = {
|
|
|
106
139
|
} | {
|
|
107
140
|
ok: false;
|
|
108
141
|
code: 'revision_conflict';
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Retiring a reference no booking holds would add a row past the tenant's `reservations` limit
|
|
145
|
+
* (cancelling a booking it has never does).
|
|
146
|
+
*/
|
|
147
|
+
| {
|
|
148
|
+
ok: false;
|
|
149
|
+
code: 'reservation_limit_reached';
|
|
150
|
+
limit: number;
|
|
109
151
|
};
|
|
110
152
|
/**
|
|
111
153
|
* VAVAILABILITY-style bookable window (RFC 7953 semantics, weekly subset): a weekly-cadence RRULE
|
|
@@ -127,6 +169,30 @@ export type CreateReservationResult = {
|
|
|
127
169
|
} | {
|
|
128
170
|
ok: false;
|
|
129
171
|
code: 'invalid_window';
|
|
172
|
+
}
|
|
173
|
+
/** No resource with that id, or (under tenant RLS or with `tenantRef`) not one of the tenant's. */
|
|
174
|
+
| {
|
|
175
|
+
ok: false;
|
|
176
|
+
code: 'resource_not_found';
|
|
177
|
+
}
|
|
178
|
+
/** The resource is archived: it takes no new booking (cancel and release still work). */
|
|
179
|
+
| {
|
|
180
|
+
ok: false;
|
|
181
|
+
code: 'resource_archived';
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* A recurring event on the resource is not stored as far as this write reaches, and cannot be
|
|
185
|
+
* extended that far (past the rolling horizon, or blocked): the time cannot be proven free.
|
|
186
|
+
*/
|
|
187
|
+
| {
|
|
188
|
+
ok: false;
|
|
189
|
+
code: 'recurrence_not_materialized';
|
|
190
|
+
}
|
|
191
|
+
/** The booking's tenant is at its `reservations` limit. */
|
|
192
|
+
| {
|
|
193
|
+
ok: false;
|
|
194
|
+
code: 'reservation_limit_reached';
|
|
195
|
+
limit: number;
|
|
130
196
|
} | {
|
|
131
197
|
ok: false;
|
|
132
198
|
code: 'overlap';
|
|
@@ -138,18 +204,60 @@ export interface SlotlockHold {
|
|
|
138
204
|
resourceId: string;
|
|
139
205
|
start: Date;
|
|
140
206
|
end: Date;
|
|
207
|
+
/** Turnaround kept busy after `end`; the booking the hold is confirmed into keeps it. */
|
|
208
|
+
bufferAfterMs: number;
|
|
141
209
|
expiresAt: Date;
|
|
142
210
|
source: string | null;
|
|
143
211
|
}
|
|
144
|
-
export type AcquireHoldResult =
|
|
212
|
+
export type AcquireHoldResult =
|
|
213
|
+
/** `idempotent`: a retry of a keyed hold, answered with the hold the first request made. */
|
|
214
|
+
{
|
|
145
215
|
ok: true;
|
|
146
216
|
hold: SlotlockHold;
|
|
217
|
+
idempotent: boolean;
|
|
147
218
|
} | {
|
|
148
219
|
ok: false;
|
|
149
220
|
code: 'invalid_window';
|
|
150
221
|
} | {
|
|
151
222
|
ok: false;
|
|
152
223
|
code: 'invalid_ttl';
|
|
224
|
+
} | {
|
|
225
|
+
ok: false;
|
|
226
|
+
code: 'invalid_buffer';
|
|
227
|
+
}
|
|
228
|
+
/** A tenant, principal or idempotency key that is malformed, or a key without both of the others. */
|
|
229
|
+
| {
|
|
230
|
+
ok: false;
|
|
231
|
+
code: 'invalid_identity';
|
|
232
|
+
}
|
|
233
|
+
/** No resource with that id, or (under tenant RLS or with `tenantRef`) not one of the tenant's. */
|
|
234
|
+
| {
|
|
235
|
+
ok: false;
|
|
236
|
+
code: 'resource_not_found';
|
|
237
|
+
}
|
|
238
|
+
/** The resource is archived: it takes no new booking (cancel and release still work). */
|
|
239
|
+
| {
|
|
240
|
+
ok: false;
|
|
241
|
+
code: 'resource_archived';
|
|
242
|
+
}
|
|
243
|
+
/** The idempotency key already made a hold with another resource, window, ttl, buffer or source. */
|
|
244
|
+
| {
|
|
245
|
+
ok: false;
|
|
246
|
+
code: 'idempotency_conflict';
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* A recurring event on the resource is not stored as far as this write reaches, and cannot be
|
|
250
|
+
* extended that far (past the rolling horizon, or blocked): the time cannot be proven free.
|
|
251
|
+
*/
|
|
252
|
+
| {
|
|
253
|
+
ok: false;
|
|
254
|
+
code: 'recurrence_not_materialized';
|
|
255
|
+
}
|
|
256
|
+
/** The resource's tenant has as many holds as its `holds` limit, expired ones deleted. */
|
|
257
|
+
| {
|
|
258
|
+
ok: false;
|
|
259
|
+
code: 'hold_limit_reached';
|
|
260
|
+
limit: number;
|
|
153
261
|
} | {
|
|
154
262
|
ok: false;
|
|
155
263
|
code: 'overlap';
|
|
@@ -164,6 +272,27 @@ export type ConfirmHoldResult = {
|
|
|
164
272
|
} | {
|
|
165
273
|
ok: false;
|
|
166
274
|
code: 'hold_expired';
|
|
275
|
+
}
|
|
276
|
+
/** A cancellation of the hold's reference (its id) arrived first and retired it. */
|
|
277
|
+
| {
|
|
278
|
+
ok: false;
|
|
279
|
+
code: 'reservation_cancelled';
|
|
280
|
+
}
|
|
281
|
+
/** Another booking of the tenant already uses the hold's id as its reference. */
|
|
282
|
+
| {
|
|
283
|
+
ok: false;
|
|
284
|
+
code: 'idempotency_conflict';
|
|
285
|
+
}
|
|
286
|
+
/** The hold's resource was archived since: it takes no new booking. */
|
|
287
|
+
| {
|
|
288
|
+
ok: false;
|
|
289
|
+
code: 'resource_archived';
|
|
290
|
+
}
|
|
291
|
+
/** The tenant is at its `reservations` limit: the hold stays a hold. */
|
|
292
|
+
| {
|
|
293
|
+
ok: false;
|
|
294
|
+
code: 'reservation_limit_reached';
|
|
295
|
+
limit: number;
|
|
167
296
|
};
|
|
168
297
|
export type ReleaseHoldResult = {
|
|
169
298
|
ok: true;
|
|
@@ -241,6 +370,11 @@ export interface SlotlockCalendarEvent extends CalendarEventContent {
|
|
|
241
370
|
updatedAt: Date;
|
|
242
371
|
}
|
|
243
372
|
export interface PutCalendarEventCommand extends CalendarEventContent {
|
|
373
|
+
/**
|
|
374
|
+
* The resource the writer read the event on: a revision is refused (`resource_tenant_mismatch`)
|
|
375
|
+
* when the stored event is on another, checked under the event's lock.
|
|
376
|
+
*/
|
|
377
|
+
expectedResourceId?: string;
|
|
244
378
|
tenantRef: string;
|
|
245
379
|
/** Defaults to the trusted `internal` namespace. Agent callers must supply their owner digest. */
|
|
246
380
|
ownerRef?: string;
|
|
@@ -281,9 +415,20 @@ export type PutCalendarEventResult = {
|
|
|
281
415
|
ok: false;
|
|
282
416
|
code: 'owner_command_quota_exceeded';
|
|
283
417
|
limit: number;
|
|
418
|
+
}
|
|
419
|
+
/** A new event would pass the tenant's `events` limit (revising one it has never does). */
|
|
420
|
+
| {
|
|
421
|
+
ok: false;
|
|
422
|
+
code: 'event_limit_reached';
|
|
423
|
+
limit: number;
|
|
284
424
|
} | {
|
|
285
425
|
ok: false;
|
|
286
426
|
code: 'resource_tenant_mismatch';
|
|
427
|
+
}
|
|
428
|
+
/** The resource is archived: it takes no new booking (cancel and release still work). */
|
|
429
|
+
| {
|
|
430
|
+
ok: false;
|
|
431
|
+
code: 'resource_archived';
|
|
287
432
|
} | {
|
|
288
433
|
ok: false;
|
|
289
434
|
code: 'event_cancelled';
|
|
@@ -292,11 +437,30 @@ export type PutCalendarEventResult = {
|
|
|
292
437
|
ok: false;
|
|
293
438
|
code: 'revision_conflict';
|
|
294
439
|
currentRevision: number;
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* A recurring event on the resource is not stored as far as this write reaches, and cannot be
|
|
443
|
+
* extended that far (past the rolling horizon, or blocked): the time cannot be proven free.
|
|
444
|
+
*/
|
|
445
|
+
| {
|
|
446
|
+
ok: false;
|
|
447
|
+
code: 'recurrence_not_materialized';
|
|
295
448
|
} | {
|
|
296
449
|
ok: false;
|
|
297
450
|
code: 'overlap';
|
|
298
451
|
conflictingReservationId: string | null;
|
|
299
452
|
};
|
|
453
|
+
export type DeleteResourceResult =
|
|
454
|
+
/** `deleted`: false when the tenant has no resource with that id (deleted already, say). */
|
|
455
|
+
{
|
|
456
|
+
ok: true;
|
|
457
|
+
deleted: boolean;
|
|
458
|
+
}
|
|
459
|
+
/** A future booking, a live hold, or a calendar event is still on it. */
|
|
460
|
+
| {
|
|
461
|
+
ok: false;
|
|
462
|
+
code: 'resource_in_use';
|
|
463
|
+
};
|
|
300
464
|
export type CancelCalendarEventResult = {
|
|
301
465
|
ok: true;
|
|
302
466
|
eventId: string;
|
|
@@ -324,8 +488,16 @@ export type CancelCalendarEventResult = {
|
|
|
324
488
|
code: 'revision_conflict';
|
|
325
489
|
currentRevision: number;
|
|
326
490
|
};
|
|
327
|
-
/**
|
|
491
|
+
/**
|
|
492
|
+
* A place in the roll's order of due masters: the last one a batch examined. The roll takes the
|
|
493
|
+
* masters it has never put back first, then those it has, longest waiting first.
|
|
494
|
+
*/
|
|
328
495
|
export interface CalendarHorizonRollCursor {
|
|
496
|
+
/**
|
|
497
|
+
* When the master was due to be examined again, if the roll had put it back; null (or, from a
|
|
498
|
+
* cursor of an earlier release, missing) when it had not.
|
|
499
|
+
*/
|
|
500
|
+
rollRetryAt?: Date | null;
|
|
329
501
|
materializedEndsAt: Date;
|
|
330
502
|
eventId: string;
|
|
331
503
|
}
|
|
@@ -333,13 +505,23 @@ export interface CalendarHorizonRollCursor {
|
|
|
333
505
|
export interface CalendarHorizonRollResult {
|
|
334
506
|
/** Due recurring masters examined in deterministic order. */
|
|
335
507
|
examined: number;
|
|
336
|
-
/**
|
|
508
|
+
/**
|
|
509
|
+
* Masters atomically rematerialized from the requested window's start through the later of their
|
|
510
|
+
* stored end and the window's: a roll never moves a series' end back.
|
|
511
|
+
*/
|
|
337
512
|
extended: number;
|
|
338
|
-
/**
|
|
513
|
+
/**
|
|
514
|
+
* Masters whose new occurrence collided; their previous coverage remains intact, and they wait an
|
|
515
|
+
* hour before a roll examines them again, behind every master it has not put back.
|
|
516
|
+
*/
|
|
339
517
|
conflicts: number;
|
|
518
|
+
/** The ids of those masters: what blocks each is a booking or hold on its new occurrences. */
|
|
519
|
+
conflictEventIds: string[];
|
|
340
520
|
/**
|
|
341
521
|
* Masters whose rule takes more than the expansion budget to reach the window (one that never
|
|
342
|
-
* occurs again, or almost never); their previous
|
|
522
|
+
* occurs again, or almost never), or whose roll failed for any other reason; their previous
|
|
523
|
+
* coverage remains intact, and they wait an hour
|
|
524
|
+
* before a roll examines them again, behind every master it has not put back.
|
|
343
525
|
*/
|
|
344
526
|
refused: number;
|
|
345
527
|
/** The ids of those masters, to find and fix them. */
|
|
@@ -347,9 +529,8 @@ export interface CalendarHorizonRollResult {
|
|
|
347
529
|
/** True when more due masters remain beyond this bounded batch. */
|
|
348
530
|
hasMore: boolean;
|
|
349
531
|
/**
|
|
350
|
-
* Pass as `after` for the next batch of this pass, so the masters this batch
|
|
351
|
-
*
|
|
352
|
-
* by every later batch. `null` when none remain.
|
|
532
|
+
* Pass as `after` for the next batch of this pass, so the masters this batch examined are not
|
|
533
|
+
* examined again by every later batch. `null` when none remain.
|
|
353
534
|
*/
|
|
354
535
|
next: CalendarHorizonRollCursor | null;
|
|
355
536
|
}
|
|
@@ -361,6 +542,26 @@ export interface CalendarEventRetentionResult {
|
|
|
361
542
|
/** True when either class filled its batch, so more rows may already be eligible. */
|
|
362
543
|
hasMore: boolean;
|
|
363
544
|
}
|
|
545
|
+
/** What erasing a tenant deleted. */
|
|
546
|
+
export interface SlotlockTenantErasure {
|
|
547
|
+
/** Rows deleted, by table (every table the tenant owns rows in is named, with 0 for none). */
|
|
548
|
+
rows: Record<string, number>;
|
|
549
|
+
/** Rows deleted in all; 0 when the tenant had already been erased. */
|
|
550
|
+
total: number;
|
|
551
|
+
}
|
|
552
|
+
export interface BookingRetentionResult {
|
|
553
|
+
/** Holds past their expiry: they occupy nothing, and only a rival's write would reap them. */
|
|
554
|
+
expiredHoldsDeleted: number;
|
|
555
|
+
/** Keyed hold answers whose hold has expired: a retry with that key makes a new hold. */
|
|
556
|
+
holdCommandsDeleted: number;
|
|
557
|
+
/**
|
|
558
|
+
* References a cancel retired with no booking, older than the window: a create with one makes a
|
|
559
|
+
* new booking. A cancelled booking's reference stays retired.
|
|
560
|
+
*/
|
|
561
|
+
tombstonesDeleted: number;
|
|
562
|
+
/** True when any class filled its batch, so more rows may already be eligible. */
|
|
563
|
+
hasMore: boolean;
|
|
564
|
+
}
|
|
364
565
|
export interface CalendarCoverageCommand {
|
|
365
566
|
tenantRef: string;
|
|
366
567
|
resourceId: string;
|
|
@@ -382,6 +583,12 @@ export type CalendarCoverageResult = {
|
|
|
382
583
|
} | {
|
|
383
584
|
ok: false;
|
|
384
585
|
code: 'stale_revision' | 'revision_conflict' | 'idempotency_conflict';
|
|
586
|
+
}
|
|
587
|
+
/** A new source would pass the tenant's `coverage` limit (advancing one it has never does). */
|
|
588
|
+
| {
|
|
589
|
+
ok: false;
|
|
590
|
+
code: 'coverage_limit_reached';
|
|
591
|
+
limit: number;
|
|
385
592
|
};
|
|
386
593
|
export interface CalendarCoverageAssessment {
|
|
387
594
|
state: 'complete' | 'partial' | 'unknown';
|
package/weekly-hours.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// Bookable hours: the one validator for weekly availability rules, and the conversion between rules
|
|
2
2
|
// and a week of windows, the form people edit them in (two windows a day at most).
|
|
3
|
-
import { expandRules } from './rules.js';
|
|
3
|
+
import { SLOTLOCK_MAX_RULE_DURATION_MINUTES, expandRules } from './rules.js';
|
|
4
4
|
/** Rules one schedule may hold: the server default, or one resource's own hours. */
|
|
5
5
|
export const SLOTLOCK_MAX_AVAILABILITY_RULES = 50;
|
|
6
6
|
const RULE_KEYS = new Set(['rrule', 'startMinutes', 'durationMinutes']);
|
|
7
7
|
const MAX_RRULE_CHARACTERS = 500;
|
|
8
|
-
const MAX_DURATION_MINUTES =
|
|
8
|
+
const MAX_DURATION_MINUTES = SLOTLOCK_MAX_RULE_DURATION_MINUTES;
|
|
9
9
|
const DAY_MINUTES = 1_440;
|
|
10
10
|
/** Two weeks every supported weekly rule produces a window in. */
|
|
11
11
|
const SAMPLE = { start: new Date('2026-01-05T00:00:00Z'), end: new Date('2026-01-19T00:00:00Z') };
|