slotlock 0.6.2 → 0.7.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 +492 -0
- package/CONTRIBUTING.md +12 -5
- package/README.md +50 -1544
- package/SECURITY.md +5 -2
- package/SPEC.md +1 -1
- package/agent-server.d.ts +160 -8
- package/agent-server.js +560 -49
- package/agent-store-backend.d.ts +39 -7
- package/agent-store-backend.js +1439 -104
- package/api-keys.d.ts +69 -3
- package/api-keys.js +218 -29
- package/application-api.d.ts +59 -6
- package/application-api.js +842 -39
- package/approvals.d.ts +88 -0
- package/approvals.js +293 -0
- package/assistant.d.ts +58 -27
- package/assistant.js +151 -53
- package/billing.d.ts +114 -0
- package/billing.js +258 -0
- 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 +42 -6
- package/config.js +114 -7
- package/dashboard-icon.d.ts +1 -0
- package/dashboard-icon.js +5 -0
- package/dashboard.d.ts +203 -7
- package/dashboard.js +1558 -64
- package/ddl.d.ts +67 -35
- package/ddl.js +2732 -162
- package/engine.d.ts +6 -9
- package/engine.js +5 -9
- package/event-feed.d.ts +53 -35
- package/event-feed.js +201 -64
- 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/index.d.ts +15 -4
- package/index.js +8 -2
- package/interop.js +1 -4
- package/maintenance.d.ts +24 -10
- package/maintenance.js +59 -28
- 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.d.ts +41 -0
- package/metrics.js +22 -1
- package/node-server.d.ts +9 -9
- package/node-server.js +25 -34
- package/openapi.json +14675 -5957
- package/otel.d.ts +86 -0
- package/otel.js +273 -0
- package/overage.d.ts +33 -0
- package/overage.js +115 -0
- package/package-version.js +2 -4
- package/package.json +1 -1
- package/partner-billing.d.ts +81 -0
- package/partner-billing.js +333 -0
- package/partner-names.d.ts +8 -0
- package/partner-names.js +10 -0
- package/partners.d.ts +147 -0
- package/partners.js +375 -0
- package/plans.d.ts +92 -0
- package/plans.js +115 -0
- package/quotas.d.ts +108 -0
- package/quotas.js +258 -0
- package/rate-limit.d.ts +27 -2
- package/rate-limit.js +106 -23
- package/resource-profile.d.ts +135 -0
- package/resource-profile.js +263 -0
- package/rest-api.d.ts +15 -14
- package/rest-api.js +79 -15
- package/rules.d.ts +6 -7
- package/rules.js +30 -42
- package/sandbox.d.ts +70 -0
- package/sandbox.js +295 -0
- package/self-host.d.ts +46 -4
- package/self-host.js +990 -41
- package/slotlock.manifest.json +1 -1
- package/store.d.ts +482 -165
- package/store.js +1460 -484
- package/sync.d.ts +44 -59
- package/sync.js +325 -469
- package/teams.d.ts +25 -0
- package/teams.js +63 -0
- package/timezone.d.ts +25 -33
- package/timezone.js +50 -53
- package/types.d.ts +130 -57
- package/usage.d.ts +106 -0
- package/usage.js +238 -0
- 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/teams.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { SlotlockSql } from './store.js';
|
|
2
|
+
/** What the dashboard reads and changes of the organizations turned on (SLOTLOCK_DASHBOARD_ORGS=enabled). */
|
|
3
|
+
export interface SlotlockDashboardTeams {
|
|
4
|
+
/** Of these organization ids, the ones turned on. */
|
|
5
|
+
enabled(ids: readonly string[]): Promise<ReadonlySet<string>>;
|
|
6
|
+
/** Turn Slotlock on for an organization, by an owner of it (`github:<id>`). True when it was off. */
|
|
7
|
+
enable(org: {
|
|
8
|
+
id: string;
|
|
9
|
+
login: string;
|
|
10
|
+
}, by: string): Promise<boolean>;
|
|
11
|
+
/** Turn it off: its keys stop at once, and its data stay. True when it was on. */
|
|
12
|
+
disable(id: string, by: string): Promise<boolean>;
|
|
13
|
+
}
|
|
14
|
+
/** The organizations turned on, through slotlock.enabled_organizations and its two writers. */
|
|
15
|
+
export declare function createSlotlockDashboardTeams(sql: SlotlockSql): SlotlockDashboardTeams;
|
|
16
|
+
/**
|
|
17
|
+
* Whether an organization is turned on, for `serve`'s keys with SLOTLOCK_DASHBOARD_ORGS=enabled:
|
|
18
|
+
* read through slotlock.enabled_organizations and kept for `ttlMs` (30 seconds), so turning one off
|
|
19
|
+
* stops its keys on every server within that, without a query per request.
|
|
20
|
+
*/
|
|
21
|
+
export declare function createSlotlockOrganizationCheck(sql: SlotlockSql, options?: {
|
|
22
|
+
ttlMs?: number;
|
|
23
|
+
now?: () => number;
|
|
24
|
+
maxEntries?: number;
|
|
25
|
+
}): (orgId: string) => Promise<boolean>;
|
package/teams.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const ORGANIZATION_ID = /^[1-9]\d{0,19}$/;
|
|
2
|
+
const GITHUB_LOGIN = /^[A-Za-z0-9][A-Za-z0-9-]{0,38}$/;
|
|
3
|
+
const PERSON = /^github:[1-9]\d{0,19}$/;
|
|
4
|
+
function refuse(message) {
|
|
5
|
+
throw Object.assign(new Error(`Slotlock organizations: ${message}`), { code: 'invalid_organization' });
|
|
6
|
+
}
|
|
7
|
+
/** The organizations turned on, through slotlock.enabled_organizations and its two writers. */
|
|
8
|
+
export function createSlotlockDashboardTeams(sql) {
|
|
9
|
+
return {
|
|
10
|
+
async enabled(ids) {
|
|
11
|
+
const valid = ids.filter((id) => ORGANIZATION_ID.test(id));
|
|
12
|
+
if (valid.length === 0)
|
|
13
|
+
return new Set();
|
|
14
|
+
const rows = await sql `
|
|
15
|
+
SELECT org_id FROM slotlock.enabled_organizations(${valid.join(',')}::text) AS org_id`;
|
|
16
|
+
return new Set(rows.map((row) => row.org_id));
|
|
17
|
+
},
|
|
18
|
+
async enable(org, by) {
|
|
19
|
+
if (!ORGANIZATION_ID.test(org.id) || !GITHUB_LOGIN.test(org.login) || !PERSON.test(by)) {
|
|
20
|
+
refuse('needs an organization id, its login and the person turning it on');
|
|
21
|
+
}
|
|
22
|
+
const [row] = await sql `
|
|
23
|
+
SELECT slotlock.enable_organization(${org.id}::text, ${org.login}::text, ${by}::text) AS changed`;
|
|
24
|
+
return row?.changed === true;
|
|
25
|
+
},
|
|
26
|
+
async disable(id, by) {
|
|
27
|
+
if (!ORGANIZATION_ID.test(id) || !PERSON.test(by))
|
|
28
|
+
refuse('needs an organization id and the person turning it off');
|
|
29
|
+
const [row] = await sql `
|
|
30
|
+
SELECT slotlock.disable_organization(${id}::text, ${by}::text) AS changed`;
|
|
31
|
+
return row?.changed === true;
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Whether an organization is turned on, for `serve`'s keys with SLOTLOCK_DASHBOARD_ORGS=enabled:
|
|
37
|
+
* read through slotlock.enabled_organizations and kept for `ttlMs` (30 seconds), so turning one off
|
|
38
|
+
* stops its keys on every server within that, without a query per request.
|
|
39
|
+
*/
|
|
40
|
+
export function createSlotlockOrganizationCheck(sql, options = {}) {
|
|
41
|
+
const ttlMs = options.ttlMs ?? 30_000;
|
|
42
|
+
const now = options.now ?? Date.now;
|
|
43
|
+
const maxEntries = options.maxEntries ?? 10_000;
|
|
44
|
+
const known = new Map();
|
|
45
|
+
const teams = createSlotlockDashboardTeams(sql);
|
|
46
|
+
return (orgId) => {
|
|
47
|
+
if (!ORGANIZATION_ID.test(orgId))
|
|
48
|
+
return Promise.resolve(false);
|
|
49
|
+
const cached = known.get(orgId);
|
|
50
|
+
if (cached && now() - cached.at < ttlMs)
|
|
51
|
+
return cached.enabled;
|
|
52
|
+
const enabled = teams.enabled([orgId]).then((found) => found.has(orgId), (error) => {
|
|
53
|
+
// A failed read is not remembered: the next request asks again.
|
|
54
|
+
known.delete(orgId);
|
|
55
|
+
throw error;
|
|
56
|
+
});
|
|
57
|
+
known.delete(orgId);
|
|
58
|
+
known.set(orgId, { at: now(), enabled });
|
|
59
|
+
if (known.size > maxEntries)
|
|
60
|
+
known.delete(known.keys().next().value);
|
|
61
|
+
return enabled;
|
|
62
|
+
};
|
|
63
|
+
}
|
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,17 +237,21 @@ 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();
|
|
257
247
|
const invalid = () => Object.assign(new Error(`Invalid timezone: ${timezone}`), { code: 'invalid_timezone' });
|
|
258
248
|
if (!name || /^[+-]/.test(name))
|
|
259
249
|
throw invalid();
|
|
250
|
+
// Each name resolves the same for the process's life (its time zone data does not change under
|
|
251
|
+
// it), and building a formatter to ask is the costly part: remembered, valid names only.
|
|
252
|
+
const known = canonicalNames.get(name);
|
|
253
|
+
if (known !== undefined)
|
|
254
|
+
return known;
|
|
260
255
|
let resolved;
|
|
261
256
|
try {
|
|
262
257
|
resolved = new Intl.DateTimeFormat('en-US', { timeZone: name }).resolvedOptions().timeZone;
|
|
@@ -266,12 +261,18 @@ export function canonicalTimeZoneName(timezone) {
|
|
|
266
261
|
}
|
|
267
262
|
if (/^[+-]/.test(resolved))
|
|
268
263
|
throw invalid();
|
|
269
|
-
|
|
264
|
+
const canonical = resolved.toLowerCase() === name.toLowerCase() ? resolved : name;
|
|
265
|
+
if (canonicalNames.size >= MAX_CANONICAL_NAMES)
|
|
266
|
+
canonicalNames.clear();
|
|
267
|
+
canonicalNames.set(name, canonical);
|
|
268
|
+
return canonical;
|
|
270
269
|
}
|
|
270
|
+
/** Names canonicalTimeZoneName resolved; a few hundred zones exist, so this rarely fills. */
|
|
271
|
+
const canonicalNames = new Map();
|
|
272
|
+
const MAX_CANONICAL_NAMES = 2_000;
|
|
271
273
|
/**
|
|
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.
|
|
274
|
+
* The civil date and ISO weekday an instant falls on in `timezone` (the inverse of {@link
|
|
275
|
+
* zonedDateTimeToUtc}): a pickup at 00:30 BST on a Friday is a Friday, whatever UTC says.
|
|
275
276
|
*/
|
|
276
277
|
export function zonedCalendarDate(instant, timezone) {
|
|
277
278
|
const wall = wallFrame(instant, timezone);
|
|
@@ -279,9 +280,8 @@ export function zonedCalendarDate(instant, timezone) {
|
|
|
279
280
|
return { date: civilDate(wall), isoWeekday: weekday === 0 ? 7 : weekday };
|
|
280
281
|
}
|
|
281
282
|
/**
|
|
282
|
-
* The civil date and wall-clock time an instant reads as in `timezone
|
|
283
|
-
* state a booking
|
|
284
|
-
* server's and not UTC.
|
|
283
|
+
* The civil date and wall-clock time an instant reads as in `timezone`, as agreements and receipts
|
|
284
|
+
* state a booking: on the resource's own clock.
|
|
285
285
|
*/
|
|
286
286
|
export function zonedWallTime(instant, timezone) {
|
|
287
287
|
const wall = wallFrame(instant, timezone);
|
|
@@ -295,7 +295,7 @@ export function zonedOffsetMinutes(instant, timezone) {
|
|
|
295
295
|
}
|
|
296
296
|
/**
|
|
297
297
|
* The wall-clock fields an instant reads as in `timezone`, to the second: what a calendar writes
|
|
298
|
-
* beside a TZID.
|
|
298
|
+
* beside a TZID.
|
|
299
299
|
*/
|
|
300
300
|
export function zonedDateTimeFields(instant, timezone) {
|
|
301
301
|
const wall = wallFrame(instant, timezone);
|
|
@@ -356,9 +356,8 @@ export function zonedDateTimeToUtc(value, timezone, choice) {
|
|
|
356
356
|
return createTimeZoneConverter(timezone).wallFrameToUtc(wallFrame, choice);
|
|
357
357
|
}
|
|
358
358
|
/**
|
|
359
|
-
* Every change of `timezone`'s offset from `fromMs` to `toMs`,
|
|
360
|
-
*
|
|
361
|
-
* days later, which a search a week at a time missed.
|
|
359
|
+
* Every change of `timezone`'s offset from `fromMs` to `toMs`, read a day at a time (ZoneOffsets):
|
|
360
|
+
* a weekly search misses short-lived changes such as Gaza's six-day one in October 2040.
|
|
362
361
|
*/
|
|
363
362
|
export function zoneTransitions(timezone, fromMs, toMs) {
|
|
364
363
|
if (timezone === 'UTC')
|
|
@@ -366,9 +365,8 @@ export function zoneTransitions(timezone, fromMs, toMs) {
|
|
|
366
365
|
return zoneOffsets(timezone).transitions(Math.floor(fromMs / 1000) * 1000, Math.ceil(toMs / 1000) * 1000);
|
|
367
366
|
}
|
|
368
367
|
/**
|
|
369
|
-
*
|
|
370
|
-
*
|
|
371
|
-
* one step are missed.
|
|
368
|
+
* Offset changes found by comparing `offsetIn` `stepMs` apart and narrowing each to the second; a
|
|
369
|
+
* change and its reversal within one step are missed.
|
|
372
370
|
*/
|
|
373
371
|
function scanTransitions(offsetIn, fromMs, toMs, stepMs) {
|
|
374
372
|
const found = [];
|
|
@@ -380,7 +378,6 @@ function scanTransitions(offsetIn, fromMs, toMs, stepMs) {
|
|
|
380
378
|
at = next;
|
|
381
379
|
continue;
|
|
382
380
|
}
|
|
383
|
-
// The last whole second at `offset` and the first at another.
|
|
384
381
|
let low = at;
|
|
385
382
|
let high = next;
|
|
386
383
|
while (high - low > 1000) {
|