inviton-powerduck 0.0.153 → 0.0.155
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/app/powerduck-initializer.ts +3 -3
- package/common/api-http.ts +20 -14
- package/common/css/ladda-themeless-zoomin.min.css +89 -89
- package/common/enum-translation/day-translator.ts +3 -2
- package/common/excel/excel-reader.ts +2 -9
- package/common/extensions/array-extensions.ts +116 -0
- package/common/extensions/string-extensions.ts +92 -0
- package/common/extensions/temporal-extensions.ts +115 -0
- package/common/scroll-utils.ts +2 -1
- package/common/temporal-helpers.ts +551 -0
- package/common/throttled-api-caller.ts +149 -0
- package/common/timezone-helper.ts +39 -29
- package/common/utils/cookie.ts +11 -8
- package/common/utils/date-localization-utils.ts +25 -19
- package/common/utils/date-utils.ts +37 -47
- package/common/utils/form-utils.ts +3 -1
- package/common/utils/language-utils.ts +21 -27
- package/common/utils/temporal-utils.ts +43 -0
- package/common/utils/upload-image-helper.ts +1 -1
- package/common/utils/utils.ts +14 -14
- package/common/validation.ts +17 -5
- package/components/chart-js/line-chart-flot.tsx +9 -9
- package/components/chart-js/thirdparty/flot/jquery.flot.categories.min.js +93 -93
- package/components/chart-js/thirdparty/flot/jquery.flot.crosshair.min.js +83 -83
- package/components/chart-js/thirdparty/flot/jquery.flot.navigate.min.js +270 -270
- package/components/chart-js/thirdparty/flot/jquery.flot.pie.min.js +507 -507
- package/components/chart-js/thirdparty/flot/jquery.flot.resize.js +7 -9
- package/components/chart-js/thirdparty/flot/jquery.flot.resize.min.js +9 -11
- package/components/chart-js/thirdparty/flot/jquery.flot.stack.min.js +104 -104
- package/components/chart-js/ts/line-chart-contracts.ts +2 -2
- package/components/container-with-breakpoints/ts/breakpoint-handler.ts +2 -2
- package/components/counter/testall.tsx +89 -75
- package/components/datatable/datatable.tsx +2379 -2375
- package/components/datatable/export-excel-modal.tsx +12 -14
- package/components/datatable/ts/reorder.ts +4 -2
- package/components/dropdown/index.tsx +48 -22
- package/components/dropdown/mobile/legacy_fdd.ts +10 -11
- package/components/dropzone/gallery-dropzone.tsx +394 -382
- package/components/fullcalendar/fullcalendar-draggable-event.tsx +8 -7
- package/components/fullcalendar/timegrid-calendar.tsx +60 -67
- package/components/image-crop/image-cropping-modal.tsx +9 -8
- package/components/image-crop/upload-and-crop.tsx +162 -162
- package/components/image-crop/vendor/jquery.Jcrop.min.css +344 -344
- package/components/import/import-mapper.tsx +2 -2
- package/components/input/daterange-picker.tsx +502 -521
- package/components/input/datetime-picker.tsx +45 -50
- package/components/input/plugins/daterangepicker/daterangepicker.min.css +400 -400
- package/components/input/plugins/daterangepicker/jquery.daterangepicker.min.js +346 -339
- package/components/input/plugins/daterangepicker/jquery.daterangepicker.ts +580 -402
- package/components/input/radio-button-group.tsx +2 -2
- package/components/input/ts/dateInputHelper.ts +1 -0
- package/components/input/wysiwig.tsx +12 -7
- package/components/svg/skilift-svg.tsx +6 -6
- package/package.json +2 -1
- package/common/date-wrapper.ts +0 -422
- package/common/utils/array-extend.ts +0 -215
- package/common/utils/array-remove.ts +0 -10
- package/common/utils/array-sort.ts +0 -56
- package/common/utils/capitalize-string.ts +0 -11
- package/common/utils/format-string.ts +0 -14
- package/common/utils/latinize-string.ts +0 -7
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
// import { Temporal } from '@js-temporal/polyfill';
|
|
2
|
+
// import PowerduckState from '../app/powerduck-state';
|
|
3
|
+
|
|
4
|
+
// /**
|
|
5
|
+
// * Helper functions for Temporal dates, maintaining compatibility with DateWrapper behavior
|
|
6
|
+
// */
|
|
7
|
+
|
|
8
|
+
// // Type alias for cleaner code
|
|
9
|
+
// type TemporalDate = Temporal.PlainDateTime;
|
|
10
|
+
|
|
11
|
+
// /**
|
|
12
|
+
// * Create Temporal date from wire format string
|
|
13
|
+
// */
|
|
14
|
+
// export const fromWire = (value: string): TemporalDate | null => {
|
|
15
|
+
// if (value != null && value.length > 0) {
|
|
16
|
+
// if (value.includes('T')) {
|
|
17
|
+
// let year: number, month: number, day: number, hour: number, minute: number, second: string | number;
|
|
18
|
+
// const dtSplit = value.split('T');
|
|
19
|
+
// const dateSplit = dtSplit[0].split('-');
|
|
20
|
+
// const timeSplit = dtSplit[1].split(':');
|
|
21
|
+
|
|
22
|
+
// if (dateSplit.length == 3) {
|
|
23
|
+
// year = Number(dateSplit[0]);
|
|
24
|
+
// month = Number(dateSplit[1]); // Keep 1-based for Temporal
|
|
25
|
+
// day = Number(dateSplit[2]);
|
|
26
|
+
|
|
27
|
+
// if (timeSplit.length > 1) {
|
|
28
|
+
// hour = Number(timeSplit[0]);
|
|
29
|
+
// minute = Number(timeSplit[1]);
|
|
30
|
+
// second = timeSplit[2] || 0;
|
|
31
|
+
|
|
32
|
+
// if (second != null && second != 0) {
|
|
33
|
+
// try {
|
|
34
|
+
// if (typeof second === 'string' && second.includes('.')) {
|
|
35
|
+
// second = Number(second.split('.')[0]);
|
|
36
|
+
// } else {
|
|
37
|
+
// second = Number(second);
|
|
38
|
+
// }
|
|
39
|
+
// } catch (e) { }
|
|
40
|
+
// }
|
|
41
|
+
|
|
42
|
+
// if (isNaN(second as number)) {
|
|
43
|
+
// second = 0;
|
|
44
|
+
// }
|
|
45
|
+
|
|
46
|
+
// return new Temporal.PlainDateTime(
|
|
47
|
+
// year,
|
|
48
|
+
// month,
|
|
49
|
+
// day,
|
|
50
|
+
// hour,
|
|
51
|
+
// minute,
|
|
52
|
+
// second as number,
|
|
53
|
+
// 0,
|
|
54
|
+
// );
|
|
55
|
+
// }
|
|
56
|
+
// }
|
|
57
|
+
// } else {
|
|
58
|
+
// const splitVal = value.split('-');
|
|
59
|
+
// if (splitVal.length == 3) {
|
|
60
|
+
// return new Temporal.PlainDateTime(
|
|
61
|
+
// Number(splitVal[0]),
|
|
62
|
+
// Number(splitVal[1]), // Keep 1-based for Temporal
|
|
63
|
+
// Number(splitVal[2]),
|
|
64
|
+
// 0,
|
|
65
|
+
// 0,
|
|
66
|
+
// 0,
|
|
67
|
+
// 0,
|
|
68
|
+
// );
|
|
69
|
+
// } else if (splitVal.length == 5) {
|
|
70
|
+
// // TODO: Handle this case
|
|
71
|
+
// }
|
|
72
|
+
// }
|
|
73
|
+
// }
|
|
74
|
+
|
|
75
|
+
// return null;
|
|
76
|
+
// };
|
|
77
|
+
|
|
78
|
+
// /**
|
|
79
|
+
// * Convert to wire format string
|
|
80
|
+
// */
|
|
81
|
+
// export const toWire = (
|
|
82
|
+
// date: TemporalDate,
|
|
83
|
+
// includeTime?: boolean,
|
|
84
|
+
// includeMs?: boolean,
|
|
85
|
+
// ): string => {
|
|
86
|
+
// const formatDatePart = (v: number) => {
|
|
87
|
+
// if (v < 10) {
|
|
88
|
+
// return `0${v.toString()}`;
|
|
89
|
+
// } else {
|
|
90
|
+
// return v.toString();
|
|
91
|
+
// }
|
|
92
|
+
// };
|
|
93
|
+
|
|
94
|
+
// let datePart = `${date.year}-${formatDatePart(date.month)}-${formatDatePart(date.day)}`;
|
|
95
|
+
// if (includeTime) {
|
|
96
|
+
// datePart += `T${formatDatePart(date.hour)}:${formatDatePart(date.minute)}:${formatDatePart(date.second)}`;
|
|
97
|
+
// }
|
|
98
|
+
|
|
99
|
+
// if (includeMs) {
|
|
100
|
+
// let ms = date.millisecond.toString();
|
|
101
|
+
// if (ms.length == 1) {
|
|
102
|
+
// ms = `00${ms}`;
|
|
103
|
+
// } else if (ms.length == 2) {
|
|
104
|
+
// ms = `0${ms}`;
|
|
105
|
+
// }
|
|
106
|
+
|
|
107
|
+
// datePart += `.${ms}Z`;
|
|
108
|
+
// }
|
|
109
|
+
|
|
110
|
+
// return datePart;
|
|
111
|
+
// };
|
|
112
|
+
|
|
113
|
+
// /**
|
|
114
|
+
// * Create a new Temporal date with individual components (0-based month for compatibility)
|
|
115
|
+
// */
|
|
116
|
+
// export const createDate = (
|
|
117
|
+
// year: number,
|
|
118
|
+
// month: number,
|
|
119
|
+
// date?: number,
|
|
120
|
+
// hours?: number,
|
|
121
|
+
// minutes?: number,
|
|
122
|
+
// seconds?: number,
|
|
123
|
+
// ms?: number,
|
|
124
|
+
// ): TemporalDate => {
|
|
125
|
+
// const plusYears = Math.floor(month / 12);
|
|
126
|
+
// return new Temporal.PlainDateTime(
|
|
127
|
+
// year + plusYears,
|
|
128
|
+
// (month % 12) + 1, // Convert from 0-based to 1-based month
|
|
129
|
+
// date || 1,
|
|
130
|
+
// hours || 0,
|
|
131
|
+
// minutes || 0,
|
|
132
|
+
// seconds || 0,
|
|
133
|
+
// ms || 0,
|
|
134
|
+
// );
|
|
135
|
+
// };
|
|
136
|
+
|
|
137
|
+
// /**
|
|
138
|
+
// * Create Temporal date from epoch milliseconds
|
|
139
|
+
// */
|
|
140
|
+
// export const dateFromEpochMilliseconds = (timestamp: number): TemporalDate => {
|
|
141
|
+
// const instant = Temporal.Instant.fromEpochMilliseconds(timestamp);
|
|
142
|
+
// return instant.toZonedDateTimeISO('UTC').toPlainDateTime();
|
|
143
|
+
// };
|
|
144
|
+
|
|
145
|
+
// /**
|
|
146
|
+
// * Create Temporal date from wire format string
|
|
147
|
+
// */
|
|
148
|
+
// export const dateFromString = (value: string): TemporalDate | null => fromWire(value);
|
|
149
|
+
|
|
150
|
+
// /**
|
|
151
|
+
// * Create Temporal date from JS Date object
|
|
152
|
+
// */
|
|
153
|
+
// export const dateFromJSDate = (jsDate: Date): TemporalDate => new Temporal.PlainDateTime(
|
|
154
|
+
// jsDate.getFullYear(),
|
|
155
|
+
// jsDate.getMonth() + 1, // Convert to 1-based month
|
|
156
|
+
// jsDate.getDate(),
|
|
157
|
+
// jsDate.getHours(),
|
|
158
|
+
// jsDate.getMinutes(),
|
|
159
|
+
// jsDate.getSeconds(),
|
|
160
|
+
// jsDate.getMilliseconds(),
|
|
161
|
+
// );
|
|
162
|
+
|
|
163
|
+
// /**
|
|
164
|
+
// * Get current date/time as Temporal date
|
|
165
|
+
// */
|
|
166
|
+
// export const dateNow = (): TemporalDate => Temporal.Now.plainDateTimeISO();
|
|
167
|
+
|
|
168
|
+
// /**
|
|
169
|
+
// * Universal date creator - accepts any format and decides based on input (for easy migration)
|
|
170
|
+
// * Supports:
|
|
171
|
+
// * - No params: current date
|
|
172
|
+
// * - Single number: epoch milliseconds
|
|
173
|
+
// * - Single string: wire format string
|
|
174
|
+
// * - Single Date: JS Date object
|
|
175
|
+
// * - Single Temporal: returns as-is
|
|
176
|
+
// * - Multiple numbers: year, month, date, etc. (0-based month)
|
|
177
|
+
// */
|
|
178
|
+
// export function createDateUniversal(): TemporalDate;
|
|
179
|
+
// export function createDateUniversal(temporal: TemporalDate): TemporalDate;
|
|
180
|
+
// export function createDateUniversal(timestamp: number): TemporalDate;
|
|
181
|
+
// export function createDateUniversal(dateString: string): TemporalDate | null;
|
|
182
|
+
// export function createDateUniversal(jsDate: Date): TemporalDate;
|
|
183
|
+
// export function createDateUniversal(temporalDate: TemporalDate): TemporalDate;
|
|
184
|
+
// export function createDateUniversal(year: number, month: number, date?: number, hours?: number, minutes?: number, seconds?: number, ms?: number): TemporalDate;
|
|
185
|
+
// export function createDateUniversal(
|
|
186
|
+
// input?: number | string | Date | TemporalDate,
|
|
187
|
+
// month?: number,
|
|
188
|
+
// date?: number,
|
|
189
|
+
// hours?: number,
|
|
190
|
+
// minutes?: number,
|
|
191
|
+
// seconds?: number,
|
|
192
|
+
// ms?: number,
|
|
193
|
+
// ): TemporalDate | null {
|
|
194
|
+
// // No parameters - current date
|
|
195
|
+
// if (input === undefined) {
|
|
196
|
+
// return dateNow();
|
|
197
|
+
// }
|
|
198
|
+
|
|
199
|
+
// // Multiple parameters - individual components
|
|
200
|
+
// if (month !== undefined) {
|
|
201
|
+
// return createDate(
|
|
202
|
+
// input as number,
|
|
203
|
+
// month, // Convert from 0-based to 1-based month
|
|
204
|
+
// date,
|
|
205
|
+
// hours,
|
|
206
|
+
// minutes,
|
|
207
|
+
// seconds,
|
|
208
|
+
// ms,
|
|
209
|
+
// );
|
|
210
|
+
// }
|
|
211
|
+
|
|
212
|
+
// // Single parameter - determine type
|
|
213
|
+
// if (typeof input === 'number') {
|
|
214
|
+
// return dateFromEpochMilliseconds(input);
|
|
215
|
+
// }
|
|
216
|
+
|
|
217
|
+
// if (typeof input === 'string') {
|
|
218
|
+
// return dateFromString(input);
|
|
219
|
+
// }
|
|
220
|
+
|
|
221
|
+
// if (input instanceof Date) {
|
|
222
|
+
// return dateFromJSDate(input);
|
|
223
|
+
// }
|
|
224
|
+
|
|
225
|
+
// if (input instanceof Temporal.PlainDateTime) {
|
|
226
|
+
// return input; // Already a Temporal date
|
|
227
|
+
// }
|
|
228
|
+
|
|
229
|
+
// return null; // Unknown input type
|
|
230
|
+
// }
|
|
231
|
+
|
|
232
|
+
// /**
|
|
233
|
+
// * Clone a Temporal date (returns the same instance since Temporal dates are immutable)
|
|
234
|
+
// */
|
|
235
|
+
// export const cloneDate = (date: TemporalDate): TemporalDate => date;
|
|
236
|
+
|
|
237
|
+
// /**
|
|
238
|
+
// * Serialize date to string
|
|
239
|
+
// */
|
|
240
|
+
// export const serializeDate = (date: TemporalDate): string => toWire(date, true);
|
|
241
|
+
|
|
242
|
+
// /**
|
|
243
|
+
// * Convert to JSON string
|
|
244
|
+
// */
|
|
245
|
+
// export const toJSON = (date: TemporalDate, key?: any): string => date.toZonedDateTime('UTC').toInstant().toJSON();
|
|
246
|
+
|
|
247
|
+
// /**
|
|
248
|
+
// * Convert to locale date string
|
|
249
|
+
// */
|
|
250
|
+
// export const toLocaleDateString = (
|
|
251
|
+
// date: TemporalDate,
|
|
252
|
+
// locale?: string,
|
|
253
|
+
// opts?: Intl.DateTimeFormatOptions & { language?: string },
|
|
254
|
+
// ): string => {
|
|
255
|
+
// const options = { ...opts, timeZone: 'UTC' };
|
|
256
|
+
|
|
257
|
+
// try {
|
|
258
|
+
// return new Intl.DateTimeFormat(locale ?? opts?.language, options).format(date as any);
|
|
259
|
+
// } catch (e) {
|
|
260
|
+
// return date.toString();
|
|
261
|
+
// }
|
|
262
|
+
// };
|
|
263
|
+
|
|
264
|
+
// /**
|
|
265
|
+
// * Convert to display string
|
|
266
|
+
// */
|
|
267
|
+
// export function toDisplayString(date: TemporalDate): string;
|
|
268
|
+
// export function toDisplayString(date: TemporalDate, showTime?: boolean, showSeconds?: boolean, monthLong?: boolean): string;
|
|
269
|
+
// export function toDisplayString(date: TemporalDate, options: {
|
|
270
|
+
// showTime?: boolean;
|
|
271
|
+
// showSeconds?: boolean;
|
|
272
|
+
// monthLong?: boolean;
|
|
273
|
+
// dayLong?: boolean;
|
|
274
|
+
// hideDay?: boolean;
|
|
275
|
+
// hideMonth?: boolean;
|
|
276
|
+
// hideYear?: boolean;
|
|
277
|
+
// language?: string;
|
|
278
|
+
// }): string;
|
|
279
|
+
// export function toDisplayString(
|
|
280
|
+
// date: TemporalDate,
|
|
281
|
+
// optionsOrShowTime?: boolean | {
|
|
282
|
+
// showTime?: boolean;
|
|
283
|
+
// showSeconds?: boolean;
|
|
284
|
+
// monthLong?: boolean;
|
|
285
|
+
// dayLong?: boolean;
|
|
286
|
+
// hideDay?: boolean;
|
|
287
|
+
// hideMonth?: boolean;
|
|
288
|
+
// hideYear?: boolean;
|
|
289
|
+
// language?: string;
|
|
290
|
+
// },
|
|
291
|
+
// showTime?: boolean,
|
|
292
|
+
// showSeconds?: boolean,
|
|
293
|
+
// monthLong?: boolean,
|
|
294
|
+
// ): string {
|
|
295
|
+
// let opts = {} as any;
|
|
296
|
+
// if (typeof optionsOrShowTime === 'object') {
|
|
297
|
+
// opts = optionsOrShowTime;
|
|
298
|
+
// } else {
|
|
299
|
+
// opts = {
|
|
300
|
+
// showTime: optionsOrShowTime,
|
|
301
|
+
// showSeconds,
|
|
302
|
+
// monthLong,
|
|
303
|
+
// };
|
|
304
|
+
// }
|
|
305
|
+
|
|
306
|
+
// const args = {
|
|
307
|
+
// year: !opts.hideYear ? 'numeric' : undefined,
|
|
308
|
+
// month: !opts.hideMonth ? (opts.monthLong ? 'long' : 'numeric') : undefined,
|
|
309
|
+
// day: !opts.hideDay && !opts.dayLong ? 'numeric' : undefined,
|
|
310
|
+
// weekday: opts.dayLong ? 'long' : undefined,
|
|
311
|
+
// timeZone: 'UTC',
|
|
312
|
+
// } as any as Intl.DateTimeFormatOptions;
|
|
313
|
+
|
|
314
|
+
// if (showTime || opts.showTime) {
|
|
315
|
+
// args.hour = 'numeric';
|
|
316
|
+
// args.minute = '2-digit';
|
|
317
|
+
// }
|
|
318
|
+
|
|
319
|
+
// if (showSeconds || opts.showSeconds) {
|
|
320
|
+
// args.second = '2-digit';
|
|
321
|
+
// }
|
|
322
|
+
|
|
323
|
+
// try {
|
|
324
|
+
// return toLocaleDateString(
|
|
325
|
+
// date,
|
|
326
|
+
// opts.language || PowerduckState.getCurrentLanguage(),
|
|
327
|
+
// args,
|
|
328
|
+
// );
|
|
329
|
+
// } catch (e) {
|
|
330
|
+
// return date?.toString();
|
|
331
|
+
// }
|
|
332
|
+
// }
|
|
333
|
+
|
|
334
|
+
// /**
|
|
335
|
+
// * Convert to locale time string
|
|
336
|
+
// */
|
|
337
|
+
// export const toLocaleTimeString = (date: TemporalDate, opts: Intl.DateTimeFormatOptions & { language?: string }): string => {
|
|
338
|
+
// const options = { ...opts, timeZone: 'UTC' };
|
|
339
|
+
|
|
340
|
+
// try {
|
|
341
|
+
// // eslint-disable-next-line no-restricted-syntax
|
|
342
|
+
// const jsDate = new Date(date.toZonedDateTime('UTC').epochMilliseconds);
|
|
343
|
+
// return jsDate.toLocaleTimeString(opts.language || PowerduckState.getCurrentLanguage(), options);
|
|
344
|
+
// } catch (e) {
|
|
345
|
+
// return date.toString();
|
|
346
|
+
// }
|
|
347
|
+
// };
|
|
348
|
+
|
|
349
|
+
// /**
|
|
350
|
+
// * Convert to display string without UTC (uses local timezone)
|
|
351
|
+
// */
|
|
352
|
+
// export const toDisplayStringNonUtc = (
|
|
353
|
+
// date: TemporalDate,
|
|
354
|
+
// showTime?: boolean,
|
|
355
|
+
// showSeconds?: boolean,
|
|
356
|
+
// monthLong?: boolean,
|
|
357
|
+
// ): string => {
|
|
358
|
+
// const args = {
|
|
359
|
+
// year: 'numeric',
|
|
360
|
+
// month: monthLong ? 'long' : 'numeric',
|
|
361
|
+
// day: 'numeric',
|
|
362
|
+
// } as any as Intl.DateTimeFormatOptions;
|
|
363
|
+
|
|
364
|
+
// if (showTime) {
|
|
365
|
+
// args.hour = 'numeric';
|
|
366
|
+
// args.minute = '2-digit';
|
|
367
|
+
// }
|
|
368
|
+
|
|
369
|
+
// if (showSeconds) {
|
|
370
|
+
// args.second = '2-digit';
|
|
371
|
+
// }
|
|
372
|
+
|
|
373
|
+
// try {
|
|
374
|
+
// // eslint-disable-next-line no-restricted-syntax
|
|
375
|
+
// const jsDate = new Date(date.toZonedDateTime('UTC').epochMilliseconds);
|
|
376
|
+
// return jsDate.toLocaleDateString(PowerduckState.getCurrentLanguage(), args);
|
|
377
|
+
// } catch (e) {
|
|
378
|
+
// return date.toString();
|
|
379
|
+
// }
|
|
380
|
+
// };
|
|
381
|
+
|
|
382
|
+
// /**
|
|
383
|
+
// * Get time as milliseconds since epoch
|
|
384
|
+
// */
|
|
385
|
+
// export const getTime = (date: TemporalDate): number => date?.toZonedDateTime?.('UTC')?.epochMilliseconds;
|
|
386
|
+
|
|
387
|
+
// /**
|
|
388
|
+
// * Get full year
|
|
389
|
+
// */
|
|
390
|
+
// export const getFullYear = (date: TemporalDate): number => date.year;
|
|
391
|
+
|
|
392
|
+
// /**
|
|
393
|
+
// * Get month (0-based to match DateWrapper behavior)
|
|
394
|
+
// */
|
|
395
|
+
// export const getMonth = (date: TemporalDate): number => date.month - 1;
|
|
396
|
+
|
|
397
|
+
// /**
|
|
398
|
+
// * Get date
|
|
399
|
+
// */
|
|
400
|
+
// export const getDate = (date: TemporalDate): number => date.day;
|
|
401
|
+
|
|
402
|
+
// /**
|
|
403
|
+
// * Get day of week (0 = Sunday)
|
|
404
|
+
// */
|
|
405
|
+
// export const getDay = (date: TemporalDate): number => date.dayOfWeek % 7;
|
|
406
|
+
|
|
407
|
+
// /**
|
|
408
|
+
// * Get hours
|
|
409
|
+
// */
|
|
410
|
+
// export const getHours = (date: TemporalDate): number => date.hour;
|
|
411
|
+
|
|
412
|
+
// /**
|
|
413
|
+
// * Get minutes
|
|
414
|
+
// */
|
|
415
|
+
// export const getMinutes = (date: TemporalDate): number => date.minute;
|
|
416
|
+
|
|
417
|
+
// /**
|
|
418
|
+
// * Get seconds
|
|
419
|
+
// */
|
|
420
|
+
// export const getSeconds = (date: TemporalDate): number => date.second;
|
|
421
|
+
|
|
422
|
+
// /**
|
|
423
|
+
// * Get milliseconds
|
|
424
|
+
// */
|
|
425
|
+
// export const getMilliseconds = (date: TemporalDate): number => date.millisecond;
|
|
426
|
+
|
|
427
|
+
// /**
|
|
428
|
+
// * Get timezone offset (always 0 since we work in UTC)
|
|
429
|
+
// */
|
|
430
|
+
// export const getTimezoneOffset = (_date: TemporalDate): number => 0;
|
|
431
|
+
|
|
432
|
+
// /**
|
|
433
|
+
// * Get current date
|
|
434
|
+
// */
|
|
435
|
+
// export const getCurrent = (): TemporalDate => dateNow();
|
|
436
|
+
|
|
437
|
+
// /**
|
|
438
|
+
// * Set time from milliseconds since epoch
|
|
439
|
+
// */
|
|
440
|
+
// export const setTime = (date: TemporalDate, time: number): TemporalDate => {
|
|
441
|
+
// const instant = Temporal.Instant.fromEpochMilliseconds(time);
|
|
442
|
+
// return instant.toZonedDateTimeISO('UTC').toPlainDateTime();
|
|
443
|
+
// };
|
|
444
|
+
|
|
445
|
+
// /**
|
|
446
|
+
// * Set seconds
|
|
447
|
+
// */
|
|
448
|
+
// export const setSeconds = (
|
|
449
|
+
// date: TemporalDate,
|
|
450
|
+
// sec: number,
|
|
451
|
+
// ms?: number,
|
|
452
|
+
// ): TemporalDate => date.with({
|
|
453
|
+
// second: sec,
|
|
454
|
+
// millisecond: ms ?? date.millisecond,
|
|
455
|
+
// });
|
|
456
|
+
|
|
457
|
+
// /**
|
|
458
|
+
// * Set minutes
|
|
459
|
+
// */
|
|
460
|
+
// export const setMinutes = (
|
|
461
|
+
// date: TemporalDate,
|
|
462
|
+
// min: number,
|
|
463
|
+
// sec?: number,
|
|
464
|
+
// ms?: number,
|
|
465
|
+
// ): TemporalDate => date.with({
|
|
466
|
+
// minute: min,
|
|
467
|
+
// second: sec ?? date.second,
|
|
468
|
+
// millisecond: ms ?? date.millisecond,
|
|
469
|
+
// });
|
|
470
|
+
|
|
471
|
+
// /**
|
|
472
|
+
// * Set hours
|
|
473
|
+
// */
|
|
474
|
+
// export const setHours = (
|
|
475
|
+
// date: TemporalDate,
|
|
476
|
+
// hours: number,
|
|
477
|
+
// min?: number,
|
|
478
|
+
// sec?: number,
|
|
479
|
+
// ms?: number,
|
|
480
|
+
// ): TemporalDate => date.with({
|
|
481
|
+
// hour: hours,
|
|
482
|
+
// minute: min ?? date.minute,
|
|
483
|
+
// second: sec ?? date.second,
|
|
484
|
+
// millisecond: ms ?? date.millisecond,
|
|
485
|
+
// });
|
|
486
|
+
|
|
487
|
+
// /**
|
|
488
|
+
// * Set date
|
|
489
|
+
// */
|
|
490
|
+
// export const setDate = (date: TemporalDate, day: number): TemporalDate => date.with({ day });
|
|
491
|
+
|
|
492
|
+
// /**
|
|
493
|
+
// * Set month (0-based input to match DateWrapper behavior)
|
|
494
|
+
// */
|
|
495
|
+
// export const setMonth = (
|
|
496
|
+
// date: TemporalDate,
|
|
497
|
+
// month: number,
|
|
498
|
+
// day?: number,
|
|
499
|
+
// ): TemporalDate => date.with({
|
|
500
|
+
// month: month + 1, // Convert from 0-based to 1-based
|
|
501
|
+
// day: day ?? date.day,
|
|
502
|
+
// });
|
|
503
|
+
|
|
504
|
+
// /**
|
|
505
|
+
// * Set full year
|
|
506
|
+
// */
|
|
507
|
+
// export const setFullYear = (
|
|
508
|
+
// date: TemporalDate,
|
|
509
|
+
// year: number,
|
|
510
|
+
// month?: number,
|
|
511
|
+
// day?: number,
|
|
512
|
+
// ): TemporalDate => date.with({
|
|
513
|
+
// year,
|
|
514
|
+
// month: month != null ? month + 1 : date.month, // Convert from 0-based to 1-based if provided
|
|
515
|
+
// day: day ?? date.day,
|
|
516
|
+
// });
|
|
517
|
+
|
|
518
|
+
// /**
|
|
519
|
+
// * Convert to ISO string
|
|
520
|
+
// */
|
|
521
|
+
// export const toISOString = (date: TemporalDate): string => toWire(
|
|
522
|
+
// date,
|
|
523
|
+
// true,
|
|
524
|
+
// true,
|
|
525
|
+
// );
|
|
526
|
+
|
|
527
|
+
// /**
|
|
528
|
+
// * Check if date is in DST (always false since we work in UTC)
|
|
529
|
+
// */
|
|
530
|
+
// export const isDST = (date: TemporalDate): boolean => false;
|
|
531
|
+
|
|
532
|
+
// /**
|
|
533
|
+
// * Get inner Date object (for compatibility)
|
|
534
|
+
// */
|
|
535
|
+
// // eslint-disable-next-line no-restricted-syntax
|
|
536
|
+
// export const getInnerDate = (date: TemporalDate): Date => new Date(date.toZonedDateTime('UTC').epochMilliseconds);
|
|
537
|
+
|
|
538
|
+
// /**
|
|
539
|
+
// * Check if string is a serialized date
|
|
540
|
+
// */
|
|
541
|
+
// export const isSerializedDate = (str: string): boolean => str != null && str.length > 18 && str.length < 29 && str.indexOf('T') == 10;
|
|
542
|
+
|
|
543
|
+
// /**
|
|
544
|
+
// * Deserialize string to Temporal date
|
|
545
|
+
// */
|
|
546
|
+
// export const deserialize = (str: string): TemporalDate => fromWire(str);
|
|
547
|
+
|
|
548
|
+
// /**
|
|
549
|
+
// * Create Temporal date from regular JS Date (alias for compatibility)
|
|
550
|
+
// */
|
|
551
|
+
// export const fromNonUtcDate = (d: Date): TemporalDate => dateFromJSDate(d);
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility for deduplicating concurrent async calls and optionally caching results for a short TTL.
|
|
3
|
+
*
|
|
4
|
+
* - Deduplication: If multiple calls are made with the same arguments while one is in-flight,
|
|
5
|
+
* they will all share the same Promise instead of creating multiple requests.
|
|
6
|
+
*
|
|
7
|
+
* - Caching: If a TTL (time-to-live) is specified, the last successful (or optionally failed) result
|
|
8
|
+
* will be reused for repeated calls within the TTL window.
|
|
9
|
+
*
|
|
10
|
+
* Example usage:
|
|
11
|
+
*
|
|
12
|
+
* const apiCaller = new ThrottledApiCaller(fetchCart, { ttlMs: 5000 });
|
|
13
|
+
* const result = await apiCaller.call({ userId: "abc" });
|
|
14
|
+
*/
|
|
15
|
+
export class ThrottledApiCaller<TArgs, TResult> {
|
|
16
|
+
/**
|
|
17
|
+
* Stores in-flight Promises keyed by args hash.
|
|
18
|
+
* Ensures that simultaneous calls with the same args share the same Promise.
|
|
19
|
+
*/
|
|
20
|
+
private inflight = new Map<string, Promise<TResult>>();
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Stores cached results (or errors) along with expiration timestamps.
|
|
24
|
+
* Only used if ttlMs > 0.
|
|
25
|
+
*/
|
|
26
|
+
private cache = new Map<
|
|
27
|
+
string,
|
|
28
|
+
{ value?: TResult; error?: unknown; expiresAt: number }
|
|
29
|
+
>();
|
|
30
|
+
|
|
31
|
+
/** Function used to create a cache key from args. */
|
|
32
|
+
private readonly keyFn: (args: TArgs) => string;
|
|
33
|
+
|
|
34
|
+
/** Time-to-live (ms) for caching results. If 0, no result caching is applied. */
|
|
35
|
+
private readonly ttlMs: number;
|
|
36
|
+
|
|
37
|
+
/** Whether to cache failed results (errors). */
|
|
38
|
+
private readonly cacheErrors: boolean;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @param fn The async function you want to call in a deduped/cached manner.
|
|
42
|
+
* @param opts Options:
|
|
43
|
+
* - keyFn: custom function to turn args into a cache key (default: stable JSON stringify).
|
|
44
|
+
* - ttlMs: cache result duration in ms (default: 0, disabled).
|
|
45
|
+
* - cacheErrors: if true, errors are cached within TTL window (default: false).
|
|
46
|
+
*/
|
|
47
|
+
constructor(
|
|
48
|
+
private readonly fn: (args: TArgs) => Promise<TResult>,
|
|
49
|
+
opts: {
|
|
50
|
+
keyFn?: (args: TArgs) => string;
|
|
51
|
+
ttlMs?: number;
|
|
52
|
+
cacheErrors?: boolean;
|
|
53
|
+
} = {}
|
|
54
|
+
) {
|
|
55
|
+
this.keyFn = opts.keyFn ?? stableKey;
|
|
56
|
+
this.ttlMs = Math.max(0, opts.ttlMs ?? 0);
|
|
57
|
+
this.cacheErrors = !!opts.cacheErrors;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Executes the wrapped function in a deduplicated and optionally cached manner.
|
|
62
|
+
* - If a cached result is still fresh, return it immediately.
|
|
63
|
+
* - If an in-flight request exists for the same args, return that Promise.
|
|
64
|
+
* - Otherwise, run the function, store its Promise in inflight, and cache its result.
|
|
65
|
+
*/
|
|
66
|
+
async call(args: TArgs): Promise<TResult> {
|
|
67
|
+
const key = this.keyFn(args);
|
|
68
|
+
const now = Date.now();
|
|
69
|
+
|
|
70
|
+
// 1. Serve from cache if still valid
|
|
71
|
+
if (this.ttlMs > 0) {
|
|
72
|
+
const hit = this.cache.get(key);
|
|
73
|
+
if (hit && hit.expiresAt > now) {
|
|
74
|
+
if (hit.error !== undefined) throw hit.error;
|
|
75
|
+
return hit.value as TResult;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 2. Share ongoing in-flight call if one exists
|
|
80
|
+
const existing = this.inflight.get(key);
|
|
81
|
+
if (existing) return existing;
|
|
82
|
+
|
|
83
|
+
// 3. Run new request
|
|
84
|
+
const promise = (async () => {
|
|
85
|
+
try {
|
|
86
|
+
const result = await this.fn(args);
|
|
87
|
+
// Cache the result if TTL is set
|
|
88
|
+
if (this.ttlMs > 0) {
|
|
89
|
+
this.cache.set(key, {
|
|
90
|
+
value: result,
|
|
91
|
+
expiresAt: now + this.ttlMs,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
return result;
|
|
95
|
+
} catch (err) {
|
|
96
|
+
// Optionally cache the error too
|
|
97
|
+
if (this.ttlMs > 0 && this.cacheErrors) {
|
|
98
|
+
this.cache.set(key, {
|
|
99
|
+
error: err,
|
|
100
|
+
expiresAt: now + this.ttlMs,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
throw err;
|
|
104
|
+
} finally {
|
|
105
|
+
// Always clear inflight entry when done
|
|
106
|
+
this.inflight.delete(key);
|
|
107
|
+
}
|
|
108
|
+
})();
|
|
109
|
+
|
|
110
|
+
this.inflight.set(key, promise);
|
|
111
|
+
return promise;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Invalidate cache for a specific args set.
|
|
116
|
+
* Next call with these args will trigger a fresh request.
|
|
117
|
+
*/
|
|
118
|
+
invalidate(args: TArgs): void {
|
|
119
|
+
this.cache.delete(this.keyFn(args));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Clear all cache and in-flight requests.
|
|
124
|
+
*/
|
|
125
|
+
clear(): void {
|
|
126
|
+
this.cache.clear();
|
|
127
|
+
this.inflight.clear();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Default stable key generator for args.
|
|
133
|
+
* Produces a deterministic string even if object keys are in different order.
|
|
134
|
+
*/
|
|
135
|
+
function stableKey(v: unknown): string {
|
|
136
|
+
const seen = new WeakSet();
|
|
137
|
+
const encode = (x: any): any => {
|
|
138
|
+
if (x && typeof x === "object") {
|
|
139
|
+
if (seen.has(x)) return "__cycle__";
|
|
140
|
+
seen.add(x);
|
|
141
|
+
if (Array.isArray(x)) return x.map(encode);
|
|
142
|
+
const out: Record<string, any> = {};
|
|
143
|
+
for (const k of Object.keys(x).sort()) out[k] = encode(x[k]);
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
146
|
+
return x;
|
|
147
|
+
};
|
|
148
|
+
return JSON.stringify(encode(v));
|
|
149
|
+
}
|