@voidhash/mimic 0.0.1-alpha.1

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 +17 -0
  2. package/package.json +33 -0
  3. package/src/Document.ts +256 -0
  4. package/src/FractionalIndex.ts +1249 -0
  5. package/src/Operation.ts +59 -0
  6. package/src/OperationDefinition.ts +23 -0
  7. package/src/OperationPath.ts +197 -0
  8. package/src/Presence.ts +142 -0
  9. package/src/Primitive.ts +32 -0
  10. package/src/Proxy.ts +8 -0
  11. package/src/ProxyEnvironment.ts +52 -0
  12. package/src/Transaction.ts +72 -0
  13. package/src/Transform.ts +13 -0
  14. package/src/client/ClientDocument.ts +1163 -0
  15. package/src/client/Rebase.ts +309 -0
  16. package/src/client/StateMonitor.ts +307 -0
  17. package/src/client/Transport.ts +318 -0
  18. package/src/client/WebSocketTransport.ts +572 -0
  19. package/src/client/errors.ts +145 -0
  20. package/src/client/index.ts +61 -0
  21. package/src/index.ts +12 -0
  22. package/src/primitives/Array.ts +457 -0
  23. package/src/primitives/Boolean.ts +128 -0
  24. package/src/primitives/Lazy.ts +89 -0
  25. package/src/primitives/Literal.ts +128 -0
  26. package/src/primitives/Number.ts +169 -0
  27. package/src/primitives/String.ts +189 -0
  28. package/src/primitives/Struct.ts +348 -0
  29. package/src/primitives/Tree.ts +1120 -0
  30. package/src/primitives/TreeNode.ts +113 -0
  31. package/src/primitives/Union.ts +329 -0
  32. package/src/primitives/shared.ts +122 -0
  33. package/src/server/ServerDocument.ts +267 -0
  34. package/src/server/errors.ts +90 -0
  35. package/src/server/index.ts +40 -0
  36. package/tests/Document.test.ts +556 -0
  37. package/tests/FractionalIndex.test.ts +377 -0
  38. package/tests/OperationPath.test.ts +151 -0
  39. package/tests/Presence.test.ts +321 -0
  40. package/tests/Primitive.test.ts +381 -0
  41. package/tests/client/ClientDocument.test.ts +1398 -0
  42. package/tests/client/WebSocketTransport.test.ts +992 -0
  43. package/tests/primitives/Array.test.ts +418 -0
  44. package/tests/primitives/Boolean.test.ts +126 -0
  45. package/tests/primitives/Lazy.test.ts +143 -0
  46. package/tests/primitives/Literal.test.ts +122 -0
  47. package/tests/primitives/Number.test.ts +133 -0
  48. package/tests/primitives/String.test.ts +128 -0
  49. package/tests/primitives/Struct.test.ts +311 -0
  50. package/tests/primitives/Tree.test.ts +467 -0
  51. package/tests/primitives/TreeNode.test.ts +50 -0
  52. package/tests/primitives/Union.test.ts +210 -0
  53. package/tests/server/ServerDocument.test.ts +528 -0
  54. package/tsconfig.build.json +24 -0
  55. package/tsconfig.json +8 -0
  56. package/tsdown.config.ts +18 -0
  57. package/vitest.mts +11 -0
@@ -0,0 +1,1249 @@
1
+ import { Effect, Random } from "effect"
2
+
3
+ // ============================================================================
4
+ // Types and Interfaces
5
+ // ============================================================================
6
+
7
+ export interface IndexCharacterSetOptions {
8
+ chars: string // sorted string of unique characters like "0123456789ABC"
9
+ jitterRange?: number // default is 1/5 of the total range created by adding 3 characters
10
+ firstPositive?: string // default is the middle character
11
+ mostPositive?: string // default is the last character
12
+ mostNegative?: string // default is the first character
13
+ }
14
+
15
+ export interface IndexedCharacterSet {
16
+ chars: string
17
+ byChar: Record<string, number>
18
+ byCode: Record<number, string>
19
+ paddingDict: Record<number, number>
20
+ length: number
21
+ first: string
22
+ last: string
23
+ firstPositive: string
24
+ mostPositive: string
25
+ firstNegative: string
26
+ mostNegative: string
27
+ jitterRange: number
28
+ }
29
+
30
+ export type IntegerLimits = {
31
+ firstPositive: string
32
+ mostPositive: string
33
+ firstNegative: string
34
+ mostNegative: string
35
+ }
36
+
37
+ export interface GeneratorOptions {
38
+ charSet?: IndexedCharacterSet
39
+ useJitter?: boolean
40
+ groupIdLength?: number
41
+ }
42
+
43
+ // ============================================================================
44
+ // Character Set Functions
45
+ // ============================================================================
46
+
47
+ type CharSetDicts = {
48
+ byCode: Record<number, string>
49
+ byChar: Record<string, number>
50
+ length: number
51
+ }
52
+
53
+ function createCharSetDicts(charSet: string): CharSetDicts {
54
+ const byCode: Record<number, string> = {}
55
+ const byChar: Record<string, number> = {}
56
+ const length = charSet.length
57
+
58
+ for (let i = 0; i < length; i++) {
59
+ const char = charSet[i]
60
+ if (char === undefined) {
61
+ throw new Error("invalid charSet: missing character at index " + i)
62
+ }
63
+ byCode[i] = char
64
+ byChar[char] = i
65
+ }
66
+ return {
67
+ byCode: byCode,
68
+ byChar: byChar,
69
+ length: length,
70
+ }
71
+ }
72
+
73
+ function integerLimits(
74
+ dicts: CharSetDicts,
75
+ firstPositive?: string,
76
+ mostPositive?: string,
77
+ mostNegative?: string
78
+ ): Effect.Effect<IntegerLimits, Error> {
79
+ return Effect.gen(function* () {
80
+ const firstPositiveIndex = firstPositive
81
+ ? dicts.byChar[firstPositive]
82
+ : Math.ceil(dicts.length / 2)
83
+ const mostPositiveIndex = mostPositive
84
+ ? dicts.byChar[mostPositive]
85
+ : dicts.length - 1
86
+ const mostNegativeIndex = mostNegative ? dicts.byChar[mostNegative] : 0
87
+
88
+ if (
89
+ firstPositiveIndex === undefined ||
90
+ mostPositiveIndex === undefined ||
91
+ mostNegativeIndex === undefined
92
+ ) {
93
+ return yield* Effect.fail(new Error("invalid charSet"))
94
+ }
95
+ if (mostPositiveIndex - firstPositiveIndex < 3) {
96
+ return yield* Effect.fail(
97
+ new Error("mostPositive must be at least 3 characters away from neutral")
98
+ )
99
+ }
100
+ if (firstPositiveIndex - mostNegativeIndex < 3) {
101
+ return yield* Effect.fail(
102
+ new Error("mostNegative must be at least 3 characters away from neutral")
103
+ )
104
+ }
105
+
106
+ const firstPositiveChar = dicts.byCode[firstPositiveIndex]
107
+ const mostPositiveChar = dicts.byCode[mostPositiveIndex]
108
+ const firstNegativeChar = dicts.byCode[firstPositiveIndex - 1]
109
+ const mostNegativeChar = dicts.byCode[mostNegativeIndex]
110
+
111
+ if (
112
+ firstPositiveChar === undefined ||
113
+ mostPositiveChar === undefined ||
114
+ firstNegativeChar === undefined ||
115
+ mostNegativeChar === undefined
116
+ ) {
117
+ return yield* Effect.fail(new Error("invalid charSet"))
118
+ }
119
+
120
+ return {
121
+ firstPositive: firstPositiveChar,
122
+ mostPositive: mostPositiveChar,
123
+ firstNegative: firstNegativeChar,
124
+ mostNegative: mostNegativeChar,
125
+ }
126
+ })
127
+ }
128
+
129
+ function paddingDict(jitterRange: number, charSetLength: number): Record<number, number> {
130
+ const paddingDict: Record<number, number> = {}
131
+ for (let i = 0; i < 100; i++) {
132
+ const value = Math.pow(charSetLength, i)
133
+ paddingDict[i] = value
134
+ if (value > jitterRange) {
135
+ break
136
+ }
137
+ }
138
+ return paddingDict
139
+ }
140
+
141
+ export function validateChars(characters: string): Effect.Effect<void, Error> {
142
+ if (characters.length < 7) {
143
+ return Effect.fail(new Error("charSet must be at least 7 characters long"))
144
+ }
145
+ const chars = characters.split("")
146
+ const sorted = chars.sort()
147
+ const isEqual = sorted.join("") === characters
148
+ if (!isEqual) {
149
+ return Effect.fail(new Error("charSet must be sorted"))
150
+ }
151
+ return Effect.void
152
+ }
153
+
154
+ export function indexCharacterSet(
155
+ options: IndexCharacterSetOptions
156
+ ): Effect.Effect<IndexedCharacterSet, Error> {
157
+ return Effect.gen(function* () {
158
+ yield* validateChars(options.chars)
159
+ const dicts = createCharSetDicts(options.chars)
160
+ const limits = yield* integerLimits(
161
+ dicts,
162
+ options.firstPositive,
163
+ options.mostPositive,
164
+ options.mostNegative
165
+ )
166
+ // 1/5 of the total range if we add 3 characters, TODO: feels a bit arbitrary and could be improved
167
+ const jitterRange =
168
+ options.jitterRange ?? Math.floor(Math.pow(dicts.length, 3) / 5)
169
+
170
+ const paddingRange = paddingDict(jitterRange, dicts.length)
171
+
172
+ const first = dicts.byCode[0]
173
+ const last = dicts.byCode[dicts.length - 1]
174
+
175
+ if (first === undefined || last === undefined) {
176
+ return yield* Effect.fail(new Error("invalid charSet"))
177
+ }
178
+
179
+ return {
180
+ chars: options.chars,
181
+ byChar: dicts.byChar,
182
+ byCode: dicts.byCode,
183
+ length: dicts.length,
184
+ first,
185
+ last,
186
+ firstPositive: limits.firstPositive,
187
+ mostPositive: limits.mostPositive,
188
+ firstNegative: limits.firstNegative,
189
+ mostNegative: limits.mostNegative,
190
+ jitterRange,
191
+ paddingDict: paddingRange,
192
+ }
193
+ })
194
+ }
195
+
196
+ // cache the base62 charSet since it's the default
197
+ let _base62CharSet: IndexedCharacterSet | null = null
198
+
199
+ export function base62CharSet(): IndexedCharacterSet {
200
+ if (_base62CharSet) return _base62CharSet
201
+ // We use Effect.runSync here because base62CharSet is a synchronous API
202
+ // and we know the parameters are valid
203
+ _base62CharSet = Effect.runSync(
204
+ indexCharacterSet({
205
+ // Base62 are all the alphanumeric characters, database and user friendly
206
+ // For shorter strings and more room you could opt for more characters
207
+ chars: "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
208
+ // This gives us nice human readable keys to start with a0 a1 etc
209
+ firstPositive: "a",
210
+ mostPositive: "z",
211
+ mostNegative: "A",
212
+ })
213
+ )
214
+ return _base62CharSet
215
+ }
216
+
217
+ // ============================================================================
218
+ // Padding Functions
219
+ // ============================================================================
220
+
221
+ export function makeSameLength(
222
+ a: string,
223
+ b: string,
224
+ pad: "start" | "end",
225
+ fillChar: string,
226
+ forceLength?: number
227
+ ): [string, string] {
228
+ const max = forceLength ?? Math.max(a.length, b.length)
229
+ if (pad === "start") {
230
+ return [a.padStart(max, fillChar), b.padStart(max, fillChar)]
231
+ }
232
+ return [a.padEnd(max, fillChar), b.padEnd(max, fillChar)]
233
+ }
234
+
235
+ // ============================================================================
236
+ // Integer Length Functions
237
+ // ============================================================================
238
+
239
+ function distanceBetween(
240
+ a: string,
241
+ b: string,
242
+ charSet: IndexedCharacterSet
243
+ ): Effect.Effect<number, Error> {
244
+ const indexA = charSet.byChar[a]
245
+ const indexB = charSet.byChar[b]
246
+ if (indexA === undefined || indexB === undefined) {
247
+ return Effect.fail(new Error("invalid character in distance calculation"))
248
+ }
249
+ return Effect.succeed(Math.abs(indexA - indexB))
250
+ }
251
+
252
+ function integerLengthFromSecondLevel(
253
+ key: string,
254
+ direction: "positive" | "negative",
255
+ charSet: IndexedCharacterSet
256
+ ): Effect.Effect<number, Error> {
257
+ if (key.length === 0) {
258
+ return Effect.succeed(0)
259
+ }
260
+ const firstChar = key[0]
261
+ if (!firstChar || firstChar > charSet.mostPositive || firstChar < charSet.mostNegative) {
262
+ return Effect.fail(new Error("invalid firstChar on key"))
263
+ }
264
+ if (firstChar === charSet.mostPositive && direction === "positive") {
265
+ return Effect.gen(function* () {
266
+ const totalPositiveRoom = yield* distanceBetween(firstChar, charSet.mostNegative, charSet)
267
+ const rest = yield* integerLengthFromSecondLevel(key.slice(1), direction, charSet)
268
+ return totalPositiveRoom + 1 + rest
269
+ })
270
+ }
271
+ if (firstChar === charSet.mostNegative && direction === "negative") {
272
+ return Effect.gen(function* () {
273
+ const totalNegativeRoom = yield* distanceBetween(firstChar, charSet.mostPositive, charSet)
274
+ const rest = yield* integerLengthFromSecondLevel(key.slice(1), direction, charSet)
275
+ return totalNegativeRoom + 1 + rest
276
+ })
277
+ }
278
+ if (direction === "positive") {
279
+ return Effect.gen(function* () {
280
+ const dist = yield* distanceBetween(firstChar, charSet.mostNegative, charSet)
281
+ return dist + 2
282
+ })
283
+ } else {
284
+ return Effect.gen(function* () {
285
+ const dist = yield* distanceBetween(firstChar, charSet.mostPositive, charSet)
286
+ return dist + 2
287
+ })
288
+ }
289
+ }
290
+
291
+ export function integerLength(
292
+ head: string,
293
+ charSet: IndexedCharacterSet
294
+ ): Effect.Effect<number, Error> {
295
+ if (head.length === 0) {
296
+ return Effect.fail(new Error("head cannot be empty"))
297
+ }
298
+ const firstChar = head[0]
299
+ if (!firstChar || firstChar > charSet.mostPositive || firstChar < charSet.mostNegative) {
300
+ return Effect.fail(new Error("invalid firstChar on key"))
301
+ }
302
+ if (firstChar === charSet.mostPositive) {
303
+ return Effect.gen(function* () {
304
+ const firstLevel = yield* distanceBetween(firstChar, charSet.firstPositive, charSet)
305
+ const rest = yield* integerLengthFromSecondLevel(head.slice(1), "positive", charSet)
306
+ return firstLevel + 1 + rest
307
+ })
308
+ }
309
+ if (firstChar === charSet.mostNegative) {
310
+ return Effect.gen(function* () {
311
+ const firstLevel = yield* distanceBetween(firstChar, charSet.firstNegative, charSet)
312
+ const rest = yield* integerLengthFromSecondLevel(head.slice(1), "negative", charSet)
313
+ return firstLevel + 1 + rest
314
+ })
315
+ }
316
+ const isPositiveRange = firstChar >= charSet.firstPositive
317
+ if (isPositiveRange) {
318
+ return Effect.gen(function* () {
319
+ const dist = yield* distanceBetween(firstChar, charSet.firstPositive, charSet)
320
+ return dist + 2
321
+ })
322
+ } else {
323
+ return Effect.gen(function* () {
324
+ const dist = yield* distanceBetween(firstChar, charSet.firstNegative, charSet)
325
+ return dist + 2
326
+ })
327
+ }
328
+ }
329
+
330
+ // ============================================================================
331
+ // Key as Number Functions
332
+ // ============================================================================
333
+
334
+ export function encodeToCharSet(int: number, charSet: IndexedCharacterSet): Effect.Effect<string, Error> {
335
+ if (int === 0) {
336
+ const zero = charSet.byCode[0]
337
+ if (zero === undefined) {
338
+ return Effect.fail(new Error("invalid charSet: missing code 0"))
339
+ }
340
+ return Effect.succeed(zero)
341
+ }
342
+ let res = ""
343
+ const max = charSet.length
344
+ while (int > 0) {
345
+ const code = charSet.byCode[int % max]
346
+ if (code === undefined) {
347
+ return Effect.fail(new Error("invalid character code in encodeToCharSet"))
348
+ }
349
+ res = code + res
350
+ int = Math.floor(int / max)
351
+ }
352
+ return Effect.succeed(res)
353
+ }
354
+
355
+ export function decodeCharSetToNumber(
356
+ key: string,
357
+ charSet: IndexedCharacterSet
358
+ ): number {
359
+ let res = 0
360
+ const length = key.length
361
+ const max = charSet.length
362
+ for (let i = 0; i < length; i++) {
363
+ const char = key[i]
364
+ if (char === undefined) {
365
+ continue
366
+ }
367
+ const charIndex = charSet.byChar[char]
368
+ if (charIndex === undefined) {
369
+ continue
370
+ }
371
+ res += charIndex * Math.pow(max, length - i - 1)
372
+ }
373
+ return res
374
+ }
375
+
376
+ export function addCharSetKeys(
377
+ a: string,
378
+ b: string,
379
+ charSet: IndexedCharacterSet
380
+ ): Effect.Effect<string, Error> {
381
+ const base = charSet.length
382
+ const [paddedA, paddedB] = makeSameLength(a, b, "start", charSet.first)
383
+
384
+ const result: string[] = []
385
+ let carry = 0
386
+
387
+ // Iterate over the digits from right to left
388
+ for (let i = paddedA.length - 1; i >= 0; i--) {
389
+ const charA = paddedA[i]
390
+ const charB = paddedB[i]
391
+ if (!charA || !charB) {
392
+ return Effect.fail(new Error("invalid character in addCharSetKeys"))
393
+ }
394
+ const digitA = charSet.byChar[charA]
395
+ const digitB = charSet.byChar[charB]
396
+ if (digitA === undefined || digitB === undefined) {
397
+ return Effect.fail(new Error("invalid character in addCharSetKeys"))
398
+ }
399
+ const sum = digitA + digitB + carry
400
+ carry = Math.floor(sum / base)
401
+ const remainder = sum % base
402
+
403
+ const codeChar = charSet.byCode[remainder]
404
+ if (codeChar === undefined) {
405
+ return Effect.fail(new Error("invalid character code in addCharSetKeys"))
406
+ }
407
+ result.unshift(codeChar)
408
+ }
409
+
410
+ // If there's a carry left, add it to the result
411
+ if (carry > 0) {
412
+ const carryChar = charSet.byCode[carry]
413
+ if (carryChar === undefined) {
414
+ return Effect.fail(new Error("invalid carry character code"))
415
+ }
416
+ result.unshift(carryChar)
417
+ }
418
+
419
+ return Effect.succeed(result.join(""))
420
+ }
421
+
422
+ export function subtractCharSetKeys(
423
+ a: string,
424
+ b: string,
425
+ charSet: IndexedCharacterSet,
426
+ stripLeadingZeros = true
427
+ ): Effect.Effect<string, Error> {
428
+ const base = charSet.length
429
+ const [paddedA, paddedB] = makeSameLength(a, b, "start", charSet.first)
430
+
431
+ const result: string[] = []
432
+ let borrow = 0
433
+
434
+ // Iterate over the digits from right to left
435
+ for (let i = paddedA.length - 1; i >= 0; i--) {
436
+ const charA = paddedA[i]
437
+ const charB = paddedB[i]
438
+ if (!charA || !charB) {
439
+ return Effect.fail(new Error("invalid character in subtractCharSetKeys"))
440
+ }
441
+ let digitA = charSet.byChar[charA]
442
+ const digitBValue = charSet.byChar[charB]
443
+ if (digitA === undefined || digitBValue === undefined) {
444
+ return Effect.fail(new Error("invalid character in subtractCharSetKeys"))
445
+ }
446
+ const digitB = digitBValue + borrow
447
+
448
+ // Handle borrowing
449
+ if (digitA < digitB) {
450
+ borrow = 1
451
+ digitA += base
452
+ } else {
453
+ borrow = 0
454
+ }
455
+
456
+ const difference = digitA - digitB
457
+ const codeChar = charSet.byCode[difference]
458
+ if (codeChar === undefined) {
459
+ return Effect.fail(new Error("invalid character code in subtractCharSetKeys"))
460
+ }
461
+ result.unshift(codeChar)
462
+ }
463
+
464
+ // If there's a borrow left, we have a negative result, which is not supported
465
+ if (borrow > 0) {
466
+ return Effect.fail(
467
+ new Error("Subtraction result is negative. Ensure a is greater than or equal to b.")
468
+ )
469
+ }
470
+
471
+ // Remove leading zeros
472
+ while (
473
+ stripLeadingZeros &&
474
+ result.length > 1 &&
475
+ result[0] === charSet.first
476
+ ) {
477
+ result.shift()
478
+ }
479
+
480
+ return Effect.succeed(result.join(""))
481
+ }
482
+
483
+ export function incrementKey(key: string, charSet: IndexedCharacterSet): Effect.Effect<string, Error> {
484
+ const one = charSet.byCode[1]
485
+ if (one === undefined) {
486
+ return Effect.fail(new Error("invalid charSet: missing code 1"))
487
+ }
488
+ return addCharSetKeys(key, one, charSet)
489
+ }
490
+
491
+ export function decrementKey(key: string, charSet: IndexedCharacterSet): Effect.Effect<string, Error> {
492
+ // we should not strip leading zeros here, this will break the sorting if the key already has leading zeros
493
+ const one = charSet.byCode[1]
494
+ if (one === undefined) {
495
+ return Effect.fail(new Error("invalid charSet: missing code 1"))
496
+ }
497
+ return subtractCharSetKeys(key, one, charSet, false)
498
+ }
499
+
500
+ export function lexicalDistance(
501
+ a: string,
502
+ b: string,
503
+ charSet: IndexedCharacterSet
504
+ ): Effect.Effect<number, Error> {
505
+ const [lower, upper] = makeSameLength(a, b, "end", charSet.first).sort()
506
+ return Effect.gen(function* () {
507
+ const distance = yield* subtractCharSetKeys(upper, lower, charSet)
508
+ return decodeCharSetToNumber(distance, charSet)
509
+ })
510
+ }
511
+
512
+ export function midPoint(
513
+ lower: string,
514
+ upper: string,
515
+ charSet: IndexedCharacterSet
516
+ ): Effect.Effect<string, Error> {
517
+ return Effect.gen(function* () {
518
+ let [paddedLower, paddedUpper] = makeSameLength(
519
+ lower,
520
+ upper,
521
+ "end",
522
+ charSet.first
523
+ )
524
+ let distance = yield* lexicalDistance(paddedLower, paddedUpper, charSet)
525
+ if (distance === 1) {
526
+ // if the numbers are consecutive we need more padding
527
+ paddedLower = paddedLower.padEnd(paddedLower.length + 1, charSet.first)
528
+ // the new distance will always be the length of the charSet
529
+ distance = charSet.length
530
+ }
531
+ const mid = yield* encodeToCharSet(Math.floor(distance / 2), charSet)
532
+ return yield* addCharSetKeys(paddedLower, mid, charSet)
533
+ })
534
+ }
535
+
536
+ // ============================================================================
537
+ // Integer Functions
538
+ // ============================================================================
539
+
540
+ export function startKey(charSet: IndexedCharacterSet): string {
541
+ return charSet.firstPositive + charSet.byCode[0]
542
+ }
543
+
544
+ export function validInteger(integer: string, charSet: IndexedCharacterSet): Effect.Effect<boolean, Error> {
545
+ return Effect.gen(function* () {
546
+ const length = yield* integerLength(integer, charSet)
547
+ return length === integer.length
548
+ })
549
+ }
550
+
551
+ export function validateOrderKey(
552
+ orderKey: string,
553
+ charSet: IndexedCharacterSet
554
+ ): Effect.Effect<void, Error> {
555
+ return Effect.gen(function* () {
556
+ yield* getIntegerPart(orderKey, charSet)
557
+ })
558
+ }
559
+
560
+ export function getIntegerPart(
561
+ orderKey: string,
562
+ charSet: IndexedCharacterSet
563
+ ): Effect.Effect<string, Error> {
564
+ return Effect.gen(function* () {
565
+ const head = integerHead(orderKey, charSet)
566
+ const integerPartLength = yield* integerLength(head, charSet)
567
+ if (integerPartLength > orderKey.length) {
568
+ return yield* Effect.fail(new Error("invalid order key length: " + orderKey))
569
+ }
570
+ return orderKey.slice(0, integerPartLength)
571
+ })
572
+ }
573
+
574
+ function validateInteger(integer: string, charSet: IndexedCharacterSet): Effect.Effect<void, Error> {
575
+ return Effect.gen(function* () {
576
+ const isValid = yield* validInteger(integer, charSet)
577
+ if (!isValid) {
578
+ return yield* Effect.fail(new Error("invalid integer length: " + integer))
579
+ }
580
+ })
581
+ }
582
+
583
+ export function integerHead(integer: string, charSet: IntegerLimits): string {
584
+ let i = 0
585
+ if (integer[0] === charSet.mostPositive) {
586
+ while (integer[i] === charSet.mostPositive) {
587
+ i = i + 1
588
+ }
589
+ }
590
+ if (integer[0] === charSet.mostNegative) {
591
+ while (integer[i] === charSet.mostNegative) {
592
+ i = i + 1
593
+ }
594
+ }
595
+ return integer.slice(0, i + 1)
596
+ }
597
+
598
+ export function splitInteger(
599
+ integer: string,
600
+ charSet: IndexedCharacterSet
601
+ ): Effect.Effect<[string, string], Error> {
602
+ return Effect.gen(function* () {
603
+ // We need to get the limits from the charSet
604
+ const head = integerHead(integer, {
605
+ firstPositive: charSet.firstPositive,
606
+ mostPositive: charSet.mostPositive,
607
+ firstNegative: charSet.firstNegative,
608
+ mostNegative: charSet.mostNegative,
609
+ })
610
+ const tail = integer.slice(head.length)
611
+ return [head, tail] as [string, string]
612
+ })
613
+ }
614
+
615
+ export function incrementIntegerHead(
616
+ head: string,
617
+ charSet: IndexedCharacterSet
618
+ ): Effect.Effect<string, Error> {
619
+ return Effect.gen(function* () {
620
+ const inPositiveRange = head >= charSet.firstPositive
621
+ const nextHead = yield* incrementKey(head, charSet)
622
+ const headIsLimitMax = head[head.length - 1] === charSet.mostPositive
623
+ const nextHeadIsLimitMax =
624
+ nextHead[nextHead.length - 1] === charSet.mostPositive
625
+
626
+ // we can not leave the head on the limit value, we have no way to know where the head ends
627
+ if (inPositiveRange && nextHeadIsLimitMax) {
628
+ return nextHead + charSet.mostNegative
629
+ }
630
+ // we are already at the limit of this level, so we need to go up a level
631
+ if (!inPositiveRange && headIsLimitMax) {
632
+ return head.slice(0, head.length - 1)
633
+ }
634
+ return nextHead
635
+ })
636
+ }
637
+
638
+ export function decrementIntegerHead(
639
+ head: string,
640
+ charSet: IndexedCharacterSet
641
+ ): Effect.Effect<string, Error> {
642
+ return Effect.gen(function* () {
643
+ const inPositiveRange = head >= charSet.firstPositive
644
+ const headIsLimitMin = head[head.length - 1] === charSet.mostNegative
645
+ if (inPositiveRange && headIsLimitMin) {
646
+ const nextLevel = head.slice(0, head.length - 1)
647
+ // we can not leave the head on the limit value, we have no way to know where the head ends
648
+ // so we take one extra step down
649
+ const decremented = yield* decrementKey(nextLevel, charSet)
650
+ return decremented
651
+ }
652
+
653
+ if (!inPositiveRange && headIsLimitMin) {
654
+ return head + charSet.mostPositive
655
+ }
656
+
657
+ return yield* decrementKey(head, charSet)
658
+ })
659
+ }
660
+
661
+ function startOnNewHead(
662
+ head: string,
663
+ limit: "upper" | "lower",
664
+ charSet: IndexedCharacterSet
665
+ ): Effect.Effect<string, Error> {
666
+ return Effect.gen(function* () {
667
+ const newLength = yield* integerLength(head, charSet)
668
+ const fillCharCode = limit === "upper" ? charSet.length - 1 : 0
669
+ const fillChar = charSet.byCode[fillCharCode]
670
+ if (fillChar === undefined) {
671
+ return yield* Effect.fail(new Error("invalid fill character code"))
672
+ }
673
+ return head + fillChar.repeat(newLength - head.length)
674
+ })
675
+ }
676
+
677
+ export function incrementInteger(
678
+ integer: string,
679
+ charSet: IndexedCharacterSet
680
+ ): Effect.Effect<string, Error> {
681
+ return Effect.gen(function* () {
682
+ yield* validateInteger(integer, charSet)
683
+ const [head, digs] = yield* splitInteger(integer, charSet)
684
+ const maxChar = charSet.byCode[charSet.length - 1]
685
+ if (maxChar === undefined) {
686
+ return yield* Effect.fail(new Error("invalid charSet: missing max character"))
687
+ }
688
+ const anyNonMaxedDigit = digs
689
+ .split("")
690
+ .some((d) => d !== maxChar)
691
+
692
+ // we have room to increment
693
+ if (anyNonMaxedDigit) {
694
+ const newDigits = yield* incrementKey(digs, charSet)
695
+ return head + newDigits
696
+ }
697
+ const nextHead = yield* incrementIntegerHead(head, charSet)
698
+ return yield* startOnNewHead(nextHead, "lower", charSet)
699
+ })
700
+ }
701
+
702
+ export function decrementInteger(
703
+ integer: string,
704
+ charSet: IndexedCharacterSet
705
+ ): Effect.Effect<string, Error> {
706
+ return Effect.gen(function* () {
707
+ yield* validateInteger(integer, charSet)
708
+ const [head, digs] = yield* splitInteger(integer, charSet)
709
+ const minChar = charSet.byCode[0]
710
+ if (minChar === undefined) {
711
+ return yield* Effect.fail(new Error("invalid charSet: missing min character"))
712
+ }
713
+ const anyNonLimitDigit = digs.split("").some((d) => d !== minChar)
714
+
715
+ // we have room to decrement
716
+ if (anyNonLimitDigit) {
717
+ const newDigits = yield* decrementKey(digs, charSet)
718
+ return head + newDigits
719
+ }
720
+ const nextHead = yield* decrementIntegerHead(head, charSet)
721
+ return yield* startOnNewHead(nextHead, "upper", charSet)
722
+ })
723
+ }
724
+
725
+ // ============================================================================
726
+ // Jittering Functions
727
+ // ============================================================================
728
+
729
+ export function jitterString(
730
+ orderKey: string,
731
+ charSet: IndexedCharacterSet
732
+ ): Effect.Effect<string, Error, Random.Random> {
733
+ return Effect.gen(function* () {
734
+ const randomValue = yield* Random.next
735
+ const shift = yield* encodeToCharSet(
736
+ Math.floor(randomValue * charSet.jitterRange),
737
+ charSet
738
+ )
739
+ return yield* addCharSetKeys(orderKey, shift, charSet)
740
+ })
741
+ }
742
+
743
+ export function padAndJitterString(
744
+ orderKey: string,
745
+ numberOfChars: number,
746
+ charSet: IndexedCharacterSet
747
+ ): Effect.Effect<string, Error, Random.Random> {
748
+ return Effect.gen(function* () {
749
+ const paddedKey = orderKey.padEnd(
750
+ orderKey.length + numberOfChars,
751
+ charSet.first
752
+ )
753
+ return yield* jitterString(paddedKey, charSet)
754
+ })
755
+ }
756
+
757
+ export function paddingNeededForDistance(
758
+ distance: number,
759
+ charSet: IndexedCharacterSet
760
+ ): number {
761
+ const gap = charSet.jitterRange - distance
762
+ const firstBigger = Object.entries(charSet.paddingDict).find(
763
+ ([_key, value]) => {
764
+ return value > gap
765
+ }
766
+ )
767
+
768
+ return firstBigger ? parseInt(firstBigger[0]) : 0
769
+ }
770
+
771
+ export function paddingNeededForJitter(
772
+ orderKey: string,
773
+ b: string | null,
774
+ charSet: IndexedCharacterSet
775
+ ): Effect.Effect<number, Error> {
776
+ return Effect.gen(function* () {
777
+ const integer = yield* getIntegerPart(orderKey, charSet)
778
+ const nextInteger = yield* incrementInteger(integer, charSet)
779
+ let needed = 0
780
+ if (b !== null) {
781
+ const distanceToB = yield* lexicalDistance(orderKey, b, charSet)
782
+ if (distanceToB < charSet.jitterRange + 1) {
783
+ needed = Math.max(needed, paddingNeededForDistance(distanceToB, charSet))
784
+ }
785
+ }
786
+ const distanceToNextInteger = yield* lexicalDistance(orderKey, nextInteger, charSet)
787
+ if (distanceToNextInteger < charSet.jitterRange + 1) {
788
+ needed = Math.max(
789
+ needed,
790
+ paddingNeededForDistance(distanceToNextInteger, charSet)
791
+ )
792
+ }
793
+
794
+ return needed
795
+ })
796
+ }
797
+
798
+ // ============================================================================
799
+ // Key Generation Functions
800
+ // ============================================================================
801
+
802
+ /**
803
+ * Generate a key between two other keys.
804
+ * If either lower or upper is null, the key will be generated at the start or end of the list.
805
+ */
806
+ export function generateKeyBetween(
807
+ lower: string | null,
808
+ upper: string | null,
809
+ charSet: IndexedCharacterSet = base62CharSet()
810
+ ): Effect.Effect<string, Error> {
811
+ return Effect.gen(function* () {
812
+ if (lower !== null) {
813
+ yield* validateOrderKey(lower, charSet)
814
+ }
815
+ if (upper !== null) {
816
+ yield* validateOrderKey(upper, charSet)
817
+ }
818
+ if (lower === null && upper === null) {
819
+ return startKey(charSet)
820
+ }
821
+ if (lower === null) {
822
+ const integer = yield* getIntegerPart(upper!, charSet)
823
+ return yield* decrementInteger(integer, charSet)
824
+ }
825
+ if (upper === null) {
826
+ const integer = yield* getIntegerPart(lower, charSet)
827
+ return yield* incrementInteger(integer, charSet)
828
+ }
829
+ if (lower >= upper) {
830
+ return yield* Effect.fail(new Error(lower + " >= " + upper))
831
+ }
832
+ return yield* midPoint(lower, upper, charSet)
833
+ })
834
+ }
835
+
836
+ type GenerateKeyBetweenFunc = (
837
+ lower: string | null,
838
+ upper: string | null,
839
+ charSet?: IndexedCharacterSet
840
+ ) => Effect.Effect<string, Error>
841
+
842
+ type GenerateNKeysBetweenFunc = (
843
+ lower: string | null,
844
+ upper: string | null,
845
+ n: number,
846
+ charSet?: IndexedCharacterSet
847
+ ) => Effect.Effect<string[], Error>
848
+
849
+ function spreadGeneratorResults(
850
+ lower: string | null,
851
+ upper: string | null,
852
+ n: number,
853
+ charSet: IndexedCharacterSet,
854
+ generateKey: GenerateKeyBetweenFunc,
855
+ generateNKeys: GenerateNKeysBetweenFunc
856
+ ): Effect.Effect<string[], Error> {
857
+ if (n === 0) {
858
+ return Effect.succeed([])
859
+ }
860
+ if (n === 1) {
861
+ return generateKey(lower, upper, charSet).pipe(Effect.map((key) => [key]))
862
+ }
863
+ if (upper == null) {
864
+ return Effect.gen(function* () {
865
+ let newUpper = yield* generateKey(lower, upper, charSet)
866
+ const result = [newUpper]
867
+ for (let i = 0; i < n - 1; i++) {
868
+ newUpper = yield* generateKey(newUpper, upper, charSet)
869
+ result.push(newUpper)
870
+ }
871
+ return result
872
+ })
873
+ }
874
+ if (lower == null) {
875
+ return Effect.gen(function* () {
876
+ let newLower = yield* generateKey(lower, upper, charSet)
877
+ const result = [newLower]
878
+ for (let i = 0; i < n - 1; i++) {
879
+ newLower = yield* generateKey(lower, newLower, charSet)
880
+ result.push(newLower)
881
+ }
882
+ result.reverse()
883
+ return result
884
+ })
885
+ }
886
+ return Effect.gen(function* () {
887
+ const mid = Math.floor(n / 2)
888
+ const midOrderKey = yield* generateKey(lower, upper, charSet)
889
+ const leftKeys = yield* generateNKeys(lower, midOrderKey, mid, charSet)
890
+ const rightKeys = yield* generateNKeys(midOrderKey, upper, n - mid - 1, charSet)
891
+ return [...leftKeys, midOrderKey, ...rightKeys]
892
+ })
893
+ }
894
+
895
+ /**
896
+ * Generate any number of keys between two other keys.
897
+ * If either lower or upper is null, the keys will be generated at the start or end of the list.
898
+ */
899
+ export function generateNKeysBetween(
900
+ a: string | null,
901
+ b: string | null,
902
+ n: number,
903
+ charSet: IndexedCharacterSet = base62CharSet()
904
+ ): Effect.Effect<string[], Error> {
905
+ return spreadGeneratorResults(
906
+ a,
907
+ b,
908
+ n,
909
+ charSet,
910
+ (lower, upper, charSet = base62CharSet()) => generateKeyBetween(lower, upper, charSet),
911
+ (lower, upper, n, charSet = base62CharSet()) => generateNKeysBetween(lower, upper, n, charSet)
912
+ )
913
+ }
914
+
915
+ /**
916
+ * Generate a key between two other keys with jitter.
917
+ * If either lower or upper is null, the key will be generated at the start or end of the list.
918
+ */
919
+ export function generateJitteredKeyBetween(
920
+ lower: string | null,
921
+ upper: string | null,
922
+ charSet: IndexedCharacterSet = base62CharSet()
923
+ ): Effect.Effect<string, Error, Random.Random> {
924
+ return Effect.gen(function* () {
925
+ const key = yield* generateKeyBetween(lower, upper, charSet)
926
+ const paddingNeeded = yield* paddingNeededForJitter(key, upper, charSet)
927
+ if (paddingNeeded) {
928
+ return yield* padAndJitterString(key, paddingNeeded, charSet)
929
+ }
930
+ return yield* jitterString(key, charSet)
931
+ })
932
+ }
933
+
934
+ /**
935
+ * Generate any number of keys between two other keys with jitter.
936
+ * If either lower or upper is null, the keys will be generated at the start or end of the list.
937
+ */
938
+ export function generateNJitteredKeysBetween(
939
+ lower: string | null,
940
+ upper: string | null,
941
+ n: number,
942
+ charSet: IndexedCharacterSet = base62CharSet()
943
+ ): Effect.Effect<string[], Error, Random.Random> {
944
+ return Effect.gen(function* () {
945
+ if (n === 0) {
946
+ return []
947
+ }
948
+ if (n === 1) {
949
+ const key = yield* generateJitteredKeyBetween(lower, upper, charSet)
950
+ return [key]
951
+ }
952
+ if (upper == null) {
953
+ let newUpper = yield* generateJitteredKeyBetween(lower, upper, charSet)
954
+ const result = [newUpper]
955
+ for (let i = 0; i < n - 1; i++) {
956
+ newUpper = yield* generateJitteredKeyBetween(newUpper, upper, charSet)
957
+ result.push(newUpper)
958
+ }
959
+ return result
960
+ }
961
+ if (lower == null) {
962
+ let newLower = yield* generateJitteredKeyBetween(lower, upper, charSet)
963
+ const result = [newLower]
964
+ for (let i = 0; i < n - 1; i++) {
965
+ newLower = yield* generateJitteredKeyBetween(lower, newLower, charSet)
966
+ result.push(newLower)
967
+ }
968
+ result.reverse()
969
+ return result
970
+ }
971
+ const mid = Math.floor(n / 2)
972
+ const midOrderKey = yield* generateJitteredKeyBetween(lower, upper, charSet)
973
+ const leftKeys = yield* generateNJitteredKeysBetween(lower, midOrderKey, mid, charSet)
974
+ const rightKeys = yield* generateNJitteredKeysBetween(midOrderKey, upper, n - mid - 1, charSet)
975
+ return [...leftKeys, midOrderKey, ...rightKeys]
976
+ })
977
+ }
978
+
979
+ // ============================================================================
980
+ // Index Generator Class
981
+ // ============================================================================
982
+
983
+ export class IndexGenerator {
984
+ private charSet: IndexedCharacterSet
985
+ private useJitter: boolean
986
+ private list: string[]
987
+ private useGroups: boolean
988
+ private groupIdLength: number
989
+
990
+ constructor(list: string[], options: GeneratorOptions = {}) {
991
+ this.charSet = options.charSet ?? base62CharSet()
992
+ this.useJitter = options.useJitter ?? true
993
+ this.list = list
994
+ this.useGroups = !!options.groupIdLength && options.groupIdLength > 0
995
+ this.groupIdLength = options.groupIdLength ?? 0
996
+ }
997
+
998
+ /**
999
+ * Updates the list that the generator uses to generate keys.
1000
+ * The generator will not mutate the internal list when generating keys.
1001
+ */
1002
+ public updateList(list: string[]) {
1003
+ this.list = [...list].sort()
1004
+ }
1005
+
1006
+ /**
1007
+ * Generate any number of keys at the start of the list (before the first key).
1008
+ * Optionally you can supply a groupId to generate keys at the start of a specific group.
1009
+ */
1010
+ public nKeysStart(n: number, groupId?: string): Effect.Effect<string[], Error, Random.Random> {
1011
+ const self = this
1012
+ return Effect.gen(function* () {
1013
+ yield* Effect.try(() => {
1014
+ self.validateGroupId(groupId)
1015
+ })
1016
+ const firstKey = self.firstOfGroup(groupId)
1017
+ return yield* self.generateNKeysBetween(null, firstKey, n, groupId)
1018
+ })
1019
+ }
1020
+
1021
+ /**
1022
+ * Generate a single key at the start of the list (before the first key).
1023
+ * Optionally you can supply a groupId to generate a key at the start of a specific group.
1024
+ */
1025
+ public keyStart(groupId?: string): Effect.Effect<string, Error, Random.Random> {
1026
+ const self = this
1027
+ return Effect.gen(function* () {
1028
+ const keys = yield* self.nKeysStart(1, groupId)
1029
+ return keys[0]!
1030
+ })
1031
+ }
1032
+
1033
+ /**
1034
+ * Generate any number of keys at the end of the list (after the last key).
1035
+ * Optionally you can supply a groupId to generate keys at the end of a specific group.
1036
+ */
1037
+ public nKeysEnd(n: number, groupId?: string): Effect.Effect<string[], Error, Random.Random> {
1038
+ const self = this
1039
+ return Effect.gen(function* () {
1040
+ yield* Effect.try(() => {
1041
+ self.validateGroupId(groupId)
1042
+ })
1043
+ const lastKey = self.lastOfGroup(groupId)
1044
+ return yield* self.generateNKeysBetween(lastKey, null, n, groupId)
1045
+ })
1046
+ }
1047
+
1048
+ /**
1049
+ * Generate a single key at the end of the list (after the last key).
1050
+ * Optionally you can supply a groupId to generate a key at the end of a specific group.
1051
+ */
1052
+ public keyEnd(groupId?: string): Effect.Effect<string, Error, Random.Random> {
1053
+ const self = this
1054
+ return Effect.gen(function* () {
1055
+ const keys = yield* self.nKeysEnd(1, groupId)
1056
+ return keys[0]!
1057
+ })
1058
+ }
1059
+
1060
+ /**
1061
+ * Generate any number of keys behind a specific key and in front of the next key.
1062
+ * GroupId will be inferred from the orderKey if working with groups
1063
+ */
1064
+ public nKeysAfter(orderKey: string, n: number): Effect.Effect<string[], Error, Random.Random> {
1065
+ const self = this
1066
+ return Effect.gen(function* () {
1067
+ const keyAfter = yield* self.getKeyAfter(orderKey)
1068
+ return yield* self.generateNKeysBetween(orderKey, keyAfter, n, self.groupId(orderKey))
1069
+ })
1070
+ }
1071
+
1072
+ /**
1073
+ * Generate a single key behind a specific key and in front of the next key.
1074
+ * GroupId will be inferred from the orderKey if working with groups
1075
+ */
1076
+ public keyAfter(orderKey: string): Effect.Effect<string, Error, Random.Random> {
1077
+ const self = this
1078
+ return Effect.gen(function* () {
1079
+ const keys = yield* self.nKeysAfter(orderKey, 1)
1080
+ return keys[0]!
1081
+ })
1082
+ }
1083
+
1084
+ /**
1085
+ * Generate any number of keys in front of a specific key and behind the previous key.
1086
+ * GroupId will be inferred from the orderKey if working with groups
1087
+ */
1088
+ public nKeysBefore(orderKey: string, n: number): Effect.Effect<string[], Error, Random.Random> {
1089
+ const self = this
1090
+ return Effect.gen(function* () {
1091
+ const keyBefore = yield* self.getKeyBefore(orderKey)
1092
+ return yield* self.generateNKeysBetween(keyBefore, orderKey, n, self.groupId(orderKey))
1093
+ })
1094
+ }
1095
+
1096
+ /**
1097
+ * Generate a single key in front of a specific key and behind the previous key.
1098
+ * GroupId will be inferred from the orderKey if working with groups
1099
+ */
1100
+ public keyBefore(orderKey: string): Effect.Effect<string, Error, Random.Random> {
1101
+ const self = this
1102
+ return Effect.gen(function* () {
1103
+ const keys = yield* self.nKeysBefore(orderKey, 1)
1104
+ return keys[0]!
1105
+ })
1106
+ }
1107
+
1108
+ /**
1109
+ * private function responsible for calling the correct generate function
1110
+ */
1111
+ private generateNKeysBetween(
1112
+ lowerKey: string | null,
1113
+ upperKey: string | null,
1114
+ n: number,
1115
+ groupId: string | undefined
1116
+ ): Effect.Effect<string[], Error, Random.Random> {
1117
+ const self = this
1118
+ const lower = self.groupLessKey(lowerKey)
1119
+ const upper = self.groupLessKey(upperKey)
1120
+ if (self.useJitter) {
1121
+ return Effect.gen(function* () {
1122
+ const keys = yield* generateNJitteredKeysBetween(lower, upper, n, self.charSet)
1123
+ return !groupId ? keys : keys.map((key) => groupId + key)
1124
+ })
1125
+ } else {
1126
+ // When not using jitter, we don't need Random, but TypeScript requires it
1127
+ // So we provide a default Random service that won't be used
1128
+ return Effect.gen(function* () {
1129
+ const keys = yield* generateNKeysBetween(lower, upper, n, self.charSet)
1130
+ return !groupId ? keys : keys.map((key) => groupId + key)
1131
+ }).pipe(Effect.provideService(Random as any, Random.make(Math.random())))
1132
+ }
1133
+ }
1134
+
1135
+ /**
1136
+ * get the key before the supplied orderKey, if it exists and is in the same group
1137
+ */
1138
+ private getKeyBefore(orderKey: string): Effect.Effect<string | null, Error> {
1139
+ const index = this.list.indexOf(orderKey)
1140
+ if (index === -1) {
1141
+ return Effect.fail(new Error(`orderKey is not in the list`))
1142
+ }
1143
+ const before = this.list[index - 1]
1144
+ return Effect.succeed(!!before && this.isSameGroup(orderKey, before) ? before : null)
1145
+ }
1146
+
1147
+ /**
1148
+ * get the key after the supplied orderKey, if it exists and is in the same group
1149
+ */
1150
+ private getKeyAfter(orderKey: string): Effect.Effect<string | null, Error> {
1151
+ const index = this.list.indexOf(orderKey)
1152
+ if (index === -1) {
1153
+ return Effect.fail(new Error(`orderKey is not in the list`))
1154
+ }
1155
+ const after = this.list[index + 1]
1156
+ return Effect.succeed(!!after && this.isSameGroup(orderKey, after) ? after : null)
1157
+ }
1158
+
1159
+ /**
1160
+ * get the first key of the group (or the first key of the list if not using groups)
1161
+ */
1162
+ private firstOfGroup(groupId: string | undefined): string | null {
1163
+ if (!this.useGroups) return this.list[0] ?? null
1164
+ const first = this.list.find((key) => this.isPartOfGroup(key, groupId))
1165
+ return first ?? null
1166
+ }
1167
+
1168
+ /**
1169
+ * get the last key of the group (or the last key of the list if not using groups)
1170
+ */
1171
+ private lastOfGroup(groupId: string | undefined): string | null {
1172
+ if (!this.useGroups) return this.list[this.list.length - 1] ?? null
1173
+ const allGroupItems = this.list.filter((key) =>
1174
+ this.isPartOfGroup(key, groupId)
1175
+ )
1176
+ const last = allGroupItems[allGroupItems.length - 1]
1177
+ return last ?? null
1178
+ }
1179
+
1180
+ /**
1181
+ * throw an error if the groupId is invalid or supplied when not using groups
1182
+ */
1183
+ private validateGroupId(groupId: string | undefined): void {
1184
+ if (!this.useGroups) {
1185
+ if (groupId) {
1186
+ console.warn("groupId should not used when not using groups")
1187
+ }
1188
+ return
1189
+ }
1190
+ if (!groupId) {
1191
+ throw new Error("groupId is required when using groups")
1192
+ }
1193
+ if (groupId.length !== this.groupIdLength) {
1194
+ throw new Error(`groupId must be the lenght supplied in the options`)
1195
+ }
1196
+ }
1197
+
1198
+ /**
1199
+ * get the groupId from the orderKey
1200
+ */
1201
+ private groupId(orderKey: string): string | undefined {
1202
+ if (!this.useGroups) return undefined
1203
+ return this.splitIntoGroupIdAndOrderKey(orderKey)[0]
1204
+ }
1205
+
1206
+ /**
1207
+ * remove the groupId from the orderKey
1208
+ */
1209
+ private groupLessKey(orderKey: string | null): string | null {
1210
+ if (!this.useGroups) return orderKey
1211
+ return this.splitIntoGroupIdAndOrderKey(orderKey)[1]
1212
+ }
1213
+
1214
+ /**
1215
+ * split the orderKey into groupId and key
1216
+ * if not using groups, orderKey will be the same as key
1217
+ */
1218
+ private splitIntoGroupIdAndOrderKey(
1219
+ orderKey: string | null
1220
+ ): [string | undefined, string | null] {
1221
+ if (!this.useGroups || !orderKey) {
1222
+ return [undefined, orderKey]
1223
+ }
1224
+ const groupId = orderKey.substring(0, this.groupIdLength)
1225
+ const key = orderKey.substring(this.groupIdLength)
1226
+ return [groupId, key]
1227
+ }
1228
+
1229
+ /**
1230
+ * check if two keys are in the same group
1231
+ * if not using groups, keys will always be in the same group
1232
+ */
1233
+ private isSameGroup(a: string, b: string): boolean {
1234
+ if (!this.useGroups) return true
1235
+ const [aGroupId] = this.splitIntoGroupIdAndOrderKey(a)
1236
+ const [bGroupId] = this.splitIntoGroupIdAndOrderKey(b)
1237
+ return aGroupId === bGroupId
1238
+ }
1239
+
1240
+ /**
1241
+ * check if the key is part of the group
1242
+ * if not using groups, key will always be part of the group
1243
+ */
1244
+ private isPartOfGroup(orderKey: string, groupId?: string): boolean {
1245
+ if (!this.useGroups) return true
1246
+ const [keyGroupId] = this.splitIntoGroupIdAndOrderKey(orderKey)
1247
+ return keyGroupId === groupId
1248
+ }
1249
+ }