@wovin/core 0.1.35 → 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.
- package/README.md +0 -12
- package/dist/applog/applog-helpers.d.ts +12 -12
- package/dist/applog/applog-helpers.d.ts.map +1 -1
- package/dist/applog/applog-utils.d.ts +25 -6
- package/dist/applog/applog-utils.d.ts.map +1 -1
- package/dist/applog/datom-types.d.ts +4 -5
- package/dist/applog/datom-types.d.ts.map +1 -1
- package/dist/applog.d.ts +3 -3
- package/dist/applog.d.ts.map +1 -1
- package/dist/{applog.min.js → applog.js} +6 -7
- package/dist/blockstore.d.ts +1 -1
- package/dist/blockstore.d.ts.map +1 -1
- package/dist/{blockstore.min.js → blockstore.js} +1 -3
- package/dist/{blockstore.min.js.map → blockstore.js.map} +1 -1
- package/dist/{chunk-KXMTKPF4.min.js → chunk-3JZMOEOD.js} +8 -8
- package/dist/chunk-3JZMOEOD.js.map +1 -0
- package/dist/chunk-3WZVG277.js +434 -0
- package/dist/chunk-3WZVG277.js.map +1 -0
- package/dist/chunk-7Z5YDQKK.js +1 -0
- package/dist/chunk-CPSDKFBG.js +147 -0
- package/dist/chunk-CPSDKFBG.js.map +1 -0
- package/dist/chunk-E46VTKTZ.js +1 -0
- package/dist/{chunk-H3VQJP56.min.js → chunk-J2FDHGOZ.js} +9 -9
- package/dist/chunk-J2FDHGOZ.js.map +1 -0
- package/dist/chunk-L5EEEGE6.js +1862 -0
- package/dist/chunk-L5EEEGE6.js.map +1 -0
- package/dist/{chunk-BRC7LSM6.min.js → chunk-PD3C7XUM.js} +5 -5
- package/dist/chunk-PD3C7XUM.js.map +1 -0
- package/dist/chunk-QZXKQCAY.js +1026 -0
- package/dist/chunk-QZXKQCAY.js.map +1 -0
- package/dist/{chunk-QPGEBDMJ.min.js → chunk-YDAKBU6Q.js} +1 -1
- package/dist/chunk-YDAKBU6Q.js.map +1 -0
- package/dist/chunk-ZAADLBSB.js +36 -0
- package/dist/chunk-ZAADLBSB.js.map +1 -0
- package/dist/index.d.ts +7 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/{index.min.js → index.js} +73 -46
- package/dist/ipfs/car.d.ts +11 -11
- package/dist/ipfs/car.d.ts.map +1 -1
- package/dist/ipfs/ipfs-utils.d.ts +2 -2
- package/dist/ipfs/ipfs-utils.d.ts.map +1 -1
- package/dist/ipfs.d.ts +3 -3
- package/dist/ipfs.d.ts.map +1 -1
- package/dist/{ipfs.min.js → ipfs.js} +7 -10
- package/dist/ipns.d.ts +1 -1
- package/dist/ipns.d.ts.map +1 -1
- package/dist/ipns.js +64 -0
- package/dist/ipns.js.map +1 -0
- package/dist/pubsub/pub-pull.d.ts +3 -3
- package/dist/pubsub/pub-pull.d.ts.map +1 -1
- package/dist/pubsub/pubsub-types.d.ts +3 -3
- package/dist/pubsub/pubsub-types.d.ts.map +1 -1
- package/dist/pubsub/snap-push.d.ts +4 -4
- package/dist/pubsub/snap-push.d.ts.map +1 -1
- package/dist/pubsub/ucan.d.ts +1 -1
- package/dist/pubsub/ucan.d.ts.map +1 -1
- package/dist/pubsub.d.ts +4 -4
- package/dist/pubsub.d.ts.map +1 -1
- package/dist/{pubsub.min.js → pubsub.js} +7 -10
- package/dist/query/attr-helpers.d.ts +5 -0
- package/dist/query/attr-helpers.d.ts.map +1 -0
- package/dist/query/basic.d.ts +85 -21
- package/dist/query/basic.d.ts.map +1 -1
- package/dist/query/divergences.d.ts +5 -5
- package/dist/query/divergences.d.ts.map +1 -1
- package/dist/query/entity-collection.d.ts +19 -0
- package/dist/query/entity-collection.d.ts.map +1 -0
- package/dist/query/matchers.d.ts +1 -1
- package/dist/query/matchers.d.ts.map +1 -1
- package/dist/query/memoized.d.ts +66 -0
- package/dist/query/memoized.d.ts.map +1 -0
- package/dist/query/situations.d.ts +2 -1
- package/dist/query/situations.d.ts.map +1 -1
- package/dist/query/subscribable.d.ts +111 -0
- package/dist/query/subscribable.d.ts.map +1 -0
- package/dist/query/types.d.ts +54 -14
- package/dist/query/types.d.ts.map +1 -1
- package/dist/query.d.ts +9 -5
- package/dist/query.d.ts.map +1 -1
- package/dist/{query.min.js → query.js} +51 -32
- package/dist/retrieve/index.d.ts +1 -1
- package/dist/retrieve/index.d.ts.map +1 -1
- package/dist/retrieve/update-thread.d.ts +3 -3
- package/dist/retrieve/update-thread.d.ts.map +1 -1
- package/dist/retrieve.d.ts +1 -1
- package/dist/retrieve.d.ts.map +1 -1
- package/dist/retrieve.js +14 -0
- package/dist/thread/basic.d.ts +15 -19
- package/dist/thread/basic.d.ts.map +1 -1
- package/dist/thread/filters.d.ts +8 -10
- package/dist/thread/filters.d.ts.map +1 -1
- package/dist/thread/indexes.d.ts +56 -0
- package/dist/thread/indexes.d.ts.map +1 -0
- package/dist/thread/mapped.d.ts +40 -11
- package/dist/thread/mapped.d.ts.map +1 -1
- package/dist/thread/utils.d.ts +5 -5
- package/dist/thread/utils.d.ts.map +1 -1
- package/dist/thread/writeable.d.ts +2 -2
- package/dist/thread/writeable.d.ts.map +1 -1
- package/dist/thread.d.ts +6 -5
- package/dist/thread.d.ts.map +1 -1
- package/dist/{thread.min.js → thread.js} +9 -6
- package/dist/types/typescript-utils.d.ts +6 -5
- package/dist/types/typescript-utils.d.ts.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/{types.min.js → types.js} +3 -4
- package/dist/utils/debug-name.d.ts +13 -0
- package/dist/utils/debug-name.d.ts.map +1 -0
- package/dist/utils.d.ts +1 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +9 -0
- package/package.json +32 -23
- package/src/applog/applog-helpers.ts +155 -0
- package/src/applog/applog-utils.test.ts +108 -0
- package/src/applog/applog-utils.ts +507 -0
- package/src/applog/datom-types.ts +148 -0
- package/src/applog.ts +3 -0
- package/src/blockstore/index.ts +36 -0
- package/src/blockstore.ts +1 -0
- package/src/index.ts +8 -0
- package/src/ipfs/car.ts +291 -0
- package/src/ipfs/fetch-snapshot-chain.ts +135 -0
- package/src/ipfs/ipfs-utils.ts +132 -0
- package/src/ipfs.ts +3 -0
- package/src/ipns/ipns-record.ts +115 -0
- package/src/ipns.ts +1 -0
- package/src/pubsub/UCAN Specs Overview.md +217 -0
- package/src/pubsub/connector.ts +9 -0
- package/src/pubsub/pub-pull.ts +31 -0
- package/src/pubsub/pubsub-types.ts +90 -0
- package/src/pubsub/snap-push.ts +277 -0
- package/src/pubsub/ucan-example.ts +61 -0
- package/src/pubsub/ucan.ts +56 -0
- package/src/pubsub.ts +4 -0
- package/src/query/attr-helpers.ts +5 -0
- package/src/query/basic.ts +1245 -0
- package/src/query/divergences.ts +50 -0
- package/src/query/entity-collection.ts +131 -0
- package/src/query/liveFilterAndMap.test.ts +102 -0
- package/src/query/matchers.ts +8 -0
- package/src/query/memoized.test.ts +151 -0
- package/src/query/memoized.ts +180 -0
- package/src/query/query-steps.ts +4 -0
- package/src/query/query.test.ts +538 -0
- package/src/query/situations.ts +261 -0
- package/src/query/subscribable.test.ts +245 -0
- package/src/query/subscribable.ts +234 -0
- package/src/query/types.ts +155 -0
- package/src/query/withoutDeleted.test.ts +204 -0
- package/src/query.ts +9 -0
- package/src/retrieve/index.ts +1 -0
- package/src/retrieve/update-thread.ts +248 -0
- package/src/retrieve.ts +1 -0
- package/src/test/perf/query.1m.perf.test.ts +94 -0
- package/src/test/perf/query.perf.test.ts +389 -0
- package/src/test/perf/query.realdata.perf.test.ts +182 -0
- package/src/thread/basic.ts +209 -0
- package/src/thread/filters.ts +227 -0
- package/src/thread/indexes.ts +250 -0
- package/src/thread/joinThreads.test.ts +304 -0
- package/src/thread/mapped.ts +226 -0
- package/src/thread/utils.ts +144 -0
- package/src/thread/writeable.ts +163 -0
- package/src/thread.ts +6 -0
- package/src/types/typescript-utils.ts +64 -0
- package/src/types.ts +1 -0
- package/src/utils/debug-name.ts +54 -0
- package/src/utils.ts +4 -0
- package/dist/chunk-2Y2PYHGR.min.js +0 -65
- package/dist/chunk-2Y2PYHGR.min.js.map +0 -1
- package/dist/chunk-5MMGBK2U.min.js +0 -1
- package/dist/chunk-7IDQIMQO.min.js +0 -1
- package/dist/chunk-BRC7LSM6.min.js.map +0 -1
- package/dist/chunk-COXXILXC.min.js +0 -512
- package/dist/chunk-COXXILXC.min.js.map +0 -1
- package/dist/chunk-GDX2OO7L.min.js +0 -9080
- package/dist/chunk-GDX2OO7L.min.js.map +0 -1
- package/dist/chunk-H3VQJP56.min.js.map +0 -1
- package/dist/chunk-HYMC7W6S.min.js +0 -1549
- package/dist/chunk-HYMC7W6S.min.js.map +0 -1
- package/dist/chunk-KEHU7HGZ.min.js +0 -5216
- package/dist/chunk-KEHU7HGZ.min.js.map +0 -1
- package/dist/chunk-KXMTKPF4.min.js.map +0 -1
- package/dist/chunk-PHITDXZT.min.js +0 -36
- package/dist/chunk-QO2KMGDN.min.js +0 -3771
- package/dist/chunk-QO2KMGDN.min.js.map +0 -1
- package/dist/chunk-QPGEBDMJ.min.js.map +0 -1
- package/dist/chunk-WXLCBTHX.min.js +0 -1606
- package/dist/chunk-WXLCBTHX.min.js.map +0 -1
- package/dist/ipns.min.js +0 -6419
- package/dist/ipns.min.js.map +0 -1
- package/dist/mobx/mobx-utils.d.ts +0 -82
- package/dist/mobx/mobx-utils.d.ts.map +0 -1
- package/dist/mobx.d.ts +0 -2
- package/dist/mobx.d.ts.map +0 -1
- package/dist/mobx.min.js +0 -141
- package/dist/retrieve.min.js +0 -17
- package/dist/types.min.js.map +0 -1
- package/dist/utils.min.js +0 -10
- package/dist/utils.min.js.map +0 -1
- /package/dist/{applog.min.js.map → applog.js.map} +0 -0
- /package/dist/{chunk-5MMGBK2U.min.js.map → chunk-7Z5YDQKK.js.map} +0 -0
- /package/dist/{chunk-7IDQIMQO.min.js.map → chunk-E46VTKTZ.js.map} +0 -0
- /package/dist/{chunk-PHITDXZT.min.js.map → index.js.map} +0 -0
- /package/dist/{index.min.js.map → ipfs.js.map} +0 -0
- /package/dist/{ipfs.min.js.map → pubsub.js.map} +0 -0
- /package/dist/{mobx.min.js.map → query.js.map} +0 -0
- /package/dist/{pubsub.min.js.map → retrieve.js.map} +0 -0
- /package/dist/{query.min.js.map → thread.js.map} +0 -0
- /package/dist/{retrieve.min.js.map → types.js.map} +0 -0
- /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
|
+
}
|