seq-ranges.ts 2.8 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273
  1. /** Lossless range encoding for JSONL `sourceEventSeqs` arrays. */
  2. import { SessionSeq } from './types.ts'
  3. import type { SessionSeq as SessionSeqType } from './types.ts'
  4. /** A stored source sequence or inclusive consecutive range. */
  5. export type EncodedSeq = number | [number, number]
  6. function isStrictlyIncreasing(values: readonly SessionSeqType[]): boolean {
  7. return values.every((value, index) => index === 0 || value > (values[index - 1] as SessionSeqType))
  8. }
  9. /**
  10. * Replace profitable consecutive runs with inclusive pairs.
  11. * @param values - validated in-memory source sequences.
  12. * @returns a lossless JSON storage form.
  13. */
  14. export function encodeSeqRanges(values: readonly SessionSeqType[]): EncodedSeq[] {
  15. if (!isStrictlyIncreasing(values)) return [...values]
  16. const encoded: EncodedSeq[] = []
  17. for (let start = 0; start < values.length;) {
  18. let end = start
  19. while (end + 1 < values.length && values[end + 1] === (values[end] as number) + 1) end += 1
  20. if (end - start >= 2) encoded.push([values[start] as number, values[end] as number])
  21. else for (let index = start; index <= end; index += 1) encoded.push(values[index] as number)
  22. start = end + 1
  23. }
  24. return encoded
  25. }
  26. /**
  27. * Expand a JSON storage-form source sequence array.
  28. * @param value - parsed storage value.
  29. * @param maxEntries - largest list permitted by the owning event.
  30. * @returns the in-memory source sequences.
  31. */
  32. export function decodeSeqRanges(value: unknown, maxEntries = Number.MAX_SAFE_INTEGER): SessionSeqType[] {
  33. if (!Array.isArray(value)) throw new TypeError('sourceEventSeqs must be an array')
  34. const decoded: SessionSeqType[] = []
  35. let hasRange = false
  36. for (const entry of value) {
  37. if (typeof entry === 'number') {
  38. assertSeq(entry)
  39. if (decoded.length >= maxEntries) throw new TypeError('sourceEventSeqs exceeds its event sequence')
  40. decoded.push(SessionSeq(entry))
  41. continue
  42. }
  43. if (!Array.isArray(entry) || entry.length !== 2) {
  44. throw new TypeError('sourceEventSeqs range entries must be [start, end] pairs')
  45. }
  46. const start: unknown = entry[0]
  47. const end: unknown = entry[1]
  48. assertSeq(start)
  49. assertSeq(end)
  50. if (end < start) throw new TypeError('sourceEventSeqs ranges require start <= end')
  51. const length = end - start + 1
  52. if (length > maxEntries - decoded.length) {
  53. throw new TypeError('sourceEventSeqs range exceeds its event sequence')
  54. }
  55. for (let seq = start; seq <= end; seq += 1) decoded.push(SessionSeq(seq))
  56. hasRange = true
  57. }
  58. if (hasRange && !isStrictlyIncreasing(decoded)) {
  59. throw new TypeError('sourceEventSeqs ranges must be strictly increasing')
  60. }
  61. return decoded
  62. }
  63. function assertSeq(value: unknown): asserts value is number {
  64. if (!Number.isSafeInteger(value) || (value as number) < 0) {
  65. throw new TypeError('sourceEventSeqs must contain non-negative safe integers')
  66. }
  67. }