@esmalley/ts-utils 1.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.
Files changed (45) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +2 -0
  3. package/dist/Arithmetic.d.ts +4 -0
  4. package/dist/Arithmetic.d.ts.map +1 -0
  5. package/dist/Arithmetic.js +5 -0
  6. package/dist/Arrayifier.d.ts +29 -0
  7. package/dist/Arrayifier.d.ts.map +1 -0
  8. package/dist/Arrayifier.js +65 -0
  9. package/dist/CSV.d.ts +10 -0
  10. package/dist/CSV.d.ts.map +1 -0
  11. package/dist/CSV.js +35 -0
  12. package/dist/Color.d.ts +93 -0
  13. package/dist/Color.d.ts.map +1 -0
  14. package/dist/Color.js +344 -0
  15. package/dist/Dates.d.ts +51 -0
  16. package/dist/Dates.d.ts.map +1 -0
  17. package/dist/Dates.js +354 -0
  18. package/dist/Kontororu/Socket.d.ts +44 -0
  19. package/dist/Kontororu/Socket.d.ts.map +1 -0
  20. package/dist/Kontororu/Socket.js +218 -0
  21. package/dist/Kontororu.d.ts +8 -0
  22. package/dist/Kontororu.d.ts.map +1 -0
  23. package/dist/Kontororu.js +23 -0
  24. package/dist/Objector.d.ts +32 -0
  25. package/dist/Objector.d.ts.map +1 -0
  26. package/dist/Objector.js +120 -0
  27. package/dist/Sorter.d.ts +8 -0
  28. package/dist/Sorter.d.ts.map +1 -0
  29. package/dist/Sorter.js +28 -0
  30. package/dist/Style.d.ts +63 -0
  31. package/dist/Style.d.ts.map +1 -0
  32. package/dist/Style.js +471 -0
  33. package/dist/Text.d.ts +5 -0
  34. package/dist/Text.d.ts.map +1 -0
  35. package/dist/Text.js +35 -0
  36. package/dist/Theme.d.ts +1413 -0
  37. package/dist/Theme.d.ts.map +1 -0
  38. package/dist/Theme.js +503 -0
  39. package/dist/Toaster.d.ts +24 -0
  40. package/dist/Toaster.d.ts.map +1 -0
  41. package/dist/Toaster.js +46 -0
  42. package/dist/index.d.ts +12 -0
  43. package/dist/index.d.ts.map +1 -0
  44. package/dist/index.js +11 -0
  45. package/package.json +58 -0
package/dist/Dates.js ADDED
@@ -0,0 +1,354 @@
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
+ }
@@ -0,0 +1,44 @@
1
+ import { Kontororu } from '@/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
@@ -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,218 @@
1
+ import { Kontororu } from '@/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();
@@ -0,0 +1,8 @@
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
@@ -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,23 @@
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
+ }