@wovin/core 0.1.36 → 0.2.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 (212) hide show
  1. package/README.md +0 -12
  2. package/dist/applog/applog-helpers.d.ts +12 -12
  3. package/dist/applog/applog-helpers.d.ts.map +1 -1
  4. package/dist/applog/applog-utils.d.ts +25 -6
  5. package/dist/applog/applog-utils.d.ts.map +1 -1
  6. package/dist/applog/datom-types.d.ts +4 -5
  7. package/dist/applog/datom-types.d.ts.map +1 -1
  8. package/dist/applog.d.ts +3 -3
  9. package/dist/applog.d.ts.map +1 -1
  10. package/dist/{applog.min.js → applog.js} +6 -7
  11. package/dist/blockstore.d.ts +1 -1
  12. package/dist/blockstore.d.ts.map +1 -1
  13. package/dist/{blockstore.min.js → blockstore.js} +1 -3
  14. package/dist/{blockstore.min.js.map → blockstore.js.map} +1 -1
  15. package/dist/{chunk-KXMTKPF4.min.js → chunk-3JZMOEOD.js} +8 -8
  16. package/dist/chunk-3JZMOEOD.js.map +1 -0
  17. package/dist/chunk-3WZVG277.js +434 -0
  18. package/dist/chunk-3WZVG277.js.map +1 -0
  19. package/dist/chunk-7Z5YDQKK.js +1 -0
  20. package/dist/chunk-CPSDKFBG.js +147 -0
  21. package/dist/chunk-CPSDKFBG.js.map +1 -0
  22. package/dist/chunk-E46VTKTZ.js +1 -0
  23. package/dist/{chunk-H3VQJP56.min.js → chunk-J2FDHGOZ.js} +9 -9
  24. package/dist/chunk-J2FDHGOZ.js.map +1 -0
  25. package/dist/chunk-L5EEEGE6.js +1862 -0
  26. package/dist/chunk-L5EEEGE6.js.map +1 -0
  27. package/dist/{chunk-BRC7LSM6.min.js → chunk-PD3C7XUM.js} +5 -5
  28. package/dist/chunk-PD3C7XUM.js.map +1 -0
  29. package/dist/chunk-QZXKQCAY.js +1026 -0
  30. package/dist/chunk-QZXKQCAY.js.map +1 -0
  31. package/dist/{chunk-QPGEBDMJ.min.js → chunk-YDAKBU6Q.js} +1 -1
  32. package/dist/chunk-YDAKBU6Q.js.map +1 -0
  33. package/dist/chunk-ZAADLBSB.js +36 -0
  34. package/dist/chunk-ZAADLBSB.js.map +1 -0
  35. package/dist/index.d.ts +7 -7
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/{index.min.js → index.js} +73 -46
  38. package/dist/ipfs/car.d.ts +11 -11
  39. package/dist/ipfs/car.d.ts.map +1 -1
  40. package/dist/ipfs/ipfs-utils.d.ts +2 -2
  41. package/dist/ipfs/ipfs-utils.d.ts.map +1 -1
  42. package/dist/ipfs.d.ts +3 -3
  43. package/dist/ipfs.d.ts.map +1 -1
  44. package/dist/{ipfs.min.js → ipfs.js} +7 -10
  45. package/dist/ipns.d.ts +1 -1
  46. package/dist/ipns.d.ts.map +1 -1
  47. package/dist/ipns.js +64 -0
  48. package/dist/ipns.js.map +1 -0
  49. package/dist/pubsub/pub-pull.d.ts +3 -3
  50. package/dist/pubsub/pub-pull.d.ts.map +1 -1
  51. package/dist/pubsub/pubsub-types.d.ts +3 -3
  52. package/dist/pubsub/pubsub-types.d.ts.map +1 -1
  53. package/dist/pubsub/snap-push.d.ts +4 -4
  54. package/dist/pubsub/snap-push.d.ts.map +1 -1
  55. package/dist/pubsub/ucan.d.ts +1 -1
  56. package/dist/pubsub/ucan.d.ts.map +1 -1
  57. package/dist/pubsub.d.ts +4 -4
  58. package/dist/pubsub.d.ts.map +1 -1
  59. package/dist/{pubsub.min.js → pubsub.js} +7 -10
  60. package/dist/query/attr-helpers.d.ts +5 -0
  61. package/dist/query/attr-helpers.d.ts.map +1 -0
  62. package/dist/query/basic.d.ts +85 -21
  63. package/dist/query/basic.d.ts.map +1 -1
  64. package/dist/query/divergences.d.ts +5 -5
  65. package/dist/query/divergences.d.ts.map +1 -1
  66. package/dist/query/entity-collection.d.ts +19 -0
  67. package/dist/query/entity-collection.d.ts.map +1 -0
  68. package/dist/query/matchers.d.ts +1 -1
  69. package/dist/query/matchers.d.ts.map +1 -1
  70. package/dist/query/memoized.d.ts +66 -0
  71. package/dist/query/memoized.d.ts.map +1 -0
  72. package/dist/query/situations.d.ts +2 -1
  73. package/dist/query/situations.d.ts.map +1 -1
  74. package/dist/query/subscribable.d.ts +111 -0
  75. package/dist/query/subscribable.d.ts.map +1 -0
  76. package/dist/query/types.d.ts +54 -14
  77. package/dist/query/types.d.ts.map +1 -1
  78. package/dist/query.d.ts +9 -5
  79. package/dist/query.d.ts.map +1 -1
  80. package/dist/{query.min.js → query.js} +51 -32
  81. package/dist/retrieve/index.d.ts +1 -1
  82. package/dist/retrieve/index.d.ts.map +1 -1
  83. package/dist/retrieve/update-thread.d.ts +3 -3
  84. package/dist/retrieve/update-thread.d.ts.map +1 -1
  85. package/dist/retrieve.d.ts +1 -1
  86. package/dist/retrieve.d.ts.map +1 -1
  87. package/dist/retrieve.js +14 -0
  88. package/dist/thread/basic.d.ts +15 -19
  89. package/dist/thread/basic.d.ts.map +1 -1
  90. package/dist/thread/filters.d.ts +8 -10
  91. package/dist/thread/filters.d.ts.map +1 -1
  92. package/dist/thread/indexes.d.ts +56 -0
  93. package/dist/thread/indexes.d.ts.map +1 -0
  94. package/dist/thread/mapped.d.ts +40 -11
  95. package/dist/thread/mapped.d.ts.map +1 -1
  96. package/dist/thread/utils.d.ts +5 -5
  97. package/dist/thread/utils.d.ts.map +1 -1
  98. package/dist/thread/writeable.d.ts +2 -2
  99. package/dist/thread/writeable.d.ts.map +1 -1
  100. package/dist/thread.d.ts +6 -5
  101. package/dist/thread.d.ts.map +1 -1
  102. package/dist/{thread.min.js → thread.js} +9 -6
  103. package/dist/types/typescript-utils.d.ts +6 -5
  104. package/dist/types/typescript-utils.d.ts.map +1 -1
  105. package/dist/types.d.ts +1 -1
  106. package/dist/types.d.ts.map +1 -1
  107. package/dist/{types.min.js → types.js} +3 -4
  108. package/dist/utils/debug-name.d.ts +13 -0
  109. package/dist/utils/debug-name.d.ts.map +1 -0
  110. package/dist/utils.d.ts +1 -1
  111. package/dist/utils.d.ts.map +1 -1
  112. package/dist/utils.js +9 -0
  113. package/package.json +32 -23
  114. package/src/applog/applog-helpers.ts +155 -0
  115. package/src/applog/applog-utils.test.ts +108 -0
  116. package/src/applog/applog-utils.ts +507 -0
  117. package/src/applog/datom-types.ts +148 -0
  118. package/src/applog.ts +3 -0
  119. package/src/blockstore/index.ts +36 -0
  120. package/src/blockstore.ts +1 -0
  121. package/src/index.ts +8 -0
  122. package/src/ipfs/car.ts +291 -0
  123. package/src/ipfs/fetch-snapshot-chain.ts +135 -0
  124. package/src/ipfs/ipfs-utils.ts +132 -0
  125. package/src/ipfs.ts +3 -0
  126. package/src/ipns/ipns-record.ts +115 -0
  127. package/src/ipns.ts +1 -0
  128. package/src/pubsub/UCAN Specs Overview.md +217 -0
  129. package/src/pubsub/connector.ts +9 -0
  130. package/src/pubsub/pub-pull.ts +31 -0
  131. package/src/pubsub/pubsub-types.ts +90 -0
  132. package/src/pubsub/snap-push.ts +277 -0
  133. package/src/pubsub/ucan-example.ts +61 -0
  134. package/src/pubsub/ucan.ts +56 -0
  135. package/src/pubsub.ts +4 -0
  136. package/src/query/attr-helpers.ts +5 -0
  137. package/src/query/basic.ts +1245 -0
  138. package/src/query/divergences.ts +50 -0
  139. package/src/query/entity-collection.ts +131 -0
  140. package/src/query/liveFilterAndMap.test.ts +102 -0
  141. package/src/query/matchers.ts +8 -0
  142. package/src/query/memoized.test.ts +151 -0
  143. package/src/query/memoized.ts +180 -0
  144. package/src/query/query-steps.ts +4 -0
  145. package/src/query/query.test.ts +538 -0
  146. package/src/query/situations.ts +261 -0
  147. package/src/query/subscribable.test.ts +245 -0
  148. package/src/query/subscribable.ts +234 -0
  149. package/src/query/types.ts +155 -0
  150. package/src/query/withoutDeleted.test.ts +204 -0
  151. package/src/query.ts +9 -0
  152. package/src/retrieve/index.ts +1 -0
  153. package/src/retrieve/update-thread.ts +248 -0
  154. package/src/retrieve.ts +1 -0
  155. package/src/test/perf/query.1m.perf.test.ts +94 -0
  156. package/src/test/perf/query.perf.test.ts +389 -0
  157. package/src/test/perf/query.realdata.perf.test.ts +182 -0
  158. package/src/thread/basic.ts +209 -0
  159. package/src/thread/filters.ts +227 -0
  160. package/src/thread/indexes.ts +250 -0
  161. package/src/thread/joinThreads.test.ts +304 -0
  162. package/src/thread/mapped.ts +226 -0
  163. package/src/thread/utils.ts +144 -0
  164. package/src/thread/writeable.ts +163 -0
  165. package/src/thread.ts +6 -0
  166. package/src/types/typescript-utils.ts +64 -0
  167. package/src/types.ts +1 -0
  168. package/src/utils/debug-name.ts +54 -0
  169. package/src/utils.ts +4 -0
  170. package/dist/chunk-2Y2PYHGR.min.js +0 -65
  171. package/dist/chunk-2Y2PYHGR.min.js.map +0 -1
  172. package/dist/chunk-5MMGBK2U.min.js +0 -1
  173. package/dist/chunk-7IDQIMQO.min.js +0 -1
  174. package/dist/chunk-BRC7LSM6.min.js.map +0 -1
  175. package/dist/chunk-COXXILXC.min.js +0 -512
  176. package/dist/chunk-COXXILXC.min.js.map +0 -1
  177. package/dist/chunk-GDX2OO7L.min.js +0 -9080
  178. package/dist/chunk-GDX2OO7L.min.js.map +0 -1
  179. package/dist/chunk-H3VQJP56.min.js.map +0 -1
  180. package/dist/chunk-HYMC7W6S.min.js +0 -1549
  181. package/dist/chunk-HYMC7W6S.min.js.map +0 -1
  182. package/dist/chunk-KEHU7HGZ.min.js +0 -5216
  183. package/dist/chunk-KEHU7HGZ.min.js.map +0 -1
  184. package/dist/chunk-KXMTKPF4.min.js.map +0 -1
  185. package/dist/chunk-PHITDXZT.min.js +0 -36
  186. package/dist/chunk-QO2KMGDN.min.js +0 -3771
  187. package/dist/chunk-QO2KMGDN.min.js.map +0 -1
  188. package/dist/chunk-QPGEBDMJ.min.js.map +0 -1
  189. package/dist/chunk-WXLCBTHX.min.js +0 -1606
  190. package/dist/chunk-WXLCBTHX.min.js.map +0 -1
  191. package/dist/ipns.min.js +0 -6419
  192. package/dist/ipns.min.js.map +0 -1
  193. package/dist/mobx/mobx-utils.d.ts +0 -82
  194. package/dist/mobx/mobx-utils.d.ts.map +0 -1
  195. package/dist/mobx.d.ts +0 -2
  196. package/dist/mobx.d.ts.map +0 -1
  197. package/dist/mobx.min.js +0 -141
  198. package/dist/retrieve.min.js +0 -17
  199. package/dist/types.min.js.map +0 -1
  200. package/dist/utils.min.js +0 -10
  201. package/dist/utils.min.js.map +0 -1
  202. /package/dist/{applog.min.js.map → applog.js.map} +0 -0
  203. /package/dist/{chunk-5MMGBK2U.min.js.map → chunk-7Z5YDQKK.js.map} +0 -0
  204. /package/dist/{chunk-7IDQIMQO.min.js.map → chunk-E46VTKTZ.js.map} +0 -0
  205. /package/dist/{chunk-PHITDXZT.min.js.map → index.js.map} +0 -0
  206. /package/dist/{index.min.js.map → ipfs.js.map} +0 -0
  207. /package/dist/{ipfs.min.js.map → pubsub.js.map} +0 -0
  208. /package/dist/{mobx.min.js.map → query.js.map} +0 -0
  209. /package/dist/{pubsub.min.js.map → retrieve.js.map} +0 -0
  210. /package/dist/{query.min.js.map → thread.js.map} +0 -0
  211. /package/dist/{retrieve.min.js.map → types.js.map} +0 -0
  212. /package/dist/{thread.min.js.map → utils.js.map} +0 -0
@@ -0,0 +1,108 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import type { Applog } from './datom-types.ts'
3
+ import { compareApplogsByTs, isLaterByTsAndPv, sortApplogsByTs } from './applog-utils.ts'
4
+
5
+ const mkLog = (overrides: Partial<Applog>): Applog => ({
6
+ cid: 'cid-default' as any,
7
+ en: 'e1' as any,
8
+ at: 'name',
9
+ vl: 'value',
10
+ ts: '2026-04-25T00:00:00.000Z',
11
+ pv: null,
12
+ ag: 'agent' as any,
13
+ ...overrides,
14
+ })
15
+
16
+ describe('compareApplogsByTs', () => {
17
+ it('orders by ts asc then by cid for tiebreak (transitive)', () => {
18
+ const a = mkLog({ cid: 'cid-a' as any, ts: '2026-04-25T00:00:00.000Z' })
19
+ const b = mkLog({ cid: 'cid-b' as any, ts: '2026-04-25T00:00:00.000Z' })
20
+ const c = mkLog({ cid: 'cid-c' as any, ts: '2026-04-25T00:00:00.001Z' })
21
+ const sorted = [c, b, a].slice().sort((x, y) => compareApplogsByTs(x, y))
22
+ expect(sorted.map(l => l.cid)).toEqual(['cid-a', 'cid-b', 'cid-c'])
23
+ })
24
+ })
25
+
26
+ describe('sortApplogsByTs — chain stabilization', () => {
27
+ const sameTs = '2026-04-25T00:00:00.000Z'
28
+
29
+ it('topologically orders 3 same-ts chain logs regardless of input order', () => {
30
+ // Chain: A → B → C (same en, at, ts)
31
+ const a = mkLog({ cid: 'aaa' as any, pv: null, ts: sameTs })
32
+ const b = mkLog({ cid: 'bbb' as any, pv: 'aaa' as any, ts: sameTs })
33
+ const c = mkLog({ cid: 'ccc' as any, pv: 'bbb' as any, ts: sameTs })
34
+
35
+ // Note: cid lex order is a, b, c — so for asc, the cid-only sort would coincidentally
36
+ // produce the right order. To prove chain awareness, use cids whose lex order differs.
37
+ const z = mkLog({ cid: 'zzz' as any, pv: null, ts: sameTs })
38
+ const y = mkLog({ cid: 'yyy' as any, pv: 'zzz' as any, ts: sameTs })
39
+ const x = mkLog({ cid: 'xxx' as any, pv: 'yyy' as any, ts: sameTs })
40
+
41
+ // asc: chain root first → z, y, x
42
+ const ascOrder = sortApplogsByTs([x, y, z].slice())
43
+ expect(ascOrder.map(l => l.cid)).toEqual(['zzz', 'yyy', 'xxx'])
44
+
45
+ // scrambled
46
+ const scrambled = sortApplogsByTs([y, z, x].slice())
47
+ expect(scrambled.map(l => l.cid)).toEqual(['zzz', 'yyy', 'xxx'])
48
+
49
+ // desc: chain tail first → x, y, z
50
+ const descOrder = sortApplogsByTs([z, x, y].slice(), 'desc')
51
+ expect(descOrder.map(l => l.cid)).toEqual(['xxx', 'yyy', 'zzz'])
52
+ })
53
+
54
+ it('does not reshuffle different (en, at) sub-groups within a same-ts cluster', () => {
55
+ // Same ts, two sub-groups, each with a chain
56
+ const a1 = mkLog({ cid: 'a1' as any, en: 'e1' as any, at: 'name', pv: null, ts: sameTs })
57
+ const a2 = mkLog({ cid: 'a2' as any, en: 'e1' as any, at: 'name', pv: 'a1' as any, ts: sameTs })
58
+ const b1 = mkLog({ cid: 'b1' as any, en: 'e2' as any, at: 'name', pv: null, ts: sameTs })
59
+ const b2 = mkLog({ cid: 'b2' as any, en: 'e2' as any, at: 'name', pv: 'b1' as any, ts: sameTs })
60
+
61
+ const sorted = sortApplogsByTs([a2, b2, a1, b1].slice())
62
+ // Each (en, at) sub-group must be in chain order. Cross-group order = cid-lex (preserved).
63
+ const e1Logs = sorted.filter(l => l.en === 'e1')
64
+ const e2Logs = sorted.filter(l => l.en === 'e2')
65
+ expect(e1Logs.map(l => l.cid)).toEqual(['a1', 'a2'])
66
+ expect(e2Logs.map(l => l.cid)).toEqual(['b1', 'b2'])
67
+ })
68
+
69
+ it('falls back to cid-lex order for non-chain-related same-ts logs', () => {
70
+ const x = mkLog({ cid: 'xxx' as any, pv: null, ts: sameTs })
71
+ const y = mkLog({ cid: 'yyy' as any, pv: null, ts: sameTs })
72
+ const sorted = sortApplogsByTs([y, x].slice())
73
+ expect(sorted.map(l => l.cid)).toEqual(['xxx', 'yyy'])
74
+ })
75
+ })
76
+
77
+ describe('isLaterByTsAndPv', () => {
78
+ const sameTs = '2026-04-25T00:00:00.000Z'
79
+
80
+ it('uses ts when ts differs', () => {
81
+ const earlier = mkLog({ cid: 'a' as any, ts: '2026-04-25T00:00:00.000Z' })
82
+ const later = mkLog({ cid: 'b' as any, ts: '2026-04-25T00:00:00.001Z' })
83
+ expect(isLaterByTsAndPv(later, earlier)).toBe(true)
84
+ expect(isLaterByTsAndPv(earlier, later)).toBe(false)
85
+ })
86
+
87
+ it('uses pv chain for same-ts same (en, at) logs', () => {
88
+ const a = mkLog({ cid: 'a' as any, pv: null, ts: sameTs })
89
+ const b = mkLog({ cid: 'b' as any, pv: 'a' as any, ts: sameTs })
90
+ expect(isLaterByTsAndPv(b, a)).toBe(true) // b chains after a
91
+ expect(isLaterByTsAndPv(a, b)).toBe(false)
92
+ })
93
+
94
+ it('falls back to cid-lex for same-ts unrelated logs', () => {
95
+ const x = mkLog({ cid: 'x' as any, pv: null, ts: sameTs })
96
+ const y = mkLog({ cid: 'y' as any, pv: null, ts: sameTs })
97
+ expect(isLaterByTsAndPv(y, x)).toBe(true)
98
+ expect(isLaterByTsAndPv(x, y)).toBe(false)
99
+ })
100
+
101
+ it('does not use pv when (en, at) differs', () => {
102
+ // Edge case: cid coincidence across keys must not be interpreted as a chain link
103
+ const a = mkLog({ cid: 'shared' as any, en: 'e1' as any, at: 'name', pv: null, ts: sameTs })
104
+ const b = mkLog({ cid: 'other' as any, en: 'e2' as any, at: 'name', pv: 'shared' as any, ts: sameTs })
105
+ // Same-ts, different (en, at) — should fall through to cid-lex
106
+ expect(isLaterByTsAndPv(b, a)).toBe('other'.localeCompare('shared') > 0)
107
+ })
108
+ })
@@ -0,0 +1,507 @@
1
+ import { Logger } from 'besonders-logger'
2
+ import { isBefore } from 'date-fns'
3
+ import { partial, pick } from 'lodash-es'
4
+ import { isEqual } from 'lodash-es'
5
+ import stringify from 'safe-stable-stringify'
6
+ import type {
7
+ Applog,
8
+ ApplogForInsert,
9
+ ApplogValue,
10
+ DatalogQueryPattern,
11
+ DatalogQueryResultEntry,
12
+ ResultContext,
13
+ SearchContext,
14
+ ValueOrMatcher,
15
+ } from './datom-types.ts'
16
+
17
+ const { WARN, LOG, DEBUG, VERBOSE, ERROR } = Logger.setup(Logger.INFO) // eslint-disable-line no-unused-vars
18
+
19
+ export const isoDateStrCompare = (strA: string, strB: string, dir: 'asc' | 'desc' = 'asc') =>
20
+ dir === 'asc'
21
+ ? strA.localeCompare(strB, 'en-US')
22
+ : strB.localeCompare(strA, 'en-US')
23
+ export const objEqualByKeys = (keys: string[], objA: object, objB: object) => {
24
+ return isEqual(pick(objA, keys), pick(objB, keys))
25
+ }
26
+
27
+ /** Transitive total-order comparator over (ts, cid). Safe for `Array.sort`. */
28
+ export const compareApplogsByTs = (logA: Applog, logB: Applog, dir: 'asc' | 'desc' = 'asc') => {
29
+ const tsCmp = isoDateStrCompare(logA.ts, logB.ts, dir)
30
+ if (tsCmp !== 0) return tsCmp
31
+ return dir === 'asc' ? logA.cid.localeCompare(logB.cid) : logB.cid.localeCompare(logA.cid)
32
+ }
33
+ export const compareApplogsByEnAt = partial(objEqualByKeys, ['en', 'at'])
34
+
35
+ /**
36
+ * Pairwise: is `a` strictly later than `b` per (ts, pv-chain, cid)?
37
+ *
38
+ * NOT TRANSITIVE. Use only for two-log decisions (e.g. lastWriteWins replacement
39
+ * check). Do NOT pass to `Array.sort` — it can produce cycles at 3+ chain links
40
+ * and break the engine's sort.
41
+ */
42
+ export function isLaterByTsAndPv(a: Applog, b: Applog): boolean {
43
+ const tsCmp = isoDateStrCompare(a.ts, b.ts)
44
+ if (tsCmp !== 0) return tsCmp > 0
45
+ if (a.en === b.en && a.at === b.at) {
46
+ if (a.pv === b.cid) return true
47
+ if (b.pv === a.cid) return false
48
+ }
49
+ return a.cid.localeCompare(b.cid) > 0
50
+ }
51
+
52
+ /**
53
+ * Sort applogs in chain-aware order (modifies array, also returns for chaining).
54
+ *
55
+ * Two-phase:
56
+ * 1. Transitive `(ts, cid)` `Array.sort` — fast, total-order, common case.
57
+ * 2. For each contiguous same-ts cluster (rare), chain-stabilize via `pv`
58
+ * so chain-tail logs land last (asc) / first (desc) within their (en, at)
59
+ * sub-group. Cross-group order within a cluster stays cid-lex.
60
+ *
61
+ * No-tie inputs pay only one linear scan past `Array.sort`.
62
+ */
63
+ export function sortApplogsByTs(appLogArray: Applog[], dir: 'asc' | 'desc' = 'asc') {
64
+ appLogArray.sort((a, b) => compareApplogsByTs(a, b, dir))
65
+ let i = 0
66
+ while (i < appLogArray.length) {
67
+ let j = i + 1
68
+ while (j < appLogArray.length && appLogArray[j].ts === appLogArray[i].ts) j++
69
+ if (j - i > 1) chainStabilizeCluster(appLogArray, i, j, dir)
70
+ i = j
71
+ }
72
+ return appLogArray
73
+ }
74
+
75
+ /**
76
+ * Re-order applogs[from..to-1] (all sharing `ts`) so each (en, at) sub-group
77
+ * appears in pv-chain order. Cross-group positions in the cluster are preserved
78
+ * — only logs within the same (en, at) sub-group get reshuffled.
79
+ */
80
+ function chainStabilizeCluster(applogs: Applog[], from: number, to: number, dir: 'asc' | 'desc') {
81
+ const groups = new Map<string, Applog[]>()
82
+ for (let k = from; k < to; k++) {
83
+ const log = applogs[k]
84
+ const key = log.en + '|' + log.at
85
+ const existing = groups.get(key)
86
+ if (existing) existing.push(log)
87
+ else groups.set(key, [log])
88
+ }
89
+ // All groups singleton — nothing to do (the common case)
90
+ if (groups.size === to - from) return
91
+
92
+ const orderedByGroup = new Map<string, Applog[]>()
93
+ for (const [key, logs] of groups) {
94
+ orderedByGroup.set(key, logs.length === 1 ? logs : topoSortByPv(logs, dir))
95
+ }
96
+ const cursors = new Map<string, number>()
97
+ for (let k = from; k < to; k++) {
98
+ const key = applogs[k].en + '|' + applogs[k].at
99
+ const cursor = cursors.get(key) ?? 0
100
+ applogs[k] = orderedByGroup.get(key)![cursor]
101
+ cursors.set(key, cursor + 1)
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Topologically order `logs` (all sharing en, at, ts) by pv-chain.
107
+ * asc → root-first; desc → tail-first. Forks / multi-roots: tie-break by cid.
108
+ */
109
+ function topoSortByPv(logs: Applog[], dir: 'asc' | 'desc'): Applog[] {
110
+ const cidSet = new Set(logs.map(l => l.cid))
111
+ const childrenByPv = new Map<string, Applog[]>()
112
+ const roots: Applog[] = []
113
+ for (const log of logs) {
114
+ if (log.pv && cidSet.has(log.pv)) {
115
+ const children = childrenByPv.get(log.pv)
116
+ if (children) children.push(log)
117
+ else childrenByPv.set(log.pv, [log])
118
+ } else {
119
+ roots.push(log)
120
+ }
121
+ }
122
+ const lexCmp = (a: Applog, b: Applog) => a.cid.localeCompare(b.cid)
123
+ roots.sort(lexCmp)
124
+ for (const children of childrenByPv.values()) children.sort(lexCmp)
125
+
126
+ const result: Applog[] = []
127
+ const visited = new Set<string>()
128
+ const stack = [...roots].reverse()
129
+ while (stack.length > 0) {
130
+ const log = stack.pop()!
131
+ if (visited.has(log.cid)) continue
132
+ visited.add(log.cid)
133
+ result.push(log)
134
+ const children = childrenByPv.get(log.cid)
135
+ if (children) for (let i = children.length - 1; i >= 0; i--) stack.push(children[i])
136
+ }
137
+ // Defensive: cycle (shouldn't happen with content-addressed pv) — append unvisited
138
+ if (result.length < logs.length) {
139
+ for (const log of logs) if (!visited.has(log.cid)) result.push(log)
140
+ }
141
+ return dir === 'desc' ? result.reverse() : result
142
+ }
143
+ export const isTsBefore = (log: Applog, logToCompare: Applog) => isBefore(new Date(log.ts), new Date(logToCompare.ts))
144
+ export const uniqueEnFromAppLogs = (appLogArray: Applog[]) => [...new Set(appLogArray.map(eachLog => eachLog.en))]
145
+ export const areApplogsEqual = (logA: Applog, logB: Applog) => isEqual(logA, logB)
146
+
147
+ export type RemoveDuplicateAppLogsMode = 'safety' | 'cleanup'
148
+
149
+ const warnMissingRemoveDuplicateMode = () => {
150
+ WARN(`[removeDuplicateAppLogs] mode not set; pass 'safety' or 'cleanup' for optimal behavior`)
151
+ }
152
+
153
+ const removeDuplicateAppLogsCleanup = (appLogArray: Applog[]) => {
154
+ const logMap = new Map<string, Applog>()
155
+ const verboseEnabled = VERBOSE.isEnabled
156
+ for (const eachLog of appLogArray) {
157
+ if (!eachLog) {
158
+ ERROR(`falsy entry in applogs`, appLogArray)
159
+ throw new Error(`falsy entry in applogs`)
160
+ }
161
+ if (!eachLog.cid) {
162
+ ERROR(`applog with missing CID`, eachLog)
163
+ throw new Error(`applog with missing CID`)
164
+ }
165
+ const key = eachLog.cid
166
+ const existing = logMap.get(key)
167
+ if (existing) {
168
+ if (verboseEnabled) VERBOSE(`Skipping duplicate applog:`, [existing, eachLog])
169
+ } else {
170
+ logMap.set(key, eachLog)
171
+ }
172
+ }
173
+ return Array.from(logMap.values())
174
+ }
175
+
176
+ const removeDuplicateAppLogsSafety = (appLogArray: Applog[]) => {
177
+ const seen = new Set<string>()
178
+ const verboseEnabled = VERBOSE.isEnabled
179
+ const existingByCid = verboseEnabled ? new Map<string, Applog>() : null
180
+ let result: Applog[] | null = null
181
+ let index = 0
182
+ for (const eachLog of appLogArray) {
183
+ if (!eachLog) {
184
+ ERROR(`falsy entry in applogs`, appLogArray)
185
+ throw new Error(`falsy entry in applogs`)
186
+ }
187
+ if (!eachLog.cid) {
188
+ ERROR(`applog with missing CID`, eachLog)
189
+ throw new Error(`applog with missing CID`)
190
+ }
191
+ const key = eachLog.cid
192
+ if (seen.has(key)) {
193
+ if (!result) {
194
+ result = appLogArray.slice(0, index)
195
+ }
196
+ if (verboseEnabled) VERBOSE(`Skipping duplicate applog:`, [existingByCid?.get(key), eachLog])
197
+ } else {
198
+ seen.add(key)
199
+ if (existingByCid) existingByCid.set(key, eachLog)
200
+ if (result) result.push(eachLog)
201
+ }
202
+ index++
203
+ }
204
+ return result ?? appLogArray
205
+ }
206
+
207
+ /**
208
+ * Deduplicate applogs by CID.
209
+ * - safety: fast duplicate check; returns original array if no duplicates found.
210
+ * - cleanup: optimized for merged arrays with likely duplicates.
211
+ */
212
+ export const removeDuplicateAppLogs = (appLogArray: Applog[], mode?: RemoveDuplicateAppLogsMode) => {
213
+ if (!mode) {
214
+ warnMissingRemoveDuplicateMode()
215
+ return removeDuplicateAppLogsCleanup(appLogArray)
216
+ }
217
+ return mode === 'safety'
218
+ ? removeDuplicateAppLogsSafety(appLogArray)
219
+ : removeDuplicateAppLogsCleanup(appLogArray)
220
+ }
221
+
222
+ // export const removeDuplicateAndMaybeDeletedAppLogs = (ds: Thread, appLogArray: Applog[], removeDeletedEntities = true) => {
223
+ // const logMap = new Map()
224
+ // for (const eachLog of appLogArray) {
225
+ // if (!removeDeletedEntities || ds.entityIsDeleted(eachLog.en))
226
+ // logMap.set(stringify(eachLog), eachLog)
227
+ // }
228
+ // return Array.from(logMap.values())
229
+ // }
230
+
231
+ export const getHashID = (stringifiable: any, lngth = 8) => cyrb53hash(stringify(stringifiable), 31, lngth) as string
232
+
233
+ export function isVariable(x: any): x is string {
234
+ return typeof x === 'string' && x.startsWith('?')
235
+ }
236
+ export function variableNameWithoutQuestionmark(str: string) {
237
+ return str.slice(1)
238
+ }
239
+ // export function isMatcher(x: any): x is string {
240
+ // return
241
+ // }
242
+ export function isStaticPattern(x: any): x is ApplogValue {
243
+ if (!['string', 'boolean', 'number', 'function'].includes(typeof x)) WARN(`Unhandled pattern value type:`, typeof x, x)
244
+ return !isVariable(x) && ['string', 'boolean', 'number'].includes(typeof x)
245
+ }
246
+ // export function isIgnorePattern(x: any): boolean {
247
+ // return x === '_'
248
+ // }
249
+
250
+ /*
251
+ * In a pattern from a Query:
252
+ * - variables that don't have a value in the search context:
253
+ * - remove from pattern
254
+ * - add to variableToFill as: { en: 'movieID' } (useful for mapTo)
255
+ * - variables that have a value set:
256
+ * - replace placeholder with actual value
257
+ */
258
+ export function resolveOrRemoveVariables(pattern: DatalogQueryPattern, candidate: SearchContext) {
259
+ let variablesToFill = {} as Partial<{ [key in keyof Applog]: string }>
260
+ const newPattern: DatalogQueryPattern = {}
261
+ for (const [patternKey, patternValue] of Object.entries(pattern)) {
262
+ if (isVariable(patternValue)) {
263
+ const varName = variableNameWithoutQuestionmark(patternValue)
264
+ const candidateValue = candidate[varName]
265
+ if (candidateValue) {
266
+ newPattern[patternKey] = candidateValue
267
+ // & not adding it to newPattern
268
+ } else {
269
+ variablesToFill[patternKey] = varName
270
+ }
271
+ } else {
272
+ newPattern[patternKey] = patternValue // keep static value
273
+ }
274
+ }
275
+
276
+ return [newPattern, variablesToFill] as const
277
+ }
278
+
279
+ function matchVariable(variable: string, triplePart: ApplogValue, context: SearchContext): SearchContext {
280
+ if (context.hasOwnProperty(variable)) {
281
+ // TODO: fix lint error with: if (Object.hasOwnProperty.call(context, variable)) {
282
+ const bound = context[variable]
283
+ const match = matchPart(bound, triplePart, context)
284
+ // if (VERBOSE.isEnabled) VERBOSE('[matchVariable] match?', variable, bound, match)
285
+ return match
286
+ }
287
+ // if (VERBOSE.isEnabled) VERBOSE('[matchVariable] initializing variable', variable, 'to', triplePart)
288
+ return { ...context, [variable]: triplePart }
289
+ }
290
+
291
+ export function matchPartStatic(field: keyof Applog, patternPart: ValueOrMatcher<ApplogValue>, atomPart: ApplogValue): boolean {
292
+ // if (VERBOSE.isEnabled) VERBOSE('[matchPartStatic]', field, patternPart, patternPart === atomPart ? '===' : '!==', atomPart)
293
+ let result
294
+ if (patternPart) {
295
+ const typ = typeof patternPart
296
+ if (typ === 'string') {
297
+ result = patternPart === atomPart // shortcut for most common use-case
298
+ } else if (typ === 'function') {
299
+ result = (patternPart as Function)(atomPart)
300
+ } else if (typeof (patternPart as any).has === 'function') {
301
+ result = (patternPart as Set<any>).has(atomPart)
302
+ } else if (Array.isArray(patternPart) && !Array.isArray(atomPart) /* ? how to handle array values */) {
303
+ result = patternPart.includes(atomPart)
304
+ } // if (field === 'at' && typ === 'string' && patternPart.endsWith('*')) {
305
+ // return typeof atomPart === 'string' && atomPart.startsWith(patternPart.slice(0, -1))
306
+ // }
307
+ else {
308
+ result = patternPart === atomPart
309
+ }
310
+ } else {
311
+ result = patternPart === atomPart
312
+ }
313
+
314
+ // if (VERBOSE.isEnabled) VERBOSE('[matchPartStatic] =>', field.startsWith('!') ? '!' : '', result)
315
+ if (field.charAt(0) === '!') {
316
+ return !result
317
+ } else {
318
+ return result
319
+ }
320
+ }
321
+ export function matchPart(patternPart: ValueOrMatcher<ApplogValue>, atomPart: ApplogValue, context: SearchContext): ResultContext {
322
+ if (!context) {
323
+ // if (VERBOSE.isEnabled) VERBOSE('[matchPart] no context')
324
+ return null
325
+ }
326
+ if (typeof patternPart === 'string') {
327
+ if (isVariable(patternPart)) {
328
+ return matchVariable(patternPart, atomPart, context)
329
+ } /* TODO: else if (isIgnorePattern(patternPart)) {
330
+ return matchVariable(patternPart, atomPart, context)
331
+ } */
332
+ }
333
+ // if (VERBOSE.isEnabled) VERBOSE('[matchPart]', patternPart, patternPart === atomPart ? '===' : '!==', atomPart)
334
+ if (typeof patternPart === 'function') {
335
+ return patternPart(atomPart) ? context : null
336
+ }
337
+ return patternPart === atomPart ? context : null
338
+ }
339
+
340
+ /**
341
+ * Check if pattern matches triple with context substitutions
342
+ */
343
+ export function matchPattern(pattern: DatalogQueryPattern, applog: Applog, context: SearchContext): ResultContext {
344
+ return Object.entries(pattern).reduce((context, [field, patternValue]) => {
345
+ const applogValue = applog[field]
346
+ // @ts-expect-error wtf no idea //HACK: ts weird
347
+ const patternValT: ValueOrMatcher<ApplogValue> = patternValue
348
+ return matchPart(patternValT, applogValue, context)
349
+ }, context)
350
+ }
351
+
352
+ export function actualize<SELECT extends string>(context: ResultContext, find: readonly SELECT[]): DatalogQueryResultEntry<SELECT> {
353
+ return Object.fromEntries(find.map((findField) => {
354
+ if (context === null) {
355
+ throw new Error(`actualize context is null ${find}`)
356
+ }
357
+ return [
358
+ isVariable(findField) ? findField.replace(/^\?/, '') : findField,
359
+ isVariable(findField) ? context[findField] : findField,
360
+ ]
361
+ })) as DatalogQueryResultEntry<SELECT>
362
+ }
363
+ const sum = function sum(array: number[]) {
364
+ var num = 0
365
+ for (var i = 0, l = array.length; i < l; i++) num += array[i]
366
+ return num
367
+ }
368
+ const mean = function mean(array: number[]) {
369
+ return sum(array) / array.length
370
+ }
371
+ export const arrStats = {
372
+ max: function(array: number[]) {
373
+ return Math.max.apply(null, array)
374
+ },
375
+
376
+ min: function(array: number[]) {
377
+ return Math.min.apply(null, array)
378
+ },
379
+
380
+ range: function(array: number[]) {
381
+ return arrStats.max(array) - arrStats.min(array)
382
+ },
383
+
384
+ midrange: function(array: number[]) {
385
+ return arrStats.range(array) / 2
386
+ },
387
+
388
+ sum,
389
+
390
+ mean,
391
+
392
+ average: mean,
393
+
394
+ median: function(array: number[]) {
395
+ array.sort(function(a, b) {
396
+ return a - b
397
+ })
398
+ var mid = array.length / 2
399
+ return mid % 1 ? array[mid - 0.5] : (array[mid - 1] + array[mid]) / 2
400
+ },
401
+
402
+ modes: function(array: number[]) {
403
+ if (!array.length) return []
404
+ var modeMap = {},
405
+ maxCount = 0,
406
+ modes = []
407
+
408
+ array.forEach(function(val) {
409
+ if (!modeMap[val]) modeMap[val] = 1
410
+ else modeMap[val]++
411
+
412
+ if (modeMap[val] > maxCount) {
413
+ modes = [val]
414
+ maxCount = modeMap[val]
415
+ } else if (modeMap[val] === maxCount) {
416
+ modes.push(val)
417
+ maxCount = modeMap[val]
418
+ }
419
+ })
420
+ return modes
421
+ },
422
+
423
+ variance: function(array: number[]) {
424
+ var mean = arrStats.mean(array)
425
+ return arrStats.mean(array.map(function(num) {
426
+ return Math.pow(num - mean, 2)
427
+ }))
428
+ },
429
+
430
+ standardDeviation: function(array: number[]) {
431
+ return Math.sqrt(arrStats.variance(array))
432
+ },
433
+
434
+ meanAbsoluteDeviation: function(array: number[]) {
435
+ var mean = arrStats.mean(array)
436
+ return arrStats.mean(array.map(function(num) {
437
+ return Math.abs(num - mean)
438
+ }))
439
+ },
440
+
441
+ zScores: function(array: number[]) {
442
+ var mean = arrStats.mean(array)
443
+ var standardDeviation = arrStats.standardDeviation(array)
444
+ return array.map(function(num) {
445
+ return (num - mean) / standardDeviation
446
+ })
447
+ },
448
+ }
449
+
450
+ // Function aliases:
451
+ arrStats.average = arrStats.mean
452
+
453
+ export const tsNearlySame = (timeA: string, timeB: string) => timeB.startsWith(timeA.slice(0, timeA.length - 4)) // HACK: to quickly check if same second
454
+
455
+ /*
456
+ cyrb53 (c) 2018 bryc (github.com/bryc)
457
+ A fast and simple hash function with decent collision resistance.
458
+ Largely inspired by MurmurHash2/3, but with a focus on speed/simplicity.
459
+ Public domain. Attribution appreciated.
460
+
461
+ ripped from https://github.com/bryc/code/blob/mast`er/jshash/experimental/cyrb53.js
462
+ */
463
+ export const cyrb53hash = function(
464
+ str: string,
465
+ seed = 13,
466
+ strLength: number, /* = 0 */
467
+ ) {
468
+ if (!str?.length) {
469
+ throw new Error(`Empty string: ${str}`)
470
+ }
471
+
472
+ let h1 = 0xdeadbeef ^ seed
473
+ let h2 = 0x41c6ce57 ^ seed
474
+ for (let i = 0, ch; i < str.length; i++) {
475
+ ch = str.charCodeAt(i)
476
+ h1 = Math.imul(h1 ^ ch, 2654435761)
477
+ h2 = Math.imul(h2 ^ ch, 1597334677)
478
+ }
479
+ h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909)
480
+ h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909)
481
+ // if (strLength) {
482
+ const asHex = (4294967296 * (2097151 & h2) + (h1 >>> 0)).toString(16)
483
+ return asHex.slice(-strLength).padStart(strLength, '0')
484
+ // }
485
+ // // if not specified return as 16 digit integer
486
+ // return 4294967296 * (2097151 & h2) + (h1 >>> 0)
487
+ }
488
+ export function arraysContainSameElements(arr1, arr2) {
489
+ if (arr1.length !== arr2.length) {
490
+ return false
491
+ }
492
+
493
+ const sortedArr1 = [...arr1].sort()
494
+ const sortedArr2 = [...arr2].sort()
495
+
496
+ for (let i = 0; i < sortedArr1.length; i++) {
497
+ if (sortedArr1[i] !== sortedArr2[i]) {
498
+ return false
499
+ }
500
+ }
501
+
502
+ return true
503
+ }
504
+ export function dateNowIso(): string {
505
+ const now = new Date()
506
+ return now.toISOString()
507
+ }