slotlock 0.7.0 → 0.9.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 +281 -0
- package/agent-server.d.ts +25 -5
- package/agent-server.js +152 -7
- package/agent-store-backend.d.ts +6 -1
- package/agent-store-backend.js +861 -55
- package/application-api.d.ts +19 -1
- package/application-api.js +578 -21
- package/approvals.d.ts +2 -0
- package/approvals.js +12 -5
- package/assistant.d.ts +64 -9
- package/assistant.js +450 -82
- package/availability-store.d.ts +193 -0
- package/availability-store.js +558 -0
- package/booking-rules.d.ts +92 -5
- package/booking-rules.js +243 -15
- package/dashboard-assistant-ui.d.ts +21 -0
- package/dashboard-assistant-ui.js +539 -0
- package/dashboard-markdown.d.ts +2 -0
- package/dashboard-markdown.js +157 -0
- package/dashboard-settings-ui.d.ts +1 -0
- package/dashboard-settings-ui.js +78 -0
- package/dashboard.d.ts +84 -11
- package/dashboard.js +1654 -384
- package/ddl.d.ts +5 -1
- package/ddl.js +760 -9
- package/event-feed.d.ts +1 -1
- package/event-feed.js +18 -0
- package/holiday-import.d.ts +38 -0
- package/holiday-import.js +57 -0
- package/holidays.d.ts +38 -0
- package/holidays.js +150 -0
- package/hours.d.ts +125 -0
- package/hours.js +224 -0
- package/index.d.ts +14 -3
- package/index.js +10 -2
- package/interop.d.ts +25 -7
- package/interop.js +145 -17
- package/lifecycle.d.ts +31 -0
- package/lifecycle.js +115 -0
- package/maintenance.d.ts +19 -0
- package/maintenance.js +55 -1
- package/metrics.js +3 -0
- package/openapi.json +16237 -7795
- package/package.json +1 -1
- package/ranges.d.ts +28 -0
- package/ranges.js +135 -0
- package/resource-search.d.ts +19 -0
- package/resource-search.js +153 -0
- package/rest-api.js +7 -0
- package/rules.d.ts +2 -0
- package/rules.js +2 -0
- package/self-host.d.ts +1 -1
- package/self-host.js +221 -21
- package/slotlock.manifest.json +1 -1
- package/store.d.ts +367 -24
- package/store.js +1567 -97
- package/types.d.ts +198 -0
package/lifecycle.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
export const SLOTLOCK_BOOKING_STATUSES = Object.freeze([
|
|
2
|
+
'held',
|
|
3
|
+
'confirmed',
|
|
4
|
+
'checked_in',
|
|
5
|
+
'completed',
|
|
6
|
+
'cancelled',
|
|
7
|
+
'no_show',
|
|
8
|
+
'released',
|
|
9
|
+
'expired',
|
|
10
|
+
'erased',
|
|
11
|
+
]);
|
|
12
|
+
/** What moves a hold or booking, and who does it. */
|
|
13
|
+
export const SLOTLOCK_BOOKING_ACTIONS = Object.freeze([
|
|
14
|
+
/** The holder books the held time (confirmHold, slotlock_confirm_slot). */
|
|
15
|
+
'confirm',
|
|
16
|
+
/** The holder lets the hold go (releaseHold, slotlock_release_slots). */
|
|
17
|
+
'release',
|
|
18
|
+
/** Its time to live runs out; the next writer in its way reaps it. */
|
|
19
|
+
'expire',
|
|
20
|
+
/** A new time or resource, at the next revision (rescheduleExternalReservation). */
|
|
21
|
+
'reschedule',
|
|
22
|
+
/** Claimed in person (checkInReservation), from an hour before the start until the end. */
|
|
23
|
+
'check_in',
|
|
24
|
+
/** Cancelled at the next revision (cancelExternalReservation). */
|
|
25
|
+
'cancel',
|
|
26
|
+
/** Not checked in within the resource's checkInMinutes: maintenance releases it. */
|
|
27
|
+
'no_show',
|
|
28
|
+
/** Its end passes: maintenance says so on the feed. */
|
|
29
|
+
'complete',
|
|
30
|
+
/** The tenant is erased. */
|
|
31
|
+
'erase',
|
|
32
|
+
]);
|
|
33
|
+
/** Where an end leaves a hold or booking: nothing moves it again, but erasure. */
|
|
34
|
+
export const SLOTLOCK_BOOKING_ENDS = new Set([
|
|
35
|
+
'cancelled',
|
|
36
|
+
'no_show',
|
|
37
|
+
'released',
|
|
38
|
+
'expired',
|
|
39
|
+
'erased',
|
|
40
|
+
]);
|
|
41
|
+
/**
|
|
42
|
+
* Each allowed move: the status it leads to and the feed change it writes (null: none; a list: all
|
|
43
|
+
* of them, in order). Anything not here is refused with `SLOTLOCK_BOOKING_REFUSALS`.
|
|
44
|
+
*/
|
|
45
|
+
export const SLOTLOCK_BOOKING_TRANSITIONS = Object.freeze({
|
|
46
|
+
held: {
|
|
47
|
+
confirm: { to: 'confirmed', change: 'hold.confirmed' },
|
|
48
|
+
release: { to: 'released', change: 'hold.released' },
|
|
49
|
+
expire: { to: 'expired', change: 'hold.expired' },
|
|
50
|
+
reschedule: { to: 'held', change: 'hold.updated' },
|
|
51
|
+
erase: { to: 'erased', change: null },
|
|
52
|
+
},
|
|
53
|
+
confirmed: {
|
|
54
|
+
reschedule: { to: 'confirmed', change: ['reservation.updated', 'reservation.rescheduled'] },
|
|
55
|
+
check_in: { to: 'checked_in', change: 'reservation.checked_in' },
|
|
56
|
+
cancel: { to: 'cancelled', change: 'reservation.cancelled' },
|
|
57
|
+
no_show: { to: 'no_show', change: 'reservation.no_show' },
|
|
58
|
+
complete: { to: 'completed', change: 'reservation.completed' },
|
|
59
|
+
erase: { to: 'erased', change: null },
|
|
60
|
+
},
|
|
61
|
+
checked_in: {
|
|
62
|
+
reschedule: { to: 'checked_in', change: ['reservation.updated', 'reservation.rescheduled'] },
|
|
63
|
+
// Checking in again answers the first check-in.
|
|
64
|
+
check_in: { to: 'checked_in', change: null },
|
|
65
|
+
cancel: { to: 'cancelled', change: 'reservation.cancelled' },
|
|
66
|
+
complete: { to: 'completed', change: 'reservation.completed' },
|
|
67
|
+
erase: { to: 'erased', change: null },
|
|
68
|
+
},
|
|
69
|
+
completed: {
|
|
70
|
+
// A system of record may correct or void a booking after it ended.
|
|
71
|
+
reschedule: { to: 'completed', change: ['reservation.updated', 'reservation.rescheduled'] },
|
|
72
|
+
cancel: { to: 'cancelled', change: 'reservation.cancelled' },
|
|
73
|
+
erase: { to: 'erased', change: null },
|
|
74
|
+
},
|
|
75
|
+
cancelled: { erase: { to: 'erased', change: null } },
|
|
76
|
+
no_show: { erase: { to: 'erased', change: null } },
|
|
77
|
+
released: {},
|
|
78
|
+
expired: {},
|
|
79
|
+
erased: {},
|
|
80
|
+
});
|
|
81
|
+
/** The refusal a move gets from a status it is not allowed from, as the store names it. */
|
|
82
|
+
export function slotlockBookingRefusal(from, action) {
|
|
83
|
+
const holdAction = action === 'confirm' || action === 'release' || action === 'expire';
|
|
84
|
+
switch (from) {
|
|
85
|
+
case 'held':
|
|
86
|
+
// A hold is not a booking: booking operations do not find it.
|
|
87
|
+
return 'reservation_not_found';
|
|
88
|
+
case 'confirmed':
|
|
89
|
+
case 'checked_in':
|
|
90
|
+
if (holdAction)
|
|
91
|
+
return 'hold_not_found';
|
|
92
|
+
// Only maintenance releases no-shows, and never one checked in.
|
|
93
|
+
return 'reservation_checked_in';
|
|
94
|
+
case 'completed':
|
|
95
|
+
return holdAction ? 'hold_not_found' : 'reservation_ended';
|
|
96
|
+
case 'cancelled':
|
|
97
|
+
case 'no_show':
|
|
98
|
+
// The reference is retired: writing it again is refused, checking it in finds nothing.
|
|
99
|
+
return action === 'check_in' ? 'reservation_not_found' : holdAction ? 'hold_not_found' : 'reservation_cancelled';
|
|
100
|
+
case 'released':
|
|
101
|
+
return action === 'confirm' ? 'hold_not_found' : 'not_found';
|
|
102
|
+
case 'expired':
|
|
103
|
+
return action === 'confirm' ? 'hold_expired' : 'not_found';
|
|
104
|
+
case 'erased':
|
|
105
|
+
return 'not_found';
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/** What `action` does from `from`: the status it leads to and the change it writes, or its refusal. */
|
|
109
|
+
export function slotlockBookingNext(from, action) {
|
|
110
|
+
const move = SLOTLOCK_BOOKING_TRANSITIONS[from][action];
|
|
111
|
+
if (!move)
|
|
112
|
+
return { ok: false, code: slotlockBookingRefusal(from, action) };
|
|
113
|
+
const changes = move.change === null ? [] : typeof move.change === 'string' ? [move.change] : move.change;
|
|
114
|
+
return { ok: true, to: move.to, changes };
|
|
115
|
+
}
|
package/maintenance.d.ts
CHANGED
|
@@ -26,6 +26,18 @@ export interface SlotlockMaintenanceResult {
|
|
|
26
26
|
waitlistOffers: number;
|
|
27
27
|
/** Approvals no one decided in time, their held time freed (`approval.expired`). */
|
|
28
28
|
approvalsExpired: number;
|
|
29
|
+
/** Bookings whose end has passed in the last week, now said on the feed (`reservation.completed`). */
|
|
30
|
+
bookingsCompleted: number;
|
|
31
|
+
/**
|
|
32
|
+
* Event reminders that came due, each now on the feed once (`reminder.due`) for a sender to send
|
|
33
|
+
* (RFC 0004 P3).
|
|
34
|
+
*/
|
|
35
|
+
remindersDue: number;
|
|
36
|
+
/**
|
|
37
|
+
* Holiday calendar subscriptions imported again (RFC 0004 B1), with `holidayFetch`; one that
|
|
38
|
+
* could not be read keeps its closures and is tried again a day later.
|
|
39
|
+
*/
|
|
40
|
+
holidayCalendarsImported: number;
|
|
29
41
|
/** More was left than one pass does: run another. */
|
|
30
42
|
capped: boolean;
|
|
31
43
|
/**
|
|
@@ -49,6 +61,11 @@ export declare function maintainSlotlockTenant(store: SlotlockStore, tenantRef:
|
|
|
49
61
|
* (SLOTLOCK_USAGE=on).
|
|
50
62
|
*/
|
|
51
63
|
usage?: boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Fetches holiday calendar subscriptions due to be imported again (the guarded fetch under
|
|
66
|
+
* `serve`); without it, none is.
|
|
67
|
+
*/
|
|
68
|
+
holidayFetch?: typeof fetch;
|
|
52
69
|
}): Promise<SlotlockMaintenanceResult>;
|
|
53
70
|
/**
|
|
54
71
|
* Run `work` holding the tenant's maintenance lock on a connection of its own, or return
|
|
@@ -83,6 +100,8 @@ export interface SlotlockMaintenanceSchedulerOptions {
|
|
|
83
100
|
batches?: number;
|
|
84
101
|
/** Roll each tenant's bookings and holds up into its usage, first (maintainSlotlockTenant `usage`). */
|
|
85
102
|
usage?: boolean;
|
|
103
|
+
/** Fetches holiday calendar subscriptions due to be imported again (maintainSlotlockTenant). */
|
|
104
|
+
holidayFetch?: typeof fetch;
|
|
86
105
|
/** A pass that ran: counts only, never the tenant. Not awaited; what it throws is ignored. */
|
|
87
106
|
onPass?: (result: SlotlockMaintenanceResult) => void;
|
|
88
107
|
/**
|
package/maintenance.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ignoreRejection } from './hooks.js';
|
|
2
2
|
import { SLOTLOCK_MAX_WAITLIST_PER_TENANT, calendarEventRollingHorizon, } from './store.js';
|
|
3
|
+
import { readSlotlockHolidayCalendar } from './holiday-import.js';
|
|
3
4
|
/**
|
|
4
5
|
* One maintenance pass for a tenant: roll its recurring events to the horizon, then purge expired
|
|
5
6
|
* holds and prune bookkeeping past retention, each in at most `batches` batches (1-100, default
|
|
@@ -78,6 +79,10 @@ export async function maintainSlotlockTenant(store, tenantRef, options = {}) {
|
|
|
78
79
|
// in the same pass.
|
|
79
80
|
const approvals = await withTenant((tenant) => tenant.expireApprovals({ tenantRef, limit: 100 }));
|
|
80
81
|
const noShows = await withTenant((tenant) => tenant.releaseNoShows({ tenantRef }));
|
|
82
|
+
// Bookings that ended since the last pass (RFC 0004 B4): no row changes, the feed says so.
|
|
83
|
+
const completed = await withTenant((tenant) => tenant.completeEndedBookings({ tenantRef, limit: 500 }));
|
|
84
|
+
const reminders = await withTenant((tenant) => tenant.dueReminders({ tenantRef, limit: 500 }));
|
|
85
|
+
const holidays = options.holidayFetch ? await importDueHolidayCalendars(withTenant, tenantRef, options) : 0;
|
|
81
86
|
// The whole waitlist in join order, a page per transaction, so a later free entry is offered
|
|
82
87
|
// however many wait before it.
|
|
83
88
|
let waitlistOffers = 0;
|
|
@@ -103,10 +108,58 @@ export async function maintainSlotlockTenant(store, tenantRef, options = {}) {
|
|
|
103
108
|
noShows,
|
|
104
109
|
waitlistOffers,
|
|
105
110
|
approvalsExpired: approvals.expired,
|
|
106
|
-
|
|
111
|
+
bookingsCompleted: completed,
|
|
112
|
+
remindersDue: reminders,
|
|
113
|
+
holidayCalendarsImported: holidays,
|
|
114
|
+
capped: rollCapped ||
|
|
115
|
+
eventsLeft ||
|
|
116
|
+
bookingsLeft ||
|
|
117
|
+
changesLeft ||
|
|
118
|
+
approvals.expired === 100 ||
|
|
119
|
+
completed === 500 ||
|
|
120
|
+
reminders === 500,
|
|
107
121
|
rollPending: rollCapped && after !== null && (after.rollRetryAt ?? null) === null,
|
|
108
122
|
};
|
|
109
123
|
}
|
|
124
|
+
/** Holiday calendar subscriptions imported again in one pass, at most: each is a fetch. */
|
|
125
|
+
const HOLIDAY_IMPORTS_PER_PASS = 5;
|
|
126
|
+
/**
|
|
127
|
+
* Import again the tenant's holiday calendar subscriptions that are due: each fetched outside any
|
|
128
|
+
* transaction, then imported in one; one that cannot be read or imported keeps its closures and
|
|
129
|
+
* notes why, to be tried again a day later.
|
|
130
|
+
*/
|
|
131
|
+
async function importDueHolidayCalendars(withTenant, tenantRef, options) {
|
|
132
|
+
const due = await withTenant((tenant) => tenant.dueHolidayCalendars({ tenantRef, limit: HOLIDAY_IMPORTS_PER_PASS }));
|
|
133
|
+
let imported = 0;
|
|
134
|
+
for (const calendar of due) {
|
|
135
|
+
options.signal?.throwIfAborted();
|
|
136
|
+
if (!calendar.url)
|
|
137
|
+
continue;
|
|
138
|
+
try {
|
|
139
|
+
const read = await readSlotlockHolidayCalendar({ url: calendar.url, subscribe: true, name: calendar.name }, options.holidayFetch);
|
|
140
|
+
await withTenant((tenant) => tenant.importHolidayCalendar({
|
|
141
|
+
tenantRef,
|
|
142
|
+
...(calendar.location !== undefined ? { location: calendar.location } : {}),
|
|
143
|
+
...(calendar.resourceId !== undefined ? { resourceId: calendar.resourceId } : {}),
|
|
144
|
+
name: calendar.name,
|
|
145
|
+
...(read.url !== undefined ? { url: read.url } : {}),
|
|
146
|
+
holidays: read.holidays,
|
|
147
|
+
span: read.span,
|
|
148
|
+
}));
|
|
149
|
+
imported++;
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
if (options.signal?.aborted)
|
|
153
|
+
throw error;
|
|
154
|
+
await withTenant((tenant) => tenant.noteHolidayCalendarFailure({
|
|
155
|
+
tenantRef,
|
|
156
|
+
id: calendar.id,
|
|
157
|
+
error: error instanceof Error ? error.message : 'The import failed',
|
|
158
|
+
}));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return imported;
|
|
162
|
+
}
|
|
110
163
|
/** Pages of 50 that cover a tenant's whole waitlist (SLOTLOCK_MAX_WAITLIST_PER_TENANT). */
|
|
111
164
|
const WAITLIST_PAGES = Math.ceil(SLOTLOCK_MAX_WAITLIST_PER_TENANT / 50);
|
|
112
165
|
/** How often the scheduler sweeps every tenant's expired assistant conversations, at most. */
|
|
@@ -183,6 +236,7 @@ export function startSlotlockMaintenance(options) {
|
|
|
183
236
|
const result = await withSlotlockMaintenanceLock(options.sql, tenantRef, () => maintainSlotlockTenant(options.store, tenantRef, {
|
|
184
237
|
...(options.batches !== undefined ? { batches: options.batches } : {}),
|
|
185
238
|
...(options.usage ? { usage: true } : {}),
|
|
239
|
+
...(options.holidayFetch ? { holidayFetch: options.holidayFetch } : {}),
|
|
186
240
|
signal: controller.signal,
|
|
187
241
|
}));
|
|
188
242
|
recordPass(tenantRef, Date.now());
|
package/metrics.js
CHANGED
|
@@ -19,6 +19,9 @@ const MAINTENANCE_COUNTS = [
|
|
|
19
19
|
'noShows',
|
|
20
20
|
'waitlistOffers',
|
|
21
21
|
'approvalsExpired',
|
|
22
|
+
'bookingsCompleted',
|
|
23
|
+
'remindersDue',
|
|
24
|
+
'holidayCalendarsImported',
|
|
22
25
|
];
|
|
23
26
|
/** A label value: an operation, protocol or outcome code. Anything else is counted as `other`. */
|
|
24
27
|
const LABEL = /^[a-z0-9_.]{1,64}$/;
|