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/timezone.d.ts
CHANGED
|
@@ -7,19 +7,60 @@ export interface LocalDateTime {
|
|
|
7
7
|
second?: number;
|
|
8
8
|
millisecond?: number;
|
|
9
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* Which instant a wall time names where the clocks change. RFC 5545 §3.3.5 reads a time in a fold
|
|
12
|
+
* (an hour the clocks repeat) as its first instant, and a time in a gap (an hour they skip) with the
|
|
13
|
+
* offset in force before the gap, which moves it forward by the gap: the defaults.
|
|
14
|
+
*/
|
|
15
|
+
export interface WallTimeChoice {
|
|
16
|
+
/** In a fold: the earlier instant (default), or the later one. */
|
|
17
|
+
fold?: 'earlier' | 'later';
|
|
18
|
+
/** In a gap: moved forward by the gap (default), or the instant the gap ends. */
|
|
19
|
+
gap?: 'shift' | 'end';
|
|
20
|
+
}
|
|
10
21
|
export interface TimeZoneConverter {
|
|
11
22
|
/**
|
|
12
23
|
* Convert UTC-framed wall-clock fields to the instant that reads as those fields in this zone.
|
|
13
|
-
*
|
|
24
|
+
* By default the first instant for a fold, and shifted forward by the gap for a hole.
|
|
14
25
|
*/
|
|
15
|
-
wallFrameToUtc(wallFrameMs: number): Date;
|
|
26
|
+
wallFrameToUtc(wallFrameMs: number, choice?: WallTimeChoice): Date;
|
|
16
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* The instant, in ms, that reads as `wallFrameMs` (wall-clock fields framed as UTC) in a zone whose
|
|
30
|
+
* offset at an instant `offsetAt` gives, in ms. The offsets in force within fourteen hours either
|
|
31
|
+
* side are the candidates (no zone is further than that from UTC, so they include both sides of any
|
|
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.
|
|
34
|
+
*/
|
|
35
|
+
export declare function wallFrameToInstant(wallFrameMs: number, offsetAt: (utcMs: number) => number, choice?: WallTimeChoice): number;
|
|
17
36
|
/**
|
|
18
37
|
* Date.UTC remaps years 0..99 to 1900..1999. Slotlock accepts ISO calendar years down to 1,
|
|
19
38
|
* so construct UTC frames through setUTCFullYear instead of inheriting that legacy quirk.
|
|
20
39
|
*/
|
|
21
40
|
export declare function utcFrame(year: number, month: number, day: number, hour: number, minute: number, second: number, millisecond: number): Date;
|
|
41
|
+
/** The UTC day an instant falls in, as ZoneOffsets reads a zone's offsets: days since 1970. */
|
|
42
|
+
export declare function zoneOffsetDay(utcMs: number): number;
|
|
43
|
+
/**
|
|
44
|
+
* How far either side of a wall time converting it to its instant reads the zone's offsets: no zone
|
|
45
|
+
* is further than this from UTC. The days from one end to the other hold every offset it reads.
|
|
46
|
+
*/
|
|
47
|
+
export declare const ZONE_OFFSET_REACH_MS: number;
|
|
48
|
+
/**
|
|
49
|
+
* How many instants reading `timezone`'s offsets on UTC day `day` asks Intl for with none of them
|
|
50
|
+
* kept, beyond the day's two midnights (one each): about twenty to find each change between them.
|
|
51
|
+
* A caller that must charge the same for an input however much the process has kept charges each
|
|
52
|
+
* midnight and this the first time it reads them.
|
|
53
|
+
*/
|
|
54
|
+
export declare function zoneOffsetChangeReads(timezone: string, day: number): number;
|
|
22
55
|
export declare function createTimeZoneConverter(timezone: string): TimeZoneConverter;
|
|
56
|
+
/**
|
|
57
|
+
* The spelling Slotlock keeps of an IANA zone name: Intl's, when Intl reads the name as itself in
|
|
58
|
+
* another case (`europe/london` is `Europe/London`); otherwise the name as given (`US/Eastern` stays,
|
|
59
|
+
* although this Node's ICU may call it `America/New_York`, so a stored name never depends on the
|
|
60
|
+
* ICU version that wrote it). A UTC offset (`+05:00`, which Intl accepts as a zone) is not an IANA
|
|
61
|
+
* name and is refused, as is anything Intl does not know: both with `code: 'invalid_timezone'`.
|
|
62
|
+
*/
|
|
63
|
+
export declare function canonicalTimeZoneName(timezone: string): string;
|
|
23
64
|
export interface ZonedCalendarDate {
|
|
24
65
|
/** Civil date of the instant in the zone, `YYYY-MM-DD`. */
|
|
25
66
|
date: string;
|
|
@@ -60,4 +101,18 @@ export interface ZonedDateTimeFields {
|
|
|
60
101
|
* beside a TZID. Reads through the cached formatters, as every conversion here does.
|
|
61
102
|
*/
|
|
62
103
|
export declare function zonedDateTimeFields(instant: Date, timezone: string): ZonedDateTimeFields;
|
|
63
|
-
export declare function zonedDateTimeToUtc(value: LocalDateTime, timezone: string): Date;
|
|
104
|
+
export declare function zonedDateTimeToUtc(value: LocalDateTime, timezone: string, choice?: WallTimeChoice): Date;
|
|
105
|
+
/** A change of a zone's offset: the instant it takes effect, and the offsets before and after. */
|
|
106
|
+
export interface ZoneTransition {
|
|
107
|
+
/** The first whole second at the new offset, in ms. */
|
|
108
|
+
at: number;
|
|
109
|
+
/** How far the zone is ahead of UTC before and after it, in ms. */
|
|
110
|
+
before: number;
|
|
111
|
+
after: number;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Every change of `timezone`'s offset from `fromMs` to `toMs`, as Intl reads the zone, from its
|
|
115
|
+
* offsets a day at a time (ZoneOffsets): Gaza's clocks go forward on 20 October 2040 and back six
|
|
116
|
+
* days later, which a search a week at a time missed.
|
|
117
|
+
*/
|
|
118
|
+
export declare function zoneTransitions(timezone: string, fromMs: number, toMs: number): ZoneTransition[];
|
package/timezone.js
CHANGED
|
@@ -1,4 +1,44 @@
|
|
|
1
1
|
const FOURTEEN_HOURS_MS = 14 * 60 * 60 * 1000;
|
|
2
|
+
/**
|
|
3
|
+
* The instant, in ms, that reads as `wallFrameMs` (wall-clock fields framed as UTC) in a zone whose
|
|
4
|
+
* offset at an instant `offsetAt` gives, in ms. The offsets in force within fourteen hours either
|
|
5
|
+
* side are the candidates (no zone is further than that from UTC, so they include both sides of any
|
|
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.
|
|
8
|
+
*/
|
|
9
|
+
export function wallFrameToInstant(wallFrameMs, offsetAt, choice = {}) {
|
|
10
|
+
const offsets = new Set([
|
|
11
|
+
offsetAt(wallFrameMs - FOURTEEN_HOURS_MS),
|
|
12
|
+
offsetAt(wallFrameMs),
|
|
13
|
+
offsetAt(wallFrameMs + FOURTEEN_HOURS_MS),
|
|
14
|
+
]);
|
|
15
|
+
const valid = [];
|
|
16
|
+
for (const offset of offsets) {
|
|
17
|
+
if (offsetAt(wallFrameMs - offset) === offset)
|
|
18
|
+
valid.push(offset);
|
|
19
|
+
}
|
|
20
|
+
if (valid.length === 1)
|
|
21
|
+
return wallFrameMs - valid[0];
|
|
22
|
+
if (valid.length > 1) {
|
|
23
|
+
return wallFrameMs - (choice.fold === 'later' ? Math.min(...valid) : Math.max(...valid));
|
|
24
|
+
}
|
|
25
|
+
const shifted = wallFrameMs - Math.min(...offsets);
|
|
26
|
+
if (choice.gap !== 'end')
|
|
27
|
+
return shifted;
|
|
28
|
+
// The gap ends at the change: the first whole second after the instant read with the offset
|
|
29
|
+
// after the gap (which is still before it) whose offset differs from that one's.
|
|
30
|
+
let before = wallFrameMs - Math.max(...offsets);
|
|
31
|
+
const offsetBefore = offsetAt(before);
|
|
32
|
+
let after = shifted;
|
|
33
|
+
while (after - before > 1000) {
|
|
34
|
+
const middle = before + Math.floor((after - before) / 2000) * 1000;
|
|
35
|
+
if (offsetAt(middle) === offsetBefore)
|
|
36
|
+
before = middle;
|
|
37
|
+
else
|
|
38
|
+
after = middle;
|
|
39
|
+
}
|
|
40
|
+
return after;
|
|
41
|
+
}
|
|
2
42
|
/**
|
|
3
43
|
* Date.UTC remaps years 0..99 to 1900..1999. Slotlock accepts ISO calendar years down to 1,
|
|
4
44
|
* so construct UTC frames through setUTCFullYear instead of inheriting that legacy quirk.
|
|
@@ -27,6 +67,143 @@ function createFormatter(timezone) {
|
|
|
27
67
|
formatters.set(timezone, formatter);
|
|
28
68
|
return formatter;
|
|
29
69
|
}
|
|
70
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
71
|
+
/** The furthest from 1970 a Date may be, in ms (ECMA-262 §21.4.1.22, "Time Values and Time Range"). */
|
|
72
|
+
const MAX_TIME_MS = 8.64e15;
|
|
73
|
+
/** The UTC day an instant falls in, as ZoneOffsets reads a zone's offsets: days since 1970. */
|
|
74
|
+
export function zoneOffsetDay(utcMs) {
|
|
75
|
+
return Math.floor(utcMs / DAY_MS);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* How far either side of a wall time converting it to its instant reads the zone's offsets: no zone
|
|
79
|
+
* is further than this from UTC. The days from one end to the other hold every offset it reads.
|
|
80
|
+
*/
|
|
81
|
+
export const ZONE_OFFSET_REACH_MS = FOURTEEN_HOURS_MS;
|
|
82
|
+
/** How many midnights one block of a zone's offsets holds. */
|
|
83
|
+
const BLOCK_DAYS = 512;
|
|
84
|
+
const NO_CHANGES = [];
|
|
85
|
+
/**
|
|
86
|
+
* A zone's offsets, read from Intl a UTC day at a time and kept. Intl's formatToParts takes about
|
|
87
|
+
* two microseconds and a conversion asks for four to six offsets, so a series that converts a time
|
|
88
|
+
* at each of thousands of occurrences spent most of its time there (forty daily London series from
|
|
89
|
+
* 2016, 35 seconds). Each UTC midnight's offset is read once; a day whose two midnights agree has
|
|
90
|
+
* that offset throughout, and one whose midnights differ has each change in it narrowed to the
|
|
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.
|
|
93
|
+
*/
|
|
94
|
+
class ZoneOffsets {
|
|
95
|
+
/** The offset at each UTC midnight read (NaN where none is), BLOCK_DAYS days to a block. */
|
|
96
|
+
midnights = new Map();
|
|
97
|
+
/** The changes of each day whose midnights differ. */
|
|
98
|
+
changes = new Map();
|
|
99
|
+
formatter;
|
|
100
|
+
constructor(formatter) {
|
|
101
|
+
this.formatter = formatter;
|
|
102
|
+
}
|
|
103
|
+
offsetAt(utcMs) {
|
|
104
|
+
const day = zoneOffsetDay(utcMs);
|
|
105
|
+
// Not a time value: Intl refuses it, as it always has.
|
|
106
|
+
if (!Number.isFinite(day))
|
|
107
|
+
return offsetAt(utcMs, this.formatter);
|
|
108
|
+
let offset = this.midnight(day);
|
|
109
|
+
for (const change of this.changesOn(day)) {
|
|
110
|
+
if (utcMs < change.at)
|
|
111
|
+
break;
|
|
112
|
+
offset = change.after;
|
|
113
|
+
}
|
|
114
|
+
return offset;
|
|
115
|
+
}
|
|
116
|
+
/** The changes on UTC day `day`, in order. */
|
|
117
|
+
changesOn(day) {
|
|
118
|
+
const offset = this.midnight(day);
|
|
119
|
+
if (offset === this.midnight(day + 1))
|
|
120
|
+
return NO_CHANGES;
|
|
121
|
+
let found = this.changes.get(day);
|
|
122
|
+
if (!found) {
|
|
123
|
+
found = scanTransitions((utcMs) => offsetAt(utcMs, this.formatter), dayStart(day), dayStart(day + 1), DAY_MS);
|
|
124
|
+
if (cachedChangeDays >= MAX_CACHED_CHANGE_DAYS) {
|
|
125
|
+
for (const zone of zoneOffsetTables.values())
|
|
126
|
+
zone.changes.clear();
|
|
127
|
+
cachedChangeDays = 0;
|
|
128
|
+
}
|
|
129
|
+
this.changes.set(day, found);
|
|
130
|
+
cachedChangeDays += 1;
|
|
131
|
+
}
|
|
132
|
+
return found;
|
|
133
|
+
}
|
|
134
|
+
/** The changes from `fromMs` (exclusive) to `toMs` (inclusive), day by day. */
|
|
135
|
+
transitions(fromMs, toMs) {
|
|
136
|
+
const found = [];
|
|
137
|
+
const last = zoneOffsetDay(toMs);
|
|
138
|
+
for (let day = zoneOffsetDay(fromMs); day <= last; day++) {
|
|
139
|
+
for (const change of this.changesOn(day)) {
|
|
140
|
+
if (change.at > fromMs && change.at <= toMs)
|
|
141
|
+
found.push(change);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return found;
|
|
145
|
+
}
|
|
146
|
+
midnight(day) {
|
|
147
|
+
const block = Math.floor(day / BLOCK_DAYS);
|
|
148
|
+
let offsets = this.midnights.get(block);
|
|
149
|
+
if (!offsets) {
|
|
150
|
+
if (cachedBlocks >= MAX_CACHED_BLOCKS) {
|
|
151
|
+
for (const zone of zoneOffsetTables.values())
|
|
152
|
+
zone.midnights.clear();
|
|
153
|
+
cachedBlocks = 0;
|
|
154
|
+
}
|
|
155
|
+
offsets = new Float64Array(BLOCK_DAYS).fill(Number.NaN);
|
|
156
|
+
this.midnights.set(block, offsets);
|
|
157
|
+
cachedBlocks += 1;
|
|
158
|
+
}
|
|
159
|
+
const index = day - block * BLOCK_DAYS;
|
|
160
|
+
let offset = offsets[index];
|
|
161
|
+
if (Number.isNaN(offset)) {
|
|
162
|
+
offset = offsetAt(dayStart(day), this.formatter);
|
|
163
|
+
offsets[index] = offset;
|
|
164
|
+
}
|
|
165
|
+
return offset;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/** A UTC day's first instant, as far as a Date reaches. */
|
|
169
|
+
function dayStart(day) {
|
|
170
|
+
return Math.min(Math.max(day * DAY_MS, -MAX_TIME_MS), MAX_TIME_MS);
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Each zone's offsets, by name, as createFormatter keeps formatters. A block of midnights takes 4
|
|
174
|
+
* KB; once there are this many (16 MB, about 57 zone-centuries), every zone's are emptied, and so
|
|
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).
|
|
177
|
+
*/
|
|
178
|
+
const zoneOffsetTables = new Map();
|
|
179
|
+
const MAX_CACHED_BLOCKS = 4_096;
|
|
180
|
+
const MAX_CACHED_CHANGE_DAYS = 20_000;
|
|
181
|
+
let cachedBlocks = 0;
|
|
182
|
+
let cachedChangeDays = 0;
|
|
183
|
+
function zoneOffsets(timezone) {
|
|
184
|
+
const cached = zoneOffsetTables.get(timezone);
|
|
185
|
+
if (cached)
|
|
186
|
+
return cached;
|
|
187
|
+
const offsets = new ZoneOffsets(createFormatter(timezone));
|
|
188
|
+
if (zoneOffsetTables.size >= MAX_CACHED_FORMATTERS) {
|
|
189
|
+
zoneOffsetTables.clear();
|
|
190
|
+
cachedBlocks = 0;
|
|
191
|
+
cachedChangeDays = 0;
|
|
192
|
+
}
|
|
193
|
+
zoneOffsetTables.set(timezone, offsets);
|
|
194
|
+
return offsets;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* How many instants reading `timezone`'s offsets on UTC day `day` asks Intl for with none of them
|
|
198
|
+
* kept, beyond the day's two midnights (one each): about twenty to find each change between them.
|
|
199
|
+
* A caller that must charge the same for an input however much the process has kept charges each
|
|
200
|
+
* midnight and this the first time it reads them.
|
|
201
|
+
*/
|
|
202
|
+
export function zoneOffsetChangeReads(timezone, day) {
|
|
203
|
+
if (timezone === 'UTC')
|
|
204
|
+
return 0;
|
|
205
|
+
return 20 * zoneOffsets(timezone).changesOn(day).length;
|
|
206
|
+
}
|
|
30
207
|
function buildFormatter(timezone) {
|
|
31
208
|
try {
|
|
32
209
|
return new Intl.DateTimeFormat('en-US', {
|
|
@@ -60,27 +237,37 @@ export function createTimeZoneConverter(timezone) {
|
|
|
60
237
|
if (timezone === 'UTC') {
|
|
61
238
|
return { wallFrameToUtc: (wallFrameMs) => new Date(wallFrameMs) };
|
|
62
239
|
}
|
|
63
|
-
const
|
|
240
|
+
const offsets = zoneOffsets(timezone);
|
|
241
|
+
const offsetIn = (utcMs) => offsets.offsetAt(utcMs);
|
|
64
242
|
return {
|
|
65
|
-
wallFrameToUtc(wallFrameMs) {
|
|
66
|
-
|
|
67
|
-
offsetAt(wallFrameMs - FOURTEEN_HOURS_MS, formatter),
|
|
68
|
-
offsetAt(wallFrameMs, formatter),
|
|
69
|
-
offsetAt(wallFrameMs + FOURTEEN_HOURS_MS, formatter),
|
|
70
|
-
]);
|
|
71
|
-
const valid = [];
|
|
72
|
-
for (const offset of offsets) {
|
|
73
|
-
if (offsetAt(wallFrameMs - offset, formatter) === offset)
|
|
74
|
-
valid.push(offset);
|
|
75
|
-
}
|
|
76
|
-
if (valid.length === 1)
|
|
77
|
-
return new Date(wallFrameMs - valid[0]);
|
|
78
|
-
if (valid.length > 1)
|
|
79
|
-
return new Date(wallFrameMs - Math.max(...valid));
|
|
80
|
-
return new Date(wallFrameMs - Math.min(...offsets));
|
|
243
|
+
wallFrameToUtc(wallFrameMs, choice) {
|
|
244
|
+
return new Date(wallFrameToInstant(wallFrameMs, offsetIn, choice));
|
|
81
245
|
},
|
|
82
246
|
};
|
|
83
247
|
}
|
|
248
|
+
/**
|
|
249
|
+
* The spelling Slotlock keeps of an IANA zone name: Intl's, when Intl reads the name as itself in
|
|
250
|
+
* another case (`europe/london` is `Europe/London`); otherwise the name as given (`US/Eastern` stays,
|
|
251
|
+
* although this Node's ICU may call it `America/New_York`, so a stored name never depends on the
|
|
252
|
+
* ICU version that wrote it). A UTC offset (`+05:00`, which Intl accepts as a zone) is not an IANA
|
|
253
|
+
* name and is refused, as is anything Intl does not know: both with `code: 'invalid_timezone'`.
|
|
254
|
+
*/
|
|
255
|
+
export function canonicalTimeZoneName(timezone) {
|
|
256
|
+
const name = timezone.trim();
|
|
257
|
+
const invalid = () => Object.assign(new Error(`Invalid timezone: ${timezone}`), { code: 'invalid_timezone' });
|
|
258
|
+
if (!name || /^[+-]/.test(name))
|
|
259
|
+
throw invalid();
|
|
260
|
+
let resolved;
|
|
261
|
+
try {
|
|
262
|
+
resolved = new Intl.DateTimeFormat('en-US', { timeZone: name }).resolvedOptions().timeZone;
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
throw invalid();
|
|
266
|
+
}
|
|
267
|
+
if (/^[+-]/.test(resolved))
|
|
268
|
+
throw invalid();
|
|
269
|
+
return resolved.toLowerCase() === name.toLowerCase() ? resolved : name;
|
|
270
|
+
}
|
|
84
271
|
/**
|
|
85
272
|
* The civil date and ISO weekday an instant falls on in `timezone` — the inverse direction of
|
|
86
273
|
* {@link zonedDateTimeToUtc}. Pricing reads "which day is this night" through here so a pickup at
|
|
@@ -127,7 +314,7 @@ function wallFrame(instant, timezone) {
|
|
|
127
314
|
if (!Number.isFinite(ms)) {
|
|
128
315
|
throw Object.assign(new Error('Invalid instant'), { code: 'invalid_instant' });
|
|
129
316
|
}
|
|
130
|
-
const offset = timezone === 'UTC' ? 0 : offsetAt(ms
|
|
317
|
+
const offset = timezone === 'UTC' ? 0 : zoneOffsets(timezone).offsetAt(ms);
|
|
131
318
|
return new Date(ms + offset);
|
|
132
319
|
}
|
|
133
320
|
function civilDate(wall) {
|
|
@@ -163,8 +350,50 @@ function assertLocalDateTime(value) {
|
|
|
163
350
|
throw Object.assign(new Error('Invalid local date-time'), { code: 'invalid_local_datetime' });
|
|
164
351
|
}
|
|
165
352
|
}
|
|
166
|
-
export function zonedDateTimeToUtc(value, timezone) {
|
|
353
|
+
export function zonedDateTimeToUtc(value, timezone, choice) {
|
|
167
354
|
assertLocalDateTime(value);
|
|
168
355
|
const wallFrame = utcFrame(value.year, value.month, value.day, value.hour ?? 0, value.minute ?? 0, value.second ?? 0, value.millisecond ?? 0).getTime();
|
|
169
|
-
return createTimeZoneConverter(timezone).wallFrameToUtc(wallFrame);
|
|
356
|
+
return createTimeZoneConverter(timezone).wallFrameToUtc(wallFrame, choice);
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Every change of `timezone`'s offset from `fromMs` to `toMs`, as Intl reads the zone, from its
|
|
360
|
+
* offsets a day at a time (ZoneOffsets): Gaza's clocks go forward on 20 October 2040 and back six
|
|
361
|
+
* days later, which a search a week at a time missed.
|
|
362
|
+
*/
|
|
363
|
+
export function zoneTransitions(timezone, fromMs, toMs) {
|
|
364
|
+
if (timezone === 'UTC')
|
|
365
|
+
return [];
|
|
366
|
+
return zoneOffsets(timezone).transitions(Math.floor(fromMs / 1000) * 1000, Math.ceil(toMs / 1000) * 1000);
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* The changes of the offset `offsetIn` reads from `fromMs` to `toMs`, found by comparing offsets
|
|
370
|
+
* `stepMs` apart and narrowing each change to the second: a change and its reversal both within
|
|
371
|
+
* one step are missed.
|
|
372
|
+
*/
|
|
373
|
+
function scanTransitions(offsetIn, fromMs, toMs, stepMs) {
|
|
374
|
+
const found = [];
|
|
375
|
+
let at = Math.floor(fromMs / 1000) * 1000;
|
|
376
|
+
let offset = offsetIn(at);
|
|
377
|
+
while (at < toMs) {
|
|
378
|
+
const next = Math.min(at + stepMs, Math.ceil(toMs / 1000) * 1000);
|
|
379
|
+
if (offsetIn(next) === offset) {
|
|
380
|
+
at = next;
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
// The last whole second at `offset` and the first at another.
|
|
384
|
+
let low = at;
|
|
385
|
+
let high = next;
|
|
386
|
+
while (high - low > 1000) {
|
|
387
|
+
const middle = Math.floor((low + high) / 2000) * 1000;
|
|
388
|
+
if (offsetIn(middle) === offset)
|
|
389
|
+
low = middle;
|
|
390
|
+
else
|
|
391
|
+
high = middle;
|
|
392
|
+
}
|
|
393
|
+
const after = offsetIn(high);
|
|
394
|
+
found.push({ at: high, before: offset, after });
|
|
395
|
+
offset = after;
|
|
396
|
+
at = high;
|
|
397
|
+
}
|
|
398
|
+
return found;
|
|
170
399
|
}
|