Skip to content

Commit 042c8cb

Browse files
authored
Improved live timezone updates, access to timezone transitions. (#18)
* Add Timezone.getAllTransitions() function. * Improve retrieval of timezone updates, taking advantage of tzexplorer.org.
1 parent 41911fb commit 042c8cb

8 files changed

Lines changed: 150 additions & 30 deletions

File tree

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1513,6 +1513,12 @@ For a given `wallTime`, expressed in milliseconds, find the most recent change i
15131513
findTransitionByWallTime(wallTime: number): Transition | null
15141514
```
15151515
1516+
Get all transitions in a timezone. Returns null for simple, single-UTC-offset timezones which have no transitions.
1517+
1518+
```typescript
1519+
getAllTransitions(): Transition[] | null
1520+
```
1521+
15161522
Get the short-form name for the timezone, dependent upon `utcTime` in milliseconds, such as the America/New_York timezone returning `'EST'` during the winter, but `'EDT'` during the summer.
15171523
15181524
```typescript

package-lock.json

Lines changed: 28 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@tubular/time",
3-
"version": "3.3.2",
3+
"version": "3.4.0",
44
"description": "Date/time, IANA timezones, leap seconds, TAI/UTC conversions, calendar with settable Julian/Gregorian switchover",
55
"main": "dist/cjs/index.js",
66
"module": "dist/fesm2015/index.js",
@@ -47,7 +47,8 @@
4747
"license": "MIT",
4848
"dependencies": {
4949
"@tubular/math": "^3.1.0",
50-
"@tubular/util": "^4.3.1"
50+
"@tubular/util": "^4.3.1",
51+
"json-z": "^3.3.2"
5152
},
5253
"optionalDependencies": {
5354
"by-request": "^1.2.7"

src/i-zone-poller.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export interface IZonePoller {
2+
getLatestVersion(url: string): Promise<string>
23
getTimezones(url: string): Promise<Record<string, string>>
34
}

src/index.ts

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import timezoneSmall from './timezone-small';
2626
import timezoneLarge from './timezone-large';
2727
import timezoneLargeAlt from './timezone-large-alt';
2828
import { parse } from './format-parse';
29-
import { forEach, isString, toNumber } from '@tubular/util';
29+
import { forEach, isBoolean, isString, toNumber } from '@tubular/util';
3030
import { CalendarType, DayOfWeek, Month, LAST } from './calendar';
3131
import { getDeltaTAtJulianDate, tdtToUt, utToTdt } from './ut-converter';
3232
import { defaultLocale, getMinDaysInWeek, getStartOfWeek, getWeekend, hasDateTimeStyle, hasIntlDateTime } from './locale-data';
@@ -105,8 +105,12 @@ export function initTimezoneLargeAlt(failQuietly = false): void {
105105
}
106106

107107
let pollingInterval: any;
108+
let lastUpdateName = 'small';
109+
let currentTzVersion = Timezone.version === 'unspecified' ? '' : Timezone.version;
108110

109-
const zonesUrl = 'https://unpkg.com/@tubular/time/dist/data/timezone-{name}.js';
111+
const versionCheckUrl = 'https://tzexplorer.org/api/tz-version';
112+
const zonesUrl1 = 'https://unpkg.com/@tubular/time/dist/data/timezone-{name}.js';
113+
const zonesUrl2 = 'https://tzexplorer.org/tzdata/timezone-{name}.js';
110114

111115
export type ZoneOptions = 'small' | 'large' | 'large-alt';
112116

@@ -115,15 +119,50 @@ export function pollForTimezoneUpdates(zonePoller: IZonePoller | false, name: Zo
115119
clearInterval(pollingInterval);
116120

117121
if (zonePoller && name && intervalDays >= 0) {
118-
const url = zonesUrl.replace('{name}', name);
119-
const poll = (): void => {
120-
zonePoller.getTimezones(url).then(zones => {
121-
dispatchUpdateNotification(Timezone.defineTimezones(zones));
122-
})
123-
.catch(err => dispatchUpdateNotification(err instanceof Error ? err : new Error(err)));
122+
const poll = async (): Promise<void> => {
123+
let latestTzVersion: string;
124+
125+
try {
126+
latestTzVersion = await zonePoller.getLatestVersion(versionCheckUrl);
127+
}
128+
catch (e) {
129+
dispatchUpdateNotification(e);
130+
return;
131+
}
132+
133+
if (latestTzVersion <= currentTzVersion && lastUpdateName === name) {
134+
dispatchUpdateNotification(false);
135+
return;
136+
}
137+
138+
let zones: any;
139+
let updated = false;
140+
141+
try {
142+
zones = await zonePoller.getTimezones(zonesUrl1.replace('{name}', name));
143+
updated = Timezone.defineTimezones(zones);
144+
}
145+
catch {}
146+
147+
if (!updated) {
148+
try {
149+
zones = await zonePoller.getTimezones(zonesUrl2.replace('{name}', name));
150+
updated = Timezone.defineTimezones(zones);
151+
}
152+
catch (e) {
153+
dispatchUpdateNotification(e);
154+
return;
155+
}
156+
}
157+
158+
if (updated)
159+
currentTzVersion = latestTzVersion;
160+
161+
lastUpdateName = name;
162+
dispatchUpdateNotification(updated);
124163
};
125164

126-
poll();
165+
poll().finally();
127166

128167
if (intervalDays > 0) {
129168
pollingInterval = setInterval(poll, Math.max(intervalDays * DAY_MSEC, 3600000));
@@ -154,6 +193,9 @@ export async function getTimezones(zonePoller: IZonePoller, name: ZoneOptions =
154193
const listeners = new Set<(result: boolean | Error) => void>();
155194

156195
function dispatchUpdateNotification(result: boolean | Error): void {
196+
if (!(result instanceof Error) && !isBoolean(result))
197+
result = new Error(String(result)); // Oddly, some errors come through as numbers like 404.
198+
157199
listeners.forEach(listener => {
158200
try {
159201
listener(result);

src/timezone.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1195,6 +1195,10 @@ export class Timezone {
11951195
}
11961196
}
11971197

1198+
getAllTransitions(): Transition[] | null {
1199+
return !this.transitions || this.transitions.length === 0 ? null : clone(this.transitions);
1200+
}
1201+
11981202
findTransitionByUtc(utcTime: number): Transition | null {
11991203
if (!this.transitions || this.transitions.length === 0)
12001204
return null;

src/zone-poller-browser.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,38 @@
11
import { IZonePoller } from './i-zone-poller';
22

33
export const zonePollerBrowser: IZonePoller = {
4+
getLatestVersion(url: string): Promise<string> {
5+
return new Promise<string>((resolve, reject) => {
6+
const head = document.querySelector('head');
7+
const script = document.createElement('script');
8+
const callbackName = 'tt_zp_callback_' + (1000000 + Math.floor(Math.random() * 9000000));
9+
let version: Error | string = '';
10+
11+
window[callbackName] = (data: any): void => {
12+
if (typeof data === 'string')
13+
version = data;
14+
else
15+
version = new Error('Invalid data for tz version from ' + url);
16+
};
17+
18+
head.appendChild(script);
19+
script.onload = (): void => {
20+
script.remove();
21+
window[callbackName] = undefined;
22+
23+
if (version instanceof Error)
24+
reject(version);
25+
else
26+
resolve(version);
27+
};
28+
script.onerror = (): void => {
29+
script.remove();
30+
reject(new Error('Failed to retrieve latest tz version from ' + url));
31+
};
32+
script.src = `${url}?callback=${callbackName}`;
33+
});
34+
},
35+
436
getTimezones(url: string): Promise<{ [p: string]: string }> {
537
return new Promise<Record<string, string>>((resolve, reject) => {
638
const head = document.querySelector('head');

src/zone-poller-node.ts

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,41 @@
11
import { IZonePoller } from './i-zone-poller';
2+
import JSONZ from 'json-z';
23

34
let requestText: (url: string, options?: any) => Promise<string>;
45
let byRequestCheckDone = false;
56

6-
export const zonePollerNode: IZonePoller = {
7-
async getTimezones(url: string): Promise<{ [p: string]: string }> {
8-
if (!byRequestCheckDone && !requestText) {
9-
byRequestCheckDone = true;
10-
11-
try { // Obscure name of by-request package to prevent webpack from generating a dependency.
12-
// @ts-ignore
13-
requestText = (await import(/* webpackIgnore: true */ 'tseuqer-yb'.split('').reverse().join(''))).requestText;
14-
}
15-
catch {}
7+
async function checkForRequestText(): Promise<void> {
8+
if (!byRequestCheckDone && !requestText) {
9+
byRequestCheckDone = true;
10+
11+
try { // Obscure name of by-request package to prevent webpack from generating a dependency.
12+
// @ts-ignore
13+
requestText = (await import(/* webpackIgnore: true */ 'tseuqer-yb'.split('').reverse().join(''))).requestText;
1614
}
15+
catch {}
1716

1817
if (!requestText) {
1918
const msg = 'npm package "by-request" should be installed to use zonePollerNode';
2019
console.error(msg);
2120
throw new Error(msg);
2221
}
22+
}
23+
}
24+
25+
export const zonePollerNode: IZonePoller = {
26+
async getLatestVersion(url: string): Promise<string> {
27+
await checkForRequestText();
28+
29+
return (await requestText(url, { timeout: 60000 })).replace(/"/g, '');
30+
},
31+
32+
async getTimezones(url: string): Promise<{ [p: string]: string }> {
33+
await checkForRequestText();
2334

24-
const zones = (await requestText(url, { timeout: 60000 })).replace(/^.*?=\s*/, '');
35+
const zones = (await requestText(url, { timeout: 60000 }))
36+
.replace(/^.*?=\s*/s, '')
37+
.replace(/}.*$/s, '}');
2538

26-
return JSON.parse(zones);
39+
return JSONZ.parse(zones);
2740
}
2841
};

0 commit comments

Comments
 (0)