@esmalley/ts-utils 2.1.0 → 4.0.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/dist/cjs/index.js +16 -27
- package/dist/cjs/index.js.map +7 -0
- package/dist/esm/index.js +16 -11
- package/dist/esm/index.js.map +7 -0
- package/dist/{cjs → types}/Kontororu/Socket.d.ts +9 -4
- package/dist/types/Kontororu/Socket.d.ts.map +1 -0
- package/package.json +9 -7
- package/dist/cjs/Arithmetic.js +0 -9
- package/dist/cjs/Arrayifier.js +0 -69
- package/dist/cjs/CSV.js +0 -39
- package/dist/cjs/Color.js +0 -348
- package/dist/cjs/Dates.js +0 -358
- package/dist/cjs/Kontororu/Socket.d.ts.map +0 -1
- package/dist/cjs/Kontororu/Socket.js +0 -221
- package/dist/cjs/Kontororu.js +0 -27
- package/dist/cjs/Objector.js +0 -124
- package/dist/cjs/Sorter.js +0 -32
- package/dist/cjs/Style.js +0 -475
- package/dist/cjs/Text.js +0 -39
- package/dist/cjs/Theme.js +0 -507
- package/dist/cjs/Toaster.js +0 -49
- package/dist/esm/Arithmetic.d.ts +0 -4
- package/dist/esm/Arithmetic.d.ts.map +0 -1
- package/dist/esm/Arithmetic.js +0 -5
- package/dist/esm/Arrayifier.d.ts +0 -29
- package/dist/esm/Arrayifier.d.ts.map +0 -1
- package/dist/esm/Arrayifier.js +0 -65
- package/dist/esm/CSV.d.ts +0 -10
- package/dist/esm/CSV.d.ts.map +0 -1
- package/dist/esm/CSV.js +0 -35
- package/dist/esm/Color.d.ts +0 -93
- package/dist/esm/Color.d.ts.map +0 -1
- package/dist/esm/Color.js +0 -344
- package/dist/esm/Dates.d.ts +0 -51
- package/dist/esm/Dates.d.ts.map +0 -1
- package/dist/esm/Dates.js +0 -354
- package/dist/esm/Kontororu/Socket.d.ts +0 -44
- package/dist/esm/Kontororu/Socket.d.ts.map +0 -1
- package/dist/esm/Kontororu/Socket.js +0 -218
- package/dist/esm/Kontororu.d.ts +0 -8
- package/dist/esm/Kontororu.d.ts.map +0 -1
- package/dist/esm/Kontororu.js +0 -23
- package/dist/esm/Objector.d.ts +0 -32
- package/dist/esm/Objector.d.ts.map +0 -1
- package/dist/esm/Objector.js +0 -120
- package/dist/esm/Sorter.d.ts +0 -8
- package/dist/esm/Sorter.d.ts.map +0 -1
- package/dist/esm/Sorter.js +0 -28
- package/dist/esm/Style.d.ts +0 -63
- package/dist/esm/Style.d.ts.map +0 -1
- package/dist/esm/Style.js +0 -471
- package/dist/esm/Text.d.ts +0 -5
- package/dist/esm/Text.d.ts.map +0 -1
- package/dist/esm/Text.js +0 -35
- package/dist/esm/Theme.d.ts +0 -1413
- package/dist/esm/Theme.d.ts.map +0 -1
- package/dist/esm/Theme.js +0 -503
- package/dist/esm/Toaster.d.ts +0 -24
- package/dist/esm/Toaster.d.ts.map +0 -1
- package/dist/esm/Toaster.js +0 -46
- package/dist/esm/index.d.ts +0 -12
- package/dist/esm/index.d.ts.map +0 -1
- /package/dist/{cjs → types}/Arithmetic.d.ts +0 -0
- /package/dist/{cjs → types}/Arithmetic.d.ts.map +0 -0
- /package/dist/{cjs → types}/Arrayifier.d.ts +0 -0
- /package/dist/{cjs → types}/Arrayifier.d.ts.map +0 -0
- /package/dist/{cjs → types}/CSV.d.ts +0 -0
- /package/dist/{cjs → types}/CSV.d.ts.map +0 -0
- /package/dist/{cjs → types}/Color.d.ts +0 -0
- /package/dist/{cjs → types}/Color.d.ts.map +0 -0
- /package/dist/{cjs → types}/Dates.d.ts +0 -0
- /package/dist/{cjs → types}/Dates.d.ts.map +0 -0
- /package/dist/{cjs → types}/Kontororu.d.ts +0 -0
- /package/dist/{cjs → types}/Kontororu.d.ts.map +0 -0
- /package/dist/{cjs → types}/Objector.d.ts +0 -0
- /package/dist/{cjs → types}/Objector.d.ts.map +0 -0
- /package/dist/{cjs → types}/Sorter.d.ts +0 -0
- /package/dist/{cjs → types}/Sorter.d.ts.map +0 -0
- /package/dist/{cjs → types}/Style.d.ts +0 -0
- /package/dist/{cjs → types}/Style.d.ts.map +0 -0
- /package/dist/{cjs → types}/Text.d.ts +0 -0
- /package/dist/{cjs → types}/Text.d.ts.map +0 -0
- /package/dist/{cjs → types}/Theme.d.ts +0 -0
- /package/dist/{cjs → types}/Theme.d.ts.map +0 -0
- /package/dist/{cjs → types}/Toaster.d.ts +0 -0
- /package/dist/{cjs → types}/Toaster.d.ts.map +0 -0
- /package/dist/{cjs → types}/index.d.ts +0 -0
- /package/dist/{cjs → types}/index.d.ts.map +0 -0
package/dist/esm/Dates.js
DELETED
|
@@ -1,354 +0,0 @@
|
|
|
1
|
-
/* eslint-disable one-var-declaration-per-line */
|
|
2
|
-
/* eslint-disable one-var */
|
|
3
|
-
export class Dates {
|
|
4
|
-
// constructor() {
|
|
5
|
-
// }
|
|
6
|
-
/*
|
|
7
|
-
* Robust Date Parsing
|
|
8
|
-
* Handles:
|
|
9
|
-
* - Date Objects / Numbers (Timestamps)
|
|
10
|
-
* - ISO Strings (2025-01-01) -> Forces Local Midnight
|
|
11
|
-
* - US Formats (01/05/2026)
|
|
12
|
-
* - Mixed Time Formats (23:00 pm, 5:00pm, 14:30:00)
|
|
13
|
-
*/
|
|
14
|
-
static parse(str, utc = false) {
|
|
15
|
-
// 1. Handle Null / Undefined -> Return Now
|
|
16
|
-
if (!str) {
|
|
17
|
-
return new Date();
|
|
18
|
-
}
|
|
19
|
-
// 2. Handle Existing Date Objects -> Return Copy
|
|
20
|
-
if (str instanceof Date) {
|
|
21
|
-
return new Date(str.getTime());
|
|
22
|
-
}
|
|
23
|
-
// 3. Handle Timestamps (Numbers)
|
|
24
|
-
if (typeof str === 'number') {
|
|
25
|
-
return new Date(str);
|
|
26
|
-
}
|
|
27
|
-
// 4. Handle Strings
|
|
28
|
-
if (typeof str === 'string') {
|
|
29
|
-
const input = str.trim();
|
|
30
|
-
// CASE A: Strict ISO Date "YYYY-MM-DD"
|
|
31
|
-
// Native JS parses this as UTC Midnight, which often shows as
|
|
32
|
-
// previous day 7pm EST. We force "T00:00:00" to make it Local Midnight.
|
|
33
|
-
if (/^\d{4}-\d{2}-\d{2}$/.test(input)) {
|
|
34
|
-
return new Date(`${input}T00:00:00`);
|
|
35
|
-
}
|
|
36
|
-
// CASE B: Manual Parsing for Complex Strings
|
|
37
|
-
// This handles "2026-01-05 23:03:19 pm", "01/05/2026", etc.
|
|
38
|
-
// Step 1: Extract Date Part (YYYY-MM-DD or MM/DD/YYYY)
|
|
39
|
-
// Regex looks for: (Group 1: Year/Month) -or/ (Group 2: Month/Day) -or/ (Group 3: Day/Year)
|
|
40
|
-
let year, month, day, timePart = '';
|
|
41
|
-
// Match YYYY-MM-DD or YYYY/MM/DD
|
|
42
|
-
const isoMatch = input.match(/^(\d{4})[-/](\d{1,2})[-/](\d{1,2})(.*)$/);
|
|
43
|
-
// Match MM/DD/YYYY or MM-DD-YYYY
|
|
44
|
-
const usMatch = input.match(/^(\d{1,2})[-/](\d{1,2})[-/](\d{4})(.*)$/);
|
|
45
|
-
if (isoMatch) {
|
|
46
|
-
year = parseInt(isoMatch[1], 10);
|
|
47
|
-
month = parseInt(isoMatch[2], 10) - 1; // JS Months are 0-11
|
|
48
|
-
day = parseInt(isoMatch[3], 10);
|
|
49
|
-
timePart = isoMatch[4];
|
|
50
|
-
}
|
|
51
|
-
else if (usMatch) {
|
|
52
|
-
year = parseInt(usMatch[3], 10);
|
|
53
|
-
month = parseInt(usMatch[1], 10) - 1;
|
|
54
|
-
day = parseInt(usMatch[2], 10);
|
|
55
|
-
timePart = usMatch[4];
|
|
56
|
-
}
|
|
57
|
-
else {
|
|
58
|
-
// Fallback: Let the browser try its best if our regex fails
|
|
59
|
-
const d = new Date(input);
|
|
60
|
-
return isNaN(d.getTime()) ? new Date() : d;
|
|
61
|
-
}
|
|
62
|
-
// Step 2: Extract Time Part
|
|
63
|
-
let hours = 0;
|
|
64
|
-
let minutes = 0;
|
|
65
|
-
let seconds = 0;
|
|
66
|
-
// Look for HH:MM(:SS) and optional AM/PM in the remaining string
|
|
67
|
-
if (timePart && timePart.trim().length > 0) {
|
|
68
|
-
// Matches: 23:03, 23:03:19, 5:00pm, 5:00 pm
|
|
69
|
-
const timeMatch = timePart.match(/(\d{1,2}):(\d{1,2})(?::(\d{1,2}))?\s*(am|pm|AM|PM)?/);
|
|
70
|
-
if (timeMatch) {
|
|
71
|
-
hours = parseInt(timeMatch[1], 10);
|
|
72
|
-
minutes = parseInt(timeMatch[2], 10);
|
|
73
|
-
seconds = timeMatch[3] ? parseInt(timeMatch[3], 10) : 0;
|
|
74
|
-
const meridiem = timeMatch[4] ? timeMatch[4].toLowerCase() : null;
|
|
75
|
-
// Step 3: Normalize Hours (12h to 24h)
|
|
76
|
-
if (meridiem === 'pm' && hours < 12) {
|
|
77
|
-
hours += 12;
|
|
78
|
-
}
|
|
79
|
-
if (meridiem === 'am' && hours === 12) {
|
|
80
|
-
hours = 0;
|
|
81
|
-
}
|
|
82
|
-
// Note: If input is "23:00 pm", we ignore the 'pm' because 23 > 12.
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
if (utc) {
|
|
86
|
-
return new Date(Date.UTC(year, month, day, hours, minutes, seconds));
|
|
87
|
-
}
|
|
88
|
-
// Step 4: Construct Date in Local Time
|
|
89
|
-
return new Date(year, month, day, hours, minutes, seconds);
|
|
90
|
-
}
|
|
91
|
-
return new Date();
|
|
92
|
-
}
|
|
93
|
-
static utc(date) {
|
|
94
|
-
const d = this.parse(date);
|
|
95
|
-
return new Date(d.getTime() + (d.getTimezoneOffset() * 60000));
|
|
96
|
-
}
|
|
97
|
-
static getMonthsShort() {
|
|
98
|
-
return ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
99
|
-
}
|
|
100
|
-
static getMonths() {
|
|
101
|
-
return ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
|
|
102
|
-
}
|
|
103
|
-
static getDaysShort() {
|
|
104
|
-
return ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
105
|
-
}
|
|
106
|
-
static getDays() {
|
|
107
|
-
return ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
|
108
|
-
}
|
|
109
|
-
/**
|
|
110
|
-
* Format a date using php syntax
|
|
111
|
-
*
|
|
112
|
-
| Token | Meaning | Example |
|
|
113
|
-
| ----- | ------------------------- | ------- |
|
|
114
|
-
| `Y` | 4-digit year | 2025 |
|
|
115
|
-
| `y` | 2-digit year | 25 |
|
|
116
|
-
| `m` | 2-digit month | 03 |
|
|
117
|
-
| `n` | month (no leading zero) | 3 |
|
|
118
|
-
| `d` | day (2-digit) | 09 |
|
|
119
|
-
| `j` | day (no leading zero) | 9 |
|
|
120
|
-
| `S` | Ordinal suffix | th |
|
|
121
|
-
| `H` | 24-hour | 14 |
|
|
122
|
-
| `G` | 24-hour (no leading zero) | 14 |
|
|
123
|
-
| `h` | 12-hour | 02 |
|
|
124
|
-
| `g` | 12-hour (no leading zero) | 2 |
|
|
125
|
-
| `i` | minutes | 05 |
|
|
126
|
-
| `s` | seconds | 09 |
|
|
127
|
-
| `A` | AM/PM | PM |
|
|
128
|
-
| `a` | am/pm | pm |
|
|
129
|
-
| `w` | day of week (0–6) | 1 |
|
|
130
|
-
| `N` | day of week (1–7) | 2 |
|
|
131
|
-
| `M` | short month name | Mar |
|
|
132
|
-
| `F` | full month name | March |
|
|
133
|
-
| `D` | short weekday | Mon |
|
|
134
|
-
| `l` | full weekday | Monday |
|
|
135
|
-
*/
|
|
136
|
-
static format(dateInput, format) {
|
|
137
|
-
const date = this.parse(dateInput);
|
|
138
|
-
const pad = (n) => String(n).padStart(2, '0');
|
|
139
|
-
const monthsShort = this.getMonthsShort();
|
|
140
|
-
const monthsLong = this.getMonths();
|
|
141
|
-
const daysShort = this.getDaysShort();
|
|
142
|
-
const daysLong = this.getDays();
|
|
143
|
-
/*
|
|
144
|
-
const Y = date.getUTCFullYear();
|
|
145
|
-
const y = String(Y).slice(-2);
|
|
146
|
-
const month = date.getUTCMonth(); // 0-11
|
|
147
|
-
const dateNum = date.getUTCDate(); // 1-31
|
|
148
|
-
const day = date.getUTCDay(); // 0-6, Sun = 0
|
|
149
|
-
const hours = date.getUTCHours(); // 0-23
|
|
150
|
-
const minutes = date.getUTCMinutes();
|
|
151
|
-
const seconds = date.getUTCSeconds();
|
|
152
|
-
*/
|
|
153
|
-
const Y = date.getFullYear();
|
|
154
|
-
const y = String(Y).slice(-2);
|
|
155
|
-
const month = date.getMonth(); // 0-11
|
|
156
|
-
const dateNum = date.getDate(); // 1-31
|
|
157
|
-
const day = date.getDay(); // 0-6, Sun = 0
|
|
158
|
-
const hours = date.getHours(); // 0-23
|
|
159
|
-
const minutes = date.getMinutes();
|
|
160
|
-
const seconds = date.getSeconds();
|
|
161
|
-
// Logic for ordinal suffix (st, nd, rd, th)
|
|
162
|
-
const getOrdinalSuffix = (n) => {
|
|
163
|
-
const v = n % 100;
|
|
164
|
-
// 11th, 12th, 13th are exceptions to the 1st, 2nd, 3rd rule
|
|
165
|
-
if (v >= 11 && v <= 13) {
|
|
166
|
-
return 'th';
|
|
167
|
-
}
|
|
168
|
-
switch (n % 10) {
|
|
169
|
-
case 1: {
|
|
170
|
-
return 'st';
|
|
171
|
-
}
|
|
172
|
-
case 2: {
|
|
173
|
-
return 'nd';
|
|
174
|
-
}
|
|
175
|
-
case 3: {
|
|
176
|
-
return 'rd';
|
|
177
|
-
}
|
|
178
|
-
default: {
|
|
179
|
-
return 'th';
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
};
|
|
183
|
-
const tokens = {
|
|
184
|
-
/// Year
|
|
185
|
-
Y: String(Y),
|
|
186
|
-
y,
|
|
187
|
-
// Month
|
|
188
|
-
m: pad(month + 1),
|
|
189
|
-
n: String(month + 1),
|
|
190
|
-
M: monthsShort[month],
|
|
191
|
-
F: monthsLong[month],
|
|
192
|
-
// Day
|
|
193
|
-
d: pad(dateNum),
|
|
194
|
-
j: String(dateNum),
|
|
195
|
-
D: daysShort[day],
|
|
196
|
-
l: daysLong[day],
|
|
197
|
-
w: String(day), // 0 (Sun) - 6
|
|
198
|
-
N: String(day === 0 ? 7 : day), // 1 (Mon) - 7 (Sun)
|
|
199
|
-
S: getOrdinalSuffix(dateNum),
|
|
200
|
-
// Time
|
|
201
|
-
H: pad(hours),
|
|
202
|
-
G: String(hours),
|
|
203
|
-
h: pad(((hours + 11) % 12) + 1),
|
|
204
|
-
g: String(((hours + 11) % 12) + 1),
|
|
205
|
-
i: pad(minutes),
|
|
206
|
-
s: pad(seconds),
|
|
207
|
-
// AM/PM
|
|
208
|
-
A: hours < 12 ? 'AM' : 'PM',
|
|
209
|
-
a: hours < 12 ? 'am' : 'pm',
|
|
210
|
-
};
|
|
211
|
-
// Replace tokens using regex
|
|
212
|
-
return format.replace(/\\(.)|([a-zA-Z])/g, (_, esc, token) => {
|
|
213
|
-
if (esc) {
|
|
214
|
-
// literal escaped char like \H or \Y → return the raw letter
|
|
215
|
-
return esc;
|
|
216
|
-
}
|
|
217
|
-
return tokens[token] ?? token;
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
static add(date, amount, unit) {
|
|
221
|
-
const d = this.parse(date);
|
|
222
|
-
if (unit === 'years') {
|
|
223
|
-
const originalDay = d.getDate();
|
|
224
|
-
d.setFullYear(d.getFullYear() + amount);
|
|
225
|
-
// Handle Leap Year Rollover:
|
|
226
|
-
// Feb 29, 2024 + 1 year -> Mar 1, 2025 (Standard JS behavior)
|
|
227
|
-
// If strict "same day or last day of month" logic is desired (turning it into Feb 28):
|
|
228
|
-
if (d.getDate() !== originalDay) {
|
|
229
|
-
d.setDate(0); // Set to last day of previous month (Feb 28)
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
else if (unit === 'months') {
|
|
233
|
-
const originalDay = d.getDate();
|
|
234
|
-
d.setMonth(d.getMonth() + amount);
|
|
235
|
-
// Handle rollover: Jan 31 + 1 month -> Feb 28/29
|
|
236
|
-
if (d.getDate() !== originalDay) {
|
|
237
|
-
d.setDate(0); // Set to last day of previous month
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
else if (unit === 'days') {
|
|
241
|
-
// Use setDate to be DST safe (24h addition via ms is unsafe across DST)
|
|
242
|
-
d.setDate(d.getDate() + amount);
|
|
243
|
-
}
|
|
244
|
-
else {
|
|
245
|
-
const map = {
|
|
246
|
-
hours: amount * 60 * 60 * 1000,
|
|
247
|
-
minutes: amount * 60 * 1000,
|
|
248
|
-
};
|
|
249
|
-
// Use getTime() for day/hour/minute units for simple millisecond addition
|
|
250
|
-
d.setTime(d.getTime() + map[unit]);
|
|
251
|
-
}
|
|
252
|
-
return d;
|
|
253
|
-
}
|
|
254
|
-
static subtract(date, amount, unit) {
|
|
255
|
-
return this.add(date, -amount, unit);
|
|
256
|
-
}
|
|
257
|
-
static fromNow(date) {
|
|
258
|
-
const d = this.parse(date);
|
|
259
|
-
const diff = Date.now() - d.getTime();
|
|
260
|
-
const mins = Math.floor(diff / 60000);
|
|
261
|
-
if (Math.abs(mins) < 1) {
|
|
262
|
-
return 'just now';
|
|
263
|
-
}
|
|
264
|
-
// Handle future dates roughly
|
|
265
|
-
if (mins < 0) {
|
|
266
|
-
return 'in the future';
|
|
267
|
-
}
|
|
268
|
-
if (mins < 60) {
|
|
269
|
-
return `${mins}m ago`;
|
|
270
|
-
}
|
|
271
|
-
const hours = Math.floor(mins / 60);
|
|
272
|
-
if (hours < 24) {
|
|
273
|
-
return `${hours}h ago`;
|
|
274
|
-
}
|
|
275
|
-
const days = Math.floor(hours / 24);
|
|
276
|
-
return `${days}d ago`;
|
|
277
|
-
}
|
|
278
|
-
/**
|
|
279
|
-
* Find the closest date in an array of dates
|
|
280
|
-
*/
|
|
281
|
-
static getClosestDate(dateToMatch, datesArray) {
|
|
282
|
-
if (!datesArray.length) {
|
|
283
|
-
return null;
|
|
284
|
-
}
|
|
285
|
-
const matchDate = this.parse(dateToMatch).getTime();
|
|
286
|
-
let closestDate = null;
|
|
287
|
-
let closestDist = Infinity;
|
|
288
|
-
// eslint-disable-next-line no-restricted-syntax
|
|
289
|
-
for (const dateStr of datesArray) {
|
|
290
|
-
const currDate = this.parse(dateStr).getTime();
|
|
291
|
-
const dist = Math.abs(currDate - matchDate);
|
|
292
|
-
if (dist < closestDist) {
|
|
293
|
-
closestDist = dist;
|
|
294
|
-
closestDate = dateStr;
|
|
295
|
-
}
|
|
296
|
-
else if (dist === closestDist) {
|
|
297
|
-
// Tie-breaker: Prefer the date that is in the future relative to the matchDate
|
|
298
|
-
// Or if both are same direction, just keep the current one (or implementation defined)
|
|
299
|
-
// Requirement: "Both 17th and 19th have same dist. It should pick 19th"
|
|
300
|
-
if (currDate > matchDate) {
|
|
301
|
-
closestDate = dateStr;
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
return closestDate;
|
|
306
|
-
}
|
|
307
|
-
static getTodayEST() {
|
|
308
|
-
return this.format(new Date().toLocaleString('en-US', { timeZone: 'America/New_York' }), 'Y-m-d');
|
|
309
|
-
}
|
|
310
|
-
static getStartOfDay(date) {
|
|
311
|
-
const d = this.parse(date);
|
|
312
|
-
d.setHours(0, 0, 0, 0);
|
|
313
|
-
return d;
|
|
314
|
-
}
|
|
315
|
-
static getStartOfMonth(date) {
|
|
316
|
-
const d = this.parse(date);
|
|
317
|
-
d.setDate(1);
|
|
318
|
-
d.setHours(0, 0, 0, 0);
|
|
319
|
-
return d;
|
|
320
|
-
}
|
|
321
|
-
static getStartOfGrid(date) {
|
|
322
|
-
const d = this.getStartOfMonth(date);
|
|
323
|
-
const dayOfWeek = d.getDay(); // 0 (Sunday) is the start in standard JS
|
|
324
|
-
// Move back to the beginning of the week
|
|
325
|
-
const result = this.parse(d);
|
|
326
|
-
// Subtract days to get to the start of the week (Sunday)
|
|
327
|
-
result.setDate(d.getDate() - dayOfWeek);
|
|
328
|
-
return result;
|
|
329
|
-
}
|
|
330
|
-
static isSameDay(date1, date2) {
|
|
331
|
-
if (!date1 || !date2) {
|
|
332
|
-
return false;
|
|
333
|
-
}
|
|
334
|
-
const d1 = this.parse(date1);
|
|
335
|
-
const d2 = this.parse(date2);
|
|
336
|
-
return (d1.getFullYear() === d2.getFullYear() &&
|
|
337
|
-
d1.getMonth() === d2.getMonth() &&
|
|
338
|
-
d1.getDate() === d2.getDate());
|
|
339
|
-
}
|
|
340
|
-
// Helper to check if one date is before another (ignoring time)
|
|
341
|
-
static isBeforeDay(date1, date2) {
|
|
342
|
-
if (!date1 || !date2) {
|
|
343
|
-
return false;
|
|
344
|
-
}
|
|
345
|
-
return this.getStartOfDay(date1).getTime() < this.getStartOfDay(date2).getTime();
|
|
346
|
-
}
|
|
347
|
-
// Helper to check if one date is after another (ignoring time)
|
|
348
|
-
static isAfterDay(date1, date2) {
|
|
349
|
-
if (!date1 || !date2) {
|
|
350
|
-
return false;
|
|
351
|
-
}
|
|
352
|
-
return this.getStartOfDay(date1).getTime() > this.getStartOfDay(date2).getTime();
|
|
353
|
-
}
|
|
354
|
-
}
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import { Kontororu } from 'src/Kontororu.js';
|
|
2
|
-
type SocketMessage = {
|
|
3
|
-
type: 'subscribe' | 'unsubscribe' | 'data' | 'heartbeat';
|
|
4
|
-
table: string;
|
|
5
|
-
id: string;
|
|
6
|
-
};
|
|
7
|
-
declare class Socket extends Kontororu {
|
|
8
|
-
private ws?;
|
|
9
|
-
private connection_state;
|
|
10
|
-
private protocol;
|
|
11
|
-
private url;
|
|
12
|
-
private session_id?;
|
|
13
|
-
private message_queue;
|
|
14
|
-
private reconnect_attempts;
|
|
15
|
-
private should_reconnect;
|
|
16
|
-
private reconnect_timeout?;
|
|
17
|
-
private last_heartbeat_timestamp;
|
|
18
|
-
private readonly HEARTBEAT_INTERVAL_MS;
|
|
19
|
-
private readonly SUSPENSION_THRESHOLD_MS;
|
|
20
|
-
private readonly DISCONNECT_THRESHOLD_MS;
|
|
21
|
-
constructor();
|
|
22
|
-
connect(session_id: string): void;
|
|
23
|
-
/**
|
|
24
|
-
* Send message if the websocket is open,
|
|
25
|
-
* otherwise add it to the message queue
|
|
26
|
-
*/
|
|
27
|
-
message({ type, table, id }: SocketMessage): void;
|
|
28
|
-
disconnect(): void;
|
|
29
|
-
/**
|
|
30
|
-
* Update the connection state, dispatch an event if it actually changed.
|
|
31
|
-
*/
|
|
32
|
-
private update_connection_state;
|
|
33
|
-
/**
|
|
34
|
-
* Helper to determine if we need to fetch missing data
|
|
35
|
-
*/
|
|
36
|
-
private check_staleness;
|
|
37
|
-
private handle_open;
|
|
38
|
-
private handle_message;
|
|
39
|
-
private handle_close;
|
|
40
|
-
private handle_error;
|
|
41
|
-
}
|
|
42
|
-
export declare const socket: Socket;
|
|
43
|
-
export {};
|
|
44
|
-
//# sourceMappingURL=Socket.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"Socket.d.ts","sourceRoot":"","sources":["../../../src/Kontororu/Socket.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAS7C,KAAK,aAAa,GAAG;IACnB,IAAI,EAAE,WAAW,GAAG,aAAa,GAAG,MAAM,GAAG,WAAW,CAAC;IACzD,KAAK,EAAE,MAAM,CAAC;IACd,EAAE,EAAE,MAAM,CAAC;CACZ,CAAA;AAUD,cAAM,MAAO,SAAQ,SAAS;IAC5B,OAAO,CAAC,EAAE,CAAC,CAAY;IAEvB,OAAO,CAAC,gBAAgB,CAAgC;IAExD,OAAO,CAAC,QAAQ,CAA4I;IAE5J,OAAO,CAAC,GAAG,CAAoE;IAE/E,OAAO,CAAC,UAAU,CAAC,CAAS;IAE5B,OAAO,CAAC,aAAa,CAAuB;IAG5C,OAAO,CAAC,kBAAkB,CAAa;IACvC,OAAO,CAAC,gBAAgB,CAAQ;IAChC,OAAO,CAAC,iBAAiB,CAAC,CAAiB;IAG3C,OAAO,CAAC,wBAAwB,CAAsB;IAEtD,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAQ;IAE9C,OAAO,CAAC,QAAQ,CAAC,uBAAuB,CAA2C;IAEnF,OAAO,CAAC,QAAQ,CAAC,uBAAuB,CAA2C;;IAyC5E,OAAO,CAAC,UAAU,EAAE,MAAM;IA+BjC;;;OAGG;IACI,OAAO,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,aAAa;IAa1C,UAAU;IAYjB;;OAEG;IACH,OAAO,CAAC,uBAAuB;IAmB/B;;OAEG;IACH,OAAO,CAAC,eAAe;IAuBvB,OAAO,CAAC,WAAW;IA+BnB,OAAO,CAAC,cAAc;IA+BtB,OAAO,CAAC,YAAY;IAkBpB,OAAO,CAAC,YAAY;CAMrB;AAED,eAAO,MAAM,MAAM,QAAe,CAAC"}
|
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
import { Kontororu } from 'src/Kontororu.js';
|
|
2
|
-
const hostname = process.env.NEXT_PUBLIC_WS_HOST;
|
|
3
|
-
const port = process.env.NEXT_PUBLIC_WS_PORT;
|
|
4
|
-
const path = process.env.NEXT_PUBLIC_WS_PATH;
|
|
5
|
-
const debug = false;
|
|
6
|
-
class Socket extends Kontororu {
|
|
7
|
-
constructor() {
|
|
8
|
-
super();
|
|
9
|
-
this.connection_state = 'connected';
|
|
10
|
-
this.protocol = (typeof window !== 'undefined' && window.location && window.location.protocol && window.location.protocol === 'https:' ? 'wss:' : 'ws:');
|
|
11
|
-
this.url = `${this.protocol}//${hostname}${port ? `:${port}` : ''}/${path}`;
|
|
12
|
-
this.message_queue = [];
|
|
13
|
-
// Reconnection logic
|
|
14
|
-
this.reconnect_attempts = 0;
|
|
15
|
-
this.should_reconnect = true;
|
|
16
|
-
// Heartbeat / Sleep Detection Logic
|
|
17
|
-
this.last_heartbeat_timestamp = Date.now();
|
|
18
|
-
// How often the server pings THIS specific client (5 seconds)
|
|
19
|
-
this.HEARTBEAT_INTERVAL_MS = 5000;
|
|
20
|
-
// Calculate threshold: 2 missed beats + 1 second of network jitter buffer
|
|
21
|
-
this.SUSPENSION_THRESHOLD_MS = (this.HEARTBEAT_INTERVAL_MS * 2) + 1000;
|
|
22
|
-
// 3 Heartbeats + 1s buffer = 16 seconds
|
|
23
|
-
this.DISCONNECT_THRESHOLD_MS = (this.HEARTBEAT_INTERVAL_MS * 3) + 1000;
|
|
24
|
-
if (typeof document !== 'undefined') {
|
|
25
|
-
document.addEventListener('visibilitychange', (event) => {
|
|
26
|
-
if (document.visibilityState === 'visible') {
|
|
27
|
-
this.check_staleness('tab_switch');
|
|
28
|
-
}
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
if (typeof window !== 'undefined') {
|
|
32
|
-
const offlineChecker = () => {
|
|
33
|
-
const check = 3;
|
|
34
|
-
let checked = 1;
|
|
35
|
-
const checker = () => {
|
|
36
|
-
if (checked > check) {
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
setTimeout(() => {
|
|
40
|
-
this.check_staleness('offline');
|
|
41
|
-
checked++;
|
|
42
|
-
checker();
|
|
43
|
-
}, (this.HEARTBEAT_INTERVAL_MS) + 500);
|
|
44
|
-
};
|
|
45
|
-
checker();
|
|
46
|
-
};
|
|
47
|
-
window.addEventListener('online', () => {
|
|
48
|
-
window.removeEventListener('offline', offlineChecker);
|
|
49
|
-
});
|
|
50
|
-
window.addEventListener('offline', offlineChecker);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
connect(session_id) {
|
|
54
|
-
if (!session_id) {
|
|
55
|
-
console.warn('session_id required to open ws');
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
if (this.ws &&
|
|
59
|
-
(this.ws.readyState === WebSocket.OPEN ||
|
|
60
|
-
this.ws.readyState === WebSocket.CONNECTING)) {
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
this.ws = new WebSocket(this.url);
|
|
64
|
-
if (debug)
|
|
65
|
-
console.log('new websocket');
|
|
66
|
-
this.session_id = session_id;
|
|
67
|
-
// Reset heartbeat timer on new connection
|
|
68
|
-
this.last_heartbeat_timestamp = Date.now();
|
|
69
|
-
this.ws.addEventListener('open', (event) => this.handle_open(event));
|
|
70
|
-
this.ws.addEventListener('message', (event) => this.handle_message(event));
|
|
71
|
-
this.ws.addEventListener('close', (event) => this.handle_close(event));
|
|
72
|
-
this.ws.addEventListener('error', (event) => this.handle_error(event));
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* Send message if the websocket is open,
|
|
76
|
-
* otherwise add it to the message queue
|
|
77
|
-
*/
|
|
78
|
-
message({ type, table, id }) {
|
|
79
|
-
const payload = { type, table, id };
|
|
80
|
-
if (this.ws &&
|
|
81
|
-
this.ws.readyState === WebSocket.OPEN) {
|
|
82
|
-
this.ws.send(JSON.stringify(payload));
|
|
83
|
-
}
|
|
84
|
-
else {
|
|
85
|
-
this.message_queue.push(payload);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
disconnect() {
|
|
89
|
-
if (debug)
|
|
90
|
-
console.log('websocket disconnect()');
|
|
91
|
-
this.update_connection_state('disconnected');
|
|
92
|
-
// if we are manually disconnecting we do not want an auto reconnect
|
|
93
|
-
this.should_reconnect = false;
|
|
94
|
-
this.ws?.close();
|
|
95
|
-
this.ws = undefined;
|
|
96
|
-
if (this.reconnect_timeout) {
|
|
97
|
-
clearTimeout(this.reconnect_timeout);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
/**
|
|
101
|
-
* Update the connection state, dispatch an event if it actually changed.
|
|
102
|
-
*/
|
|
103
|
-
update_connection_state(new_connection_state) {
|
|
104
|
-
const old_connection_state = this.connection_state;
|
|
105
|
-
if (this.connection_state !== new_connection_state) {
|
|
106
|
-
if (debug)
|
|
107
|
-
console.log('update_connection_state', new_connection_state);
|
|
108
|
-
this.connection_state = new_connection_state;
|
|
109
|
-
if (debug)
|
|
110
|
-
console.warn(`[Socket] State changed to: ${this.connection_state}`);
|
|
111
|
-
this.dispatchEvent(new CustomEvent('connection_state', { detail: this.connection_state }));
|
|
112
|
-
if ((new_connection_state === 'connected' || new_connection_state === 'reconnected') &&
|
|
113
|
-
(old_connection_state === 'stale' || old_connection_state === 'disconnected')) {
|
|
114
|
-
this.dispatchEvent(new CustomEvent('refresh', { bubbles: true }));
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
/**
|
|
119
|
-
* Helper to determine if we need to fetch missing data
|
|
120
|
-
*/
|
|
121
|
-
check_staleness(source) {
|
|
122
|
-
const now = Date.now();
|
|
123
|
-
const time_since_last = now - this.last_heartbeat_timestamp;
|
|
124
|
-
if (debug)
|
|
125
|
-
console.log('websocket check_staleness()', source, time_since_last);
|
|
126
|
-
if (!this.ws ||
|
|
127
|
-
this.ws.readyState === this.ws.CLOSED ||
|
|
128
|
-
time_since_last > this.DISCONNECT_THRESHOLD_MS) {
|
|
129
|
-
this.update_connection_state('disconnected');
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
// If gap is larger than threshold, we missed messages
|
|
133
|
-
if (time_since_last > this.SUSPENSION_THRESHOLD_MS) {
|
|
134
|
-
this.update_connection_state('stale');
|
|
135
|
-
}
|
|
136
|
-
else {
|
|
137
|
-
this.update_connection_state('connected');
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
handle_open(event) {
|
|
141
|
-
if (debug)
|
|
142
|
-
console.log('websocket handle open()');
|
|
143
|
-
if (this.ws &&
|
|
144
|
-
this.ws.readyState === this.ws.OPEN) {
|
|
145
|
-
this.update_connection_state('connected');
|
|
146
|
-
}
|
|
147
|
-
// clear reconnect timeout if we connected
|
|
148
|
-
if (this.reconnect_timeout) {
|
|
149
|
-
clearTimeout(this.reconnect_timeout);
|
|
150
|
-
}
|
|
151
|
-
// If we are reconnecting (attempts > 0), we definitely missed data.
|
|
152
|
-
if (this.reconnect_attempts > 0) {
|
|
153
|
-
if (debug)
|
|
154
|
-
console.log('[Socket] Reconnected. Triggering refresh.');
|
|
155
|
-
this.dispatchEvent(new CustomEvent('refresh', { bubbles: true }));
|
|
156
|
-
}
|
|
157
|
-
// reset the reconnect attempts
|
|
158
|
-
this.reconnect_attempts = 0;
|
|
159
|
-
this.ws?.send(JSON.stringify({ type: 'session', table: 'session', id: this.session_id }));
|
|
160
|
-
while (this.message_queue.length > 0) {
|
|
161
|
-
const queuedMsg = this.message_queue.shift();
|
|
162
|
-
this.ws?.send(JSON.stringify(queuedMsg));
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
handle_message(event) {
|
|
166
|
-
if (debug)
|
|
167
|
-
console.log('websocket handle message()');
|
|
168
|
-
try {
|
|
169
|
-
const data = JSON.parse(event.data);
|
|
170
|
-
if (debug)
|
|
171
|
-
console.log('data', data);
|
|
172
|
-
// HEARTBEAT CHECK: Intercept heartbeat messages
|
|
173
|
-
if (data.type === 'heartbeat') {
|
|
174
|
-
this.check_staleness('heartbeat');
|
|
175
|
-
this.last_heartbeat_timestamp = Date.now();
|
|
176
|
-
return; // Do not bubble 'heartbeat' to the UI
|
|
177
|
-
}
|
|
178
|
-
// todo not sure I like this
|
|
179
|
-
const messageEvent = new CustomEvent('message', {
|
|
180
|
-
detail: JSON.parse(event.data),
|
|
181
|
-
bubbles: true,
|
|
182
|
-
});
|
|
183
|
-
this.dispatchEvent(messageEvent);
|
|
184
|
-
}
|
|
185
|
-
catch (e) {
|
|
186
|
-
const messageEvent = new CustomEvent('message', {
|
|
187
|
-
detail: event.data,
|
|
188
|
-
bubbles: true,
|
|
189
|
-
});
|
|
190
|
-
this.dispatchEvent(messageEvent);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
handle_close(event) {
|
|
194
|
-
if (debug)
|
|
195
|
-
console.log('websocket handle close()');
|
|
196
|
-
this.update_connection_state('disconnected');
|
|
197
|
-
if (this.should_reconnect) {
|
|
198
|
-
const delay = Math.min(1000 * Math.pow(2, this.reconnect_attempts), 30000);
|
|
199
|
-
if (debug)
|
|
200
|
-
console.log(`Connection lost. Retrying in ${delay}ms... (Attempt ${this.reconnect_attempts + 1})`);
|
|
201
|
-
this.reconnect_timeout = setTimeout(() => {
|
|
202
|
-
this.reconnect_attempts++;
|
|
203
|
-
if (this.session_id) {
|
|
204
|
-
this.connect(this.session_id);
|
|
205
|
-
}
|
|
206
|
-
}, delay);
|
|
207
|
-
}
|
|
208
|
-
this.ws = undefined;
|
|
209
|
-
}
|
|
210
|
-
handle_error(event) {
|
|
211
|
-
if (debug)
|
|
212
|
-
console.log('websocket handle error()');
|
|
213
|
-
this.update_connection_state('disconnected');
|
|
214
|
-
console.error('WebSocket Error:', event);
|
|
215
|
-
this.ws?.close();
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
export const socket = new Socket();
|
package/dist/esm/Kontororu.d.ts
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
export declare class Kontororu extends EventTarget {
|
|
2
|
-
constructor();
|
|
3
|
-
private listeners;
|
|
4
|
-
addEventListener(type: string, listener: (...args: unknown[]) => void): void;
|
|
5
|
-
removeEventListener(type: string, listener: (...args: unknown[]) => void): void;
|
|
6
|
-
getListeners(type: string): ((...args: unknown[]) => void)[];
|
|
7
|
-
}
|
|
8
|
-
//# sourceMappingURL=Kontororu.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"Kontororu.d.ts","sourceRoot":"","sources":["../../src/Kontororu.ts"],"names":[],"mappings":"AAGA,qBAAa,SAAU,SAAQ,WAAW;;IAMxC,OAAO,CAAC,SAAS,CAEf;IAEF,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI;IASrE,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI;IAQxE,YAAY,CAAC,IAAI,EAAE,MAAM,cApBS,OAAO,EAAE,KAAK,IAAI;CAuBrD"}
|
package/dist/esm/Kontororu.js
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
// コントロール
|
|
2
|
-
export class Kontororu extends EventTarget {
|
|
3
|
-
constructor() {
|
|
4
|
-
super();
|
|
5
|
-
this.listeners = {};
|
|
6
|
-
}
|
|
7
|
-
addEventListener(type, listener) {
|
|
8
|
-
super.addEventListener(type, listener);
|
|
9
|
-
if (!this.listeners[type]) {
|
|
10
|
-
this.listeners[type] = [];
|
|
11
|
-
}
|
|
12
|
-
this.listeners[type].push(listener);
|
|
13
|
-
}
|
|
14
|
-
removeEventListener(type, listener) {
|
|
15
|
-
super.removeEventListener(type, listener);
|
|
16
|
-
if (this.listeners[type]) {
|
|
17
|
-
this.listeners[type] = this.listeners[type].filter((l) => l !== listener);
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
getListeners(type) {
|
|
21
|
-
return this.listeners[type] || [];
|
|
22
|
-
}
|
|
23
|
-
}
|