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