@genesislcap/mock-server 15.60.0 → 15.62.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 (57) hide show
  1. package/README.md +287 -40
  2. package/dist/db/criteria.d.ts +28 -9
  3. package/dist/db/criteria.js +856 -258
  4. package/dist/db/criteriaDialect.d.ts +6 -0
  5. package/dist/db/criteriaDialect.js +441 -0
  6. package/dist/db/criteriaTokens.d.ts +12 -0
  7. package/dist/db/criteriaTokens.js +53 -0
  8. package/dist/db/groovySyntax.d.ts +26 -0
  9. package/dist/db/groovySyntax.js +481 -0
  10. package/dist/db/groovyValidator.d.ts +11 -0
  11. package/dist/db/groovyValidator.js +523 -0
  12. package/dist/db/gsfExpr.d.ts +21 -0
  13. package/dist/db/gsfExpr.js +333 -0
  14. package/dist/db/identity.d.ts +2 -1
  15. package/dist/db/identity.js +24 -3
  16. package/dist/db/legacyCriteria.d.ts +7 -0
  17. package/dist/db/legacyCriteria.js +280 -0
  18. package/dist/db/ordering.d.ts +2 -0
  19. package/dist/db/ordering.js +24 -3
  20. package/dist/db/schema.d.ts +2 -0
  21. package/dist/db/schema.js +34 -3
  22. package/dist/db/store.d.ts +7 -0
  23. package/dist/db/store.js +55 -7
  24. package/dist/db/values.d.ts +19 -0
  25. package/dist/db/values.js +243 -0
  26. package/dist/handlers/commitEvent.js +3 -2
  27. package/dist/handlers/dataLogon.d.ts +2 -1
  28. package/dist/handlers/dataLogon.js +108 -51
  29. package/dist/handlers/eventValidation.d.ts +2 -1
  30. package/dist/handlers/eventValidation.js +33 -6
  31. package/dist/handlers/requestReply.js +43 -16
  32. package/dist/index.d.ts +1 -1
  33. package/dist/index.js +1 -1
  34. package/dist/protocol/connection.d.ts +1 -1
  35. package/dist/server.js +15 -8
  36. package/dist/types.d.ts +1 -0
  37. package/package.json +1 -1
  38. package/src/db/criteria.ts +979 -290
  39. package/src/db/criteriaDialect.ts +487 -0
  40. package/src/db/criteriaTokens.ts +72 -0
  41. package/src/db/groovySyntax.ts +500 -0
  42. package/src/db/groovyValidator.ts +546 -0
  43. package/src/db/gsfExpr.ts +392 -0
  44. package/src/db/identity.ts +22 -4
  45. package/src/db/legacyCriteria.ts +301 -0
  46. package/src/db/ordering.ts +27 -3
  47. package/src/db/schema.ts +43 -8
  48. package/src/db/store.ts +65 -7
  49. package/src/db/values.ts +247 -0
  50. package/src/handlers/commitEvent.ts +3 -2
  51. package/src/handlers/dataLogon.ts +134 -56
  52. package/src/handlers/eventValidation.ts +40 -13
  53. package/src/handlers/requestReply.ts +47 -14
  54. package/src/index.ts +6 -1
  55. package/src/protocol/connection.ts +3 -4
  56. package/src/server.ts +16 -15
  57. package/src/types.ts +7 -0
@@ -0,0 +1,392 @@
1
+ // What GSF's criteria functions compute (genesis-commons v8.15.29: Expr.java,
2
+ // TimeUtils.java, ExtraOperators.kt, TimeRange.kt, TimeContext.kt), for the
3
+ // evaluator in criteria.ts. GSF runs criteria in UTC (Joda's default zone,
4
+ // set by RxDb.kt), so every date here is a UTC one.
5
+ //
6
+ // A Java exception inside a compiled criteria (a date that doesn't parse, a
7
+ // null field, an unknown TimeRange) makes the row not match, however the
8
+ // expression goes on: `!Expr.dateIsEqual(F, 'bad')` matches nothing. These
9
+ // functions throw CriteriaEvaluationError for those, and the evaluator's
10
+ // predicate answers false.
11
+
12
+ export class CriteriaEvaluationError extends Error {
13
+ override name = 'CriteriaEvaluationError';
14
+ }
15
+
16
+ export const evaluationError = (message: string): never => {
17
+ throw new CriteriaEvaluationError(message);
18
+ };
19
+
20
+ const HOURS_PER_DAY = 24;
21
+ const MINUTES_PER_HOUR = 60;
22
+ const SECONDS_PER_MINUTE = 60;
23
+ const MS_PER_SECOND = 1000;
24
+ const MS_PER_MINUTE = SECONDS_PER_MINUTE * MS_PER_SECOND;
25
+ const MS_PER_HOUR = MINUTES_PER_HOUR * MS_PER_MINUTE;
26
+ export const MS_PER_DAY = HOURS_PER_DAY * MS_PER_HOUR;
27
+ const DAYS_PER_WEEK = 7;
28
+ const MONTHS_PER_YEAR = 12;
29
+
30
+ // --- the calendar (proleptic Gregorian, as Joda's ISO chronology) ------------
31
+
32
+ const DAYS_PER_ERA = 146_097;
33
+ const YEARS_PER_ERA = 400;
34
+ const MARCH_BASED_SHIFT = 3; // months before March belong to the year before
35
+ const DAYS_IN_FIVE_MONTHS = 153;
36
+ const FIVE_MONTHS = 5;
37
+ const DAYS_TO_1970 = 719_468;
38
+ const LEAP_EVERY = 4;
39
+ const CENTURY = 100;
40
+ const DAYS_PER_YEAR = 365;
41
+ const DAYS_PER_4_YEARS = 1460;
42
+ const DAYS_PER_100_YEARS = 36_524;
43
+ const DAYS_PER_400_YEARS_MINUS_ONE = 146_096;
44
+ const FEBRUARY = 2;
45
+ const LONG_FEBRUARY = 29;
46
+ const SHORT_FEBRUARY = 28;
47
+ const APRIL = 4;
48
+ const JUNE = 6;
49
+ const SEPTEMBER = 9;
50
+ const NOVEMBER = 11;
51
+ const THIRTY_DAY_MONTHS = new Set([APRIL, JUNE, SEPTEMBER, NOVEMBER]);
52
+ const SHORT_MONTH = 30;
53
+ const LONG_MONTH = 31;
54
+
55
+ // Days since 1970-01-01 of a date (month 1-12), any year.
56
+ export function daysFromCivil(year: number, month: number, day: number): number {
57
+ const y = month <= 2 ? year - 1 : year;
58
+ const era = Math.floor(y / YEARS_PER_ERA);
59
+ const yearOfEra = y - era * YEARS_PER_ERA;
60
+ const monthFromMarch = (month + MONTHS_PER_YEAR - MARCH_BASED_SHIFT) % MONTHS_PER_YEAR;
61
+ const dayOfYear = Math.floor((DAYS_IN_FIVE_MONTHS * monthFromMarch + 2) / FIVE_MONTHS) + day - 1;
62
+ const dayOfEra =
63
+ yearOfEra * DAYS_PER_YEAR +
64
+ Math.floor(yearOfEra / LEAP_EVERY) -
65
+ Math.floor(yearOfEra / CENTURY) +
66
+ dayOfYear;
67
+ return era * DAYS_PER_ERA + dayOfEra - DAYS_TO_1970;
68
+ }
69
+
70
+ // The date of a day since 1970-01-01.
71
+ export function civilFromDays(days: number): { year: number; month: number; day: number } {
72
+ const z = days + DAYS_TO_1970;
73
+ const era = Math.floor(z / DAYS_PER_ERA);
74
+ const dayOfEra = z - era * DAYS_PER_ERA;
75
+ const yearOfEra = Math.floor(
76
+ (dayOfEra -
77
+ Math.floor(dayOfEra / DAYS_PER_4_YEARS) +
78
+ Math.floor(dayOfEra / DAYS_PER_100_YEARS) -
79
+ Math.floor(dayOfEra / DAYS_PER_400_YEARS_MINUS_ONE)) /
80
+ DAYS_PER_YEAR,
81
+ );
82
+ const dayOfYear =
83
+ dayOfEra -
84
+ (DAYS_PER_YEAR * yearOfEra +
85
+ Math.floor(yearOfEra / LEAP_EVERY) -
86
+ Math.floor(yearOfEra / CENTURY));
87
+ const monthFromMarch = Math.floor((FIVE_MONTHS * dayOfYear + 2) / DAYS_IN_FIVE_MONTHS);
88
+ const day = dayOfYear - Math.floor((DAYS_IN_FIVE_MONTHS * monthFromMarch + 2) / FIVE_MONTHS) + 1;
89
+ const month =
90
+ monthFromMarch < MONTHS_PER_YEAR - 2
91
+ ? monthFromMarch + MARCH_BASED_SHIFT
92
+ : monthFromMarch - (MONTHS_PER_YEAR - MARCH_BASED_SHIFT);
93
+ const year = yearOfEra + era * YEARS_PER_ERA + (month <= 2 ? 1 : 0);
94
+ return { year, month, day };
95
+ }
96
+
97
+ const isLeapYear = (year: number) =>
98
+ year % LEAP_EVERY === 0 && (year % CENTURY !== 0 || year % YEARS_PER_ERA === 0);
99
+
100
+ function daysInMonth(year: number, month: number): number {
101
+ if (month === FEBRUARY) return isLeapYear(year) ? LONG_FEBRUARY : SHORT_FEBRUARY;
102
+ return THIRTY_DAY_MONTHS.has(month) ? SHORT_MONTH : LONG_MONTH;
103
+ }
104
+
105
+ export const epochDay = (millis: number) => Math.floor(millis / MS_PER_DAY);
106
+
107
+ // --- TimeUtils.ALLOWED_DATE_PARSER -------------------------------------------
108
+
109
+ // One field of a Joda pattern as its parser reads it: up to `max` digits
110
+ // (at least one, more are never backtracked), a year's optionally signed.
111
+ interface PatternField {
112
+ field: 'year' | 'month' | 'day' | 'hour' | 'minute' | 'second' | 'fraction';
113
+ max: number;
114
+ }
115
+ type Pattern = Array<PatternField | string>;
116
+
117
+ // `yyyy` reads at most four digits when another number follows it, and nine
118
+ // otherwise (DateTimeFormat.parsePatternTo); MM, dd, HH, mm and ss two;
119
+ // SSS three.
120
+ const YEAR: PatternField = { field: 'year', max: 4 };
121
+ const LONG_YEAR: PatternField = { field: 'year', max: 9 };
122
+ const MONTH: PatternField = { field: 'month', max: 2 };
123
+ const DAY: PatternField = { field: 'day', max: 2 };
124
+ const HOUR: PatternField = { field: 'hour', max: 2 };
125
+ const MINUTE: PatternField = { field: 'minute', max: 2 };
126
+ const SECOND: PatternField = { field: 'second', max: 2 };
127
+ const FRACTION: PatternField = { field: 'fraction', max: 3 };
128
+
129
+ // ProcessConstants: DATE_FORMAT_FULL, _TO_SECONDS, _TO_MINUTES, _TO_DATE
130
+ // and DATE_FORMAT_ISO8601_TO_DATE, in ALLOWED_DATE_PARSER's order.
131
+ const TO_DATE: Pattern = [YEAR, MONTH, DAY];
132
+ const TO_MINUTES: Pattern = [...TO_DATE, '-', HOUR, ':', MINUTE];
133
+ const TO_SECONDS: Pattern = [...TO_MINUTES, ':', SECOND];
134
+ const FULL: Pattern = [...TO_SECONDS, '.', FRACTION];
135
+ const ISO8601_TO_DATE: Pattern = [LONG_YEAR, '-', MONTH, '-', DAY];
136
+ const ALLOWED_PATTERNS = [FULL, TO_SECONDS, TO_MINUTES, TO_DATE, ISO8601_TO_DATE];
137
+
138
+ interface DateFields {
139
+ year: number;
140
+ month: number;
141
+ day: number;
142
+ hour: number;
143
+ minute: number;
144
+ second: number;
145
+ millis: number;
146
+ }
147
+
148
+ const DIGIT = /\d/;
149
+ const FRACTION_DIGITS = 3;
150
+
151
+ // The fields of `text` when the pattern reads all of it, else undefined.
152
+ function readPattern(text: string, pattern: Pattern): DateFields | undefined {
153
+ const fields: DateFields = {
154
+ year: 0,
155
+ month: 1,
156
+ day: 1,
157
+ hour: 0,
158
+ minute: 0,
159
+ second: 0,
160
+ millis: 0,
161
+ };
162
+ let at = 0;
163
+ for (const part of pattern) {
164
+ if (typeof part === 'string') {
165
+ if (text[at] !== part) return undefined;
166
+ at += 1;
167
+ continue;
168
+ }
169
+ let sign = 1;
170
+ if (part.field === 'year' && (text[at] === '-' || text[at] === '+')) {
171
+ if (!DIGIT.test(text[at + 1] ?? '')) return undefined;
172
+ sign = text[at] === '-' ? -1 : 1;
173
+ at += 1;
174
+ }
175
+ let digits = '';
176
+ while (digits.length < part.max && DIGIT.test(text[at] ?? '')) {
177
+ digits += text[at];
178
+ at += 1;
179
+ }
180
+ if (digits === '') return undefined;
181
+ if (part.field === 'fraction') {
182
+ fields.millis = Number(digits.padEnd(FRACTION_DIGITS, '0'));
183
+ } else {
184
+ fields[part.field] = sign * Number(digits);
185
+ }
186
+ }
187
+ return at === text.length ? fields : undefined;
188
+ }
189
+
190
+ // Joda's ISO chronology years.
191
+ const MIN_YEAR = -292_275_054;
192
+ const MAX_YEAR = 292_278_993;
193
+ const MAX_HOUR = 23;
194
+ const MAX_MINUTE = 59;
195
+ const MAX_SECOND = 59;
196
+
197
+ // The fields' instant, or a Joda IllegalFieldValueException.
198
+ function instantOf(fields: DateFields, text: string): number {
199
+ const { year, month, day, hour, minute, second, millis } = fields;
200
+ const valid =
201
+ year >= MIN_YEAR &&
202
+ year <= MAX_YEAR &&
203
+ month >= 1 &&
204
+ month <= MONTHS_PER_YEAR &&
205
+ day >= 1 &&
206
+ day <= daysInMonth(year, month) &&
207
+ hour <= MAX_HOUR &&
208
+ minute <= MAX_MINUTE &&
209
+ second <= MAX_SECOND;
210
+ if (!valid) evaluationError(`Cannot parse "${text}": a field value is out of range`);
211
+ return (
212
+ daysFromCivil(year, month, day) * MS_PER_DAY +
213
+ hour * MS_PER_HOUR +
214
+ minute * MS_PER_MINUTE +
215
+ second * MS_PER_SECOND +
216
+ millis
217
+ );
218
+ }
219
+
220
+ function parseWith(text: string, patterns: Pattern[]): number {
221
+ for (const pattern of patterns) {
222
+ const fields = readPattern(text, pattern);
223
+ if (fields) return instantOf(fields, text);
224
+ }
225
+ return evaluationError(`Invalid format: "${text}"`);
226
+ }
227
+
228
+ // Expr.stringDateTime / TimeUtils.parseDateTime: the instant a criteria date
229
+ // names — yyyyMMdd-HH:mm:ss.SSS, yyyyMMdd-HH:mm:ss, yyyyMMdd-HH:mm, yyyyMMdd
230
+ // or yyyy-MM-dd, in UTC. Joda reads fewer digits than the pattern shows
231
+ // ('2024-1-1', '20240101-9:5') and a fraction of a second ('.5' is 500 ms),
232
+ // nothing else: no 'T', no offset, no spaces.
233
+ export const stringDateTime = (text: string): number => parseWith(text, ALLOWED_PATTERNS);
234
+
235
+ // Expr.stringLocalDate / TimeUtils.parseLocalDate: the same formats, as a day
236
+ // (days since 1970-01-01).
237
+ export const stringLocalDate = (text: string): number => epochDay(stringDateTime(text));
238
+
239
+ // Expr.singlePatternLocalDate: the day a yyyyMMdd date names — the only form
240
+ // the Long and DateTime overloads of dateIs* take for the date compared to.
241
+ export const singlePatternLocalDate = (text: string): number =>
242
+ epochDay(parseWith(text, [TO_DATE]));
243
+
244
+ // DateTime.now().toLocalDate() in UTC.
245
+ export const today = () => epochDay(Date.now());
246
+
247
+ // --- strings -----------------------------------------------------------------
248
+
249
+ // Character.isWhitespace: the Unicode space separators except the no-break
250
+ // ones, and the ASCII controls \t \n \v \f \r and \u001C-\u001F.
251
+ const JAVA_WHITESPACE = /^[\t\n\v\f\r\p{Zl}\p{Zp}\p{Zs}]$/u;
252
+ const NO_BREAK_SPACE = /^[\u00A0\u2007\u202F]$/u;
253
+ const FILE_SEPARATOR = 0x1c;
254
+ const UNIT_SEPARATOR = 0x1f;
255
+
256
+ const isJavaWhitespace = (char: string) => {
257
+ const code = char.charCodeAt(0);
258
+ if (code >= FILE_SEPARATOR && code <= UNIT_SEPARATOR) return true;
259
+ return JAVA_WHITESPACE.test(char) && !NO_BREAK_SPACE.test(char);
260
+ };
261
+
262
+ // String.isBlank.
263
+ export const isBlank = (text: string) => [...text].every(isJavaWhitespace);
264
+
265
+ const upper = (char: string) => {
266
+ const result = char.toUpperCase();
267
+ return result.length === 1 ? result : char;
268
+ };
269
+ const lower = (char: string) => {
270
+ const result = char.toLowerCase();
271
+ return result.length === 1 ? result : char;
272
+ };
273
+
274
+ // String.regionMatches(true, ...): char by char, equal as they are, upper
275
+ // cased, or upper then lower cased.
276
+ function regionMatchesIgnoreCase(
277
+ text: string,
278
+ offset: number,
279
+ other: string,
280
+ length: number,
281
+ ): boolean {
282
+ for (let index = 0; index < length; index += 1) {
283
+ const a = text[offset + index];
284
+ const b = other[index];
285
+ if (a === b) continue;
286
+ const [ua, ub] = [upper(a), upper(b)];
287
+ if (ua === ub || lower(ua) === lower(ub)) continue;
288
+ return false;
289
+ }
290
+ return true;
291
+ }
292
+
293
+ // Expr.containsIgnoreCase and the String?.containsIgnoreCase extension.
294
+ export function containsIgnoreCase(text: string | null, search: string | null): boolean {
295
+ if (text === null || search === null) return false;
296
+ if (search.length === 0) return true;
297
+ for (let at = text.length - search.length; at >= 0; at -= 1) {
298
+ if (regionMatchesIgnoreCase(text, at, search, search.length)) return true;
299
+ }
300
+ return false;
301
+ }
302
+
303
+ // Expr.containsWordsStartingWithIgnoreCase and its String? extension: whether
304
+ // a word of `text` — split on the delimiter, ' ' by default — starts with
305
+ // `search`, ignoring case. A word is never shorter than the search.
306
+ export function containsWordsStartingWithIgnoreCase(
307
+ text: string | null,
308
+ search: string | null,
309
+ delimiter = ' ',
310
+ ): boolean {
311
+ if (text === null || search === null) return false;
312
+ const length = search.length;
313
+ if (length === 0) return true;
314
+ let start = 0;
315
+ let end: number;
316
+ do {
317
+ end = text.indexOf(delimiter, start);
318
+ if (end === -1) end = text.length;
319
+ if (end - start >= length && regionMatchesIgnoreCase(text, start, search, length)) {
320
+ return true;
321
+ }
322
+ start = end + 1;
323
+ } while (end !== text.length);
324
+ return false;
325
+ }
326
+
327
+ // --- Expr.dateTimeIsInRange --------------------------------------------------
328
+
329
+ const TIME_CONTEXT_OFFSETS: Record<string, number> = { PREVIOUS: 1, CURRENT: 0 };
330
+ const SATURDAY = 6;
331
+ const SUNDAY = 7;
332
+ const THURSDAY_OFFSET = 3; // 1970-01-01 was a Thursday
333
+
334
+ // ISO day of week, Monday 1 to Sunday 7.
335
+ const dayOfWeek = (day: number) =>
336
+ ((((day + THURSDAY_OFFSET) % DAYS_PER_WEEK) + DAYS_PER_WEEK) % DAYS_PER_WEEK) + 1;
337
+
338
+ const startOfMonth = (year: number, month: number) => {
339
+ const months = year * MONTHS_PER_YEAR + (month - 1);
340
+ const y = Math.floor(months / MONTHS_PER_YEAR);
341
+ return daysFromCivil(y, months - y * MONTHS_PER_YEAR + 1, 1) * MS_PER_DAY;
342
+ };
343
+
344
+ // TimeRange.getTimeBounds: [beginning, end) in epoch millis. An enum name
345
+ // TimeRange.valueOf / TimeContext.valueOf doesn't know (they are case
346
+ // sensitive) throws.
347
+ export function timeBounds(range: string, context: string, now: number): [number, number] {
348
+ const offset = Object.hasOwn(TIME_CONTEXT_OFFSETS, context)
349
+ ? TIME_CONTEXT_OFFSETS[context]
350
+ : evaluationError(`No enum constant TimeContext.${context}`);
351
+ const day = epochDay(now);
352
+ const { year, month } = civilFromDays(day);
353
+ switch (range) {
354
+ case 'HOUR': {
355
+ const beginning = Math.floor(now / MS_PER_HOUR) * MS_PER_HOUR - offset * MS_PER_HOUR;
356
+ return [beginning, beginning + MS_PER_HOUR];
357
+ }
358
+ case 'DAY': {
359
+ const beginning = (day - offset) * MS_PER_DAY;
360
+ return [beginning, beginning + MS_PER_DAY];
361
+ }
362
+ case 'WEEK': {
363
+ const shifted = day - offset * DAYS_PER_WEEK;
364
+ const monday = shifted - (dayOfWeek(shifted) - 1);
365
+ return [monday * MS_PER_DAY, (monday + DAYS_PER_WEEK) * MS_PER_DAY];
366
+ }
367
+ case 'WEEK_DAY': {
368
+ const base = day - offset;
369
+ if (offset === 0) {
370
+ return dayOfWeek(base) >= SATURDAY ? [0, 0] : [base * MS_PER_DAY, (base + 1) * MS_PER_DAY];
371
+ }
372
+ const weekday = dayOfWeek(base);
373
+ const friday = weekday === SATURDAY ? base - 1 : weekday === SUNDAY ? base - 2 : base;
374
+ return [friday * MS_PER_DAY, (friday + 1) * MS_PER_DAY];
375
+ }
376
+ case 'MONTH':
377
+ return [startOfMonth(year, month - offset), startOfMonth(year, month - offset + 1)];
378
+ case 'YEAR':
379
+ return [
380
+ daysFromCivil(year - offset, 1, 1) * MS_PER_DAY,
381
+ daysFromCivil(year - offset + 1, 1, 1) * MS_PER_DAY,
382
+ ];
383
+ default:
384
+ return evaluationError(`No enum constant TimeRange.${range}`);
385
+ }
386
+ }
387
+
388
+ // TimeRange.valueOf(range).isInTimeRange(dateTime, TimeContext.valueOf(context)).
389
+ export function dateTimeIsInRange(millis: number, range: string, context: string): boolean {
390
+ const [beginning, end] = timeBounds(range, context, Date.now());
391
+ return millis >= beginning && millis < end;
392
+ }
@@ -17,21 +17,39 @@ export const RECORD_ID = 'RECORD_ID';
17
17
  export const TIMESTAMP = 'TIMESTAMP';
18
18
  export const RECORD_FIELDS: readonly string[] = [RECORD_ID, TIMESTAMP];
19
19
 
20
+ const INTEGER_TEXT = /^-?\d+$/;
21
+
20
22
  // Hands out RECORD_ID/TIMESTAMP values: the current epoch millis, or one more
21
23
  // than the last value when that is not ahead of it (seeding a thousand rows
22
- // runs the counter a second or so ahead of the clock).
24
+ // runs the counter a second or so ahead of the clock). Once a seed brings a
25
+ // real server's id above 2^53 (as a string, which JSON can't carry exactly as
26
+ // a number), later stamps continue from it as strings, so a new row is still
27
+ // the newest one.
23
28
  export class RecordClock {
24
29
  private last = 0;
30
+ private beyondSafe: bigint | undefined;
25
31
 
26
- next(): number {
32
+ next(): number | string {
33
+ if (this.beyondSafe !== undefined) {
34
+ this.beyondSafe += 1n;
35
+ return String(this.beyondSafe);
36
+ }
27
37
  this.last = Math.max(Date.now(), this.last + 1);
28
38
  return this.last;
29
39
  }
30
40
 
31
41
  // Keeps later stamps above a value a seed row brought with it.
32
42
  observe(value: unknown): void {
33
- if (typeof value === 'number' && Number.isSafeInteger(value) && value > this.last) {
34
- this.last = value;
43
+ if (typeof value === 'number' && Number.isSafeInteger(value)) {
44
+ if (value > this.last) this.last = value;
45
+ return;
46
+ }
47
+ if (typeof value !== 'string' || !INTEGER_TEXT.test(value)) return;
48
+ const big = BigInt(value);
49
+ if (big <= BigInt(Number.MAX_SAFE_INTEGER)) {
50
+ if (Number(big) > this.last) this.last = Number(big);
51
+ } else if (this.beyondSafe === undefined || big > this.beyondSafe) {
52
+ this.beyondSafe = big;
35
53
  }
36
54
  }
37
55
  }