@wovin/core 0.0.0-ciao-mobx-955482e8

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 (180) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +3 -0
  3. package/dist/applog/applog-helpers.d.ts +47 -0
  4. package/dist/applog/applog-helpers.d.ts.map +1 -0
  5. package/dist/applog/applog-utils.d.ts +57 -0
  6. package/dist/applog/applog-utils.d.ts.map +1 -0
  7. package/dist/applog/datom-types.d.ts +128 -0
  8. package/dist/applog/datom-types.d.ts.map +1 -0
  9. package/dist/applog.d.ts +4 -0
  10. package/dist/applog.d.ts.map +1 -0
  11. package/dist/applog.js +101 -0
  12. package/dist/applog.js.map +1 -0
  13. package/dist/blockstore/index.d.ts +21 -0
  14. package/dist/blockstore/index.d.ts.map +1 -0
  15. package/dist/blockstore.d.ts +2 -0
  16. package/dist/blockstore.d.ts.map +1 -0
  17. package/dist/blockstore.js +24 -0
  18. package/dist/blockstore.js.map +1 -0
  19. package/dist/chunk-6MQKRL6W.js +86 -0
  20. package/dist/chunk-6MQKRL6W.js.map +1 -0
  21. package/dist/chunk-7MW34UEO.js +40 -0
  22. package/dist/chunk-7MW34UEO.js.map +1 -0
  23. package/dist/chunk-7Z5YDQKK.js +1 -0
  24. package/dist/chunk-7Z5YDQKK.js.map +1 -0
  25. package/dist/chunk-CY4NLISM.js +144 -0
  26. package/dist/chunk-CY4NLISM.js.map +1 -0
  27. package/dist/chunk-E46VTKTZ.js +1 -0
  28. package/dist/chunk-E46VTKTZ.js.map +1 -0
  29. package/dist/chunk-O43W7UW6.js +434 -0
  30. package/dist/chunk-O43W7UW6.js.map +1 -0
  31. package/dist/chunk-XIQSYEV3.js +1604 -0
  32. package/dist/chunk-XIQSYEV3.js.map +1 -0
  33. package/dist/chunk-XVGW4QC3.js +55 -0
  34. package/dist/chunk-XVGW4QC3.js.map +1 -0
  35. package/dist/chunk-YDAKBU6Q.js +9 -0
  36. package/dist/chunk-YDAKBU6Q.js.map +1 -0
  37. package/dist/chunk-ZAADLBSB.js +36 -0
  38. package/dist/chunk-ZAADLBSB.js.map +1 -0
  39. package/dist/chunk-ZXCJRYD7.js +883 -0
  40. package/dist/chunk-ZXCJRYD7.js.map +1 -0
  41. package/dist/index.d.ts +8 -0
  42. package/dist/index.d.ts.map +1 -0
  43. package/dist/index.js +354 -0
  44. package/dist/index.js.map +1 -0
  45. package/dist/ipfs/car.d.ts +59 -0
  46. package/dist/ipfs/car.d.ts.map +1 -0
  47. package/dist/ipfs/fetch-snapshot-chain.d.ts +32 -0
  48. package/dist/ipfs/fetch-snapshot-chain.d.ts.map +1 -0
  49. package/dist/ipfs/ipfs-utils.d.ts +35 -0
  50. package/dist/ipfs/ipfs-utils.d.ts.map +1 -0
  51. package/dist/ipfs.d.ts +4 -0
  52. package/dist/ipfs.d.ts.map +1 -0
  53. package/dist/ipfs.js +60 -0
  54. package/dist/ipfs.js.map +1 -0
  55. package/dist/ipns/ipns-record.d.ts +34 -0
  56. package/dist/ipns/ipns-record.d.ts.map +1 -0
  57. package/dist/ipns.d.ts +2 -0
  58. package/dist/ipns.d.ts.map +1 -0
  59. package/dist/ipns.js +64 -0
  60. package/dist/ipns.js.map +1 -0
  61. package/dist/pubsub/connector.d.ts +9 -0
  62. package/dist/pubsub/connector.d.ts.map +1 -0
  63. package/dist/pubsub/pub-pull.d.ts +14 -0
  64. package/dist/pubsub/pub-pull.d.ts.map +1 -0
  65. package/dist/pubsub/pubsub-types.d.ts +72 -0
  66. package/dist/pubsub/pubsub-types.d.ts.map +1 -0
  67. package/dist/pubsub/snap-push.d.ts +41 -0
  68. package/dist/pubsub/snap-push.d.ts.map +1 -0
  69. package/dist/pubsub/ucan-example.d.ts +3 -0
  70. package/dist/pubsub/ucan-example.d.ts.map +1 -0
  71. package/dist/pubsub/ucan.d.ts +16 -0
  72. package/dist/pubsub/ucan.d.ts.map +1 -0
  73. package/dist/pubsub.d.ts +5 -0
  74. package/dist/pubsub.d.ts.map +1 -0
  75. package/dist/pubsub.js +31 -0
  76. package/dist/pubsub.js.map +1 -0
  77. package/dist/query/basic.d.ts +105 -0
  78. package/dist/query/basic.d.ts.map +1 -0
  79. package/dist/query/divergences.d.ts +12 -0
  80. package/dist/query/divergences.d.ts.map +1 -0
  81. package/dist/query/matchers.d.ts +4 -0
  82. package/dist/query/matchers.d.ts.map +1 -0
  83. package/dist/query/memoized.d.ts +66 -0
  84. package/dist/query/memoized.d.ts.map +1 -0
  85. package/dist/query/query-steps.d.ts +4 -0
  86. package/dist/query/query-steps.d.ts.map +1 -0
  87. package/dist/query/situations.d.ts +80 -0
  88. package/dist/query/situations.d.ts.map +1 -0
  89. package/dist/query/subscribable.d.ts +102 -0
  90. package/dist/query/subscribable.d.ts.map +1 -0
  91. package/dist/query/types.d.ts +70 -0
  92. package/dist/query/types.d.ts.map +1 -0
  93. package/dist/query.d.ts +8 -0
  94. package/dist/query.d.ts.map +1 -0
  95. package/dist/query.js +108 -0
  96. package/dist/query.js.map +1 -0
  97. package/dist/retrieve/index.d.ts +2 -0
  98. package/dist/retrieve/index.d.ts.map +1 -0
  99. package/dist/retrieve/update-thread.d.ts +64 -0
  100. package/dist/retrieve/update-thread.d.ts.map +1 -0
  101. package/dist/retrieve.d.ts +2 -0
  102. package/dist/retrieve.d.ts.map +1 -0
  103. package/dist/retrieve.js +14 -0
  104. package/dist/retrieve.js.map +1 -0
  105. package/dist/thread/basic.d.ts +60 -0
  106. package/dist/thread/basic.d.ts.map +1 -0
  107. package/dist/thread/filters.d.ts +47 -0
  108. package/dist/thread/filters.d.ts.map +1 -0
  109. package/dist/thread/mapped.d.ts +31 -0
  110. package/dist/thread/mapped.d.ts.map +1 -0
  111. package/dist/thread/utils.d.ts +23 -0
  112. package/dist/thread/utils.d.ts.map +1 -0
  113. package/dist/thread/writeable.d.ts +41 -0
  114. package/dist/thread/writeable.d.ts.map +1 -0
  115. package/dist/thread.d.ts +6 -0
  116. package/dist/thread.d.ts.map +1 -0
  117. package/dist/thread.js +54 -0
  118. package/dist/thread.js.map +1 -0
  119. package/dist/types/typescript-utils.d.ts +34 -0
  120. package/dist/types/typescript-utils.d.ts.map +1 -0
  121. package/dist/types.d.ts +2 -0
  122. package/dist/types.d.ts.map +1 -0
  123. package/dist/types.js +26 -0
  124. package/dist/types.js.map +1 -0
  125. package/dist/utils/debug-name.d.ts +13 -0
  126. package/dist/utils/debug-name.d.ts.map +1 -0
  127. package/dist/utils.d.ts +4 -0
  128. package/dist/utils.d.ts.map +1 -0
  129. package/dist/utils.js +9 -0
  130. package/dist/utils.js.map +1 -0
  131. package/package.json +110 -0
  132. package/src/applog/applog-helpers.ts +150 -0
  133. package/src/applog/applog-utils.ts +398 -0
  134. package/src/applog/datom-types.ts +148 -0
  135. package/src/applog.ts +3 -0
  136. package/src/blockstore/index.ts +36 -0
  137. package/src/blockstore.ts +1 -0
  138. package/src/index.ts +8 -0
  139. package/src/ipfs/car.ts +291 -0
  140. package/src/ipfs/fetch-snapshot-chain.ts +135 -0
  141. package/src/ipfs/ipfs-utils.ts +132 -0
  142. package/src/ipfs.ts +3 -0
  143. package/src/ipns/ipns-record.ts +115 -0
  144. package/src/ipns.ts +1 -0
  145. package/src/pubsub/UCAN Specs Overview.md +217 -0
  146. package/src/pubsub/connector.ts +9 -0
  147. package/src/pubsub/pub-pull.ts +31 -0
  148. package/src/pubsub/pubsub-types.ts +90 -0
  149. package/src/pubsub/snap-push.ts +277 -0
  150. package/src/pubsub/ucan-example.ts +61 -0
  151. package/src/pubsub/ucan.ts +56 -0
  152. package/src/pubsub.ts +4 -0
  153. package/src/query/basic.ts +1061 -0
  154. package/src/query/divergences.ts +50 -0
  155. package/src/query/matchers.ts +8 -0
  156. package/src/query/memoized.test.ts +151 -0
  157. package/src/query/memoized.ts +180 -0
  158. package/src/query/query-steps.ts +4 -0
  159. package/src/query/query.test.ts +536 -0
  160. package/src/query/situations.ts +261 -0
  161. package/src/query/subscribable.test.ts +245 -0
  162. package/src/query/subscribable.ts +225 -0
  163. package/src/query/types.ts +155 -0
  164. package/src/query.ts +7 -0
  165. package/src/retrieve/index.ts +1 -0
  166. package/src/retrieve/update-thread.ts +248 -0
  167. package/src/retrieve.ts +1 -0
  168. package/src/test/perf/query.1m.perf.test.ts +94 -0
  169. package/src/test/perf/query.perf.test.ts +389 -0
  170. package/src/test/perf/query.realdata.perf.test.ts +175 -0
  171. package/src/thread/basic.ts +209 -0
  172. package/src/thread/filters.ts +234 -0
  173. package/src/thread/mapped.ts +166 -0
  174. package/src/thread/utils.ts +146 -0
  175. package/src/thread/writeable.ts +163 -0
  176. package/src/thread.ts +5 -0
  177. package/src/types/typescript-utils.ts +64 -0
  178. package/src/types.ts +1 -0
  179. package/src/utils/debug-name.ts +54 -0
  180. package/src/utils.ts +4 -0
@@ -0,0 +1,94 @@
1
+ /**
2
+ * 1M applog stress test — static query + live query incremental update.
3
+ */
4
+ import { describe, it, expect } from 'vitest'
5
+ import type { Applog, ApplogForInsert } from '../../applog/datom-types.ts'
6
+ import { ThreadInMemory } from '../../thread/writeable.ts'
7
+ import { liveQuery, query } from '../../query/basic.ts'
8
+
9
+ const AGENT = 'stress-agent'
10
+
11
+ function generateLarge(entityCount: number): Applog[] {
12
+ const inputs: ApplogForInsert[] = []
13
+ const types = ['block', 'page', 'image', 'link', 'heading']
14
+ let ts = Date.now() - entityCount * 6
15
+
16
+ for (let i = 0; i < entityCount; i++) {
17
+ const en = `e${i}`
18
+ const nextTs = () => new Date(ts++).toISOString()
19
+ inputs.push({ en, at: 'entity/name', vl: `Entity ${i}`, ag: AGENT, ts: nextTs() } as any)
20
+ inputs.push({ en, at: 'entity/type', vl: types[i % types.length], ag: AGENT, ts: nextTs() } as any)
21
+ inputs.push({ en, at: 'entity/status', vl: i % 3 === 0 ? 'active' : 'draft', ag: AGENT, ts: nextTs() } as any)
22
+ if (i % 2 === 0) {
23
+ inputs.push({ en, at: 'entity/content', vl: `Content ${i}`, ag: AGENT, ts: nextTs() } as any)
24
+ }
25
+ if (i % 4 === 0) {
26
+ inputs.push({ en, at: 'relation/parent', vl: `e${Math.floor(i / 4)}`, ag: AGENT, ts: nextTs() } as any)
27
+ }
28
+ }
29
+
30
+ // Already sorted by construction (ts is monotonically increasing)
31
+ return inputs.map(i => ({ pv: null, ...i })) as Applog[]
32
+ }
33
+
34
+ describe('1M applog stress test', () => {
35
+ let db: ReturnType<typeof ThreadInMemory.fromArray>
36
+
37
+ it('generate ~1M applogs + load', () => {
38
+ const start = performance.now()
39
+ const dataset = generateLarge(200_000)
40
+ const genTime = performance.now() - start
41
+
42
+ const start2 = performance.now()
43
+ db = ThreadInMemory.fromArray(dataset, 'stress-1m')
44
+ const loadTime = performance.now() - start2
45
+
46
+ console.log(`\n [1M] Generated ${dataset.length.toLocaleString()} applogs in ${genTime.toFixed(0)}ms`)
47
+ console.log(` [1M] ThreadInMemory.fromArray: ${loadTime.toFixed(0)}ms`)
48
+ console.log(` [1M] Total thread size: ${db.size.toLocaleString()}`)
49
+ expect(db.size).toBeGreaterThan(700_000)
50
+ })
51
+
52
+ it('static query() — single step on ~750K applogs', () => {
53
+ const start = performance.now()
54
+ const result = query(db, [{ at: 'entity/type', vl: 'block' }])
55
+ const elapsed = performance.now() - start
56
+
57
+ console.log(` [1M] query() single-step: ${elapsed.toFixed(1)}ms → ${result.nodes.length.toLocaleString()} results`)
58
+ expect(result.nodes.length).toBe(40_000) // 200K / 5 types
59
+ })
60
+
61
+ it('liveQuery() — setup + single insert propagation on ~750K applogs', () => {
62
+ // Setup
63
+ const setupStart = performance.now()
64
+ const live = liveQuery(db, [{ at: 'entity/type', vl: 'block' }])
65
+ const setupTime = performance.now() - setupStart
66
+ const initialCount = live.nodes.length
67
+
68
+ console.log(` [1M] liveQuery() setup: ${setupTime.toFixed(1)}ms → ${initialCount.toLocaleString()} initial results`)
69
+
70
+ // Insert one matching applog and measure propagation
71
+ let eventTime: number | null = null
72
+ live.subscribe(() => {
73
+ eventTime = performance.now()
74
+ })
75
+
76
+ const insertStart = performance.now()
77
+ db.insert([{
78
+ en: 'new-entity-1m',
79
+ at: 'entity/type',
80
+ vl: 'block',
81
+ ag: AGENT,
82
+ }])
83
+ const totalTime = performance.now() - insertStart
84
+ const subscribeDelta = eventTime ? eventTime - insertStart : null
85
+
86
+ const newCount = live.nodes.length
87
+ console.log(` [1M] Insert→result updated: ${totalTime.toFixed(3)}ms`)
88
+ console.log(` [1M] Insert→subscribe fired: ${subscribeDelta?.toFixed(3) ?? 'N/A'}ms`)
89
+ console.log(` [1M] Nodes: ${initialCount.toLocaleString()} → ${newCount.toLocaleString()}`)
90
+
91
+ expect(newCount).toBe(initialCount + 1)
92
+ live.dispose()
93
+ })
94
+ }, { timeout: 120_000 })
@@ -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,175 @@
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 } from 'vitest'
6
+ import { 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
+ // ─── Load real data ─────────────────────────────────────────────
13
+
14
+ const lines = readFileSync('/repo/tmp/real-applogs.jsonl', 'utf-8').trim().split('\n')
15
+ const raw = lines.map(l => JSON.parse(l)) as Applog[]
16
+ sortApplogsByTs(raw)
17
+ console.log(`\n [REAL] Loaded ${raw.length.toLocaleString()} real applogs from note3 thread (sorted)`)
18
+
19
+ // Check what attributes exist
20
+ const attrCounts = new Map<string, number>()
21
+ for (const log of raw) {
22
+ attrCounts.set(log.at, (attrCounts.get(log.at) || 0) + 1)
23
+ }
24
+ const top = [...attrCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 15)
25
+ console.log(` [REAL] Top attributes:`, top.map(([at, n]) => `${at}(${n})`).join(', '))
26
+
27
+ describe('real note3 data — query performance', () => {
28
+ let db: ThreadInMemory
29
+ let lww: ReturnType<typeof lastWriteWins>
30
+ let clean: ReturnType<typeof withoutDeleted>
31
+
32
+ it('setup: load + lastWriteWins + withoutDeleted', () => {
33
+ const t0 = performance.now()
34
+ db = ThreadInMemory.fromArray([...raw], 'real-note3')
35
+ const loadTime = performance.now() - t0
36
+
37
+ const t1 = performance.now()
38
+ lww = lastWriteWins(db)
39
+ const lwwTime = performance.now() - t1
40
+
41
+ const t2 = performance.now()
42
+ clean = withoutDeleted(lww)
43
+ const wodTime = performance.now() - t2
44
+
45
+ console.log(` [REAL] Load: ${loadTime.toFixed(1)}ms (${db.size.toLocaleString()} applogs)`)
46
+ console.log(` [REAL] lastWriteWins: ${lwwTime.toFixed(1)}ms (${db.size.toLocaleString()} → ${lww.size.toLocaleString()})`)
47
+ console.log(` [REAL] withoutDeleted: ${wodTime.toFixed(1)}ms (${lww.size.toLocaleString()} → ${clean.size.toLocaleString()})`)
48
+ })
49
+
50
+ // ── Real query 1: all blocks ───────────────────────────────
51
+ it('all blocks with content (1-step)', () => {
52
+ const t0 = performance.now()
53
+ const blocks = query(clean, [
54
+ { en: '?blockID', at: 'block/content' },
55
+ ])
56
+ const elapsed = performance.now() - t0
57
+ console.log(` [REAL] all blocks: ${elapsed.toFixed(3)}ms → ${blocks.size} blocks`)
58
+ })
59
+
60
+ // ── Real query 2: useParents (2-step) ──────────────────────
61
+ it('useParents (2-step): find parent of a block', () => {
62
+ // Find a block that actually has a parent
63
+ const relations = query(clean, [
64
+ { en: '?relID', at: 'relation/block', vl: '?blockID' },
65
+ { en: '?relID', at: 'relation/childOf', vl: '?parentID' },
66
+ ])
67
+ console.log(` [REAL] total relations: ${relations.size}`)
68
+
69
+ if (relations.size > 0) {
70
+ const blockID = relations.records[0].blockID
71
+ const t0 = performance.now()
72
+ const parents = queryAndMap(clean, [
73
+ { en: '?relID', at: 'relation/block', vl: blockID },
74
+ { en: '?relID', at: 'relation/childOf', vl: '?parentID' },
75
+ ], 'parentID')
76
+ const elapsed = performance.now() - t0
77
+ console.log(` [REAL] useParents (block=${blockID}): ${elapsed.toFixed(3)}ms → ${(parents as any[]).length} parents`)
78
+ }
79
+ })
80
+
81
+ // ── Real query 3: useRoots (1-step + queryNot) ─────────────
82
+ it('useRoots: all blocks, then queryNot(has parent)', () => {
83
+ const t0 = performance.now()
84
+ const blocks = query(clean, [
85
+ { en: '?blockID', at: 'block/content' },
86
+ ])
87
+ const queryTime = performance.now() - t0
88
+
89
+ const t1 = performance.now()
90
+ // Single-step suffices: relation entities are deleted when unparenting,
91
+ // so any relation/block pointing to blockID means it has a parent.
92
+ const roots = queryNot(clean, blocks, { en: '?relID', at: 'relation/block', vl: '?blockID' })
93
+ const notTime = performance.now() - t1
94
+
95
+ console.log(` [REAL] useRoots — query blocks: ${queryTime.toFixed(3)}ms → ${blocks.size} blocks`)
96
+ console.log(` [REAL] useRoots — queryNot(parent): ${notTime.toFixed(3)}ms → ${roots.size} roots (of ${blocks.size} blocks)`)
97
+
98
+ // Debug: if roots == 0 or roots == blocks, investigate
99
+ if (roots.size === 0 || roots.size === blocks.size) {
100
+ // Check how many blocks actually have relations
101
+ const blockIDs = blocks.records.map(r => r.blockID) as string[]
102
+ const relBlocks = query(clean, [
103
+ { en: '?relID', at: 'relation/block', vl: blockIDs.slice(0, 5) },
104
+ ])
105
+ console.log(` [REAL] DEBUG: first 5 blocks have ${relBlocks.size} relations`)
106
+
107
+ // Check what relation/childOf values exist
108
+ const childOfs = query(clean, [{ at: 'relation/childOf' }])
109
+ console.log(` [REAL] DEBUG: total relation/childOf applogs: ${childOfs.size}`)
110
+ if (childOfs.size > 0) {
111
+ const sample = childOfs.nodes.slice(0, 3).map(n => `en=${n.logsOfThisNode.applogs[0]?.en} vl=${n.logsOfThisNode.applogs[0]?.vl}`)
112
+ console.log(` [REAL] DEBUG: sample childOf:`, sample)
113
+ }
114
+ }
115
+ })
116
+
117
+ // ── Real query 4: 3-step query ────────────────────────────
118
+ it('3-step: block → relation → parent name', () => {
119
+ const t0 = performance.now()
120
+ const result = query(clean, [
121
+ { en: '?blockID', at: 'block/content', vl: '?content' },
122
+ { en: '?relID', at: 'relation/block', vl: '?blockID' },
123
+ { en: '?relID', at: 'relation/childOf', vl: '?parentID' },
124
+ ])
125
+ const elapsed = performance.now() - t0
126
+ console.log(` [REAL] 3-step (block→relation→parent): ${elapsed.toFixed(3)}ms → ${result.size} results`)
127
+ })
128
+
129
+ // ── Live: useRoots reactive + insert ──────────────────────
130
+ it('liveQuery useRoots + insert', () => {
131
+ const t0 = performance.now()
132
+ const liveBlocks = liveQuery(clean, [
133
+ { en: '?blockID', at: 'block/content' },
134
+ ])
135
+ const setupTime = performance.now() - t0
136
+ const initialBlocks = liveBlocks.size
137
+
138
+ const t1 = performance.now()
139
+ // Single-step: relation entities are deleted on unparent (see useRoots comment above)
140
+ const liveRoots = liveQueryNot(clean, liveBlocks, { en: '?relID', at: 'relation/block', vl: '?blockID' })
141
+ const notSetupTime = performance.now() - t1
142
+ const initialRoots = liveRoots.size
143
+
144
+ console.log(` [REAL] liveQuery blocks setup: ${setupTime.toFixed(1)}ms → ${initialBlocks} blocks`)
145
+ console.log(` [REAL] liveQueryNot roots setup: ${notSetupTime.toFixed(1)}ms → ${initialRoots} roots`)
146
+
147
+ // Insert a new block
148
+ const t2 = performance.now()
149
+ db.insert([{ en: 'new-block-perf', at: 'block/content', vl: 'Perf test block', ag: 'perf-test' }])
150
+ const insertBlockTime = performance.now() - t2
151
+
152
+ console.log(` [REAL] Insert block: ${insertBlockTime.toFixed(3)}ms`)
153
+ console.log(` [REAL] Blocks: ${initialBlocks} → ${liveBlocks.size}`)
154
+ console.log(` [REAL] Roots: ${initialRoots} → ${liveRoots.size}`)
155
+
156
+ // Insert a relation for the new block (should remove from roots)
157
+ // Note: vl must be a real parent ID (string), not null.
158
+ // queryNot treats each pattern independently — step 1 ({relation/block, vl: ?blockID})
159
+ // does the actual exclusion. vl:null in step 2 is a no-op (matches only literal null).
160
+ const rootsBefore = liveRoots.size
161
+ const t3 = performance.now()
162
+ db.insert([
163
+ { en: 'rel-perf-1', at: 'relation/block', vl: 'new-block-perf', ag: 'perf-test' },
164
+ { en: 'rel-perf-1', at: 'relation/childOf', vl: 'some-parent-id', ag: 'perf-test' },
165
+ ])
166
+ const insertRelTime = performance.now() - t3
167
+
168
+ console.log(` [REAL] Insert parent relation: ${insertRelTime.toFixed(3)}ms`)
169
+ console.log(` [REAL] Roots: ${rootsBefore} → ${liveRoots.size}`)
170
+ expect(liveRoots.size).toBe(rootsBefore - 1)
171
+
172
+ liveRoots.dispose()
173
+ liveBlocks.dispose()
174
+ })
175
+ }, { timeout: 60_000 })