slotlock 0.6.1 → 0.6.3
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 +34 -0
- package/README.md +13 -6
- package/SECURITY.md +15 -4
- package/agent-server.js +1 -5
- package/agent-store-backend.d.ts +5 -6
- package/agent-store-backend.js +41 -67
- package/api-keys.js +1 -1
- package/application-api.d.ts +3 -3
- package/application-api.js +16 -24
- package/assistant.d.ts +18 -27
- package/assistant.js +36 -53
- package/booking-rules.d.ts +30 -41
- package/booking-rules.js +17 -25
- package/change-watch.d.ts +1 -0
- package/change-watch.js +1 -1
- package/cli.js +3 -4
- package/config.d.ts +17 -2
- package/config.js +44 -6
- package/dashboard.js +14 -1
- package/ddl.d.ts +20 -27
- package/ddl.js +166 -108
- package/engine.d.ts +6 -9
- package/engine.js +5 -9
- package/event-feed.d.ts +23 -31
- package/event-feed.js +30 -45
- package/event-schema.js +6 -9
- package/hooks.d.ts +2 -4
- package/hooks.js +2 -4
- package/ical-worker-thread.js +3 -4
- package/ical-worker.d.ts +8 -10
- package/ical-worker.js +7 -9
- package/interop.js +1 -4
- package/maintenance.d.ts +14 -10
- package/maintenance.js +43 -26
- package/mcp-app.d.ts +7 -9
- package/mcp-app.js +7 -9
- package/mcp-modern.d.ts +14 -16
- package/mcp-modern.js +23 -27
- package/metrics.js +0 -1
- package/node-server.d.ts +9 -9
- package/node-server.js +25 -34
- package/package-version.js +2 -4
- package/package.json +1 -1
- package/rate-limit.d.ts +6 -1
- package/rate-limit.js +24 -8
- package/rest-api.d.ts +13 -14
- package/rest-api.js +11 -12
- package/rules.d.ts +6 -7
- package/rules.js +30 -42
- package/self-host.d.ts +16 -2
- package/self-host.js +42 -21
- package/slotlock.manifest.json +1 -1
- package/store.d.ts +109 -161
- package/store.js +253 -371
- package/sync.d.ts +44 -59
- package/sync.js +325 -469
- package/timezone.d.ts +25 -33
- package/timezone.js +37 -52
- package/types.d.ts +35 -56
- package/webhooks.d.ts +22 -25
- package/webhooks.js +35 -40
- package/weekly-hours.d.ts +3 -0
- package/weekly-hours.js +1 -1
package/timezone.d.ts
CHANGED
|
@@ -8,9 +8,8 @@ export interface LocalDateTime {
|
|
|
8
8
|
millisecond?: number;
|
|
9
9
|
}
|
|
10
10
|
/**
|
|
11
|
-
* Which instant a wall time names where the clocks change. RFC 5545 §3.3.5
|
|
12
|
-
* (
|
|
13
|
-
* offset in force before the gap, which moves it forward by the gap: the defaults.
|
|
11
|
+
* Which instant a wall time names where the clocks change. RFC 5545 §3.3.5 defaults: a fold
|
|
12
|
+
* (repeated hour) reads as its first instant, a gap (skipped hour) is moved forward by the gap.
|
|
14
13
|
*/
|
|
15
14
|
export interface WallTimeChoice {
|
|
16
15
|
/** In a fold: the earlier instant (default), or the later one. */
|
|
@@ -20,45 +19,41 @@ export interface WallTimeChoice {
|
|
|
20
19
|
}
|
|
21
20
|
export interface TimeZoneConverter {
|
|
22
21
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
22
|
+
* UTC-framed wall-clock fields to the instant that reads as them in this zone (WallTimeChoice
|
|
23
|
+
* defaults).
|
|
25
24
|
*/
|
|
26
25
|
wallFrameToUtc(wallFrameMs: number, choice?: WallTimeChoice): Date;
|
|
27
26
|
}
|
|
28
27
|
/**
|
|
29
|
-
* The instant
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* change the wall time is near); one that maps back to the wall time names it. Two do in a fold,
|
|
33
|
-
* none in a gap, where the offset before the gap is the smaller one.
|
|
28
|
+
* The instant (ms) that reads as `wallFrameMs` (wall-clock fields framed as UTC) in a zone with
|
|
29
|
+
* offsets from `offsetAt`. Candidates are the offsets within fourteen hours either side (no zone is
|
|
30
|
+
* further from UTC); one that maps back names it. Two do in a fold, none in a gap.
|
|
34
31
|
*/
|
|
35
32
|
export declare function wallFrameToInstant(wallFrameMs: number, offsetAt: (utcMs: number) => number, choice?: WallTimeChoice): number;
|
|
36
33
|
/**
|
|
37
|
-
* Date.UTC remaps years 0..99 to 1900..1999
|
|
38
|
-
*
|
|
34
|
+
* Date.UTC remaps years 0..99 to 1900..1999 and Slotlock accepts years down to 1, so frames are
|
|
35
|
+
* built with setUTCFullYear.
|
|
39
36
|
*/
|
|
40
37
|
export declare function utcFrame(year: number, month: number, day: number, hour: number, minute: number, second: number, millisecond: number): Date;
|
|
41
38
|
/** The UTC day an instant falls in, as ZoneOffsets reads a zone's offsets: days since 1970. */
|
|
42
39
|
export declare function zoneOffsetDay(utcMs: number): number;
|
|
43
40
|
/**
|
|
44
|
-
* How far either side of a wall time
|
|
45
|
-
*
|
|
41
|
+
* How far either side of a wall time its conversion reads offsets: no zone is further than this
|
|
42
|
+
* from UTC.
|
|
46
43
|
*/
|
|
47
44
|
export declare const ZONE_OFFSET_REACH_MS: number;
|
|
48
45
|
/**
|
|
49
|
-
* How many
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* midnight and this the first time it reads them.
|
|
46
|
+
* How many Intl reads finding the changes on UTC day `day` takes beyond its two midnights (about
|
|
47
|
+
* twenty per change). Callers that must charge the same whatever the process has cached charge this
|
|
48
|
+
* on first read.
|
|
53
49
|
*/
|
|
54
50
|
export declare function zoneOffsetChangeReads(timezone: string, day: number): number;
|
|
55
51
|
export declare function createTimeZoneConverter(timezone: string): TimeZoneConverter;
|
|
56
52
|
/**
|
|
57
|
-
* The spelling Slotlock keeps of an IANA zone
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* name and is refused, as is anything Intl does not know: both with `code: 'invalid_timezone'`.
|
|
53
|
+
* The spelling Slotlock keeps of an IANA zone: Intl's when it reads the name as itself in another
|
|
54
|
+
* case (`europe/london` is `Europe/London`), else the name as given (`US/Eastern` stays, so a
|
|
55
|
+
* stored name never depends on the ICU version). A UTC offset such as `+05:00` and unknown names
|
|
56
|
+
* are refused with `code: 'invalid_timezone'`.
|
|
62
57
|
*/
|
|
63
58
|
export declare function canonicalTimeZoneName(timezone: string): string;
|
|
64
59
|
export interface ZonedCalendarDate {
|
|
@@ -68,9 +63,8 @@ export interface ZonedCalendarDate {
|
|
|
68
63
|
isoWeekday: number;
|
|
69
64
|
}
|
|
70
65
|
/**
|
|
71
|
-
* The civil date and ISO weekday an instant falls on in `timezone`
|
|
72
|
-
*
|
|
73
|
-
* 00:30 BST on a Friday is a Friday, not the Thursday its UTC timestamp reads as.
|
|
66
|
+
* The civil date and ISO weekday an instant falls on in `timezone` (the inverse of {@link
|
|
67
|
+
* zonedDateTimeToUtc}): a pickup at 00:30 BST on a Friday is a Friday, whatever UTC says.
|
|
74
68
|
*/
|
|
75
69
|
export declare function zonedCalendarDate(instant: Date, timezone: string): ZonedCalendarDate;
|
|
76
70
|
export interface ZonedWallTime {
|
|
@@ -80,9 +74,8 @@ export interface ZonedWallTime {
|
|
|
80
74
|
time: string;
|
|
81
75
|
}
|
|
82
76
|
/**
|
|
83
|
-
* The civil date and wall-clock time an instant reads as in `timezone
|
|
84
|
-
* state a booking
|
|
85
|
-
* server's and not UTC.
|
|
77
|
+
* The civil date and wall-clock time an instant reads as in `timezone`, as agreements and receipts
|
|
78
|
+
* state a booking: on the resource's own clock.
|
|
86
79
|
*/
|
|
87
80
|
export declare function zonedWallTime(instant: Date, timezone: string): ZonedWallTime;
|
|
88
81
|
/** How far `timezone` is ahead of UTC at `instant`, in whole minutes (negative west of UTC). */
|
|
@@ -98,7 +91,7 @@ export interface ZonedDateTimeFields {
|
|
|
98
91
|
}
|
|
99
92
|
/**
|
|
100
93
|
* The wall-clock fields an instant reads as in `timezone`, to the second: what a calendar writes
|
|
101
|
-
* beside a TZID.
|
|
94
|
+
* beside a TZID.
|
|
102
95
|
*/
|
|
103
96
|
export declare function zonedDateTimeFields(instant: Date, timezone: string): ZonedDateTimeFields;
|
|
104
97
|
export declare function zonedDateTimeToUtc(value: LocalDateTime, timezone: string, choice?: WallTimeChoice): Date;
|
|
@@ -111,8 +104,7 @@ export interface ZoneTransition {
|
|
|
111
104
|
after: number;
|
|
112
105
|
}
|
|
113
106
|
/**
|
|
114
|
-
* Every change of `timezone`'s offset from `fromMs` to `toMs`,
|
|
115
|
-
*
|
|
116
|
-
* days later, which a search a week at a time missed.
|
|
107
|
+
* Every change of `timezone`'s offset from `fromMs` to `toMs`, read a day at a time (ZoneOffsets):
|
|
108
|
+
* a weekly search misses short-lived changes such as Gaza's six-day one in October 2040.
|
|
117
109
|
*/
|
|
118
110
|
export declare function zoneTransitions(timezone: string, fromMs: number, toMs: number): ZoneTransition[];
|
package/timezone.js
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
const FOURTEEN_HOURS_MS = 14 * 60 * 60 * 1000;
|
|
2
2
|
/**
|
|
3
|
-
* The instant
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* change the wall time is near); one that maps back to the wall time names it. Two do in a fold,
|
|
7
|
-
* none in a gap, where the offset before the gap is the smaller one.
|
|
3
|
+
* The instant (ms) that reads as `wallFrameMs` (wall-clock fields framed as UTC) in a zone with
|
|
4
|
+
* offsets from `offsetAt`. Candidates are the offsets within fourteen hours either side (no zone is
|
|
5
|
+
* further from UTC); one that maps back names it. Two do in a fold, none in a gap.
|
|
8
6
|
*/
|
|
9
7
|
export function wallFrameToInstant(wallFrameMs, offsetAt, choice = {}) {
|
|
10
8
|
const offsets = new Set([
|
|
@@ -25,8 +23,8 @@ export function wallFrameToInstant(wallFrameMs, offsetAt, choice = {}) {
|
|
|
25
23
|
const shifted = wallFrameMs - Math.min(...offsets);
|
|
26
24
|
if (choice.gap !== 'end')
|
|
27
25
|
return shifted;
|
|
28
|
-
// The gap ends at the change: the first whole second after the instant read with the
|
|
29
|
-
//
|
|
26
|
+
// The gap ends at the change: the first whole second after the instant read with the after-gap
|
|
27
|
+
// offset whose offset differs from it.
|
|
30
28
|
let before = wallFrameMs - Math.max(...offsets);
|
|
31
29
|
const offsetBefore = offsetAt(before);
|
|
32
30
|
let after = shifted;
|
|
@@ -40,8 +38,8 @@ export function wallFrameToInstant(wallFrameMs, offsetAt, choice = {}) {
|
|
|
40
38
|
return after;
|
|
41
39
|
}
|
|
42
40
|
/**
|
|
43
|
-
* Date.UTC remaps years 0..99 to 1900..1999
|
|
44
|
-
*
|
|
41
|
+
* Date.UTC remaps years 0..99 to 1900..1999 and Slotlock accepts years down to 1, so frames are
|
|
42
|
+
* built with setUTCFullYear.
|
|
45
43
|
*/
|
|
46
44
|
export function utcFrame(year, month, day, hour, minute, second, millisecond) {
|
|
47
45
|
const frame = new Date(0);
|
|
@@ -50,10 +48,8 @@ export function utcFrame(year, month, day, hour, minute, second, millisecond) {
|
|
|
50
48
|
return frame;
|
|
51
49
|
}
|
|
52
50
|
/**
|
|
53
|
-
* Formatters
|
|
54
|
-
*
|
|
55
|
-
* changes, so one per zone serves every caller. Intl accepts every capitalisation of each zone's
|
|
56
|
-
* name, so the map is emptied when it fills rather than growing with whatever names arrive.
|
|
51
|
+
* Formatters by zone name: building one costs far more than using it. Intl accepts any
|
|
52
|
+
* capitalisation of a name, so the map is emptied when full rather than growing.
|
|
57
53
|
*/
|
|
58
54
|
const formatters = new Map();
|
|
59
55
|
const MAX_CACHED_FORMATTERS = 256;
|
|
@@ -75,21 +71,19 @@ export function zoneOffsetDay(utcMs) {
|
|
|
75
71
|
return Math.floor(utcMs / DAY_MS);
|
|
76
72
|
}
|
|
77
73
|
/**
|
|
78
|
-
* How far either side of a wall time
|
|
79
|
-
*
|
|
74
|
+
* How far either side of a wall time its conversion reads offsets: no zone is further than this
|
|
75
|
+
* from UTC.
|
|
80
76
|
*/
|
|
81
77
|
export const ZONE_OFFSET_REACH_MS = FOURTEEN_HOURS_MS;
|
|
82
78
|
/** How many midnights one block of a zone's offsets holds. */
|
|
83
79
|
const BLOCK_DAYS = 512;
|
|
84
80
|
const NO_CHANGES = [];
|
|
85
81
|
/**
|
|
86
|
-
* A zone's offsets, read from Intl a UTC day at a time and kept
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
* second (scanTransitions). A change and its reversal within one day would be missed; the tests
|
|
92
|
-
* compare the zones whose clocks change most with an hourly search.
|
|
82
|
+
* A zone's offsets, read from Intl a UTC day at a time and kept: formatToParts costs about two
|
|
83
|
+
* microseconds and a conversion needs four to six offsets. Each midnight is read once; a day whose
|
|
84
|
+
* midnights agree has that offset throughout, and a day whose midnights differ has each change
|
|
85
|
+
* narrowed to the second (scanTransitions). A change and its reversal within one day would be
|
|
86
|
+
* missed; tests check the most volatile zones against an hourly search.
|
|
93
87
|
*/
|
|
94
88
|
class ZoneOffsets {
|
|
95
89
|
/** The offset at each UTC midnight read (NaN where none is), BLOCK_DAYS days to a block. */
|
|
@@ -102,7 +96,7 @@ class ZoneOffsets {
|
|
|
102
96
|
}
|
|
103
97
|
offsetAt(utcMs) {
|
|
104
98
|
const day = zoneOffsetDay(utcMs);
|
|
105
|
-
// Not a time value: Intl
|
|
99
|
+
// Not a time value: let Intl refuse it.
|
|
106
100
|
if (!Number.isFinite(day))
|
|
107
101
|
return offsetAt(utcMs, this.formatter);
|
|
108
102
|
let offset = this.midnight(day);
|
|
@@ -170,10 +164,8 @@ function dayStart(day) {
|
|
|
170
164
|
return Math.min(Math.max(day * DAY_MS, -MAX_TIME_MS), MAX_TIME_MS);
|
|
171
165
|
}
|
|
172
166
|
/**
|
|
173
|
-
* Each zone's offsets
|
|
174
|
-
*
|
|
175
|
-
* are the days with changes once there are this many of those (zones change twice a year at most
|
|
176
|
-
* but for a few).
|
|
167
|
+
* Each zone's offsets by name. A block of midnights is 4 KB; past this many blocks (16 MB) every
|
|
168
|
+
* zone's are emptied, and likewise for days with changes.
|
|
177
169
|
*/
|
|
178
170
|
const zoneOffsetTables = new Map();
|
|
179
171
|
const MAX_CACHED_BLOCKS = 4_096;
|
|
@@ -194,10 +186,9 @@ function zoneOffsets(timezone) {
|
|
|
194
186
|
return offsets;
|
|
195
187
|
}
|
|
196
188
|
/**
|
|
197
|
-
* How many
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
* midnight and this the first time it reads them.
|
|
189
|
+
* How many Intl reads finding the changes on UTC day `day` takes beyond its two midnights (about
|
|
190
|
+
* twenty per change). Callers that must charge the same whatever the process has cached charge this
|
|
191
|
+
* on first read.
|
|
201
192
|
*/
|
|
202
193
|
export function zoneOffsetChangeReads(timezone, day) {
|
|
203
194
|
if (timezone === 'UTC')
|
|
@@ -222,8 +213,8 @@ function buildFormatter(timezone) {
|
|
|
222
213
|
}
|
|
223
214
|
}
|
|
224
215
|
function offsetAt(utcMs, formatter) {
|
|
225
|
-
// Intl parts expose seconds
|
|
226
|
-
//
|
|
216
|
+
// Intl parts expose seconds, not milliseconds: compute the offset at the whole second so
|
|
217
|
+
// sub-second input is not rounded into it.
|
|
227
218
|
const wholeSecondMs = Math.floor(utcMs / 1000) * 1000;
|
|
228
219
|
const values = {};
|
|
229
220
|
for (const part of formatter.formatToParts(new Date(wholeSecondMs))) {
|
|
@@ -246,11 +237,10 @@ export function createTimeZoneConverter(timezone) {
|
|
|
246
237
|
};
|
|
247
238
|
}
|
|
248
239
|
/**
|
|
249
|
-
* The spelling Slotlock keeps of an IANA zone
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
* name and is refused, as is anything Intl does not know: both with `code: 'invalid_timezone'`.
|
|
240
|
+
* The spelling Slotlock keeps of an IANA zone: Intl's when it reads the name as itself in another
|
|
241
|
+
* case (`europe/london` is `Europe/London`), else the name as given (`US/Eastern` stays, so a
|
|
242
|
+
* stored name never depends on the ICU version). A UTC offset such as `+05:00` and unknown names
|
|
243
|
+
* are refused with `code: 'invalid_timezone'`.
|
|
254
244
|
*/
|
|
255
245
|
export function canonicalTimeZoneName(timezone) {
|
|
256
246
|
const name = timezone.trim();
|
|
@@ -269,9 +259,8 @@ export function canonicalTimeZoneName(timezone) {
|
|
|
269
259
|
return resolved.toLowerCase() === name.toLowerCase() ? resolved : name;
|
|
270
260
|
}
|
|
271
261
|
/**
|
|
272
|
-
* The civil date and ISO weekday an instant falls on in `timezone`
|
|
273
|
-
*
|
|
274
|
-
* 00:30 BST on a Friday is a Friday, not the Thursday its UTC timestamp reads as.
|
|
262
|
+
* The civil date and ISO weekday an instant falls on in `timezone` (the inverse of {@link
|
|
263
|
+
* zonedDateTimeToUtc}): a pickup at 00:30 BST on a Friday is a Friday, whatever UTC says.
|
|
275
264
|
*/
|
|
276
265
|
export function zonedCalendarDate(instant, timezone) {
|
|
277
266
|
const wall = wallFrame(instant, timezone);
|
|
@@ -279,9 +268,8 @@ export function zonedCalendarDate(instant, timezone) {
|
|
|
279
268
|
return { date: civilDate(wall), isoWeekday: weekday === 0 ? 7 : weekday };
|
|
280
269
|
}
|
|
281
270
|
/**
|
|
282
|
-
* The civil date and wall-clock time an instant reads as in `timezone
|
|
283
|
-
* state a booking
|
|
284
|
-
* server's and not UTC.
|
|
271
|
+
* The civil date and wall-clock time an instant reads as in `timezone`, as agreements and receipts
|
|
272
|
+
* state a booking: on the resource's own clock.
|
|
285
273
|
*/
|
|
286
274
|
export function zonedWallTime(instant, timezone) {
|
|
287
275
|
const wall = wallFrame(instant, timezone);
|
|
@@ -295,7 +283,7 @@ export function zonedOffsetMinutes(instant, timezone) {
|
|
|
295
283
|
}
|
|
296
284
|
/**
|
|
297
285
|
* The wall-clock fields an instant reads as in `timezone`, to the second: what a calendar writes
|
|
298
|
-
* beside a TZID.
|
|
286
|
+
* beside a TZID.
|
|
299
287
|
*/
|
|
300
288
|
export function zonedDateTimeFields(instant, timezone) {
|
|
301
289
|
const wall = wallFrame(instant, timezone);
|
|
@@ -356,9 +344,8 @@ export function zonedDateTimeToUtc(value, timezone, choice) {
|
|
|
356
344
|
return createTimeZoneConverter(timezone).wallFrameToUtc(wallFrame, choice);
|
|
357
345
|
}
|
|
358
346
|
/**
|
|
359
|
-
* Every change of `timezone`'s offset from `fromMs` to `toMs`,
|
|
360
|
-
*
|
|
361
|
-
* days later, which a search a week at a time missed.
|
|
347
|
+
* Every change of `timezone`'s offset from `fromMs` to `toMs`, read a day at a time (ZoneOffsets):
|
|
348
|
+
* a weekly search misses short-lived changes such as Gaza's six-day one in October 2040.
|
|
362
349
|
*/
|
|
363
350
|
export function zoneTransitions(timezone, fromMs, toMs) {
|
|
364
351
|
if (timezone === 'UTC')
|
|
@@ -366,9 +353,8 @@ export function zoneTransitions(timezone, fromMs, toMs) {
|
|
|
366
353
|
return zoneOffsets(timezone).transitions(Math.floor(fromMs / 1000) * 1000, Math.ceil(toMs / 1000) * 1000);
|
|
367
354
|
}
|
|
368
355
|
/**
|
|
369
|
-
*
|
|
370
|
-
*
|
|
371
|
-
* one step are missed.
|
|
356
|
+
* Offset changes found by comparing `offsetIn` `stepMs` apart and narrowing each to the second; a
|
|
357
|
+
* change and its reversal within one step are missed.
|
|
372
358
|
*/
|
|
373
359
|
function scanTransitions(offsetIn, fromMs, toMs, stepMs) {
|
|
374
360
|
const found = [];
|
|
@@ -380,7 +366,6 @@ function scanTransitions(offsetIn, fromMs, toMs, stepMs) {
|
|
|
380
366
|
at = next;
|
|
381
367
|
continue;
|
|
382
368
|
}
|
|
383
|
-
// The last whole second at `offset` and the first at another.
|
|
384
369
|
let low = at;
|
|
385
370
|
let high = next;
|
|
386
371
|
while (high - low > 1000) {
|
package/types.d.ts
CHANGED
|
@@ -11,10 +11,9 @@ export interface SlotlockResource {
|
|
|
11
11
|
/** Null only for legacy/parity-shadow resources created before authority primitives existed. */
|
|
12
12
|
tenantRef: string | null;
|
|
13
13
|
/**
|
|
14
|
-
* IANA timezone the
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* Rules remain stable across daylight-saving changes because wall time is resolved in this zone.
|
|
14
|
+
* IANA timezone the availability rules are authored in: expandRules evaluates BYDAY and
|
|
15
|
+
* startMinutes as local wall-clock time in this zone, so rules stay stable across daylight-saving
|
|
16
|
+
* changes.
|
|
18
17
|
*/
|
|
19
18
|
timezone: string;
|
|
20
19
|
/**
|
|
@@ -23,9 +22,8 @@ export interface SlotlockResource {
|
|
|
23
22
|
*/
|
|
24
23
|
availabilityRules?: WeeklyAvailabilityRule[];
|
|
25
24
|
/**
|
|
26
|
-
* `shared` resources are listed to
|
|
27
|
-
*
|
|
28
|
-
* store always says which; a resource built elsewhere without it is shared.
|
|
25
|
+
* `shared` resources are listed to and booked by agents; `private` ones are invisible to agents.
|
|
26
|
+
* Fixed at creation; absent means shared.
|
|
29
27
|
*/
|
|
30
28
|
visibility?: 'shared' | 'private';
|
|
31
29
|
/** When the tenant archived it: no longer listed or booked, its history kept. Absent in use. */
|
|
@@ -33,14 +31,13 @@ export interface SlotlockResource {
|
|
|
33
31
|
/** Its own booking rules (`SlotlockBookingRules`). Absent when it has none. */
|
|
34
32
|
bookingRules?: SlotlockBookingRules;
|
|
35
33
|
/**
|
|
36
|
-
* What a booking on it must satisfy:
|
|
37
|
-
*
|
|
38
|
-
* rule applies.
|
|
34
|
+
* What a booking on it must satisfy: tenant defaults, overlaid by its pool's, overlaid by its own
|
|
35
|
+
* (`setBookingRuleDefaults`). Absent when no rule applies.
|
|
39
36
|
*/
|
|
40
37
|
effectiveBookingRules?: SlotlockBookingRules;
|
|
41
38
|
/**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
39
|
+
* Its pool: interchangeable resources (a vehicle class, a room type) booked by the class
|
|
40
|
+
* (`slotlock_book_pool`).
|
|
44
41
|
*/
|
|
45
42
|
pool?: string;
|
|
46
43
|
}
|
|
@@ -48,10 +45,7 @@ export interface SlotlockResource {
|
|
|
48
45
|
export interface SlotlockReservation {
|
|
49
46
|
id: string;
|
|
50
47
|
resourceId: string;
|
|
51
|
-
/**
|
|
52
|
-
* Null only for live holds, rows on a resource that has no tenant (legacy), and parity-shadow rows.
|
|
53
|
-
* A booking made by confirming a hold, or by createReservation, on a tenant's resource has one.
|
|
54
|
-
*/
|
|
48
|
+
/** Null only for live holds, rows on a tenantless (legacy) resource, and parity-shadow rows. */
|
|
55
49
|
tenantRef: string | null;
|
|
56
50
|
/**
|
|
57
51
|
* Stable booking identity inside tenantRef; null exactly when tenantRef is. A confirmed hold, and
|
|
@@ -170,17 +164,15 @@ export type CancelExternalReservationResult = {
|
|
|
170
164
|
limit: number;
|
|
171
165
|
};
|
|
172
166
|
/**
|
|
173
|
-
* VAVAILABILITY-style bookable window (RFC 7953
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
* resource's stored zone for `findNextAvailableFor`, UTC when none is given).
|
|
167
|
+
* VAVAILABILITY-style bookable window (RFC 7953, weekly subset): a weekly RRULE picks the days, and
|
|
168
|
+
* each day's window is `[startMinutes, startMinutes+durationMinutes)` after local midnight in the
|
|
169
|
+
* evaluation timezone (UTC when none is given).
|
|
177
170
|
*/
|
|
178
171
|
export interface WeeklyAvailabilityRule {
|
|
179
172
|
/** RRULE content line without the `RRULE:` prefix, e.g. `FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR`. */
|
|
180
173
|
rrule: string;
|
|
181
|
-
/** Window start, minutes after midnight
|
|
174
|
+
/** Window start, minutes after local midnight (0..1439). */
|
|
182
175
|
startMinutes: number;
|
|
183
|
-
/** Window length in minutes (> 0). */
|
|
184
176
|
durationMinutes: number;
|
|
185
177
|
}
|
|
186
178
|
export type CreateReservationResult = {
|
|
@@ -243,7 +235,6 @@ export interface SlotlockAgendaEntry {
|
|
|
243
235
|
resourceId: string;
|
|
244
236
|
start: Date;
|
|
245
237
|
end: Date;
|
|
246
|
-
/** A booking, a calendar event's occurrence, or a live hold. */
|
|
247
238
|
kind: 'booking' | 'event' | 'hold';
|
|
248
239
|
/** The booking's reference; null for an event's occurrence and a hold. */
|
|
249
240
|
reference: string | null;
|
|
@@ -447,18 +438,17 @@ export interface PutCalendarEventCommand extends CalendarEventContent {
|
|
|
447
438
|
materializationWindow?: Interval;
|
|
448
439
|
source?: string;
|
|
449
440
|
/**
|
|
450
|
-
* Judge the event as a booking made now against the resource's booking rules (
|
|
451
|
-
*
|
|
452
|
-
*
|
|
453
|
-
*
|
|
441
|
+
* Judge the event as a booking made now against the resource's booking rules (first occurrence
|
|
442
|
+
* for notice and advance, every occurrence for step and hours), and keep the resource's
|
|
443
|
+
* turnaround. Off by default, since an imported event already happened elsewhere; the agent tools
|
|
444
|
+
* set it.
|
|
454
445
|
*/
|
|
455
446
|
enforceBookingRules?: boolean;
|
|
456
447
|
/**
|
|
457
|
-
* Book the time a live hold keeps: the hold must be `holderRef`'s
|
|
458
|
-
*
|
|
459
|
-
*
|
|
460
|
-
*
|
|
461
|
-
* again.
|
|
448
|
+
* Book the time a live hold keeps: the hold must be `holderRef`'s, on this resource, and cover
|
|
449
|
+
* the event (a single occurrence). The hold's group is released and the event takes the time in
|
|
450
|
+
* one transaction, so no rival can come between. Notice and advance were judged at hold time;
|
|
451
|
+
* step and hours are judged again.
|
|
462
452
|
*/
|
|
463
453
|
fromHold?: {
|
|
464
454
|
holdId: string;
|
|
@@ -581,14 +571,11 @@ export type CancelCalendarEventResult = {
|
|
|
581
571
|
code: 'revision_conflict';
|
|
582
572
|
currentRevision: number;
|
|
583
573
|
};
|
|
584
|
-
/**
|
|
585
|
-
* A place in the roll's order of due masters: the last one a batch examined. The roll takes the
|
|
586
|
-
* masters it has never put back first, then those it has, longest waiting first.
|
|
587
|
-
*/
|
|
574
|
+
/** A place in the roll's order of due masters: the last one a batch examined. */
|
|
588
575
|
export interface CalendarHorizonRollCursor {
|
|
589
576
|
/**
|
|
590
|
-
* When the master was due
|
|
591
|
-
* cursor
|
|
577
|
+
* When the master was due again, if the roll had deferred it; null (or missing, from an older
|
|
578
|
+
* cursor) when not.
|
|
592
579
|
*/
|
|
593
580
|
rollRetryAt?: Date | null;
|
|
594
581
|
materializedEndsAt: Date;
|
|
@@ -596,34 +583,27 @@ export interface CalendarHorizonRollCursor {
|
|
|
596
583
|
}
|
|
597
584
|
/** Content-minimized outcome of one bounded recurring-event horizon maintenance batch. */
|
|
598
585
|
export interface CalendarHorizonRollResult {
|
|
599
|
-
/** Due recurring masters examined in deterministic order. */
|
|
600
586
|
examined: number;
|
|
601
587
|
/**
|
|
602
|
-
* Masters
|
|
603
|
-
*
|
|
588
|
+
* Masters rematerialized through the later of their stored end and the window's: a roll never
|
|
589
|
+
* moves a series' end back.
|
|
604
590
|
*/
|
|
605
591
|
extended: number;
|
|
606
|
-
/**
|
|
607
|
-
* Masters whose new occurrence collided; their previous coverage remains intact, and they wait an
|
|
608
|
-
* hour before a roll examines them again, behind every master it has not put back.
|
|
609
|
-
*/
|
|
592
|
+
/** Masters whose new occurrence collided: their coverage stays, and they are deferred an hour. */
|
|
610
593
|
conflicts: number;
|
|
611
594
|
/** The ids of those masters: what blocks each is a booking or hold on its new occurrences. */
|
|
612
595
|
conflictEventIds: string[];
|
|
613
596
|
/**
|
|
614
|
-
* Masters
|
|
615
|
-
*
|
|
616
|
-
* coverage remains intact, and they wait an hour
|
|
617
|
-
* before a roll examines them again, behind every master it has not put back.
|
|
597
|
+
* Masters over the expansion budget, or whose roll failed otherwise: their coverage stays, and
|
|
598
|
+
* they are deferred an hour.
|
|
618
599
|
*/
|
|
619
600
|
refused: number;
|
|
620
|
-
/** The ids of those masters, to find and fix them. */
|
|
621
601
|
refusedEventIds: string[];
|
|
622
602
|
/** True when more due masters remain beyond this bounded batch. */
|
|
623
603
|
hasMore: boolean;
|
|
624
604
|
/**
|
|
625
|
-
* Pass as `after` for the next batch
|
|
626
|
-
*
|
|
605
|
+
* Pass as `after` for the next batch, so this batch's masters are not examined again. `null` when
|
|
606
|
+
* none remain.
|
|
627
607
|
*/
|
|
628
608
|
next: CalendarHorizonRollCursor | null;
|
|
629
609
|
}
|
|
@@ -635,7 +615,6 @@ export interface CalendarEventRetentionResult {
|
|
|
635
615
|
/** True when either class filled its batch, so more rows may already be eligible. */
|
|
636
616
|
hasMore: boolean;
|
|
637
617
|
}
|
|
638
|
-
/** What erasing a tenant deleted. */
|
|
639
618
|
/** A tenant's default booking rules (`SlotlockStore.getBookingRuleDefaults`). */
|
|
640
619
|
export interface SlotlockBookingRuleDefaults {
|
|
641
620
|
/** The tenant's own defaults, under every pool's; null when it has none. */
|
|
@@ -650,13 +629,13 @@ export interface SlotlockTenantErasure {
|
|
|
650
629
|
total: number;
|
|
651
630
|
}
|
|
652
631
|
export interface BookingRetentionResult {
|
|
653
|
-
/** Holds past their expiry: they occupy nothing,
|
|
632
|
+
/** Holds past their expiry: they occupy nothing, but only a rival's write would reap them. */
|
|
654
633
|
expiredHoldsDeleted: number;
|
|
655
634
|
/** Keyed hold answers whose hold has expired: a retry with that key makes a new hold. */
|
|
656
635
|
holdCommandsDeleted: number;
|
|
657
636
|
/**
|
|
658
|
-
* References a cancel retired with no booking, older than the window
|
|
659
|
-
*
|
|
637
|
+
* References a cancel retired with no booking, older than the window. A cancelled booking's
|
|
638
|
+
* reference stays retired.
|
|
660
639
|
*/
|
|
661
640
|
tombstonesDeleted: number;
|
|
662
641
|
/** True when any class filled its batch, so more rows may already be eligible. */
|
package/webhooks.d.ts
CHANGED
|
@@ -17,13 +17,13 @@ export interface SlotlockCloudEvent {
|
|
|
17
17
|
}
|
|
18
18
|
/**
|
|
19
19
|
* A change as CloudEvents 1.0 (structured mode): `type` is `io.pylota.slotlock.<change type>`, with
|
|
20
|
-
*
|
|
21
|
-
*
|
|
20
|
+
* `slotlockorigin` and `slotlockkeyid` when recorded. A `coverage.*` change is redacted as the feed
|
|
21
|
+
* serves it (slotlockServedChange).
|
|
22
22
|
*/
|
|
23
23
|
export declare function slotlockCloudEvent(change: SlotlockChange): SlotlockCloudEvent;
|
|
24
24
|
/**
|
|
25
|
-
* The Standard Webhooks signature
|
|
26
|
-
* secret's bytes
|
|
25
|
+
* The Standard Webhooks signature: `v1,` and the base64 HMAC-SHA256 of `id.timestamp.body`, keyed
|
|
26
|
+
* by the secret's bytes.
|
|
27
27
|
*/
|
|
28
28
|
export declare function signSlotlockWebhook(params: {
|
|
29
29
|
secret: string;
|
|
@@ -32,9 +32,8 @@ export declare function signSlotlockWebhook(params: {
|
|
|
32
32
|
body: string;
|
|
33
33
|
}): string;
|
|
34
34
|
/**
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* sooner than the endpoint's Retry-After (capped at a day).
|
|
35
|
+
* Retry delay by consecutive failures: the Standard Webhooks example schedule (5 s, 5 min, 30 min,
|
|
36
|
+
* 2 h, 5 h, then 10 h) with ±10% jitter, never sooner than Retry-After (capped at a day).
|
|
38
37
|
*/
|
|
39
38
|
export declare function slotlockWebhookRetryDelayMs(attempts: number, retryAfterMs?: number, random?: () => number): number;
|
|
40
39
|
/** Whether `address` is one a webhook may not reach: loopback, private, link-local, and the like. */
|
|
@@ -50,19 +49,18 @@ export type SlotlockWebhookSender = (request: {
|
|
|
50
49
|
body: string;
|
|
51
50
|
}) => Promise<SlotlockWebhookResponse>;
|
|
52
51
|
/**
|
|
53
|
-
* POST with Node's http client: no redirect is followed,
|
|
54
|
-
*
|
|
55
|
-
*
|
|
52
|
+
* POST with Node's http client: no redirect is followed, at most 64 KiB of body is read, and every
|
|
53
|
+
* resolved address is checked at connect time, so a name resolving (or rebound) to a private
|
|
54
|
+
* network is refused unless `allowPrivateNetworks`.
|
|
56
55
|
*/
|
|
57
56
|
export declare function createSlotlockWebhookSender(options?: {
|
|
58
57
|
timeoutMs?: number;
|
|
59
58
|
allowPrivateNetworks?: boolean;
|
|
60
59
|
}): SlotlockWebhookSender;
|
|
61
60
|
/**
|
|
62
|
-
* A `dns.lookup` that
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* check is still refused. `resolve` stands in for `dns.lookup` (tests).
|
|
61
|
+
* A `dns.lookup` that returns every address and refuses (with `code`) a name with any private one
|
|
62
|
+
* unless `allowPrivateNetworks`. Used at socket connect, so a rebound name is still refused.
|
|
63
|
+
* `resolve` stands in for `dns.lookup` in tests.
|
|
66
64
|
*/
|
|
67
65
|
export declare function slotlockPrivateAddressLookup(options: {
|
|
68
66
|
allowPrivateNetworks: boolean;
|
|
@@ -71,11 +69,10 @@ export declare function slotlockPrivateAddressLookup(options: {
|
|
|
71
69
|
resolve?: typeof dnsLookup;
|
|
72
70
|
}): typeof dnsLookup;
|
|
73
71
|
/**
|
|
74
|
-
* A `fetch` for
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
* returned as is) and no proxy from the environment is used. Takes a string body only.
|
|
72
|
+
* A `fetch` for URLs a tenant chose (the assistant's model), with the webhooks' address check: a
|
|
73
|
+
* private address or a host resolving to one at connect time is refused with `code`
|
|
74
|
+
* `private_address` unless `allowPrivateNetworks`. No redirects, no environment proxy, string
|
|
75
|
+
* bodies only.
|
|
79
76
|
*/
|
|
80
77
|
export declare function createSlotlockGuardedFetch(options?: {
|
|
81
78
|
allowPrivateNetworks?: boolean;
|
|
@@ -88,9 +85,9 @@ export interface SlotlockWebhookPassResult {
|
|
|
88
85
|
failed: number;
|
|
89
86
|
}
|
|
90
87
|
/**
|
|
91
|
-
* One delivery pass for a tenant: claim each endpoint with changes due, send
|
|
92
|
-
*
|
|
93
|
-
*
|
|
88
|
+
* One delivery pass for a tenant: claim each endpoint with changes due, send in order until one
|
|
89
|
+
* fails, and record progress. 2xx is delivered; 410 disables the endpoint; anything else is retried
|
|
90
|
+
* on the schedule. Sending happens outside any transaction.
|
|
94
91
|
*/
|
|
95
92
|
export declare function deliverSlotlockWebhooks(params: {
|
|
96
93
|
feed: Pick<SlotlockEventFeed, 'claimWebhookDeliveries' | 'settleWebhookDelivery'>;
|
|
@@ -108,9 +105,9 @@ export interface SlotlockWebhookScheduler {
|
|
|
108
105
|
close(): Promise<void>;
|
|
109
106
|
}
|
|
110
107
|
/**
|
|
111
|
-
* Delivery passes every `intervalMs` (250-60,000, default 2,000) for
|
|
112
|
-
*
|
|
113
|
-
*
|
|
108
|
+
* Delivery passes every `intervalMs` (250-60,000, default 2,000) for `tenants()` (the store's
|
|
109
|
+
* webhook_tenants(), say) and touched tenants. A failed pass is reported and the next runs as
|
|
110
|
+
* usual.
|
|
114
111
|
*/
|
|
115
112
|
export declare function startSlotlockWebhookDelivery(options: {
|
|
116
113
|
tenants: () => Promise<readonly string[]>;
|