@wovin/core 0.1.36 → 0.2.2

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 (213) 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 +40 -6
  5. package/dist/applog/applog-utils.d.ts.map +1 -1
  6. package/dist/applog/datom-types.d.ts +67 -12
  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} +12 -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-22WDFLXO.js +138 -0
  16. package/dist/chunk-22WDFLXO.js.map +1 -0
  17. package/dist/chunk-3SUFNJEZ.js +1026 -0
  18. package/dist/chunk-3SUFNJEZ.js.map +1 -0
  19. package/dist/chunk-6ALNRM3J.js +435 -0
  20. package/dist/chunk-6ALNRM3J.js.map +1 -0
  21. package/dist/chunk-7Z5YDQKK.js +1 -0
  22. package/dist/{chunk-KXMTKPF4.min.js → chunk-BLF5MAWU.js} +8 -8
  23. package/dist/chunk-BLF5MAWU.js.map +1 -0
  24. package/dist/chunk-E46VTKTZ.js +1 -0
  25. package/dist/{chunk-H3VQJP56.min.js → chunk-HUIQ54TT.js} +9 -9
  26. package/dist/chunk-HUIQ54TT.js.map +1 -0
  27. package/dist/{chunk-BRC7LSM6.min.js → chunk-OC6Z6CQW.js} +5 -5
  28. package/dist/chunk-OC6Z6CQW.js.map +1 -0
  29. package/dist/chunk-SHUHRHOT.js +1923 -0
  30. package/dist/chunk-SHUHRHOT.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} +81 -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 +87 -23
  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 +12 -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} +55 -34
  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 +57 -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 +551 -0
  117. package/src/applog/datom-types.ts +167 -0
  118. package/src/applog/object-values.test.ts +106 -0
  119. package/src/applog.ts +3 -0
  120. package/src/blockstore/index.ts +36 -0
  121. package/src/blockstore.ts +1 -0
  122. package/src/index.ts +8 -0
  123. package/src/ipfs/car.ts +291 -0
  124. package/src/ipfs/fetch-snapshot-chain.ts +135 -0
  125. package/src/ipfs/ipfs-utils.ts +132 -0
  126. package/src/ipfs.ts +3 -0
  127. package/src/ipns/ipns-record.ts +115 -0
  128. package/src/ipns.ts +1 -0
  129. package/src/pubsub/UCAN Specs Overview.md +217 -0
  130. package/src/pubsub/connector.ts +9 -0
  131. package/src/pubsub/pub-pull.ts +31 -0
  132. package/src/pubsub/pubsub-types.ts +90 -0
  133. package/src/pubsub/snap-push.ts +278 -0
  134. package/src/pubsub/ucan-example.ts +61 -0
  135. package/src/pubsub/ucan.ts +56 -0
  136. package/src/pubsub.ts +4 -0
  137. package/src/query/attr-helpers.ts +5 -0
  138. package/src/query/basic.ts +1245 -0
  139. package/src/query/divergences.ts +50 -0
  140. package/src/query/entity-collection.ts +132 -0
  141. package/src/query/liveFilterAndMap.test.ts +102 -0
  142. package/src/query/matchers.ts +30 -0
  143. package/src/query/memoized.test.ts +151 -0
  144. package/src/query/memoized.ts +180 -0
  145. package/src/query/query-steps.ts +4 -0
  146. package/src/query/query.test.ts +538 -0
  147. package/src/query/situations.ts +261 -0
  148. package/src/query/subscribable.test.ts +245 -0
  149. package/src/query/subscribable.ts +234 -0
  150. package/src/query/types.ts +155 -0
  151. package/src/query/withoutDeleted.test.ts +204 -0
  152. package/src/query.ts +9 -0
  153. package/src/retrieve/index.ts +1 -0
  154. package/src/retrieve/update-thread.ts +248 -0
  155. package/src/retrieve.ts +1 -0
  156. package/src/test/perf/query.1m.perf.test.ts +94 -0
  157. package/src/test/perf/query.perf.test.ts +389 -0
  158. package/src/test/perf/query.realdata.perf.test.ts +182 -0
  159. package/src/thread/basic.ts +209 -0
  160. package/src/thread/filters.ts +227 -0
  161. package/src/thread/indexes.ts +256 -0
  162. package/src/thread/joinThreads.test.ts +304 -0
  163. package/src/thread/mapped.ts +226 -0
  164. package/src/thread/utils.ts +144 -0
  165. package/src/thread/writeable.ts +163 -0
  166. package/src/thread.ts +6 -0
  167. package/src/types/typescript-utils.ts +64 -0
  168. package/src/types.ts +1 -0
  169. package/src/utils/debug-name.ts +54 -0
  170. package/src/utils.ts +4 -0
  171. package/dist/chunk-2Y2PYHGR.min.js +0 -65
  172. package/dist/chunk-2Y2PYHGR.min.js.map +0 -1
  173. package/dist/chunk-5MMGBK2U.min.js +0 -1
  174. package/dist/chunk-7IDQIMQO.min.js +0 -1
  175. package/dist/chunk-BRC7LSM6.min.js.map +0 -1
  176. package/dist/chunk-COXXILXC.min.js +0 -512
  177. package/dist/chunk-COXXILXC.min.js.map +0 -1
  178. package/dist/chunk-GDX2OO7L.min.js +0 -9080
  179. package/dist/chunk-GDX2OO7L.min.js.map +0 -1
  180. package/dist/chunk-H3VQJP56.min.js.map +0 -1
  181. package/dist/chunk-HYMC7W6S.min.js +0 -1549
  182. package/dist/chunk-HYMC7W6S.min.js.map +0 -1
  183. package/dist/chunk-KEHU7HGZ.min.js +0 -5216
  184. package/dist/chunk-KEHU7HGZ.min.js.map +0 -1
  185. package/dist/chunk-KXMTKPF4.min.js.map +0 -1
  186. package/dist/chunk-PHITDXZT.min.js +0 -36
  187. package/dist/chunk-QO2KMGDN.min.js +0 -3771
  188. package/dist/chunk-QO2KMGDN.min.js.map +0 -1
  189. package/dist/chunk-QPGEBDMJ.min.js.map +0 -1
  190. package/dist/chunk-WXLCBTHX.min.js +0 -1606
  191. package/dist/chunk-WXLCBTHX.min.js.map +0 -1
  192. package/dist/ipns.min.js +0 -6419
  193. package/dist/ipns.min.js.map +0 -1
  194. package/dist/mobx/mobx-utils.d.ts +0 -82
  195. package/dist/mobx/mobx-utils.d.ts.map +0 -1
  196. package/dist/mobx.d.ts +0 -2
  197. package/dist/mobx.d.ts.map +0 -1
  198. package/dist/mobx.min.js +0 -141
  199. package/dist/retrieve.min.js +0 -17
  200. package/dist/types.min.js.map +0 -1
  201. package/dist/utils.min.js +0 -10
  202. package/dist/utils.min.js.map +0 -1
  203. /package/dist/{applog.min.js.map → applog.js.map} +0 -0
  204. /package/dist/{chunk-5MMGBK2U.min.js.map → chunk-7Z5YDQKK.js.map} +0 -0
  205. /package/dist/{chunk-7IDQIMQO.min.js.map → chunk-E46VTKTZ.js.map} +0 -0
  206. /package/dist/{chunk-PHITDXZT.min.js.map → index.js.map} +0 -0
  207. /package/dist/{index.min.js.map → ipfs.js.map} +0 -0
  208. /package/dist/{ipfs.min.js.map → pubsub.js.map} +0 -0
  209. /package/dist/{mobx.min.js.map → query.js.map} +0 -0
  210. /package/dist/{pubsub.min.js.map → retrieve.js.map} +0 -0
  211. /package/dist/{query.min.js.map → thread.js.map} +0 -0
  212. /package/dist/{retrieve.min.js.map → types.js.map} +0 -0
  213. /package/dist/{thread.min.js.map → utils.js.map} +0 -0
@@ -0,0 +1,389 @@
1
+ /**
2
+ * Performance benchmarks for the query engine.
3
+ *
4
+ * Run with: npx vitest run src/query/query.perf.test.ts
5
+ *
6
+ * Purpose: compare performance between MobX-based (main) and push-based (ciao-mobx)
7
+ * implementations. Each benchmark uses performance.now() and reports median/p95
8
+ * across multiple iterations.
9
+ */
10
+ import { describe, it, expect } from 'vitest'
11
+ import { sortApplogsByTs } from '../../applog/applog-utils.ts'
12
+ import type { Applog, ApplogForInsert } from '../../applog/datom-types.ts'
13
+ import { ThreadInMemory } from '../../thread/writeable.ts'
14
+ import { rollingFilter } from '../../thread/filters.ts'
15
+ import { liveQuery, query, lastWriteWins, withoutDeleted } from '../../query/basic.ts'
16
+
17
+ // ─── Benchmark Helpers ──────────────────────────────────────────
18
+
19
+ function benchmarkSync(name: string, fn: () => void, iterations = 100): { median: number; p95: number; min: number } {
20
+ // Warmup
21
+ for (let i = 0; i < 3; i++) fn()
22
+
23
+ const times: number[] = []
24
+ for (let i = 0; i < iterations; i++) {
25
+ const start = performance.now()
26
+ fn()
27
+ times.push(performance.now() - start)
28
+ }
29
+ times.sort((a, b) => a - b)
30
+ const median = times[Math.floor(times.length / 2)]
31
+ const p95 = times[Math.floor(times.length * 0.95)]
32
+ const min = times[0]
33
+ console.log(` [PERF] ${name}: median=${median.toFixed(3)}ms p95=${p95.toFixed(3)}ms min=${min.toFixed(3)}ms (${iterations} runs)`)
34
+ return { median, p95, min }
35
+ }
36
+
37
+ // ─── Data Generation ────────────────────────────────────────────
38
+
39
+ const ENTITY_COUNT = 500
40
+ const TYPES = ['block', 'page', 'image', 'link', 'heading', 'list', 'table']
41
+ const STATUSES = ['draft', 'published', 'archived', 'deleted']
42
+ const AGENT = 'perf-agent'
43
+
44
+ function generateDataset(entityCount: number): Applog[] {
45
+ const inputs: ApplogForInsert[] = []
46
+ let tsCounter = Date.now() - entityCount * 10 // stagger timestamps
47
+
48
+ for (let i = 0; i < entityCount; i++) {
49
+ const en = `e${i}`
50
+ const ts = new Date(tsCounter).toISOString()
51
+ tsCounter += 5
52
+
53
+ // entity/name
54
+ inputs.push({ en, at: 'entity/name', vl: `Entity ${i}`, ag: AGENT })
55
+ // entity/type
56
+ inputs.push({ en, at: 'entity/type', vl: TYPES[i % TYPES.length], ag: AGENT })
57
+ // entity/status
58
+ inputs.push({ en, at: 'entity/status', vl: STATUSES[i % STATUSES.length], ag: AGENT })
59
+
60
+ // relation/parent — ~80% of entities have a parent
61
+ if (i > 0 && i % 5 !== 0) {
62
+ const parentIdx = Math.floor(i / 5) * 5 // parent is the "section head"
63
+ inputs.push({ en, at: 'relation/parent', vl: `e${parentIdx}`, ag: AGENT })
64
+ }
65
+
66
+ // Some entities get extra attributes to increase log count
67
+ if (i % 3 === 0) {
68
+ inputs.push({ en, at: 'entity/content', vl: `Content for entity ${i} with some text to simulate real data`, ag: AGENT })
69
+ }
70
+ if (i % 7 === 0) {
71
+ inputs.push({ en, at: 'entity/created', vl: new Date(tsCounter - 1000).toISOString(), ag: AGENT })
72
+ }
73
+ }
74
+
75
+ // Add some deletions (~5%)
76
+ for (let i = 0; i < entityCount; i++) {
77
+ if (i % 20 === 0) {
78
+ inputs.push({ en: `e${i}`, at: 'isDeleted', vl: true, ag: AGENT })
79
+ }
80
+ }
81
+
82
+ // Add duplicate-attribute writes (for lastWriteWins testing) — ~10% of entities get a second status
83
+ for (let i = 0; i < entityCount; i++) {
84
+ if (i % 10 === 0) {
85
+ inputs.push({ en: `e${i}`, at: 'entity/status', vl: 'updated', ag: AGENT })
86
+ }
87
+ }
88
+
89
+ const logs = inputs.map(input => ({
90
+ ts: new Date(tsCounter++).toISOString(),
91
+ pv: null,
92
+ ag: input.ag || AGENT,
93
+ ...input,
94
+ })) as Applog[]
95
+ sortApplogsByTs(logs)
96
+ return logs
97
+ }
98
+
99
+ // ─── Datasets ───────────────────────────────────────────────────
100
+
101
+ const dataset500 = generateDataset(500)
102
+ const dataset2000 = generateDataset(2000)
103
+
104
+ console.log(`\n[PERF] Dataset sizes: 500-entity=${dataset500.length} applogs, 2000-entity=${dataset2000.length} applogs\n`)
105
+
106
+ // ═════════════════════════════════════════════════════════════════
107
+ // BENCHMARKS
108
+ // ═════════════════════════════════════════════════════════════════
109
+
110
+ describe('query() performance', () => {
111
+ it('single-step: find all entities by type', () => {
112
+ const db = ThreadInMemory.fromArray([...dataset2000], 'perf-single-step')
113
+ const result = benchmarkSync('query single-step (2K entities)', () => {
114
+ query(db, [{ at: 'entity/type', vl: 'block' }])
115
+ }, 200)
116
+ // Sanity: should find ~1/7 of entities
117
+ const check = query(db, [{ at: 'entity/type', vl: 'block' }])
118
+ expect(check.nodes.length).toBeGreaterThan(100)
119
+ expect(result.median).toBeLessThan(500) // generous upper bound
120
+ })
121
+
122
+ it('multi-step: two-step variable binding', () => {
123
+ const db = ThreadInMemory.fromArray([...dataset2000], 'perf-multi-step')
124
+ const result = benchmarkSync('query multi-step (2K entities)', () => {
125
+ query(db, [
126
+ { en: '?id', at: 'entity/type', vl: 'block' },
127
+ { en: '?id', at: 'entity/status', vl: '?status' },
128
+ ])
129
+ }, 100)
130
+ const check = query(db, [
131
+ { en: '?id', at: 'entity/type', vl: 'block' },
132
+ { en: '?id', at: 'entity/status', vl: '?status' },
133
+ ])
134
+ expect(check.records.length).toBeGreaterThan(100)
135
+ expect(result.median).toBeLessThan(1000)
136
+ })
137
+
138
+ it('three-step: entity -> parent -> parent name', () => {
139
+ const db = ThreadInMemory.fromArray([...dataset500], 'perf-three-step')
140
+ const result = benchmarkSync('query three-step (500 entities)', () => {
141
+ query(db, [
142
+ { en: '?childId', at: 'entity/type', vl: 'block' },
143
+ { en: '?childId', at: 'relation/parent', vl: '?parentId' },
144
+ { en: '?parentId', at: 'entity/name', vl: '?parentName' },
145
+ ])
146
+ }, 50)
147
+ expect(result.median).toBeLessThan(2000)
148
+ })
149
+
150
+ it('memoization: same query 1000x', () => {
151
+ const db = ThreadInMemory.fromArray([...dataset2000], 'perf-memo')
152
+ // First call populates cache
153
+ query(db, [{ at: 'entity/type', vl: 'block' }])
154
+
155
+ const result = benchmarkSync('query memoized cache hit (2K entities)', () => {
156
+ query(db, [{ at: 'entity/type', vl: 'block' }])
157
+ }, 1000)
158
+ // Cache hits should be sub-millisecond
159
+ expect(result.median).toBeLessThan(1)
160
+ })
161
+ })
162
+
163
+ describe('liveQuery() performance', () => {
164
+ it('setup cost: create live query and get initial results', () => {
165
+ const result = benchmarkSync('liveQuery setup (2K entities)', () => {
166
+ const db = ThreadInMemory.fromArray([...dataset2000], 'perf-live-setup')
167
+ const lq = liveQuery(db, [{ en: '?id', at: 'entity/type', vl: '?type' }])
168
+ expect(lq.nodes.length).toBeGreaterThan(0)
169
+ lq.dispose()
170
+ }, 50)
171
+ expect(result.median).toBeLessThan(2000)
172
+ })
173
+
174
+ it('incremental update: insert 1 applog into live query', () => {
175
+ const db = ThreadInMemory.fromArray([...dataset2000], 'perf-live-incr')
176
+ const lq = liveQuery(db, [{ at: 'entity/type', vl: 'block' }])
177
+ const initialCount = lq.nodes.length
178
+
179
+ let insertCounter = 0
180
+ const result = benchmarkSync('liveQuery incremental insert (2K entities)', () => {
181
+ db.insert([{
182
+ en: `new-entity-${insertCounter++}`,
183
+ at: 'entity/type',
184
+ vl: 'block',
185
+ ag: AGENT,
186
+ }])
187
+ }, 200)
188
+
189
+ expect(lq.nodes.length).toBeGreaterThan(initialCount)
190
+ lq.dispose()
191
+ // Incremental should be fast
192
+ expect(result.median).toBeLessThan(50)
193
+ })
194
+
195
+ it('incremental update: insert into multi-step live query', () => {
196
+ const db = ThreadInMemory.fromArray([...dataset500], 'perf-live-multi-incr')
197
+ const lq = liveQuery(db, [
198
+ { en: '?id', at: 'entity/type', vl: 'block' },
199
+ { en: '?id', at: 'entity/status', vl: '?status' },
200
+ ])
201
+ const initialCount = lq.nodes.length
202
+
203
+ let insertCounter = 0
204
+ const result = benchmarkSync('liveQuery multi-step incremental (500 entities)', () => {
205
+ const en = `new-multi-${insertCounter++}`
206
+ db.insert([
207
+ { en, at: 'entity/type', vl: 'block', ag: AGENT },
208
+ { en, at: 'entity/status', vl: 'draft', ag: AGENT },
209
+ ])
210
+ }, 100)
211
+
212
+ expect(lq.nodes.length).toBeGreaterThan(initialCount)
213
+ lq.dispose()
214
+ expect(result.median).toBeLessThan(100)
215
+ })
216
+
217
+ it('subscribe event delivery latency', () => {
218
+ const db = ThreadInMemory.fromArray([...dataset2000], 'perf-live-subscribe')
219
+ const lq = liveQuery(db, [{ at: 'entity/type', vl: 'block' }])
220
+ const times: number[] = []
221
+
222
+ lq.subscribe(() => {
223
+ times.push(performance.now())
224
+ })
225
+
226
+ let insertCounter = 0
227
+ for (let i = 0; i < 100; i++) {
228
+ const start = performance.now()
229
+ db.insert([{
230
+ en: `sub-entity-${insertCounter++}`,
231
+ at: 'entity/type',
232
+ vl: 'block',
233
+ ag: AGENT,
234
+ }])
235
+ // times array is populated synchronously by the subscribe callback
236
+ }
237
+
238
+ expect(times).toHaveLength(100)
239
+ lq.dispose()
240
+ console.log(` [PERF] liveQuery subscribe: 100 events delivered synchronously`)
241
+ })
242
+ })
243
+
244
+ describe('withoutDeleted() performance', () => {
245
+ it('initial filtering on large thread', () => {
246
+ const result = benchmarkSync('withoutDeleted initial (2K entities)', () => {
247
+ const db = ThreadInMemory.fromArray([...dataset2000], 'perf-wod')
248
+ const filtered = withoutDeleted(db)
249
+ expect(filtered.applogs.length).toBeLessThan(dataset2000.length)
250
+ }, 50)
251
+ expect(result.median).toBeLessThan(1000)
252
+ })
253
+
254
+ it('incremental: insert deletion into large thread', () => {
255
+ const db = ThreadInMemory.fromArray([...dataset2000], 'perf-wod-incr')
256
+ const filtered = withoutDeleted(db)
257
+ const initialCount = filtered.applogs.length
258
+
259
+ let deleteCounter = 0
260
+ const result = benchmarkSync('withoutDeleted incremental delete (2K entities)', () => {
261
+ db.insert([{
262
+ en: `e${100 + deleteCounter++}`, // delete existing entities
263
+ at: 'isDeleted',
264
+ vl: true,
265
+ ag: AGENT,
266
+ }])
267
+ }, 100)
268
+
269
+ expect(filtered.applogs.length).toBeLessThanOrEqual(initialCount)
270
+ expect(result.median).toBeLessThan(50)
271
+ })
272
+ })
273
+
274
+ describe('lastWriteWins() performance', () => {
275
+ it('initial deduplication on large thread', () => {
276
+ const result = benchmarkSync('lastWriteWins initial (2K entities)', () => {
277
+ const db = ThreadInMemory.fromArray([...dataset2000], 'perf-lww')
278
+ const lww = lastWriteWins(db)
279
+ expect(lww.applogs.length).toBeLessThan(dataset2000.length)
280
+ }, 50)
281
+ expect(result.median).toBeLessThan(1000)
282
+ })
283
+
284
+ it('incremental: insert overwriting attribute', () => {
285
+ const db = ThreadInMemory.fromArray([...dataset2000], 'perf-lww-incr')
286
+ const lww = lastWriteWins(db)
287
+ const initialCount = lww.applogs.length
288
+
289
+ let updateCounter = 0
290
+ const result = benchmarkSync('lastWriteWins incremental update (2K entities)', () => {
291
+ db.insert([{
292
+ en: `e${updateCounter++ % 500}`,
293
+ at: 'entity/status',
294
+ vl: `updated-${updateCounter}`,
295
+ ag: AGENT,
296
+ }])
297
+ }, 200)
298
+
299
+ // Count should stay roughly the same (overwrites, not additions)
300
+ expect(lww.applogs.length).toBeLessThanOrEqual(initialCount + 200) // some might be new en+at combos
301
+ expect(result.median).toBeLessThan(50)
302
+ })
303
+ })
304
+
305
+ describe('rollingFilter() performance', () => {
306
+ it('initial filter on large thread', () => {
307
+ const result = benchmarkSync('rollingFilter initial (2K entities)', () => {
308
+ const db = ThreadInMemory.fromArray([...dataset2000], 'perf-rf')
309
+ const filtered = rollingFilter(db, { at: 'entity/type', vl: 'block' })
310
+ expect(filtered.applogs.length).toBeGreaterThan(0)
311
+ }, 50)
312
+ expect(result.median).toBeLessThan(500)
313
+ })
314
+
315
+ it('incremental: insert matching applog', () => {
316
+ const db = ThreadInMemory.fromArray([...dataset2000], 'perf-rf-incr')
317
+ const filtered = rollingFilter(db, { at: 'entity/type', vl: 'block' })
318
+ const initialCount = filtered.applogs.length
319
+
320
+ let insertCounter = 0
321
+ const result = benchmarkSync('rollingFilter incremental insert (2K entities)', () => {
322
+ db.insert([{
323
+ en: `rf-entity-${insertCounter++}`,
324
+ at: 'entity/type',
325
+ vl: 'block',
326
+ ag: AGENT,
327
+ }])
328
+ }, 200)
329
+
330
+ expect(filtered.applogs.length).toBeGreaterThan(initialCount)
331
+ expect(result.median).toBeLessThan(50)
332
+ })
333
+
334
+ it('incremental: insert non-matching applog (should be fast no-op)', () => {
335
+ const db = ThreadInMemory.fromArray([...dataset2000], 'perf-rf-nomatch')
336
+ const filtered = rollingFilter(db, { at: 'entity/type', vl: 'block' })
337
+ const initialCount = filtered.applogs.length
338
+
339
+ let insertCounter = 0
340
+ const result = benchmarkSync('rollingFilter non-matching insert (2K entities)', () => {
341
+ db.insert([{
342
+ en: `rf-nomatch-${insertCounter++}`,
343
+ at: 'entity/type',
344
+ vl: 'image',
345
+ ag: AGENT,
346
+ }])
347
+ }, 200)
348
+
349
+ expect(filtered.applogs.length).toBe(initialCount)
350
+ expect(result.median).toBeLessThan(50)
351
+ })
352
+ })
353
+
354
+ describe('combined pipeline performance', () => {
355
+ it('full pipeline: withoutDeleted -> lastWriteWins -> query', () => {
356
+ const result = benchmarkSync('full pipeline (500 entities)', () => {
357
+ const db = ThreadInMemory.fromArray([...dataset500], 'perf-pipeline')
358
+ const noDeleted = withoutDeleted(db)
359
+ const lww = lastWriteWins(noDeleted)
360
+ const qr = query(lww, [
361
+ { en: '?id', at: 'entity/type', vl: 'block' },
362
+ { en: '?id', at: 'entity/name', vl: '?name' },
363
+ ])
364
+ expect(qr.records.length).toBeGreaterThan(0)
365
+ }, 30)
366
+ expect(result.median).toBeLessThan(3000)
367
+ })
368
+
369
+ it('full pipeline incremental: insert into active pipeline', () => {
370
+ const db = ThreadInMemory.fromArray([...dataset500], 'perf-pipeline-incr')
371
+ const noDeleted = withoutDeleted(db)
372
+ const lww = lastWriteWins(noDeleted)
373
+ const lq = liveQuery(lww, [{ en: '?id', at: 'entity/type', vl: 'block' }])
374
+ const initialCount = lq.nodes.length
375
+
376
+ let insertCounter = 0
377
+ const result = benchmarkSync('full pipeline incremental (500 entities)', () => {
378
+ db.insert([
379
+ { en: `pipe-${insertCounter}`, at: 'entity/type', vl: 'block', ag: AGENT },
380
+ { en: `pipe-${insertCounter}`, at: 'entity/name', vl: `New ${insertCounter}`, ag: AGENT },
381
+ ])
382
+ insertCounter++
383
+ }, 100)
384
+
385
+ expect(lq.nodes.length).toBeGreaterThan(initialCount)
386
+ lq.dispose()
387
+ expect(result.median).toBeLessThan(100)
388
+ })
389
+ })
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Real-data performance test — uses exported applogs from a real note3 thread (47K applogs).
3
+ * Tests real query patterns from note3's reactive.ts.
4
+ */
5
+ import { describe, it, expect, beforeAll } from 'vitest'
6
+ import { existsSync, readFileSync } from 'fs'
7
+ import type { Applog } from '../../applog/datom-types.ts'
8
+ import { sortApplogsByTs } from '../../applog/applog-utils.ts'
9
+ import { ThreadInMemory } from '../../thread/writeable.ts'
10
+ import { liveQuery, liveQueryNot, query, queryNot, queryAndMap, lastWriteWins, withoutDeleted } from '../../query/basic.ts'
11
+
12
+ // ─── Real-data fixture ──────────────────────────────────────────
13
+ // Opt-in: requires `tmp/real-applogs.jsonl` (an exported real note3 thread,
14
+ // ~47K applogs). Suite is skipped when the fixture is missing.
15
+
16
+ const REAL_APPLOGS_PATH = '/repo/tmp/real-applogs.jsonl'
17
+ const hasRealData = existsSync(REAL_APPLOGS_PATH)
18
+
19
+ describe.skipIf(!hasRealData)('real note3 data — query performance', () => {
20
+ let raw: Applog[]
21
+ let db: ThreadInMemory
22
+ let lww: ReturnType<typeof lastWriteWins>
23
+ let clean: ReturnType<typeof withoutDeleted>
24
+
25
+ beforeAll(() => {
26
+ const lines = readFileSync(REAL_APPLOGS_PATH, 'utf-8').trim().split('\n')
27
+ raw = lines.map(l => JSON.parse(l)) as Applog[]
28
+ sortApplogsByTs(raw)
29
+ console.log(`\n [REAL] Loaded ${raw.length.toLocaleString()} real applogs from note3 thread (sorted)`)
30
+
31
+ const attrCounts = new Map<string, number>()
32
+ for (const log of raw) {
33
+ attrCounts.set(log.at, (attrCounts.get(log.at) || 0) + 1)
34
+ }
35
+ const top = [...attrCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 15)
36
+ console.log(` [REAL] Top attributes:`, top.map(([at, n]) => `${at}(${n})`).join(', '))
37
+ })
38
+
39
+ it('setup: load + lastWriteWins + withoutDeleted', () => {
40
+ const t0 = performance.now()
41
+ db = ThreadInMemory.fromArray([...raw], 'real-note3')
42
+ const loadTime = performance.now() - t0
43
+
44
+ const t1 = performance.now()
45
+ lww = lastWriteWins(db)
46
+ const lwwTime = performance.now() - t1
47
+
48
+ const t2 = performance.now()
49
+ clean = withoutDeleted(lww)
50
+ const wodTime = performance.now() - t2
51
+
52
+ console.log(` [REAL] Load: ${loadTime.toFixed(1)}ms (${db.size.toLocaleString()} applogs)`)
53
+ console.log(` [REAL] lastWriteWins: ${lwwTime.toFixed(1)}ms (${db.size.toLocaleString()} → ${lww.size.toLocaleString()})`)
54
+ console.log(` [REAL] withoutDeleted: ${wodTime.toFixed(1)}ms (${lww.size.toLocaleString()} → ${clean.size.toLocaleString()})`)
55
+ })
56
+
57
+ // ── Real query 1: all blocks ───────────────────────────────
58
+ it('all blocks with content (1-step)', () => {
59
+ const t0 = performance.now()
60
+ const blocks = query(clean, [
61
+ { en: '?blockID', at: 'block/content' },
62
+ ])
63
+ const elapsed = performance.now() - t0
64
+ console.log(` [REAL] all blocks: ${elapsed.toFixed(3)}ms → ${blocks.size} blocks`)
65
+ })
66
+
67
+ // ── Real query 2: useParents (2-step) ──────────────────────
68
+ it('useParents (2-step): find parent of a block', () => {
69
+ // Find a block that actually has a parent
70
+ const relations = query(clean, [
71
+ { en: '?relID', at: 'relation/block', vl: '?blockID' },
72
+ { en: '?relID', at: 'relation/childOf', vl: '?parentID' },
73
+ ])
74
+ console.log(` [REAL] total relations: ${relations.size}`)
75
+
76
+ if (relations.size > 0) {
77
+ const blockID = relations.records[0].blockID
78
+ const t0 = performance.now()
79
+ const parents = queryAndMap(clean, [
80
+ { en: '?relID', at: 'relation/block', vl: blockID },
81
+ { en: '?relID', at: 'relation/childOf', vl: '?parentID' },
82
+ ], 'parentID')
83
+ const elapsed = performance.now() - t0
84
+ console.log(` [REAL] useParents (block=${blockID}): ${elapsed.toFixed(3)}ms → ${(parents as any[]).length} parents`)
85
+ }
86
+ })
87
+
88
+ // ── Real query 3: useRoots (1-step + queryNot) ─────────────
89
+ it('useRoots: all blocks, then queryNot(has parent)', () => {
90
+ const t0 = performance.now()
91
+ const blocks = query(clean, [
92
+ { en: '?blockID', at: 'block/content' },
93
+ ])
94
+ const queryTime = performance.now() - t0
95
+
96
+ const t1 = performance.now()
97
+ // Single-step suffices: relation entities are deleted when unparenting,
98
+ // so any relation/block pointing to blockID means it has a parent.
99
+ const roots = queryNot(clean, blocks, { en: '?relID', at: 'relation/block', vl: '?blockID' })
100
+ const notTime = performance.now() - t1
101
+
102
+ console.log(` [REAL] useRoots — query blocks: ${queryTime.toFixed(3)}ms → ${blocks.size} blocks`)
103
+ console.log(` [REAL] useRoots — queryNot(parent): ${notTime.toFixed(3)}ms → ${roots.size} roots (of ${blocks.size} blocks)`)
104
+
105
+ // Debug: if roots == 0 or roots == blocks, investigate
106
+ if (roots.size === 0 || roots.size === blocks.size) {
107
+ // Check how many blocks actually have relations
108
+ const blockIDs = blocks.records.map(r => r.blockID) as string[]
109
+ const relBlocks = query(clean, [
110
+ { en: '?relID', at: 'relation/block', vl: blockIDs.slice(0, 5) },
111
+ ])
112
+ console.log(` [REAL] DEBUG: first 5 blocks have ${relBlocks.size} relations`)
113
+
114
+ // Check what relation/childOf values exist
115
+ const childOfs = query(clean, [{ at: 'relation/childOf' }])
116
+ console.log(` [REAL] DEBUG: total relation/childOf applogs: ${childOfs.size}`)
117
+ if (childOfs.size > 0) {
118
+ const sample = childOfs.nodes.slice(0, 3).map(n => `en=${n.logsOfThisNode.applogs[0]?.en} vl=${n.logsOfThisNode.applogs[0]?.vl}`)
119
+ console.log(` [REAL] DEBUG: sample childOf:`, sample)
120
+ }
121
+ }
122
+ })
123
+
124
+ // ── Real query 4: 3-step query ────────────────────────────
125
+ it('3-step: block → relation → parent name', () => {
126
+ const t0 = performance.now()
127
+ const result = query(clean, [
128
+ { en: '?blockID', at: 'block/content', vl: '?content' },
129
+ { en: '?relID', at: 'relation/block', vl: '?blockID' },
130
+ { en: '?relID', at: 'relation/childOf', vl: '?parentID' },
131
+ ])
132
+ const elapsed = performance.now() - t0
133
+ console.log(` [REAL] 3-step (block→relation→parent): ${elapsed.toFixed(3)}ms → ${result.size} results`)
134
+ })
135
+
136
+ // ── Live: useRoots reactive + insert ──────────────────────
137
+ it('liveQuery useRoots + insert', () => {
138
+ const t0 = performance.now()
139
+ const liveBlocks = liveQuery(clean, [
140
+ { en: '?blockID', at: 'block/content' },
141
+ ])
142
+ const setupTime = performance.now() - t0
143
+ const initialBlocks = liveBlocks.size
144
+
145
+ const t1 = performance.now()
146
+ // Single-step: relation entities are deleted on unparent (see useRoots comment above)
147
+ const liveRoots = liveQueryNot(clean, liveBlocks, { en: '?relID', at: 'relation/block', vl: '?blockID' })
148
+ const notSetupTime = performance.now() - t1
149
+ const initialRoots = liveRoots.size
150
+
151
+ console.log(` [REAL] liveQuery blocks setup: ${setupTime.toFixed(1)}ms → ${initialBlocks} blocks`)
152
+ console.log(` [REAL] liveQueryNot roots setup: ${notSetupTime.toFixed(1)}ms → ${initialRoots} roots`)
153
+
154
+ // Insert a new block
155
+ const t2 = performance.now()
156
+ db.insert([{ en: 'new-block-perf', at: 'block/content', vl: 'Perf test block', ag: 'perf-test' }])
157
+ const insertBlockTime = performance.now() - t2
158
+
159
+ console.log(` [REAL] Insert block: ${insertBlockTime.toFixed(3)}ms`)
160
+ console.log(` [REAL] Blocks: ${initialBlocks} → ${liveBlocks.size}`)
161
+ console.log(` [REAL] Roots: ${initialRoots} → ${liveRoots.size}`)
162
+
163
+ // Insert a relation for the new block (should remove from roots)
164
+ // Note: vl must be a real parent ID (string), not null.
165
+ // queryNot treats each pattern independently — step 1 ({relation/block, vl: ?blockID})
166
+ // does the actual exclusion. vl:null in step 2 is a no-op (matches only literal null).
167
+ const rootsBefore = liveRoots.size
168
+ const t3 = performance.now()
169
+ db.insert([
170
+ { en: 'rel-perf-1', at: 'relation/block', vl: 'new-block-perf', ag: 'perf-test' },
171
+ { en: 'rel-perf-1', at: 'relation/childOf', vl: 'some-parent-id', ag: 'perf-test' },
172
+ ])
173
+ const insertRelTime = performance.now() - t3
174
+
175
+ console.log(` [REAL] Insert parent relation: ${insertRelTime.toFixed(3)}ms`)
176
+ console.log(` [REAL] Roots: ${rootsBefore} → ${liveRoots.size}`)
177
+ expect(liveRoots.size).toBe(rootsBefore - 1)
178
+
179
+ liveRoots.dispose()
180
+ liveBlocks.dispose()
181
+ })
182
+ }, { timeout: 60_000 })