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/store.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { type SlotlockEventFeed } from './event-feed.js';
|
|
2
|
+
import { type SlotlockBookingRules } from './booking-rules.js';
|
|
1
3
|
import type { Sql, TransactionSql } from 'postgres';
|
|
2
4
|
import { SLOTLOCK_CALENDAR_HORIZON_DAYS } from './rules.js';
|
|
3
|
-
import type { AcquireHoldResult, CalendarCoverageCommand, CalendarCoverageResult, CalendarEventRetentionResult, CalendarFreeBusy, CalendarHorizonRollCursor, CalendarHorizonRollResult, CancelCalendarEventResult, CancelExternalReservationResult, ConfirmHoldResult, CreateReservationResult, ExternalReservationCommand, ExternalReservationResult, Interval, SlotlockCalendarEvent, SlotlockResource, PutCalendarEventCommand, PutCalendarEventResult, ReleaseHoldResult, WeeklyAvailabilityRule } from './types.js';
|
|
5
|
+
import type { AcquireHoldResult, CalendarCoverageCommand, CalendarCoverageResult, CalendarEventContent, CalendarEventRetentionResult, BookingRetentionResult, SlotlockTenantErasure, CalendarFreeBusy, CalendarHorizonRollCursor, CalendarHorizonRollResult, CancelCalendarEventResult, CancelExternalReservationResult, ConfirmHoldResult, CreateReservationResult, DeleteResourceResult, ExternalReservationCommand, ExternalReservationResult, Interval, SlotlockCalendarEvent, SlotlockAgendaEntry, SlotlockHold, SlotlockReservation, SlotlockResource, PutCalendarEventCommand, PutCalendarEventResult, ReleaseHoldResult, WeeklyAvailabilityRule } from './types.js';
|
|
4
6
|
export { SLOTLOCK_CALENDAR_HORIZON_DAYS };
|
|
5
7
|
/**
|
|
6
8
|
* Longest one-off event the store accepts (ten years and some leap days): long enough for any lease
|
|
@@ -8,8 +10,22 @@ export { SLOTLOCK_CALENDAR_HORIZON_DAYS };
|
|
|
8
10
|
* series is bounded by its 367-day materialization window instead.
|
|
9
11
|
*/
|
|
10
12
|
export declare const SLOTLOCK_MAX_EVENT_DURATION_DAYS = 3660;
|
|
13
|
+
/**
|
|
14
|
+
* Longest window a recurring series is stored (materialized) for: the 367-day search window plus
|
|
15
|
+
* two days. The rolling horizon starts a day before the current UTC day and must reach 367 days
|
|
16
|
+
* past its end, so that a search of the full 367 days from any moment of the day is complete.
|
|
17
|
+
*/
|
|
18
|
+
export declare const SLOTLOCK_MATERIALIZATION_WINDOW_DAYS: number;
|
|
11
19
|
/** Trusted provider/maintenance namespace used by backwards-compatible store callers. */
|
|
12
20
|
export declare const SLOTLOCK_INTERNAL_EVENT_OWNER_REF = "internal";
|
|
21
|
+
/** How many live holds one holder (a keyed hold's principal) may have in a tenant at once. */
|
|
22
|
+
export declare const SLOTLOCK_MAX_HOLDS_PER_HOLDER = 20;
|
|
23
|
+
/** Windows one holder may wait for at once (joinWaitlist). */
|
|
24
|
+
export declare const SLOTLOCK_MAX_WAITLIST_PER_HOLDER = 20;
|
|
25
|
+
/** Waiting entries one tenant may have. */
|
|
26
|
+
export declare const SLOTLOCK_MAX_WAITLIST_PER_TENANT = 10000;
|
|
27
|
+
/** How long before its start a booking may be checked in. */
|
|
28
|
+
export declare const SLOTLOCK_CHECK_IN_OPENS_MINUTES = 60;
|
|
13
29
|
/** Default hard ceiling for retained active + tombstoned identities per agent principal. */
|
|
14
30
|
export declare const SLOTLOCK_AGENT_OWNER_EVENT_QUOTA = 1000;
|
|
15
31
|
/** Hard retained command ceiling; includes reserved cancellation capacity for every identity. */
|
|
@@ -21,28 +37,85 @@ export declare const SLOTLOCK_AGENT_OWNER_COMMAND_QUOTA = 10000;
|
|
|
21
37
|
export declare const SLOTLOCK_EVENT_COMMAND_RETENTION_DAYS = 30;
|
|
22
38
|
/** Store-owned coverage source for bounded, locally materialized recurring events. */
|
|
23
39
|
export declare const SLOTLOCK_LOCAL_RECURRENCE_COVERAGE_SOURCE = "slotlock:local-recurrence-materialization";
|
|
24
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* Stable daily target shared by the event writer, maintenance worker, and readiness proof: from a
|
|
42
|
+
* day before the current UTC day to 367 days past its end (SLOTLOCK_MATERIALIZATION_WINDOW_DAYS).
|
|
43
|
+
*/
|
|
25
44
|
export declare function calendarEventRollingHorizon(now?: Date): Interval;
|
|
26
45
|
/** What `createSlotlockStore` accepts: a `postgres` client, or a transaction/savepoint from one. */
|
|
27
46
|
export type SlotlockSql = Sql | TransactionSql;
|
|
28
47
|
type StoreSql = SlotlockSql;
|
|
29
|
-
|
|
48
|
+
/** How a migration (applySchema, applyTenantRls, grantApplicationRole) waits and how long it runs. */
|
|
49
|
+
export interface SlotlockMigrationOptions {
|
|
50
|
+
/**
|
|
51
|
+
* The longest it waits for any one lock, in milliseconds (1-600,000; default 1,000). While a
|
|
52
|
+
* migration waits for a lock on a table, every query that comes after it on that table waits
|
|
53
|
+
* behind it; past this it gives up, lets them through, and tries again.
|
|
54
|
+
*/
|
|
55
|
+
lockTimeoutMs?: number;
|
|
56
|
+
/** The longest any one of its statements runs, in milliseconds (1-86,400,000; default 900,000). */
|
|
57
|
+
statementTimeoutMs?: number;
|
|
58
|
+
/** How many times it tries in all (1-20; default 5) before refusing with `schema_lock_timeout`. */
|
|
59
|
+
attempts?: number;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The schema version recorded by the last migration, or `null` when there is none this role can
|
|
63
|
+
* read: no schema, a schema from 0.2 or earlier, or a role not yet granted the function (run
|
|
64
|
+
* `slotlock migrate`, which grants it).
|
|
65
|
+
*/
|
|
66
|
+
export declare function readSlotlockSchemaVersion(sql: SlotlockSql): Promise<number | null>;
|
|
67
|
+
/**
|
|
68
|
+
* Refuse to serve a schema another release migrated: `schema_outdated` when it is older than this
|
|
69
|
+
* release (or unversioned), so `slotlock migrate` must run first; `schema_newer` when a newer release
|
|
70
|
+
* migrated it, whose schema this release's code does not know. Both carry `installed` and `expected`.
|
|
71
|
+
*/
|
|
72
|
+
export declare function assertSlotlockSchemaCurrent(sql: SlotlockSql): Promise<void>;
|
|
73
|
+
/**
|
|
74
|
+
* What makes a put the same request: its identity and the event it asks for. Not the window its
|
|
75
|
+
* occurrences are stored over, which for a series is the rolling horizon from the current UTC
|
|
76
|
+
* midnight: with it in the hash, a retry after midnight was refused as a different request.
|
|
77
|
+
*/
|
|
78
|
+
declare function putCommandHash(params: {
|
|
79
|
+
command: PutCalendarEventCommand;
|
|
80
|
+
ownerRef: string;
|
|
81
|
+
event: CalendarEventContent;
|
|
82
|
+
}): string;
|
|
83
|
+
/**
|
|
84
|
+
* The hash 0.3.0 and earlier stored, with the window in it, so a command stored before the upgrade
|
|
85
|
+
* still replays: always for a one-off event, whose window is its own span, and on the same UTC day
|
|
86
|
+
* for a series. Remove once no stored command can be that old (commands are pruned after 30 days).
|
|
87
|
+
*/
|
|
88
|
+
export declare function legacyPutCommandHash(params: Parameters<typeof putCommandHash>[0] & {
|
|
89
|
+
window: Interval;
|
|
90
|
+
}): string;
|
|
91
|
+
/** Members a pool booking or search considers at most, in id order. */
|
|
92
|
+
export declare const SLOTLOCK_MAX_POOL_MEMBERS = 100;
|
|
93
|
+
export declare function isPoolName(value: unknown): value is string;
|
|
94
|
+
export interface SlotlockStore extends SlotlockEventFeed {
|
|
30
95
|
/**
|
|
31
96
|
* Apply the Slotlock DDL (idempotent). Owns the `slotlock` schema on the connected DB. Refuses
|
|
32
97
|
* (`unsafe_slotlock_schema`, with the `reasons` found) a `slotlock` schema that a role other than the
|
|
33
98
|
* connected one (superusers aside) owns or can create objects in, that holds an object such a
|
|
34
99
|
* role owns (an extension included), or whose tables such a role may add triggers to or have a
|
|
35
100
|
* trigger running a function such a role owns. Ownership through a group role counts as another
|
|
36
|
-
* role's: deploy as the owning role itself (`SET ROLE`).
|
|
101
|
+
* role's: deploy as the owning role itself (`SET ROLE`). Refuses (`schema_newer`) a schema a newer
|
|
102
|
+
* release migrated, changing nothing. Records SLOTLOCK_SCHEMA_VERSION. A schema this release
|
|
103
|
+
* already migrated, unchanged since, is left as it is without a lock on any table. Constraints an
|
|
104
|
+
* upgrade adds NOT VALID are validated in a second transaction, whose locks no serving read or
|
|
105
|
+
* write waits for (one inside a caller's transaction shares its locks until it ends); one that old
|
|
106
|
+
* rows still break stays NOT VALID, and each later migration validates it again. Each lock it
|
|
107
|
+
* waits for is bounded by `lockTimeoutMs`, after which it tries again (`schema_lock_timeout` once
|
|
108
|
+
* out of attempts).
|
|
37
109
|
*/
|
|
38
|
-
applySchema(): Promise<void>;
|
|
110
|
+
applySchema(options?: SlotlockMigrationOptions): Promise<void>;
|
|
39
111
|
/**
|
|
40
112
|
* Atomically enable forced tenant RLS using this store's configured context setting. Refuses the
|
|
41
|
-
* same `slotlock` schemas as `applySchema()`.
|
|
113
|
+
* same `slotlock` schemas as `applySchema()`. Tables already forced under exactly the policies it
|
|
114
|
+
* installs, bound to this setting, are left as they are without a lock on any table.
|
|
42
115
|
*/
|
|
43
116
|
applyTenantRls(options?: {
|
|
44
117
|
allowRebind?: boolean;
|
|
45
|
-
}): Promise<void>;
|
|
118
|
+
} & SlotlockMigrationOptions): Promise<void>;
|
|
46
119
|
/**
|
|
47
120
|
* Grant an existing application role exactly what the store needs at runtime: USAGE on the
|
|
48
121
|
* `slotlock` schema and DML on its tables. Run as the deployment role after every `applySchema()`.
|
|
@@ -55,23 +128,35 @@ export interface SlotlockStore {
|
|
|
55
128
|
* setting. The check runs when the grant is made, so a right added later is not
|
|
56
129
|
* detected until it runs again, and it is not exhaustive (SECURITY.md lists what it does not read).
|
|
57
130
|
*/
|
|
58
|
-
grantApplicationRole(role: string): Promise<void>;
|
|
131
|
+
grantApplicationRole(role: string, options?: SlotlockMigrationOptions): Promise<void>;
|
|
59
132
|
/**
|
|
60
|
-
* Run store operations with one transaction-local tenant context on the same connection.
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
133
|
+
* Run store operations with one transaction-local tenant context on the same connection. On a
|
|
134
|
+
* client it opens a READ COMMITTED transaction, whatever the role's default. Inside a caller's
|
|
135
|
+
* transaction of another level its writes are refused (`invalid_transaction_isolation`), since
|
|
136
|
+
* that snapshot can miss a suspension, and its reads go on; with `isolation: 'read committed'`
|
|
137
|
+
* such a transaction is refused outright. The serving role needs EXECUTE on
|
|
138
|
+
* slotlock.tenant_suspended(text): without it this refuses with `application_role_not_granted`
|
|
139
|
+
* (grantApplicationRole grants it), and on a schema without it with `schema_outdated`. With the
|
|
140
|
+
* store's `maxTenantTransactions`, an aborted `signal` takes a call still waiting for its turn out
|
|
141
|
+
* of the line, refused with the signal's reason.
|
|
64
142
|
*/
|
|
65
143
|
withTenant<T>(tenantRef: string, callback: (store: SlotlockTenantStore) => Promise<T>, options?: {
|
|
66
144
|
isolation?: 'read committed';
|
|
145
|
+
signal?: AbortSignal;
|
|
146
|
+
/**
|
|
147
|
+
* Who is making these changes (an API key's id, an agent's subject), 1-200 characters
|
|
148
|
+
* without control characters: the history (listHistory) records it with each change.
|
|
149
|
+
*/
|
|
150
|
+
actor?: string;
|
|
67
151
|
}): Promise<T>;
|
|
68
152
|
/**
|
|
69
153
|
* INVARIANT (erasure completeness): a tenant integration must use a stable, erasable business
|
|
70
154
|
* identifier for externalRef and include this schema in its own data-erasure workflow.
|
|
71
155
|
*
|
|
72
|
-
* With `maxTenantResources` (1-100,000, tenant-owned resources only)
|
|
73
|
-
* (
|
|
74
|
-
*
|
|
156
|
+
* With `maxTenantResources` (1-100,000, tenant-owned resources only), or the store's `resources`
|
|
157
|
+
* limit (the lower of the two when both are set), a new resource of a tenant is refused
|
|
158
|
+
* (`resource_limit_reached`, with the `limit`) once the tenant has that many; an existing
|
|
159
|
+
* `externalRef` is still updated. Capped creates for one tenant run one at a time under a database lock, so racing ones
|
|
75
160
|
* on any number of servers stop at the cap. The count needs READ COMMITTED: a capped create opens
|
|
76
161
|
* one itself, and inside a caller's transaction of another level it refuses
|
|
77
162
|
* (`invalid_transaction_isolation`). Uncapped creates do not wait for the lock and are not
|
|
@@ -82,12 +167,46 @@ export interface SlotlockStore {
|
|
|
82
167
|
tenantRef?: string;
|
|
83
168
|
timezone?: string;
|
|
84
169
|
maxTenantResources?: number;
|
|
170
|
+
/**
|
|
171
|
+
* `shared` (the default) or `private`, which agents never see. Fixed at creation: creating an
|
|
172
|
+
* existing reference again with none named returns it as it is, and with another visibility is
|
|
173
|
+
* refused (`resource_visibility_conflict`).
|
|
174
|
+
*/
|
|
175
|
+
visibility?: SlotlockResource['visibility'];
|
|
85
176
|
}): Promise<SlotlockResource>;
|
|
86
|
-
/**
|
|
177
|
+
/**
|
|
178
|
+
* Resolve one tenant-owned resource without revealing another tenant's matching id. An archived
|
|
179
|
+
* one is returned (with `archivedAt`); with `visibility`, only one of that visibility.
|
|
180
|
+
*/
|
|
87
181
|
getResource(params: {
|
|
88
182
|
tenantRef: string;
|
|
89
183
|
id: string;
|
|
184
|
+
visibility?: SlotlockResource['visibility'];
|
|
90
185
|
}): Promise<SlotlockResource | null>;
|
|
186
|
+
/**
|
|
187
|
+
* Archive one of the tenant's resources: no list shows it and it takes no new booking, hold or
|
|
188
|
+
* event (`resource_archived`), while what is on it stays and can still be cancelled or released.
|
|
189
|
+
* Archiving it again keeps the first time. Null for an id that is not the tenant's.
|
|
190
|
+
*/
|
|
191
|
+
archiveResource(params: {
|
|
192
|
+
tenantRef: string;
|
|
193
|
+
id: string;
|
|
194
|
+
}): Promise<SlotlockResource | null>;
|
|
195
|
+
/** Put an archived resource back in use. Null for an id that is not the tenant's. */
|
|
196
|
+
restoreResource(params: {
|
|
197
|
+
tenantRef: string;
|
|
198
|
+
id: string;
|
|
199
|
+
}): Promise<SlotlockResource | null>;
|
|
200
|
+
/**
|
|
201
|
+
* Delete one of the tenant's resources with its history (its past bookings, coverage and the
|
|
202
|
+
* occupation of cancelled events), freeing its place under the resource cap. Refused
|
|
203
|
+
* (`resource_in_use`) while a booking on it has not ended, a hold on it is live, or a calendar
|
|
204
|
+
* event (one-off not yet over, or recurring) is on it: cancel or move those first.
|
|
205
|
+
*/
|
|
206
|
+
deleteResource(params: {
|
|
207
|
+
tenantRef: string;
|
|
208
|
+
id: string;
|
|
209
|
+
}): Promise<DeleteResourceResult>;
|
|
91
210
|
/**
|
|
92
211
|
* Set one resource's own weekly bookable hours, or clear them (`null`) so it uses the default;
|
|
93
212
|
* `[]` makes it never bookable. Null for an id that is not the tenant's. Refuses rules Slotlock
|
|
@@ -98,6 +217,44 @@ export interface SlotlockStore {
|
|
|
98
217
|
id: string;
|
|
99
218
|
rules: readonly WeeklyAvailabilityRule[] | null;
|
|
100
219
|
}): Promise<SlotlockResource | null>;
|
|
220
|
+
/**
|
|
221
|
+
* Set one resource's booking rules (`SlotlockBookingRules`), or clear them (`null`). They apply to
|
|
222
|
+
* bookings made from then on: holds, reservations, moves, and events written with
|
|
223
|
+
* `enforceBookingRules`. Null for an id that is not the tenant's. Refuses rules outside their
|
|
224
|
+
* bounds (`invalid_booking_rules`).
|
|
225
|
+
*/
|
|
226
|
+
setResourceBookingRules(params: {
|
|
227
|
+
tenantRef: string;
|
|
228
|
+
id: string;
|
|
229
|
+
rules: SlotlockBookingRules | null;
|
|
230
|
+
}): Promise<SlotlockResource | null>;
|
|
231
|
+
/**
|
|
232
|
+
* Put one resource in a pool, or take it out (`null`): `pool` is 1-100 characters of a-z, 0-9,
|
|
233
|
+
* `.`, `_` and `-`, starting with a letter or digit (`invalid_pool` otherwise). A resource is in
|
|
234
|
+
* one pool at most. Null for an id that is not the tenant's.
|
|
235
|
+
*/
|
|
236
|
+
setResourcePool(params: {
|
|
237
|
+
tenantRef: string;
|
|
238
|
+
id: string;
|
|
239
|
+
pool: string | null;
|
|
240
|
+
}): Promise<SlotlockResource | null>;
|
|
241
|
+
/**
|
|
242
|
+
* The members of one of the tenant's pools that may be booked, in the order a pool booking tries
|
|
243
|
+
* them (by id): archived ones left out, and with `visibility: 'shared'` private ones too.
|
|
244
|
+
*/
|
|
245
|
+
listPoolMembers(params: {
|
|
246
|
+
tenantRef: string;
|
|
247
|
+
pool: string;
|
|
248
|
+
visibility?: 'shared';
|
|
249
|
+
}): Promise<SlotlockResource[]>;
|
|
250
|
+
/** The tenant's pools by name, with how many members each has that may be booked (its capacity). */
|
|
251
|
+
listPools(params: {
|
|
252
|
+
tenantRef: string;
|
|
253
|
+
visibility?: 'shared';
|
|
254
|
+
}): Promise<{
|
|
255
|
+
pool: string;
|
|
256
|
+
capacity: number;
|
|
257
|
+
}[]>;
|
|
101
258
|
/** Set (or clear, with `null`) the bookable hours of every resource the tenant has; returns how many. */
|
|
102
259
|
setTenantAvailability(params: {
|
|
103
260
|
tenantRef: string;
|
|
@@ -108,18 +265,26 @@ export interface SlotlockStore {
|
|
|
108
265
|
tenantRef: string;
|
|
109
266
|
limit?: number;
|
|
110
267
|
after?: string;
|
|
268
|
+
/** Only resources of this visibility; omitted, both. */
|
|
269
|
+
visibility?: SlotlockResource['visibility'];
|
|
270
|
+
/** Archived resources too; omitted, only those in use. */
|
|
271
|
+
includeArchived?: boolean;
|
|
111
272
|
}): Promise<SlotlockResource[]>;
|
|
112
273
|
/**
|
|
113
274
|
* Insert a confirmed reservation with the EXCLUDE constraint as the overlap arbiter. Never
|
|
114
275
|
* throws for the expected outcomes: an invalid window or a lost race come back structured.
|
|
115
276
|
* Expired holds overlapping the window are lazily reaped in the same transaction — an
|
|
116
|
-
* expired-but-unpurged hold can never block (no cron required).
|
|
277
|
+
* expired-but-unpurged hold can never block (no cron required). On a tenant's resource the
|
|
278
|
+
* booking takes the tenant, and its own id as reference, so cancel and reschedule reach it. With
|
|
279
|
+
* `tenantRef`, only a resource of that tenant (`resource_not_found` otherwise, whatever the
|
|
280
|
+
* connection's row-level security).
|
|
117
281
|
*/
|
|
118
282
|
createReservation(params: {
|
|
119
283
|
resourceId: string;
|
|
120
284
|
start: Date;
|
|
121
285
|
end: Date;
|
|
122
286
|
source?: string;
|
|
287
|
+
tenantRef?: string;
|
|
123
288
|
}): Promise<CreateReservationResult>;
|
|
124
289
|
/**
|
|
125
290
|
* Idempotently create a tenant-owned booking reservation by its stable business identity. The
|
|
@@ -137,7 +302,10 @@ export interface SlotlockStore {
|
|
|
137
302
|
}): Promise<CancelExternalReservationResult>;
|
|
138
303
|
/**
|
|
139
304
|
* Occupy a window WITHOUT committing: the hold blocks rivals under the same EXCLUDE arbiter
|
|
140
|
-
* until `expiresAt` (DB clock —
|
|
305
|
+
* until `expiresAt` (DB clock — the statement's time + ttl, never the app clock), then frees via
|
|
306
|
+
* lazy reap.
|
|
307
|
+
* With `tenantRef`, only a resource of that tenant (`resource_not_found` otherwise, whatever the
|
|
308
|
+
* connection's row-level security).
|
|
141
309
|
*/
|
|
142
310
|
acquireHold(params: {
|
|
143
311
|
resourceId: string;
|
|
@@ -145,20 +313,111 @@ export interface SlotlockStore {
|
|
|
145
313
|
end: Date;
|
|
146
314
|
ttlMs: number;
|
|
147
315
|
source?: string;
|
|
316
|
+
tenantRef?: string;
|
|
317
|
+
/** Turnaround kept busy after `end`, 0 to 30 days; the confirmed booking keeps it. */
|
|
318
|
+
bufferAfterMs?: number;
|
|
319
|
+
/**
|
|
320
|
+
* A retry identity, 1-500 UTF-8 bytes, unique inside `tenantRef` and `principalRef` (both
|
|
321
|
+
* required with it): a request repeated with the same key and body answers the hold the first
|
|
322
|
+
* made (`idempotent: true`), and one with another body is `idempotency_conflict`. The answer is
|
|
323
|
+
* kept until the hold would have expired, released or confirmed; a retry after that makes a new
|
|
324
|
+
* hold.
|
|
325
|
+
*/
|
|
326
|
+
idempotencyKey?: string;
|
|
327
|
+
/** Whose keys `idempotencyKey` is among, 1-500 UTF-8 bytes (an API key's subject, say). */
|
|
328
|
+
principalRef?: string;
|
|
329
|
+
/**
|
|
330
|
+
* Who the hold belongs to, 1-500 UTF-8 bytes, inside `tenantRef` (required with it): an agent
|
|
331
|
+
* acting for one person, say. `releaseHold` and `putCalendarEvent`'s `fromHold` with a
|
|
332
|
+
* `holderRef` take only their own, and a holder may have `maxHoldsPerHolder` (20) live at once,
|
|
333
|
+
* holding at most `maxHeldHoursPerHolder` together when that is set. Without it the hold has no
|
|
334
|
+
* holder and no cap: an application holding for its customers.
|
|
335
|
+
*/
|
|
336
|
+
holderRef?: string;
|
|
337
|
+
/**
|
|
338
|
+
* Holds taken as alternatives for one booking share a group (a UUID): booking one with
|
|
339
|
+
* `fromHold` releases the rest, and releasing one with a holder releases them all.
|
|
340
|
+
*/
|
|
341
|
+
groupId?: string;
|
|
148
342
|
}): Promise<AcquireHoldResult>;
|
|
343
|
+
/** One of the tenant's live holds, or null (none, expired, confirmed, or another tenant's). */
|
|
344
|
+
getHold(params: {
|
|
345
|
+
tenantRef: string;
|
|
346
|
+
holdId: string;
|
|
347
|
+
}): Promise<SlotlockHold | null>;
|
|
348
|
+
/** The tenant's live holds on one resource, by start then id, keyset-paginated like events. */
|
|
349
|
+
listHolds(params: {
|
|
350
|
+
tenantRef: string;
|
|
351
|
+
resourceId: string;
|
|
352
|
+
limit?: number;
|
|
353
|
+
after?: {
|
|
354
|
+
start: Date;
|
|
355
|
+
id: string;
|
|
356
|
+
};
|
|
357
|
+
}): Promise<SlotlockHold[]>;
|
|
149
358
|
/**
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
359
|
+
* What occupies the tenant's resources in `window` (at most 367 days), every resource at once, by
|
|
360
|
+
* start then id: bookings, calendar events' occurrences and live holds, up to `limit` (1-1000,
|
|
361
|
+
* default 100). An agenda for people to read; listReservations, listCalendarEvents and listHolds
|
|
362
|
+
* page through each.
|
|
363
|
+
*/
|
|
364
|
+
listAgenda(params: {
|
|
365
|
+
tenantRef: string;
|
|
366
|
+
window: Interval;
|
|
367
|
+
limit?: number;
|
|
368
|
+
}): Promise<SlotlockAgendaEntry[]>;
|
|
369
|
+
/**
|
|
370
|
+
* The tenant's booking with this reference as it now is (its current revision), or null: none,
|
|
371
|
+
* cancelled, or a calendar event's occupation, which is not a booking.
|
|
372
|
+
*/
|
|
373
|
+
getReservation(params: {
|
|
374
|
+
tenantRef: string;
|
|
375
|
+
externalRef: string;
|
|
376
|
+
}): Promise<SlotlockReservation | null>;
|
|
377
|
+
/**
|
|
378
|
+
* The tenant's bookings on one resource whose occupied time meets `window` (at most 367 days),
|
|
379
|
+
* by start then id, keyset-paginated: pass the last one's `{ start, id }` as `after`.
|
|
380
|
+
*/
|
|
381
|
+
listReservations(params: {
|
|
382
|
+
tenantRef: string;
|
|
383
|
+
resourceId: string;
|
|
384
|
+
window: Interval;
|
|
385
|
+
limit?: number;
|
|
386
|
+
after?: {
|
|
387
|
+
start: Date;
|
|
388
|
+
id: string;
|
|
389
|
+
};
|
|
390
|
+
}): Promise<SlotlockReservation[]>;
|
|
391
|
+
/**
|
|
392
|
+
* Flip an unexpired hold to a confirmed booking (expiry cleared). On a tenant's resource the
|
|
393
|
+
* booking takes the tenant, and the hold's id as its reference at revision 1, so cancel and
|
|
394
|
+
* reschedule reach it like any other (`reservation_cancelled` when a cancel of that reference
|
|
395
|
+
* came first). Confirming it again answers the booking as it now is. With `tenantRef`, only a
|
|
396
|
+
* hold on one of that tenant's resources counts: another tenant's is `hold_not_found` whatever
|
|
397
|
+
* the connection's row-level security.
|
|
153
398
|
*/
|
|
154
399
|
confirmHold(holdId: string, options?: SlotlockHoldTenantOptions): Promise<ConfirmHoldResult>;
|
|
155
400
|
/**
|
|
156
401
|
* Delete a live hold; released=false when it was already gone (expired-reaped or confirmed). With
|
|
157
402
|
* `tenantRef`, only a hold on one of that tenant's resources, as for confirmHold.
|
|
158
403
|
*/
|
|
159
|
-
releaseHold(holdId: string, options?: SlotlockHoldTenantOptions
|
|
160
|
-
|
|
161
|
-
|
|
404
|
+
releaseHold(holdId: string, options?: SlotlockHoldTenantOptions & {
|
|
405
|
+
/**
|
|
406
|
+
* Only a hold this holder took (`acquireHold`'s `holderRef`), with the rest of its group:
|
|
407
|
+
* released=false for another holder's.
|
|
408
|
+
*/
|
|
409
|
+
holderRef?: string;
|
|
410
|
+
}): Promise<ReleaseHoldResult>;
|
|
411
|
+
/**
|
|
412
|
+
* Reservations overlapping `window` for one resource, as merged-ready half-open intervals. With
|
|
413
|
+
* `tenantRef`, only a resource of that tenant (`resource_not_found` otherwise, whatever the
|
|
414
|
+
* connection's row-level security). An id the store never issues is `invalid_identity`. With
|
|
415
|
+
* `limit`, at most that many, the first by start.
|
|
416
|
+
*/
|
|
417
|
+
listBusy(resourceId: string, window: Interval, options?: {
|
|
418
|
+
tenantRef?: string;
|
|
419
|
+
limit?: number;
|
|
420
|
+
}): Promise<Interval[]>;
|
|
162
421
|
/** Create or revise one trusted event under expected-revision + tenant idempotency guards. */
|
|
163
422
|
putCalendarEvent(params: PutCalendarEventCommand): Promise<PutCalendarEventResult>;
|
|
164
423
|
/** Read one active event. A cancelled/tombstoned identity reads as absent. */
|
|
@@ -168,11 +427,24 @@ export interface SlotlockStore {
|
|
|
168
427
|
ownerRef?: string;
|
|
169
428
|
externalRef: string;
|
|
170
429
|
}): Promise<SlotlockCalendarEvent | null>;
|
|
430
|
+
/**
|
|
431
|
+
* One of the tenant's active events by id, whichever owner (the tenant itself or an agent key)
|
|
432
|
+
* wrote it: null when there is none, it was cancelled, or it is another tenant's.
|
|
433
|
+
*/
|
|
434
|
+
getCalendarEventById(params: {
|
|
435
|
+
tenantRef: string;
|
|
436
|
+
eventId: string;
|
|
437
|
+
}): Promise<SlotlockCalendarEvent | null>;
|
|
171
438
|
/** List active events with at least one materialized occurrence intersecting the window. */
|
|
172
439
|
listCalendarEvents(params: {
|
|
173
440
|
tenantRef: string;
|
|
174
441
|
/** Defaults to the trusted `internal` namespace. */
|
|
175
442
|
ownerRef?: string;
|
|
443
|
+
/**
|
|
444
|
+
* Every owner's events, each with its `ownerRef`, so a tenant reaches what any of its keys
|
|
445
|
+
* booked, a revoked one's included. Not with `ownerRef`.
|
|
446
|
+
*/
|
|
447
|
+
allOwners?: boolean;
|
|
176
448
|
resourceId: string;
|
|
177
449
|
window: Interval;
|
|
178
450
|
limit?: number;
|
|
@@ -190,6 +462,26 @@ export interface SlotlockStore {
|
|
|
190
462
|
externalRef: string;
|
|
191
463
|
idempotencyKey: string;
|
|
192
464
|
expectedRevision: number;
|
|
465
|
+
/**
|
|
466
|
+
* The resource the writer read the event on: `event_not_found` when the stored event is on
|
|
467
|
+
* another, checked under the event's lock.
|
|
468
|
+
*/
|
|
469
|
+
expectedResourceId?: string;
|
|
470
|
+
}): Promise<CancelCalendarEventResult>;
|
|
471
|
+
/**
|
|
472
|
+
* The tenant's cancellation of one of its events by id, whichever owner wrote it: an agent key's
|
|
473
|
+
* included, after the key is revoked and when it has spent its command quota. The event's
|
|
474
|
+
* identity is cancelled (its tombstone is its owner's, so that key cannot re-create it), and the
|
|
475
|
+
* command is the tenant's own: its idempotency key is in the trusted `internal` namespace, where
|
|
476
|
+
* it replays. A cancelled event, an unknown id or another tenant's is `event_not_found`.
|
|
477
|
+
*/
|
|
478
|
+
cancelCalendarEventById(params: {
|
|
479
|
+
tenantRef: string;
|
|
480
|
+
eventId: string;
|
|
481
|
+
idempotencyKey: string;
|
|
482
|
+
expectedRevision: number;
|
|
483
|
+
/** As for cancelCalendarEvent: `event_not_found` when the event is on another resource. */
|
|
484
|
+
expectedResourceId?: string;
|
|
193
485
|
}): Promise<CancelCalendarEventResult>;
|
|
194
486
|
/**
|
|
195
487
|
* Atomically rematerialize due recurring masters into one rolling window without changing their
|
|
@@ -218,9 +510,198 @@ export interface SlotlockStore {
|
|
|
218
510
|
tenantRef: string;
|
|
219
511
|
/** Rows per class per call, 1-1000. Defaults to 100. */
|
|
220
512
|
limit?: number;
|
|
221
|
-
/** Replay window in whole days, 1-3650. Defaults to `
|
|
513
|
+
/** Replay window in whole days, 1-3650. Defaults to the store's `retentionDays`, at least 30. */
|
|
222
514
|
retentionDays?: number;
|
|
223
515
|
}): Promise<CalendarEventRetentionResult>;
|
|
516
|
+
/**
|
|
517
|
+
* Delete one bounded batch of a tenant's dead booking rows: holds past their expiry and the
|
|
518
|
+
* answers kept for keyed ones (at once), and references retired by a cancel no booking held,
|
|
519
|
+
* older than the retention window. A cancelled booking's tombstone is never deleted here (only by
|
|
520
|
+
* erasing the tenant), so its reference is never booked again. Rows another transaction holds
|
|
521
|
+
* are skipped, not waited on. It also gives a confirmed booking a 0.2 server wrote without its
|
|
522
|
+
* tenant the tenant and its own id as reference, as the migration does. Call until `hasMore` is
|
|
523
|
+
* false.
|
|
524
|
+
*/
|
|
525
|
+
/**
|
|
526
|
+
* Time zone rules that changed since an event was booked (a new tzdata): a future one-off event
|
|
527
|
+
* whose wall-clock time in its zone now falls at another instant. Each is reported once on the
|
|
528
|
+
* feed (`event.timezone_shifted`, with the instant it has and the one its wall time now means).
|
|
529
|
+
* With `move`, it is moved there instead (a new revision, `event.updated` naming `slotlock:tzdata`),
|
|
530
|
+
* unless that overlaps another booking: then it stays and `event.timezone_conflict` reports it,
|
|
531
|
+
* for a person or an agent to decide. Up to `limit` (1-1,000, default 100) events a call.
|
|
532
|
+
*/
|
|
533
|
+
followZoneRules(params: {
|
|
534
|
+
tenantRef: string;
|
|
535
|
+
move?: boolean;
|
|
536
|
+
limit?: number;
|
|
537
|
+
}): Promise<{
|
|
538
|
+
shifted: number;
|
|
539
|
+
moved: number;
|
|
540
|
+
conflicts: number;
|
|
541
|
+
}>;
|
|
542
|
+
/**
|
|
543
|
+
* A source failed to read (a feed answered 500, a token expired): kept as its last error, at most
|
|
544
|
+
* 500 characters, beside its coverage, which stays as it was. `coverage_not_found` for a source
|
|
545
|
+
* that has never recorded coverage on the resource.
|
|
546
|
+
*/
|
|
547
|
+
recordCoverageFailure(params: {
|
|
548
|
+
tenantRef: string;
|
|
549
|
+
resourceId: string;
|
|
550
|
+
source: string;
|
|
551
|
+
error: string;
|
|
552
|
+
}): Promise<{
|
|
553
|
+
ok: true;
|
|
554
|
+
} | {
|
|
555
|
+
ok: false;
|
|
556
|
+
code: 'coverage_not_found' | 'invalid_identity';
|
|
557
|
+
}>;
|
|
558
|
+
/**
|
|
559
|
+
* Retire a source (a sync disconnected or replaced): its coverage of the resource is forgotten, so
|
|
560
|
+
* free/busy and the booking rule `requireCertain` stop waiting on it. False when there was none.
|
|
561
|
+
*/
|
|
562
|
+
deleteCoverage(params: {
|
|
563
|
+
tenantRef: string;
|
|
564
|
+
resourceId: string;
|
|
565
|
+
source: string;
|
|
566
|
+
}): Promise<boolean>;
|
|
567
|
+
/**
|
|
568
|
+
* Each source's coverage of one resource and how healthy it is: when it was last read, its last
|
|
569
|
+
* error, and whether it is `stale` (not read for `maxAgeMs`, default a day, as free/busy judges).
|
|
570
|
+
*/
|
|
571
|
+
listCoverage(params: {
|
|
572
|
+
tenantRef: string;
|
|
573
|
+
resourceId: string;
|
|
574
|
+
maxAgeMs?: number;
|
|
575
|
+
}): Promise<SlotlockCoverageHealth[]>;
|
|
576
|
+
/**
|
|
577
|
+
* Put each source that has gone stale (not read for `maxAgeMs`, default a day) on the feed once
|
|
578
|
+
* (`coverage.stale`), until it is read again. Up to `limit` (default 100); how many.
|
|
579
|
+
*/
|
|
580
|
+
reportStaleCoverage(params: {
|
|
581
|
+
tenantRef: string;
|
|
582
|
+
maxAgeMs?: number;
|
|
583
|
+
limit?: number;
|
|
584
|
+
}): Promise<number>;
|
|
585
|
+
/**
|
|
586
|
+
* Claim a booking by its reference (check-in), so a resource whose booking rules ask for check-in
|
|
587
|
+
* (`checkInMinutes`) does not release it. Check-in opens `SLOTLOCK_CHECK_IN_OPENS_MINUTES` before
|
|
588
|
+
* the start (`check_in_not_open` sooner), and a move to another start clears it. Checking in again
|
|
589
|
+
* keeps the first time. Refused once the booking has ended (`reservation_ended`); a reference with
|
|
590
|
+
* no live booking is `reservation_not_found`. The feed says so (`reservation.checked_in`).
|
|
591
|
+
*/
|
|
592
|
+
checkInReservation(params: {
|
|
593
|
+
tenantRef: string;
|
|
594
|
+
externalRef: string;
|
|
595
|
+
}): Promise<CheckInReservationResult>;
|
|
596
|
+
/**
|
|
597
|
+
* Release up to `limit` (default 100) bookings left unclaimed: on a resource whose booking rules
|
|
598
|
+
* ask for check-in, made since they did, not checked in `checkInMinutes` after the start, and not
|
|
599
|
+
* yet ended. Each is cancelled as `cancelExternalReservation` would (its reference retired at the
|
|
600
|
+
* next revision) and the feed says why (`reservation.no_show`). How many.
|
|
601
|
+
*/
|
|
602
|
+
releaseNoShows(params: {
|
|
603
|
+
tenantRef: string;
|
|
604
|
+
limit?: number;
|
|
605
|
+
}): Promise<number>;
|
|
606
|
+
/**
|
|
607
|
+
* Wait for a window on one resource (`resourceId`) or any member of a pool (`pool`): when it comes
|
|
608
|
+
* free, `offerWaitlist` holds it for `holderRef` for `offerTtlMs` (1 minute to a day, default 15
|
|
609
|
+
* minutes), first come first offered. A holder may wait for `SLOTLOCK_MAX_WAITLIST_PER_HOLDER`
|
|
610
|
+
* windows at once and a tenant `SLOTLOCK_MAX_WAITLIST_PER_TENANT`.
|
|
611
|
+
*/
|
|
612
|
+
joinWaitlist(params: {
|
|
613
|
+
tenantRef: string;
|
|
614
|
+
resourceId?: string;
|
|
615
|
+
pool?: string;
|
|
616
|
+
start: Date;
|
|
617
|
+
end: Date;
|
|
618
|
+
holderRef: string;
|
|
619
|
+
offerTtlMs?: number;
|
|
620
|
+
}): Promise<JoinWaitlistResult>;
|
|
621
|
+
/** The tenant's waitlist entries, oldest first, optionally of one resource, pool or holder. */
|
|
622
|
+
listWaitlist(params: {
|
|
623
|
+
tenantRef: string;
|
|
624
|
+
resourceId?: string;
|
|
625
|
+
pool?: string;
|
|
626
|
+
holderRef?: string;
|
|
627
|
+
limit?: number;
|
|
628
|
+
}): Promise<SlotlockWaitlistEntry[]>;
|
|
629
|
+
/**
|
|
630
|
+
* Stop waiting; with `holderRef`, only that holder's entry. A hold offered and not yet confirmed is
|
|
631
|
+
* released with it. False when there was none.
|
|
632
|
+
*/
|
|
633
|
+
leaveWaitlist(params: {
|
|
634
|
+
tenantRef: string;
|
|
635
|
+
id: string;
|
|
636
|
+
holderRef?: string;
|
|
637
|
+
}): Promise<boolean>;
|
|
638
|
+
/**
|
|
639
|
+
* Offer freed time to the waitlist, in the order entries joined: each waiting entry whose window
|
|
640
|
+
* is free (on its resource, or on the first free member of its pool) gets a hold for its holder
|
|
641
|
+
* and the feed says so (`waitlist.offered`, with the hold). Entries whose window has started are
|
|
642
|
+
* expired (`waitlist.expired`); finished ones older than the change feed's retention are deleted.
|
|
643
|
+
* One tenant's offers run one at a time. One page of `limit` (1-100, default 50) entries: pass
|
|
644
|
+
* `next` back as `after` for the next, until it is null.
|
|
645
|
+
*/
|
|
646
|
+
offerWaitlist(params: {
|
|
647
|
+
tenantRef: string;
|
|
648
|
+
limit?: number;
|
|
649
|
+
after?: {
|
|
650
|
+
createdAt: Date;
|
|
651
|
+
id: string;
|
|
652
|
+
};
|
|
653
|
+
}): Promise<{
|
|
654
|
+
offered: number;
|
|
655
|
+
expired: number;
|
|
656
|
+
next: {
|
|
657
|
+
createdAt: Date;
|
|
658
|
+
id: string;
|
|
659
|
+
} | null;
|
|
660
|
+
}>;
|
|
661
|
+
pruneBookingRetention(params: {
|
|
662
|
+
tenantRef: string;
|
|
663
|
+
/** Rows per class per call, 1-1000. Defaults to 100. */
|
|
664
|
+
limit?: number;
|
|
665
|
+
/**
|
|
666
|
+
* How long a stray retirement is kept, in whole days, 1-3650. Defaults to the store's
|
|
667
|
+
* `retentionDays`, at least 30.
|
|
668
|
+
*/
|
|
669
|
+
retentionDays?: number;
|
|
670
|
+
}): Promise<BookingRetentionResult>;
|
|
671
|
+
/**
|
|
672
|
+
* Suspend a tenant: from when this returns, every withTenant transaction for it (as every server
|
|
673
|
+
* runs its requests) may read, and its writes are refused (`tenant_suspended`); one in flight is
|
|
674
|
+
* waited for. Its keys still authenticate, but it gets no new key or secret. Maintenance and
|
|
675
|
+
* erasure go on. True when it was not suspended already. Only the deployment role, which owns the
|
|
676
|
+
* schema, may suspend (`tenant_suspension_not_permitted` for another).
|
|
677
|
+
*/
|
|
678
|
+
suspendTenant(params: {
|
|
679
|
+
tenantRef: string;
|
|
680
|
+
/** Why, for the operator: 1-500 characters without control characters. */
|
|
681
|
+
reason?: string;
|
|
682
|
+
}): Promise<{
|
|
683
|
+
suspended: boolean;
|
|
684
|
+
}>;
|
|
685
|
+
/** Lift a suspension; true when the tenant was suspended. The deployment role only. */
|
|
686
|
+
unsuspendTenant(params: {
|
|
687
|
+
tenantRef: string;
|
|
688
|
+
}): Promise<{
|
|
689
|
+
unsuspended: boolean;
|
|
690
|
+
}>;
|
|
691
|
+
/** Whether the tenant is suspended. */
|
|
692
|
+
isTenantSuspended(params: {
|
|
693
|
+
tenantRef: string;
|
|
694
|
+
}): Promise<boolean>;
|
|
695
|
+
/**
|
|
696
|
+
* Erase a tenant: delete every row it owns (resources, holds, bookings, events with their
|
|
697
|
+
* attendees and reminders, cancellations, idempotency records, coverage, API keys, and a personal
|
|
698
|
+
* tenant's dashboard records) in one transaction, waiting for its transactions in flight. API key
|
|
699
|
+
* digests are retired so no erased key works again; a suspension stays. Erasing again deletes
|
|
700
|
+
* nothing.
|
|
701
|
+
*/
|
|
702
|
+
eraseTenant(params: {
|
|
703
|
+
tenantRef: string;
|
|
704
|
+
}): Promise<SlotlockTenantErasure>;
|
|
224
705
|
/** Record the bounded, complete source window represented by a provider cursor/revision. */
|
|
225
706
|
recordCalendarCoverage(params: CalendarCoverageCommand): Promise<CalendarCoverageResult>;
|
|
226
707
|
/** Return merged busy intervals plus explicit completeness for caller-required provider sources. */
|
|
@@ -232,27 +713,165 @@ export interface SlotlockStore {
|
|
|
232
713
|
coverageMaxAgeMs?: number;
|
|
233
714
|
}): Promise<CalendarFreeBusy>;
|
|
234
715
|
/**
|
|
235
|
-
*
|
|
236
|
-
*
|
|
716
|
+
* The first slot of `durationMs` inside the hours, whose time and turnaround buffer are free:
|
|
717
|
+
* getFreeBusy × expandRules (in the resource's stored timezone) → findNextAvailable. Null when no
|
|
718
|
+
* slot is free, and when one cannot be proven free: the coverage of the window (with the buffer's
|
|
719
|
+
* tail) is not complete, because a recurring series is not stored that far or a required provider
|
|
720
|
+
* calendar is not read that far. Another tenant's resource, an unknown id or one the store never
|
|
721
|
+
* issues is `resource_not_found`. A search window longer than 367 days is `invalid_window`.
|
|
237
722
|
*/
|
|
238
723
|
findNextAvailableFor(params: {
|
|
724
|
+
/** The resource's tenant; `null` for a resource that has none (the legacy namespace). */
|
|
725
|
+
tenantRef: string | null;
|
|
239
726
|
resourceId: string;
|
|
240
|
-
|
|
727
|
+
/** The hours to search; omitted, the resource's own (none: no slot). */
|
|
728
|
+
rules?: WeeklyAvailabilityRule[];
|
|
241
729
|
searchWindow: Interval;
|
|
242
730
|
durationMs: number;
|
|
731
|
+
/** Turnaround after the slot that must be free too (it may run past the hours), 0-30 days. */
|
|
732
|
+
bufferAfterMs?: number;
|
|
733
|
+
/** Provider sources that must prove complete coverage, as for getFreeBusy. */
|
|
734
|
+
requiredSources?: string[];
|
|
735
|
+
coverageMaxAgeMs?: number;
|
|
243
736
|
}): Promise<Interval | null>;
|
|
244
737
|
}
|
|
738
|
+
/** One entry of a tenant's waitlist (joinWaitlist). */
|
|
739
|
+
export interface SlotlockWaitlistEntry {
|
|
740
|
+
id: string;
|
|
741
|
+
/** The resource it waits on, or null when it waits on a pool. */
|
|
742
|
+
resourceId: string | null;
|
|
743
|
+
pool: string | null;
|
|
744
|
+
start: Date;
|
|
745
|
+
end: Date;
|
|
746
|
+
holderRef: string;
|
|
747
|
+
offerTtlMs: number;
|
|
748
|
+
/** `waiting`, `offered` (a hold was made: `holdId` on `offeredResourceId`) or `expired`. */
|
|
749
|
+
status: 'waiting' | 'offered' | 'expired';
|
|
750
|
+
holdId: string | null;
|
|
751
|
+
offeredResourceId: string | null;
|
|
752
|
+
offeredAt: Date | null;
|
|
753
|
+
createdAt: Date;
|
|
754
|
+
}
|
|
755
|
+
export type JoinWaitlistResult = {
|
|
756
|
+
ok: true;
|
|
757
|
+
entry: SlotlockWaitlistEntry;
|
|
758
|
+
} | {
|
|
759
|
+
ok: false;
|
|
760
|
+
code: 'invalid_identity' | 'invalid_window' | 'invalid_ttl' | 'invalid_pool' | 'resource_not_found' | 'resource_archived' | 'pool_not_found' | 'waitlist_limit_reached';
|
|
761
|
+
};
|
|
762
|
+
export type CheckInReservationResult = {
|
|
763
|
+
ok: true;
|
|
764
|
+
reference: string;
|
|
765
|
+
checkedInAt: Date;
|
|
766
|
+
} | {
|
|
767
|
+
ok: false;
|
|
768
|
+
code: 'invalid_identity' | 'reservation_not_found' | 'reservation_ended' | 'check_in_not_open';
|
|
769
|
+
};
|
|
770
|
+
/** One source's coverage of a resource, with its health (listCoverage). */
|
|
771
|
+
export interface SlotlockCoverageHealth {
|
|
772
|
+
source: string;
|
|
773
|
+
start: Date;
|
|
774
|
+
end: Date;
|
|
775
|
+
revision: number;
|
|
776
|
+
/** When it was last read. */
|
|
777
|
+
observedAt: Date;
|
|
778
|
+
lastError: string | null;
|
|
779
|
+
lastErrorAt: Date | null;
|
|
780
|
+
/** Not read for longer than free/busy trusts: time it covers is unproven, not free. */
|
|
781
|
+
stale: boolean;
|
|
782
|
+
}
|
|
245
783
|
export type SlotlockTenantStore = Omit<SlotlockStore, 'applySchema' | 'applyTenantRls' | 'grantApplicationRole' | 'withTenant'>;
|
|
246
784
|
export interface SlotlockStoreOptions {
|
|
247
785
|
/** Namespaced PostgreSQL setting installed by applyTenantRls. Defaults to `slotlock.tenant_ref`. */
|
|
248
786
|
tenantContextSetting?: string;
|
|
787
|
+
/**
|
|
788
|
+
* The bookable hours of a resource without its own, where its booking rules enforce hours
|
|
789
|
+
* (`enforceHours`). None by default: such a resource is then never bookable.
|
|
790
|
+
*/
|
|
791
|
+
defaultAvailabilityRules?: readonly WeeklyAvailabilityRule[];
|
|
249
792
|
/** Hard active + tombstone identity ceiling per non-internal owner. Defaults to 1,000. */
|
|
250
793
|
agentOwnerEventQuota?: number;
|
|
251
794
|
/** Hard retained-command ceiling per non-internal owner. Defaults to 10,000; cannot be disabled. */
|
|
252
795
|
agentOwnerCommandQuota?: number;
|
|
796
|
+
/**
|
|
797
|
+
* What one tenant may store, for a database tenants share (none by default). A write that would
|
|
798
|
+
* pass a limit is refused with it (`*_limit_reached`); what the tenant has can still be revised
|
|
799
|
+
* (a move adds no booking or event, so it is not refused), cancelled and released. Counted under
|
|
800
|
+
* a lock per tenant and limit, so racing writers never pass it, in READ COMMITTED transactions
|
|
801
|
+
* only (`invalid_transaction_isolation` otherwise). Bookings and events count while they are still
|
|
802
|
+
* to come, and for 30 days after they were made, whatever time they hold; history older than that
|
|
803
|
+
* does not count, and a count stops reading at the limit.
|
|
804
|
+
*/
|
|
805
|
+
tenantLimits?: SlotlockTenantLimits;
|
|
806
|
+
/**
|
|
807
|
+
* How many withTenant transactions of one tenant run at once on this store (1-1,000; no limit by
|
|
808
|
+
* default). The rest wait in this process, in order, so one tenant's transactions waiting on its
|
|
809
|
+
* own locks hold at most this many of a pool's connections. Not counted inside a caller's
|
|
810
|
+
* transaction, which holds its connection already.
|
|
811
|
+
*/
|
|
812
|
+
maxTenantTransactions?: number;
|
|
813
|
+
/**
|
|
814
|
+
* The longest a withTenant transaction waits for any one lock, in milliseconds (1-600,000; none by
|
|
815
|
+
* default). Past it the transaction is rolled back and refused with `lock_timeout`, which is safe
|
|
816
|
+
* to retry. Not set inside a caller's transaction.
|
|
817
|
+
*/
|
|
818
|
+
tenantLockTimeoutMs?: number;
|
|
819
|
+
/**
|
|
820
|
+
* How many days history is kept, 1-3,650 (default 30): the change feed (and how far back a cursor
|
|
821
|
+
* reaches) and waitlist entries that ended; and, when longer than 30 days, retired idempotency
|
|
822
|
+
* answers, booking references and past events' commands (never shorter, so a delayed retry is still
|
|
823
|
+
* answered as the first try was). Maintenance prunes what is older. A cursor before a change any
|
|
824
|
+
* store has pruned answers `cursor_expired`, so stores set differently never skip a change.
|
|
825
|
+
*/
|
|
826
|
+
retentionDays?: number;
|
|
827
|
+
/**
|
|
828
|
+
* Live holds one holder (`acquireHold`'s `holderRef`) may have at once, 1-10,000. Defaults to
|
|
829
|
+
* SLOTLOCK_MAX_HOLDS_PER_HOLDER (20).
|
|
830
|
+
*/
|
|
831
|
+
maxHoldsPerHolder?: number;
|
|
832
|
+
/**
|
|
833
|
+
* Hours one holder's live holds may hold together, 1-876,000 (none by default): past it a hold is
|
|
834
|
+
* refused with `held_time_limit_reached`, however few holds it has.
|
|
835
|
+
*/
|
|
836
|
+
maxHeldHoursPerHolder?: number;
|
|
837
|
+
}
|
|
838
|
+
/** Per-tenant row limits, each a whole number from 1 to 10,000,000 (resources: to 100,000). */
|
|
839
|
+
export interface SlotlockTenantLimits {
|
|
840
|
+
/** Resources, archived ones included; the lower of this and a call's `maxTenantResources`. */
|
|
841
|
+
resources?: number;
|
|
842
|
+
/**
|
|
843
|
+
* Holds on the tenant's resources, expired ones included until they are deleted. A keyed hold
|
|
844
|
+
* keeps its place until it would have expired, released or confirmed, while its answer is kept
|
|
845
|
+
* for retries. A hold at the limit first deletes the tenant's expired holds and forgets their
|
|
846
|
+
* answers, as maintenance does, so only live holds keep a new one out.
|
|
847
|
+
*/
|
|
848
|
+
holds?: number;
|
|
849
|
+
/**
|
|
850
|
+
* Bookings still to come or made in the last 30 days, cancellations made in the last 30 days,
|
|
851
|
+
* and references a cancel retired with no booking until maintenance prunes them.
|
|
852
|
+
*/
|
|
853
|
+
reservations?: number;
|
|
854
|
+
/**
|
|
855
|
+
* Calendar events of every owner still to come (a series until it is known to have ended) or made
|
|
856
|
+
* in the last 30 days, cancellations of every owner made in the last 30 days, and agent-owned
|
|
857
|
+
* cancellations until maintenance prunes them. Each event stores at most 2,000 occurrences.
|
|
858
|
+
*/
|
|
859
|
+
events?: number;
|
|
860
|
+
/** Provider coverage rows: one per resource and source. */
|
|
861
|
+
coverage?: number;
|
|
253
862
|
}
|
|
254
863
|
export interface SlotlockHoldTenantOptions {
|
|
255
864
|
/** Only a hold on one of this tenant's resources: 1-500 UTF-8 bytes. */
|
|
256
865
|
tenantRef?: string;
|
|
257
866
|
}
|
|
867
|
+
/**
|
|
868
|
+
* At most `max` holders per tenant at once; the rest wait in order. An aborted signal takes a
|
|
869
|
+
* waiter out of the line, rejecting with its reason. Each holder calls the release it gets once.
|
|
870
|
+
*/
|
|
871
|
+
export declare function createTenantSlots(max: number): (tenantRef: string, signal?: AbortSignal) => Promise<() => void>;
|
|
258
872
|
export declare function createSlotlockStore(sql: StoreSql, options?: SlotlockStoreOptions): SlotlockStore;
|
|
873
|
+
/**
|
|
874
|
+
* Every store method that writes for a tenant: what a suspended tenant's withTenant store refuses.
|
|
875
|
+
* Maintenance (rolling series, pruning, purging) and erasure are not among them.
|
|
876
|
+
*/
|
|
877
|
+
export declare const SLOTLOCK_TENANT_WRITE_METHODS: readonly ["createResource", "archiveResource", "restoreResource", "deleteResource", "setResourceAvailability", "setResourceBookingRules", "setResourcePool", "setTenantAvailability", "createReservation", "createExternalReservation", "rescheduleExternalReservation", "cancelExternalReservation", "acquireHold", "confirmHold", "releaseHold", "putCalendarEvent", "cancelCalendarEvent", "cancelCalendarEventById", "recordCalendarCoverage", "createWebhookEndpoint", "deleteWebhookEndpoint", "enableWebhookEndpoint", "recordCoverageFailure", "deleteCoverage", "checkInReservation", "joinWaitlist", "leaveWaitlist"];
|