@snaha/swarm-id 0.0.1
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 +431 -0
- package/dist/chunk/bmt.d.ts +17 -0
- package/dist/chunk/bmt.d.ts.map +1 -0
- package/dist/chunk/cac.d.ts +18 -0
- package/dist/chunk/cac.d.ts.map +1 -0
- package/dist/chunk/constants.d.ts +10 -0
- package/dist/chunk/constants.d.ts.map +1 -0
- package/dist/chunk/encrypted-cac.d.ts +48 -0
- package/dist/chunk/encrypted-cac.d.ts.map +1 -0
- package/dist/chunk/encryption.d.ts +86 -0
- package/dist/chunk/encryption.d.ts.map +1 -0
- package/dist/chunk/index.d.ts +6 -0
- package/dist/chunk/index.d.ts.map +1 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/proxy/act/act.d.ts +78 -0
- package/dist/proxy/act/act.d.ts.map +1 -0
- package/dist/proxy/act/crypto.d.ts +44 -0
- package/dist/proxy/act/crypto.d.ts.map +1 -0
- package/dist/proxy/act/grantee-list.d.ts +82 -0
- package/dist/proxy/act/grantee-list.d.ts.map +1 -0
- package/dist/proxy/act/history.d.ts +183 -0
- package/dist/proxy/act/history.d.ts.map +1 -0
- package/dist/proxy/act/index.d.ts +104 -0
- package/dist/proxy/act/index.d.ts.map +1 -0
- package/dist/proxy/chunking-encrypted.d.ts +14 -0
- package/dist/proxy/chunking-encrypted.d.ts.map +1 -0
- package/dist/proxy/chunking.d.ts +15 -0
- package/dist/proxy/chunking.d.ts.map +1 -0
- package/dist/proxy/download-data.d.ts +16 -0
- package/dist/proxy/download-data.d.ts.map +1 -0
- package/dist/proxy/feed-manifest.d.ts +62 -0
- package/dist/proxy/feed-manifest.d.ts.map +1 -0
- package/dist/proxy/feeds/epochs/async-finder.d.ts +77 -0
- package/dist/proxy/feeds/epochs/async-finder.d.ts.map +1 -0
- package/dist/proxy/feeds/epochs/epoch.d.ts +88 -0
- package/dist/proxy/feeds/epochs/epoch.d.ts.map +1 -0
- package/dist/proxy/feeds/epochs/finder.d.ts +67 -0
- package/dist/proxy/feeds/epochs/finder.d.ts.map +1 -0
- package/dist/proxy/feeds/epochs/index.d.ts +35 -0
- package/dist/proxy/feeds/epochs/index.d.ts.map +1 -0
- package/dist/proxy/feeds/epochs/test-utils.d.ts +93 -0
- package/dist/proxy/feeds/epochs/test-utils.d.ts.map +1 -0
- package/dist/proxy/feeds/epochs/types.d.ts +109 -0
- package/dist/proxy/feeds/epochs/types.d.ts.map +1 -0
- package/dist/proxy/feeds/epochs/updater.d.ts +68 -0
- package/dist/proxy/feeds/epochs/updater.d.ts.map +1 -0
- package/dist/proxy/feeds/epochs/utils.d.ts +22 -0
- package/dist/proxy/feeds/epochs/utils.d.ts.map +1 -0
- package/dist/proxy/feeds/index.d.ts +5 -0
- package/dist/proxy/feeds/index.d.ts.map +1 -0
- package/dist/proxy/feeds/sequence/async-finder.d.ts +14 -0
- package/dist/proxy/feeds/sequence/async-finder.d.ts.map +1 -0
- package/dist/proxy/feeds/sequence/finder.d.ts +17 -0
- package/dist/proxy/feeds/sequence/finder.d.ts.map +1 -0
- package/dist/proxy/feeds/sequence/index.d.ts +23 -0
- package/dist/proxy/feeds/sequence/index.d.ts.map +1 -0
- package/dist/proxy/feeds/sequence/types.d.ts +80 -0
- package/dist/proxy/feeds/sequence/types.d.ts.map +1 -0
- package/dist/proxy/feeds/sequence/updater.d.ts +26 -0
- package/dist/proxy/feeds/sequence/updater.d.ts.map +1 -0
- package/dist/proxy/index.d.ts +6 -0
- package/dist/proxy/index.d.ts.map +1 -0
- package/dist/proxy/manifest-builder.d.ts +183 -0
- package/dist/proxy/manifest-builder.d.ts.map +1 -0
- package/dist/proxy/mantaray-encrypted.d.ts +27 -0
- package/dist/proxy/mantaray-encrypted.d.ts.map +1 -0
- package/dist/proxy/mantaray.d.ts +26 -0
- package/dist/proxy/mantaray.d.ts.map +1 -0
- package/dist/proxy/types.d.ts +29 -0
- package/dist/proxy/types.d.ts.map +1 -0
- package/dist/proxy/upload-data.d.ts +17 -0
- package/dist/proxy/upload-data.d.ts.map +1 -0
- package/dist/proxy/upload-encrypted-data.d.ts +103 -0
- package/dist/proxy/upload-encrypted-data.d.ts.map +1 -0
- package/dist/schemas.d.ts +240 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/storage/debounced-uploader.d.ts +62 -0
- package/dist/storage/debounced-uploader.d.ts.map +1 -0
- package/dist/storage/utilization-store.d.ts +108 -0
- package/dist/storage/utilization-store.d.ts.map +1 -0
- package/dist/swarm-id-auth.d.ts +74 -0
- package/dist/swarm-id-auth.d.ts.map +1 -0
- package/dist/swarm-id-auth.js +2 -0
- package/dist/swarm-id-auth.js.map +1 -0
- package/dist/swarm-id-client.d.ts +878 -0
- package/dist/swarm-id-client.d.ts.map +1 -0
- package/dist/swarm-id-client.js +2 -0
- package/dist/swarm-id-client.js.map +1 -0
- package/dist/swarm-id-proxy.d.ts +236 -0
- package/dist/swarm-id-proxy.d.ts.map +1 -0
- package/dist/swarm-id-proxy.js +2 -0
- package/dist/swarm-id-proxy.js.map +1 -0
- package/dist/swarm-id.esm.js +2 -0
- package/dist/swarm-id.esm.js.map +1 -0
- package/dist/swarm-id.umd.js +2 -0
- package/dist/swarm-id.umd.js.map +1 -0
- package/dist/sync/index.d.ts +9 -0
- package/dist/sync/index.d.ts.map +1 -0
- package/dist/sync/key-derivation.d.ts +25 -0
- package/dist/sync/key-derivation.d.ts.map +1 -0
- package/dist/sync/restore-account.d.ts +28 -0
- package/dist/sync/restore-account.d.ts.map +1 -0
- package/dist/sync/serialization.d.ts +16 -0
- package/dist/sync/serialization.d.ts.map +1 -0
- package/dist/sync/store-interfaces.d.ts +53 -0
- package/dist/sync/store-interfaces.d.ts.map +1 -0
- package/dist/sync/sync-account.d.ts +44 -0
- package/dist/sync/sync-account.d.ts.map +1 -0
- package/dist/sync/types.d.ts +13 -0
- package/dist/sync/types.d.ts.map +1 -0
- package/dist/test-fixtures.d.ts +17 -0
- package/dist/test-fixtures.d.ts.map +1 -0
- package/dist/types-BD_VkNn0.js +2 -0
- package/dist/types-BD_VkNn0.js.map +1 -0
- package/dist/types-lJCaT-50.js +2 -0
- package/dist/types-lJCaT-50.js.map +1 -0
- package/dist/types.d.ts +2157 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/utils/account-payload.d.ts +94 -0
- package/dist/utils/account-payload.d.ts.map +1 -0
- package/dist/utils/account-state-snapshot.d.ts +38 -0
- package/dist/utils/account-state-snapshot.d.ts.map +1 -0
- package/dist/utils/backup-encryption.d.ts +127 -0
- package/dist/utils/backup-encryption.d.ts.map +1 -0
- package/dist/utils/batch-utilization.d.ts +432 -0
- package/dist/utils/batch-utilization.d.ts.map +1 -0
- package/dist/utils/constants.d.ts +11 -0
- package/dist/utils/constants.d.ts.map +1 -0
- package/dist/utils/hex.d.ts +17 -0
- package/dist/utils/hex.d.ts.map +1 -0
- package/dist/utils/key-derivation.d.ts +92 -0
- package/dist/utils/key-derivation.d.ts.map +1 -0
- package/dist/utils/storage-managers.d.ts +65 -0
- package/dist/utils/storage-managers.d.ts.map +1 -0
- package/dist/utils/swarm-id-export.d.ts +24 -0
- package/dist/utils/swarm-id-export.d.ts.map +1 -0
- package/dist/utils/ttl.d.ts +49 -0
- package/dist/utils/ttl.d.ts.map +1 -0
- package/dist/utils/url.d.ts +41 -0
- package/dist/utils/url.d.ts.map +1 -0
- package/dist/utils/versioned-storage.d.ts +131 -0
- package/dist/utils/versioned-storage.d.ts.map +1 -0
- package/package.json +78 -0
- package/src/chunk/bmt.test.ts +217 -0
- package/src/chunk/bmt.ts +57 -0
- package/src/chunk/cac.test.ts +214 -0
- package/src/chunk/cac.ts +65 -0
- package/src/chunk/constants.ts +18 -0
- package/src/chunk/encrypted-cac.test.ts +385 -0
- package/src/chunk/encrypted-cac.ts +131 -0
- package/src/chunk/encryption.test.ts +352 -0
- package/src/chunk/encryption.ts +300 -0
- package/src/chunk/index.ts +47 -0
- package/src/index.ts +430 -0
- package/src/proxy/act/act.test.ts +278 -0
- package/src/proxy/act/act.ts +158 -0
- package/src/proxy/act/bee-compat.test.ts +948 -0
- package/src/proxy/act/crypto.test.ts +436 -0
- package/src/proxy/act/crypto.ts +376 -0
- package/src/proxy/act/grantee-list.test.ts +393 -0
- package/src/proxy/act/grantee-list.ts +239 -0
- package/src/proxy/act/history.test.ts +360 -0
- package/src/proxy/act/history.ts +413 -0
- package/src/proxy/act/index.test.ts +748 -0
- package/src/proxy/act/index.ts +853 -0
- package/src/proxy/chunking-encrypted.ts +95 -0
- package/src/proxy/chunking.ts +65 -0
- package/src/proxy/download-data.ts +448 -0
- package/src/proxy/feed-manifest.ts +174 -0
- package/src/proxy/feeds/epochs/async-finder.ts +372 -0
- package/src/proxy/feeds/epochs/epoch.test.ts +249 -0
- package/src/proxy/feeds/epochs/epoch.ts +181 -0
- package/src/proxy/feeds/epochs/finder.ts +282 -0
- package/src/proxy/feeds/epochs/index.ts +73 -0
- package/src/proxy/feeds/epochs/integration.test.ts +1336 -0
- package/src/proxy/feeds/epochs/test-utils.ts +274 -0
- package/src/proxy/feeds/epochs/types.ts +128 -0
- package/src/proxy/feeds/epochs/updater.ts +192 -0
- package/src/proxy/feeds/epochs/utils.ts +62 -0
- package/src/proxy/feeds/index.ts +5 -0
- package/src/proxy/feeds/sequence/async-finder.ts +31 -0
- package/src/proxy/feeds/sequence/finder.ts +73 -0
- package/src/proxy/feeds/sequence/index.ts +54 -0
- package/src/proxy/feeds/sequence/integration.test.ts +966 -0
- package/src/proxy/feeds/sequence/types.ts +103 -0
- package/src/proxy/feeds/sequence/updater.ts +71 -0
- package/src/proxy/index.ts +5 -0
- package/src/proxy/manifest-builder.test.ts +427 -0
- package/src/proxy/manifest-builder.ts +679 -0
- package/src/proxy/mantaray-encrypted.ts +78 -0
- package/src/proxy/mantaray.ts +104 -0
- package/src/proxy/types.ts +32 -0
- package/src/proxy/upload-data.ts +189 -0
- package/src/proxy/upload-encrypted-data.ts +658 -0
- package/src/schemas.ts +299 -0
- package/src/storage/debounced-uploader.ts +192 -0
- package/src/storage/utilization-store.ts +397 -0
- package/src/swarm-id-client.test.ts +99 -0
- package/src/swarm-id-client.ts +3095 -0
- package/src/swarm-id-proxy.ts +3891 -0
- package/src/sync/index.ts +28 -0
- package/src/sync/restore-account.ts +90 -0
- package/src/sync/serialization.ts +39 -0
- package/src/sync/store-interfaces.ts +62 -0
- package/src/sync/sync-account.test.ts +302 -0
- package/src/sync/sync-account.ts +396 -0
- package/src/sync/types.ts +11 -0
- package/src/test-fixtures.ts +109 -0
- package/src/types.ts +1651 -0
- package/src/utils/account-state-snapshot.test.ts +595 -0
- package/src/utils/account-state-snapshot.ts +94 -0
- package/src/utils/backup-encryption.test.ts +442 -0
- package/src/utils/backup-encryption.ts +352 -0
- package/src/utils/batch-utilization.ts +1309 -0
- package/src/utils/constants.ts +20 -0
- package/src/utils/hex.ts +27 -0
- package/src/utils/key-derivation.ts +197 -0
- package/src/utils/storage-managers.ts +365 -0
- package/src/utils/ttl.ts +129 -0
- package/src/utils/url.test.ts +136 -0
- package/src/utils/url.ts +71 -0
- package/src/utils/versioned-storage.ts +323 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for epoch structure and tree navigation
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect } from "vitest"
|
|
6
|
+
import { EpochIndex, lca, next, MAX_LEVEL } from "./epoch"
|
|
7
|
+
|
|
8
|
+
describe("EpochIndex", () => {
|
|
9
|
+
describe("constructor", () => {
|
|
10
|
+
it("should create epoch with valid level", () => {
|
|
11
|
+
const epoch = new EpochIndex(0n, 0)
|
|
12
|
+
expect(epoch.start).toBe(0n)
|
|
13
|
+
expect(epoch.level).toBe(0)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it("should throw error for negative level", () => {
|
|
17
|
+
expect(() => new EpochIndex(0n, -1)).toThrow()
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it("should throw error for level > MAX_LEVEL", () => {
|
|
21
|
+
expect(() => new EpochIndex(0n, MAX_LEVEL + 1)).toThrow()
|
|
22
|
+
})
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
describe("length", () => {
|
|
26
|
+
it("should return 1 for level 0", () => {
|
|
27
|
+
const epoch = new EpochIndex(0n, 0)
|
|
28
|
+
expect(epoch.length()).toBe(1n)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it("should return 2^level", () => {
|
|
32
|
+
expect(new EpochIndex(0n, 1).length()).toBe(2n)
|
|
33
|
+
expect(new EpochIndex(0n, 2).length()).toBe(4n)
|
|
34
|
+
expect(new EpochIndex(0n, 3).length()).toBe(8n)
|
|
35
|
+
expect(new EpochIndex(0n, 10).length()).toBe(1024n)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it("should handle MAX_LEVEL", () => {
|
|
39
|
+
const epoch = new EpochIndex(0n, MAX_LEVEL)
|
|
40
|
+
expect(epoch.length()).toBe(1n << BigInt(MAX_LEVEL))
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
describe("parent", () => {
|
|
45
|
+
it("should calculate parent epoch", () => {
|
|
46
|
+
const child = new EpochIndex(0n, 0)
|
|
47
|
+
const parent = child.parent()
|
|
48
|
+
expect(parent.level).toBe(1)
|
|
49
|
+
expect(parent.start).toBe(0n)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it("should normalize start to parent boundary", () => {
|
|
53
|
+
const child = new EpochIndex(3n, 0)
|
|
54
|
+
const parent = child.parent()
|
|
55
|
+
expect(parent.level).toBe(1)
|
|
56
|
+
expect(parent.start).toBe(2n) // 3/2*2 = 2
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it("should handle multiple levels", () => {
|
|
60
|
+
let epoch = new EpochIndex(0n, 0)
|
|
61
|
+
epoch = epoch.parent()
|
|
62
|
+
expect(epoch.level).toBe(1)
|
|
63
|
+
epoch = epoch.parent()
|
|
64
|
+
expect(epoch.level).toBe(2)
|
|
65
|
+
epoch = epoch.parent()
|
|
66
|
+
expect(epoch.level).toBe(3)
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
describe("left", () => {
|
|
71
|
+
it("should return left sibling at same level", () => {
|
|
72
|
+
const epoch = new EpochIndex(2n, 1)
|
|
73
|
+
const left = epoch.left()
|
|
74
|
+
expect(left.level).toBe(1) // Same level
|
|
75
|
+
expect(left.start).toBe(0n) // 2 - 2 = 0
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it("should calculate left sibling correctly", () => {
|
|
79
|
+
const epoch = new EpochIndex(8n, 2)
|
|
80
|
+
const left = epoch.left()
|
|
81
|
+
expect(left.level).toBe(2) // Same level
|
|
82
|
+
expect(left.start).toBe(4n) // 8 - 4 = 4
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
describe("childAt", () => {
|
|
87
|
+
it("should return left child when at is in left half", () => {
|
|
88
|
+
const parent = new EpochIndex(0n, 2)
|
|
89
|
+
const child = parent.childAt(1n)
|
|
90
|
+
expect(child.level).toBe(1)
|
|
91
|
+
expect(child.start).toBe(0n)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it("should return right child when at is in right half", () => {
|
|
95
|
+
const parent = new EpochIndex(0n, 2)
|
|
96
|
+
const child = parent.childAt(3n)
|
|
97
|
+
expect(child.level).toBe(1)
|
|
98
|
+
expect(child.start).toBe(2n)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it("should handle multiple descents", () => {
|
|
102
|
+
let epoch = new EpochIndex(0n, 3)
|
|
103
|
+
epoch = epoch.childAt(5n)
|
|
104
|
+
expect(epoch.level).toBe(2)
|
|
105
|
+
expect(epoch.start).toBe(4n)
|
|
106
|
+
epoch = epoch.childAt(5n)
|
|
107
|
+
expect(epoch.level).toBe(1)
|
|
108
|
+
expect(epoch.start).toBe(4n)
|
|
109
|
+
epoch = epoch.childAt(5n)
|
|
110
|
+
expect(epoch.level).toBe(0)
|
|
111
|
+
expect(epoch.start).toBe(5n)
|
|
112
|
+
})
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
describe("isLeft", () => {
|
|
116
|
+
it("should return true for left child", () => {
|
|
117
|
+
const parent = new EpochIndex(0n, 2)
|
|
118
|
+
const left = parent.childAt(0n)
|
|
119
|
+
expect(left.isLeft()).toBe(true)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it("should return false for right child", () => {
|
|
123
|
+
const parent = new EpochIndex(0n, 2)
|
|
124
|
+
const right = parent.childAt(3n)
|
|
125
|
+
expect(right.isLeft()).toBe(false)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it("should handle level 0", () => {
|
|
129
|
+
expect(new EpochIndex(0n, 0).isLeft()).toBe(true)
|
|
130
|
+
expect(new EpochIndex(1n, 0).isLeft()).toBe(false)
|
|
131
|
+
expect(new EpochIndex(2n, 0).isLeft()).toBe(true)
|
|
132
|
+
expect(new EpochIndex(3n, 0).isLeft()).toBe(false)
|
|
133
|
+
})
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
describe("marshalBinary", () => {
|
|
137
|
+
it("should produce 32-byte hash", async () => {
|
|
138
|
+
const epoch = new EpochIndex(0n, 0)
|
|
139
|
+
const hash = await epoch.marshalBinary()
|
|
140
|
+
expect(hash.length).toBe(32)
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it("should produce different hashes for different epochs", async () => {
|
|
144
|
+
const epoch1 = new EpochIndex(0n, 0)
|
|
145
|
+
const epoch2 = new EpochIndex(1n, 0)
|
|
146
|
+
const epoch3 = new EpochIndex(0n, 1)
|
|
147
|
+
|
|
148
|
+
const hash1 = await epoch1.marshalBinary()
|
|
149
|
+
const hash2 = await epoch2.marshalBinary()
|
|
150
|
+
const hash3 = await epoch3.marshalBinary()
|
|
151
|
+
|
|
152
|
+
expect(hash1).not.toEqual(hash2)
|
|
153
|
+
expect(hash1).not.toEqual(hash3)
|
|
154
|
+
expect(hash2).not.toEqual(hash3)
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it("should produce same hash for same epoch", async () => {
|
|
158
|
+
const epoch1 = new EpochIndex(42n, 10)
|
|
159
|
+
const epoch2 = new EpochIndex(42n, 10)
|
|
160
|
+
|
|
161
|
+
const hash1 = await epoch1.marshalBinary()
|
|
162
|
+
const hash2 = await epoch2.marshalBinary()
|
|
163
|
+
|
|
164
|
+
expect(hash1).toEqual(hash2)
|
|
165
|
+
})
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
describe("toString", () => {
|
|
169
|
+
it("should format as start/level", () => {
|
|
170
|
+
expect(new EpochIndex(0n, 0).toString()).toBe("0/0")
|
|
171
|
+
expect(new EpochIndex(42n, 10).toString()).toBe("42/10")
|
|
172
|
+
expect(new EpochIndex(1000n, 20).toString()).toBe("1000/20")
|
|
173
|
+
})
|
|
174
|
+
})
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
describe("lca (Lowest Common Ancestor)", () => {
|
|
178
|
+
it("should return top epoch when after is 0", () => {
|
|
179
|
+
const result = lca(100n, 0n)
|
|
180
|
+
expect(result.level).toBe(MAX_LEVEL)
|
|
181
|
+
expect(result.start).toBe(0n)
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it("should return level 1 for adjacent timestamps", () => {
|
|
185
|
+
const result = lca(5n, 4n)
|
|
186
|
+
expect(result.level).toBe(1) // Spans [4, 6), contains both 4 and 5
|
|
187
|
+
expect(result.start).toBe(4n)
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
it("should return level 2 for timestamps 2 apart", () => {
|
|
191
|
+
const result = lca(6n, 4n)
|
|
192
|
+
expect(result.level).toBe(2) // Spans [4, 8), contains both 4 and 6
|
|
193
|
+
expect(result.start).toBe(4n)
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it("should return level 4 for timestamps 4 apart", () => {
|
|
197
|
+
const result = lca(8n, 4n)
|
|
198
|
+
expect(result.level).toBe(4) // Spans [0, 16), contains both 4 and 8
|
|
199
|
+
expect(result.start).toBe(0n)
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
it("should handle same timestamp", () => {
|
|
203
|
+
const result = lca(5n, 5n)
|
|
204
|
+
expect(result.level).toBe(0)
|
|
205
|
+
expect(result.start).toBe(5n)
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
it("should handle large gaps", () => {
|
|
209
|
+
const result = lca(1000n, 100n)
|
|
210
|
+
expect(result.level).toBeGreaterThan(8) // 2^9 = 512, 2^10 = 1024
|
|
211
|
+
})
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
describe("next", () => {
|
|
215
|
+
it("should return top epoch for first update", () => {
|
|
216
|
+
const result = next(undefined, 0n, 100n)
|
|
217
|
+
expect(result.level).toBe(MAX_LEVEL)
|
|
218
|
+
expect(result.start).toBe(0n)
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it("should calculate next epoch within same parent", () => {
|
|
222
|
+
// Last update was at time 0 in epoch (0, 2)
|
|
223
|
+
const last = new EpochIndex(0n, 2)
|
|
224
|
+
// New update at time 1, which is within [0, 4)
|
|
225
|
+
const result = next(last, 0n, 1n)
|
|
226
|
+
// Should descend to child at time 1
|
|
227
|
+
expect(result.start).toBe(0n)
|
|
228
|
+
expect(result.level).toBe(1)
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
it("should handle updates spanning different epochs", () => {
|
|
232
|
+
// Last update was at time 100 in some epoch
|
|
233
|
+
const last = new EpochIndex(100n, 0)
|
|
234
|
+
// New update at time 200
|
|
235
|
+
const result = next(last, 100n, 200n)
|
|
236
|
+
// Should use lca(200, 100) and descend
|
|
237
|
+
expect(result.start).toBeLessThanOrEqual(200n)
|
|
238
|
+
expect(result.level).toBeGreaterThanOrEqual(0)
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
it("proxy-style seeded parent state should descend to exact level-0 epoch", () => {
|
|
242
|
+
for (let at = 1n; at < 128n; at++) {
|
|
243
|
+
const seededParent = new EpochIndex(at & ~1n, 1)
|
|
244
|
+
const result = next(seededParent, at - 1n, at)
|
|
245
|
+
expect(result.level).toBe(0)
|
|
246
|
+
expect(result.start).toBe(at)
|
|
247
|
+
}
|
|
248
|
+
})
|
|
249
|
+
})
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Epoch-based Feed Indexing
|
|
3
|
+
*
|
|
4
|
+
* Implements time-based indexing using a binary tree structure where each epoch
|
|
5
|
+
* represents a time range with start time and level (0-32).
|
|
6
|
+
*
|
|
7
|
+
* Based on the Swarm Book of Feeds and Bee's epochs implementation.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Binary } from "cafe-utility"
|
|
11
|
+
|
|
12
|
+
/** Maximum epoch level (2^32 seconds ≈ 136 years) */
|
|
13
|
+
export const MAX_LEVEL = 32
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Epoch interface - represents a time slot in the epoch tree
|
|
17
|
+
*/
|
|
18
|
+
export interface Epoch {
|
|
19
|
+
start: bigint // Unix timestamp (seconds)
|
|
20
|
+
level: number // 0-32, where length = 2^level
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* EpochIndex class - implements epoch-based feed indexing
|
|
25
|
+
*
|
|
26
|
+
* Each epoch represents a time range:
|
|
27
|
+
* - start: beginning timestamp of the epoch
|
|
28
|
+
* - level: determines the epoch's time span (2^level seconds)
|
|
29
|
+
*
|
|
30
|
+
* Epochs form a binary tree structure for efficient sparse updates.
|
|
31
|
+
*/
|
|
32
|
+
export class EpochIndex implements Epoch {
|
|
33
|
+
constructor(
|
|
34
|
+
public readonly start: bigint,
|
|
35
|
+
public readonly level: number,
|
|
36
|
+
) {
|
|
37
|
+
if (level < 0 || level > MAX_LEVEL) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
`Epoch level must be between 0 and ${MAX_LEVEL}, got ${level}`,
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Marshal epoch to binary format for hashing
|
|
46
|
+
* Returns Keccak256 hash of start (8 bytes big-endian) + level (1 byte)
|
|
47
|
+
*/
|
|
48
|
+
async marshalBinary(): Promise<Uint8Array> {
|
|
49
|
+
const buffer = new Uint8Array(9)
|
|
50
|
+
const view = new DataView(buffer.buffer)
|
|
51
|
+
|
|
52
|
+
// Write start as 8-byte big-endian
|
|
53
|
+
view.setBigUint64(0, this.start, false) // false = big-endian
|
|
54
|
+
|
|
55
|
+
// Write level as 1 byte
|
|
56
|
+
buffer[8] = this.level
|
|
57
|
+
|
|
58
|
+
return Binary.keccak256(buffer)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Calculate the next epoch for a new update
|
|
63
|
+
* @param last - Timestamp of last update
|
|
64
|
+
* @param at - Timestamp of new update
|
|
65
|
+
*/
|
|
66
|
+
next(last: bigint, at: bigint): EpochIndex {
|
|
67
|
+
if (this.start + this.length() > at) {
|
|
68
|
+
return this.childAt(at)
|
|
69
|
+
}
|
|
70
|
+
return lca(at, last).childAt(at)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Calculate epoch length in seconds (2^level)
|
|
75
|
+
*/
|
|
76
|
+
length(): bigint {
|
|
77
|
+
return 1n << BigInt(this.level)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Get parent epoch
|
|
82
|
+
* UNSAFE: Do not call on top-level epoch (level 32)
|
|
83
|
+
*/
|
|
84
|
+
parent(): EpochIndex {
|
|
85
|
+
const length = this.length() << 1n
|
|
86
|
+
const start = (this.start / length) * length
|
|
87
|
+
return new EpochIndex(start, this.level + 1)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Get left sibling epoch
|
|
92
|
+
* UNSAFE: Do not call on left sibling (when start is aligned to 2*length)
|
|
93
|
+
*/
|
|
94
|
+
left(): EpochIndex {
|
|
95
|
+
return new EpochIndex(this.start - this.length(), this.level)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Get child epoch containing timestamp `at`
|
|
100
|
+
* UNSAFE: Do not call with `at` outside this epoch's range
|
|
101
|
+
*/
|
|
102
|
+
childAt(at: bigint): EpochIndex {
|
|
103
|
+
const newLevel = this.level - 1
|
|
104
|
+
const length = 1n << BigInt(newLevel)
|
|
105
|
+
let start = this.start
|
|
106
|
+
|
|
107
|
+
// If `at` falls in right half, adjust start
|
|
108
|
+
if ((at & length) > 0n) {
|
|
109
|
+
start |= length
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return new EpochIndex(start, newLevel)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Check if this epoch is a left child of its parent
|
|
117
|
+
*/
|
|
118
|
+
isLeft(): boolean {
|
|
119
|
+
return (this.start & this.length()) === 0n
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* String representation: "start/level"
|
|
124
|
+
*/
|
|
125
|
+
toString(): string {
|
|
126
|
+
return `${this.start}/${this.level}`
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Calculate Lowest Common Ancestor epoch for two timestamps
|
|
132
|
+
*
|
|
133
|
+
* The LCA is the smallest epoch that contains both `at` and `after`.
|
|
134
|
+
* This is used to find the optimal starting point for feed lookups.
|
|
135
|
+
*
|
|
136
|
+
* @param at - Target timestamp
|
|
137
|
+
* @param after - Reference timestamp (0 if no previous update)
|
|
138
|
+
* @returns LCA epoch
|
|
139
|
+
*/
|
|
140
|
+
export function lca(at: bigint, after: bigint): EpochIndex {
|
|
141
|
+
// If no previous update, start from top-level epoch
|
|
142
|
+
if (after === 0n) {
|
|
143
|
+
return new EpochIndex(0n, MAX_LEVEL)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const diff = at - after
|
|
147
|
+
let length = 1n
|
|
148
|
+
let level = 0
|
|
149
|
+
|
|
150
|
+
// Find the smallest epoch level where both timestamps fall in different epochs
|
|
151
|
+
// OR at and after are in the same epoch
|
|
152
|
+
while (
|
|
153
|
+
level < MAX_LEVEL &&
|
|
154
|
+
(length < diff || at / length !== after / length)
|
|
155
|
+
) {
|
|
156
|
+
length <<= 1n
|
|
157
|
+
level++
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Calculate start of the epoch containing both timestamps
|
|
161
|
+
const start = (after / length) * length
|
|
162
|
+
|
|
163
|
+
return new EpochIndex(start, level)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Helper function to get next epoch for update, handling null previous epoch
|
|
168
|
+
* @param prevEpoch - Previous epoch (null if first update)
|
|
169
|
+
* @param last - Timestamp of last update
|
|
170
|
+
* @param at - Timestamp of new update
|
|
171
|
+
*/
|
|
172
|
+
export function next(
|
|
173
|
+
prevEpoch: EpochIndex | undefined,
|
|
174
|
+
last: bigint,
|
|
175
|
+
at: bigint,
|
|
176
|
+
): EpochIndex {
|
|
177
|
+
if (!prevEpoch) {
|
|
178
|
+
return new EpochIndex(0n, MAX_LEVEL)
|
|
179
|
+
}
|
|
180
|
+
return prevEpoch.next(last, at)
|
|
181
|
+
}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Synchronous Epoch Feed Finder
|
|
3
|
+
*
|
|
4
|
+
* Recursive implementation for finding feed updates at specific timestamps
|
|
5
|
+
* using epoch-based indexing.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Binary } from "cafe-utility"
|
|
9
|
+
import type { Bee, BeeRequestOptions } from "@ethersphere/bee-js"
|
|
10
|
+
import { EthAddress, Reference, Topic } from "@ethersphere/bee-js"
|
|
11
|
+
import { EpochIndex, lca, MAX_LEVEL } from "./epoch"
|
|
12
|
+
import type { EpochFinder } from "./types"
|
|
13
|
+
import { findPreviousLeaf } from "./utils"
|
|
14
|
+
|
|
15
|
+
const EPOCH_LOOKUP_TIMEOUT_MS = 2000
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Synchronous recursive finder for epoch-based feeds
|
|
19
|
+
*
|
|
20
|
+
* Traverses the epoch tree recursively to find the feed update
|
|
21
|
+
* valid at a specific timestamp.
|
|
22
|
+
*
|
|
23
|
+
* Implements the EpochFinder interface.
|
|
24
|
+
*/
|
|
25
|
+
export class SyncEpochFinder implements EpochFinder {
|
|
26
|
+
constructor(
|
|
27
|
+
private readonly bee: Bee,
|
|
28
|
+
private readonly topic: Topic,
|
|
29
|
+
private readonly owner: EthAddress,
|
|
30
|
+
) {}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Find the feed update valid at time `at`
|
|
34
|
+
* @param at - Target unix timestamp (seconds)
|
|
35
|
+
* @param after - Hint of latest known update timestamp (0 if unknown)
|
|
36
|
+
* @returns 32-byte Swarm reference, or undefined if no update found
|
|
37
|
+
*/
|
|
38
|
+
async findAt(
|
|
39
|
+
at: bigint,
|
|
40
|
+
after: bigint = 0n,
|
|
41
|
+
): Promise<Uint8Array | undefined> {
|
|
42
|
+
// Fast path: exact timestamp updates are written at level-0 and should be
|
|
43
|
+
// retrievable without traversing potentially poisoned ancestors.
|
|
44
|
+
const exactEpoch = new EpochIndex(at, 0)
|
|
45
|
+
try {
|
|
46
|
+
const exact = await this.getEpochChunk(at, exactEpoch)
|
|
47
|
+
if (exact) {
|
|
48
|
+
return exact
|
|
49
|
+
}
|
|
50
|
+
} catch {
|
|
51
|
+
// Ignore and fall back to standard traversal.
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const { epoch, chunk } = await this.common(at, after)
|
|
55
|
+
|
|
56
|
+
if (!chunk && epoch.level === MAX_LEVEL) {
|
|
57
|
+
// No update found at all
|
|
58
|
+
return undefined
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const traversed = await this.atEpoch(at, epoch, chunk)
|
|
62
|
+
if (traversed) {
|
|
63
|
+
return traversed
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Recovery fallback for poisoned-ancestor histories: only enable bounded
|
|
67
|
+
// leaf back-scan when root epoch exists but is invalid for `at`.
|
|
68
|
+
try {
|
|
69
|
+
const rootProbe = await this.getEpochChunk(
|
|
70
|
+
at,
|
|
71
|
+
new EpochIndex(0n, MAX_LEVEL),
|
|
72
|
+
)
|
|
73
|
+
if (rootProbe === undefined) {
|
|
74
|
+
return this.findPreviousLeaf(at, after)
|
|
75
|
+
}
|
|
76
|
+
} catch {
|
|
77
|
+
// Root missing - no evidence of poisoned ancestors.
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return undefined
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Find the lowest common ancestor epoch with an existing chunk
|
|
85
|
+
*
|
|
86
|
+
* Traverses UP the epoch tree from the LCA until finding a chunk,
|
|
87
|
+
* or reaching the top level.
|
|
88
|
+
*
|
|
89
|
+
* @param at - Target timestamp
|
|
90
|
+
* @param after - Reference timestamp
|
|
91
|
+
* @returns Epoch and chunk found at that epoch (if any)
|
|
92
|
+
*/
|
|
93
|
+
private async common(
|
|
94
|
+
at: bigint,
|
|
95
|
+
after: bigint,
|
|
96
|
+
): Promise<{ epoch: EpochIndex; chunk?: Uint8Array }> {
|
|
97
|
+
let epoch = lca(at, after)
|
|
98
|
+
|
|
99
|
+
while (true) {
|
|
100
|
+
try {
|
|
101
|
+
const chunk = await this.getEpochChunk(at, epoch)
|
|
102
|
+
|
|
103
|
+
// getEpochChunk validates timestamp and returns undefined if invalid
|
|
104
|
+
if (chunk) {
|
|
105
|
+
return { epoch, chunk }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Chunk found but timestamp invalid, continue searching up
|
|
109
|
+
} catch (error) {
|
|
110
|
+
// Chunk not found, continue searching up
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Check if at top before trying to go to parent
|
|
114
|
+
if (epoch.level === MAX_LEVEL) {
|
|
115
|
+
// Reached top without finding anything
|
|
116
|
+
return { epoch }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Move to parent epoch
|
|
120
|
+
epoch = epoch.parent()
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Recursive descent to find exact update at timestamp
|
|
126
|
+
*
|
|
127
|
+
* Traverses DOWN the epoch tree, handling left/right branches
|
|
128
|
+
* to find the most recent update not later than `at`.
|
|
129
|
+
*
|
|
130
|
+
* @param at - Target timestamp
|
|
131
|
+
* @param epoch - Current epoch being examined
|
|
132
|
+
* @param currentChunk - Best chunk found so far
|
|
133
|
+
* @returns Final chunk reference, or undefined
|
|
134
|
+
*/
|
|
135
|
+
private async atEpoch(
|
|
136
|
+
at: bigint,
|
|
137
|
+
epoch: EpochIndex,
|
|
138
|
+
currentChunk?: Uint8Array,
|
|
139
|
+
): Promise<Uint8Array | undefined> {
|
|
140
|
+
let chunk: Uint8Array | undefined
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
chunk = await this.getEpochChunk(at, epoch)
|
|
144
|
+
} catch (error) {
|
|
145
|
+
// Epoch chunk not found
|
|
146
|
+
if (epoch.isLeft()) {
|
|
147
|
+
// No lower resolution available, return what we have
|
|
148
|
+
return currentChunk
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Traverse earlier branch (left sibling)
|
|
152
|
+
return this.atEpoch(epoch.start - 1n, epoch.left(), currentChunk)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Chunk validation (timestamp) is handled by getEpochChunk
|
|
156
|
+
if (!chunk) {
|
|
157
|
+
// Timestamp invalid (update too recent)
|
|
158
|
+
if (epoch.level > 0) {
|
|
159
|
+
const down = await this.atEpoch(at, epoch.childAt(at), currentChunk)
|
|
160
|
+
if (down) {
|
|
161
|
+
return down
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (epoch.isLeft()) {
|
|
165
|
+
return currentChunk
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Traverse earlier branch
|
|
169
|
+
return this.atEpoch(epoch.start - 1n, epoch.left(), currentChunk)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Update is valid
|
|
173
|
+
if (epoch.level === 0) {
|
|
174
|
+
// Finest resolution - this is our answer
|
|
175
|
+
return chunk
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Continue traversing down to finer resolution
|
|
179
|
+
return this.atEpoch(at, epoch.childAt(at), chunk)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Fetch chunk for a specific epoch
|
|
184
|
+
*
|
|
185
|
+
* Calculates the chunk address from topic, epoch, and owner,
|
|
186
|
+
* then downloads it from the Bee node.
|
|
187
|
+
*
|
|
188
|
+
* @param at - Target timestamp for validation
|
|
189
|
+
* @param epoch - Epoch to fetch
|
|
190
|
+
* @returns Chunk reference (32 or 64 bytes), or undefined if timestamp invalid
|
|
191
|
+
* @throws Error if chunk not found or network error
|
|
192
|
+
*/
|
|
193
|
+
private async getEpochChunk(
|
|
194
|
+
at: bigint,
|
|
195
|
+
epoch: EpochIndex,
|
|
196
|
+
): Promise<Uint8Array | undefined> {
|
|
197
|
+
const requestOptions: BeeRequestOptions = {
|
|
198
|
+
timeout: EPOCH_LOOKUP_TIMEOUT_MS,
|
|
199
|
+
}
|
|
200
|
+
// Calculate epoch identifier: Keccak256(topic || Keccak256(start || level))
|
|
201
|
+
const epochHash = await epoch.marshalBinary()
|
|
202
|
+
const identifier = Binary.keccak256(
|
|
203
|
+
Binary.concatBytes(this.topic.toUint8Array(), epochHash),
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
// Calculate chunk address: Keccak256(identifier || owner)
|
|
207
|
+
const address = new Reference(
|
|
208
|
+
Binary.keccak256(
|
|
209
|
+
Binary.concatBytes(identifier, this.owner.toUint8Array()),
|
|
210
|
+
),
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
// Download chunk
|
|
214
|
+
const chunkData = await this.bee.downloadChunk(
|
|
215
|
+
address.toHex(),
|
|
216
|
+
undefined,
|
|
217
|
+
requestOptions,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
// Extract payload from SOC (Single Owner Chunk)
|
|
221
|
+
// SOC structure: [identifier (32 bytes)][signature (65 bytes)][span (8 bytes)][payload]
|
|
222
|
+
const IDENTIFIER_SIZE = 32
|
|
223
|
+
const SIGNATURE_SIZE = 65
|
|
224
|
+
const SPAN_SIZE = 8
|
|
225
|
+
const TIMESTAMP_SIZE = 8
|
|
226
|
+
const SOC_HEADER_SIZE = IDENTIFIER_SIZE + SIGNATURE_SIZE
|
|
227
|
+
|
|
228
|
+
// Read span to get payload length
|
|
229
|
+
const spanStart = SOC_HEADER_SIZE
|
|
230
|
+
const span = chunkData.slice(spanStart, spanStart + SPAN_SIZE)
|
|
231
|
+
const spanView = new DataView(span.buffer, span.byteOffset, span.byteLength)
|
|
232
|
+
const payloadLength = Number(spanView.getBigUint64(0, true)) // little-endian
|
|
233
|
+
|
|
234
|
+
// Extract full payload (timestamp + reference)
|
|
235
|
+
const payloadStart = spanStart + SPAN_SIZE
|
|
236
|
+
const payload = chunkData.slice(payloadStart, payloadStart + payloadLength)
|
|
237
|
+
|
|
238
|
+
// Detect payload format based on length:
|
|
239
|
+
// - 40 bytes: timestamp(8) + reference(32) - from /soc endpoint
|
|
240
|
+
// - 48 bytes: span(8) + timestamp(8) + reference(32) - from /chunks with v1 format
|
|
241
|
+
// - 72 bytes: timestamp(8) + encrypted_reference(64) - from /soc endpoint
|
|
242
|
+
// - 80 bytes: span(8) + timestamp(8) + encrypted_reference(64) - from /chunks with v1 format
|
|
243
|
+
const VALID_PAYLOAD_LENGTHS = [40, 48, 72, 80]
|
|
244
|
+
if (!VALID_PAYLOAD_LENGTHS.includes(payload.length)) {
|
|
245
|
+
console.warn(
|
|
246
|
+
`Unexpected feed payload length: ${payload.length}. Expected 40, 48, 72, or 80 bytes.`,
|
|
247
|
+
)
|
|
248
|
+
return undefined
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const hasSpanPrefix = payload.length === 48 || payload.length === 80
|
|
252
|
+
|
|
253
|
+
const timestampOffset = hasSpanPrefix ? 8 : 0
|
|
254
|
+
const timestampBytes = payload.slice(
|
|
255
|
+
timestampOffset,
|
|
256
|
+
timestampOffset + TIMESTAMP_SIZE,
|
|
257
|
+
)
|
|
258
|
+
const timestampView = new DataView(
|
|
259
|
+
timestampBytes.buffer,
|
|
260
|
+
timestampBytes.byteOffset,
|
|
261
|
+
timestampBytes.byteLength,
|
|
262
|
+
)
|
|
263
|
+
const timestamp = timestampView.getBigUint64(0, false) // big-endian
|
|
264
|
+
|
|
265
|
+
// Validate timestamp - update must be at or before target time
|
|
266
|
+
if (timestamp > at) {
|
|
267
|
+
return undefined
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Return reference only (skip timestamp and optional span prefix)
|
|
271
|
+
return payload.slice(timestampOffset + TIMESTAMP_SIZE)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private findPreviousLeaf(
|
|
275
|
+
at: bigint,
|
|
276
|
+
after: bigint,
|
|
277
|
+
): Promise<Uint8Array | undefined> {
|
|
278
|
+
return findPreviousLeaf(at, after, (targetAt, epoch) =>
|
|
279
|
+
this.getEpochChunk(targetAt, epoch),
|
|
280
|
+
)
|
|
281
|
+
}
|
|
282
|
+
}
|