slotlock 0.2.0 → 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.
Files changed (51) hide show
  1. package/CHANGELOG.md +330 -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 +277 -53
  7. package/SECURITY.md +31 -2
  8. package/SPEC.md +142 -15
  9. package/agent-server.d.ts +12 -3
  10. package/agent-server.js +108 -78
  11. package/agent-store-backend.d.ts +6 -0
  12. package/agent-store-backend.js +355 -36
  13. package/api-keys.d.ts +11 -3
  14. package/api-keys.js +29 -6
  15. package/application-api.d.ts +14 -1
  16. package/application-api.js +274 -12
  17. package/config.d.ts +93 -0
  18. package/config.js +192 -0
  19. package/dashboard.d.ts +6 -0
  20. package/dashboard.js +17 -1
  21. package/ddl.d.ts +45 -3
  22. package/ddl.js +538 -12
  23. package/engine.js +20 -6
  24. package/event-schema.d.ts +59 -0
  25. package/event-schema.js +85 -0
  26. package/ical-worker-thread.js +4 -2
  27. package/ical-worker.d.ts +8 -5
  28. package/ical-worker.js +9 -2
  29. package/index.d.ts +5 -5
  30. package/index.js +3 -3
  31. package/maintenance.d.ts +15 -5
  32. package/maintenance.js +59 -27
  33. package/node-server.d.ts +9 -1
  34. package/node-server.js +7 -2
  35. package/openapi.json +6024 -0
  36. package/package.json +3 -2
  37. package/rate-limit.d.ts +71 -0
  38. package/rate-limit.js +718 -0
  39. package/rest-api.js +10 -0
  40. package/rules.js +6 -3
  41. package/self-host.d.ts +22 -8
  42. package/self-host.js +307 -34
  43. package/slotlock-manifest.schema.json +6 -1
  44. package/slotlock.manifest.json +7 -2
  45. package/store.d.ts +334 -24
  46. package/store.js +2029 -290
  47. package/sync.d.ts +43 -2
  48. package/sync.js +1515 -323
  49. package/timezone.d.ts +58 -3
  50. package/timezone.js +249 -20
  51. package/types.d.ts +213 -9
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
- /** Null only for legacy, hold, and parity-shadow rows. */
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
- /** Stable booking identity inside tenantRef; null for legacy, hold, and shadow rows. */
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
- /** A place in the roll's order of due masters: the last one a batch examined. */
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
  }
@@ -338,11 +510,18 @@ export interface CalendarHorizonRollResult {
338
510
  * stored end and the window's: a roll never moves a series' end back.
339
511
  */
340
512
  extended: number;
341
- /** Masters whose new occurrence collided; their previous coverage remains intact. */
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
+ */
342
517
  conflicts: number;
518
+ /** The ids of those masters: what blocks each is a booking or hold on its new occurrences. */
519
+ conflictEventIds: string[];
343
520
  /**
344
521
  * 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.
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.
346
525
  */
347
526
  refused: number;
348
527
  /** The ids of those masters, to find and fix them. */
@@ -350,9 +529,8 @@ export interface CalendarHorizonRollResult {
350
529
  /** True when more due masters remain beyond this bounded batch. */
351
530
  hasMore: boolean;
352
531
  /**
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.
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.
356
534
  */
357
535
  next: CalendarHorizonRollCursor | null;
358
536
  }
@@ -364,6 +542,26 @@ export interface CalendarEventRetentionResult {
364
542
  /** True when either class filled its batch, so more rows may already be eligible. */
365
543
  hasMore: boolean;
366
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
+ }
367
565
  export interface CalendarCoverageCommand {
368
566
  tenantRef: string;
369
567
  resourceId: string;
@@ -385,6 +583,12 @@ export type CalendarCoverageResult = {
385
583
  } | {
386
584
  ok: false;
387
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;
388
592
  };
389
593
  export interface CalendarCoverageAssessment {
390
594
  state: 'complete' | 'partial' | 'unknown';