@fizzyflow/endless-vector 0.0.7 → 0.0.10

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.
@@ -0,0 +1,301 @@
1
+ /**
2
+ * Integration test: walrus blob items through history and archive lifecycle.
3
+ *
4
+ * Key invariants from the Move contract:
5
+ * - Blob items have an on-object storage_volume of 32 bytes (only the reference
6
+ * is stored; the payload lives in Walrus).
7
+ * - Clamp triggers when (new_item_storage + current_storage) > SAFE_INNER_SIZE (128 KB).
8
+ * - A single blob (32 bytes) + one 120 KB bytes item = ~122 KB < 128 KB → no clamp yet.
9
+ * - Adding a second small bytes item (~9 KB) tips the total over 128 KB → clamp fires.
10
+ * - fillToHistory() encapsulates this two-push pattern.
11
+ * - The FILL_SMALL item that triggers clamp goes into current (not history), because
12
+ * clamp(ev, some(new_item)) routes the triggering item into the fresh current segment.
13
+ *
14
+ * Run: pnpm test:walrus-blobs-history (or pnpm test to run all suites)
15
+ */
16
+
17
+ import { beforeAll, afterAll, describe, it, expect } from 'vitest';
18
+ import { EndlessVector } from '../index.js';
19
+ import { equalUint8Arrays, randomBytesOfLength } from './helpers.js';
20
+ import { setupEndlessVectorLocalnet, teardownEndlessVectorLocalnet } from './fixture.js';
21
+
22
+ const TX_TIMEOUT = 120_000;
23
+
24
+ // Clamp triggers when (new_item_storage + current_storage) > SAFE_INNER_SIZE (128 KB).
25
+ // A single blob costs only 32 bytes of on-object storage, so it never triggers clamp
26
+ // alone. We use a two-push fill: first a 120 KB bytes item (max tx path, 122 880 bytes),
27
+ // then a 9 KB bytes item — combined with the blob that brings total to ~132 KB > 128 KB.
28
+ const FILL_LARGE = 120 * 1024; // first fill push (via tx, stays under 120 KB limit)
29
+ const FILL_SMALL = 9 * 1024; // second fill push (tips over 128 KB threshold)
30
+
31
+ let suiMaster;
32
+ let walrusServer;
33
+ let walrusClient;
34
+ let packageId;
35
+
36
+ beforeAll(async () => {
37
+ ({ suiMaster, walrusServer, walrusClient, packageId } = await setupEndlessVectorLocalnet());
38
+ });
39
+
40
+ afterAll(async () => {
41
+ await teardownEndlessVectorLocalnet();
42
+ });
43
+
44
+ function makeSignAndExecute() {
45
+ return async (tx) => {
46
+ const result = await suiMaster.signAndExecuteTransaction({
47
+ transaction: tx,
48
+ // requestType: 'WaitForLocalExecution',
49
+ });
50
+ return result.digest;
51
+ };
52
+ }
53
+
54
+ function makeEV() {
55
+ return EndlessVector.create({
56
+ suiClient: suiMaster.client,
57
+ packageId,
58
+ walrusClient,
59
+ aggregatorUrl: walrusServer?.url,
60
+ senderAddress: suiMaster.address,
61
+ signAndExecuteTransaction: makeSignAndExecute(),
62
+ });
63
+ }
64
+
65
+ /**
66
+ * Push two bytes items via tx to tip storage over 128 KB and trigger clamp.
67
+ * Items currently in `ev` go to history; FILL_SMALL lands in the new current segment.
68
+ * Net effect on length: +2 items (FILL_LARGE in history, FILL_SMALL in current).
69
+ */
70
+ async function fillToHistory(ev) {
71
+ await ev.push(randomBytesOfLength(FILL_LARGE)); // storage grows but stays < 128 KB
72
+ await ev.push(randomBytesOfLength(FILL_SMALL)); // this one triggers clamp
73
+ }
74
+
75
+ // ─── blobs in history ─────────────────────────────────────────────────────────
76
+
77
+ describe('blob pushed into history via vector<u8> fill', () => {
78
+ it('blob at index 0 is readable after being pushed into history', async () => {
79
+ const ev = await makeEV();
80
+
81
+ const blobData = randomBytesOfLength(200 * 1024);
82
+ await ev.push(blobData); // blob → current (32 bytes on-object storage)
83
+ await fillToHistory(ev); // blob + FILL_LARGE → history[0], FILL_SMALL → current
84
+
85
+ await ev.initialize();
86
+ // blob (index 0) + FILL_LARGE (index 1) in history; FILL_SMALL (index 2) in current
87
+ expect(ev.length).toBe(3);
88
+ expect(ev.historyItemsCount).toBe(1);
89
+
90
+ const back = await ev.at(0);
91
+ expect(equalUint8Arrays(back, blobData)).toBe(true);
92
+ }, TX_TIMEOUT);
93
+
94
+ it('multiple blobs in history, all readable', async () => {
95
+ const ev = await makeEV();
96
+
97
+ // Must be > 120 KB (> 122 880 bytes) so push() routes them to walrus as blob
98
+ // items; walrus blobs cost only 32 bytes of on-object storage each.
99
+ const blobs = [
100
+ randomBytesOfLength(130 * 1024),
101
+ randomBytesOfLength(150 * 1024),
102
+ randomBytesOfLength(200 * 1024),
103
+ ];
104
+
105
+ // push 3 blobs (total on-object storage = 3 × 32 = 96 bytes, well under 128 KB)
106
+ for (const b of blobs) await ev.push(b);
107
+
108
+ // fillToHistory: FILL_LARGE goes in alongside blobs, FILL_SMALL triggers clamp
109
+ // → all 4 (blobs + FILL_LARGE) go to history[0]; FILL_SMALL lands in current
110
+ await fillToHistory(ev);
111
+
112
+ await ev.initialize();
113
+ expect(ev.length).toBe(5); // 3 blobs + FILL_LARGE in history + FILL_SMALL in current
114
+ expect(ev.historyItemsCount).toBe(1);
115
+
116
+ for (let i = 0; i < blobs.length; i++) {
117
+ const back = await ev.at(i);
118
+ expect(equalUint8Arrays(back, blobs[i])).toBe(true);
119
+ }
120
+ }, TX_TIMEOUT * 2);
121
+
122
+ it('blobs and bytes items coexist in the same history segment', async () => {
123
+ const ev = await makeEV();
124
+
125
+ const bytesItem = randomBytesOfLength(1024);
126
+ const blobItem = randomBytesOfLength(200 * 1024);
127
+
128
+ await ev.push(bytesItem); // bytes → current (1 KB storage)
129
+ await ev.push(blobItem); // blob → current (32 bytes storage)
130
+ await fillToHistory(ev); // bytesItem + blobItem + FILL_LARGE → history; FILL_SMALL → current
131
+
132
+ await ev.initialize();
133
+ expect(ev.length).toBe(4);
134
+ expect(ev.historyItemsCount).toBe(1);
135
+
136
+ expect(equalUint8Arrays(await ev.at(0), bytesItem)).toBe(true);
137
+ expect(equalUint8Arrays(await ev.at(1), blobItem)).toBe(true);
138
+ }, TX_TIMEOUT * 2);
139
+ });
140
+
141
+ // ─── multiple history segments ────────────────────────────────────────────────
142
+
143
+ describe('blobs across multiple history segments', () => {
144
+ it('blob readable from any history segment after two fill cycles', async () => {
145
+ const ev = await makeEV();
146
+
147
+ const blob1 = randomBytesOfLength(130 * 1024);
148
+ const blob2 = randomBytesOfLength(150 * 1024);
149
+
150
+ // segment 0: blob1 + FILL_LARGE → history[0]; FILL_SMALL → current
151
+ await ev.push(blob1);
152
+ await fillToHistory(ev);
153
+
154
+ // segment 1: blob2 + prev-FILL_SMALL + FILL_LARGE → history[1]; FILL_SMALL → current
155
+ await ev.push(blob2);
156
+ await fillToHistory(ev);
157
+
158
+ await ev.initialize();
159
+ // exact segment count varies (leftover FILL_SMALL from cycle 1 may add an extra
160
+ // clamp in cycle 2), but there must be at least 2 history segments
161
+ expect(ev.historyItemsCount).toBeGreaterThanOrEqual(2);
162
+
163
+ expect(equalUint8Arrays(await ev.at(0), blob1)).toBe(true);
164
+ // blob2 lands at index 3 (blob1, FILL_LARGE, FILL_SMALL from cycle 1)
165
+ expect(equalUint8Arrays(await ev.at(3), blob2)).toBe(true);
166
+ }, TX_TIMEOUT);
167
+ });
168
+
169
+ // ─── blobs in archive ─────────────────────────────────────────────────────────
170
+
171
+ describe('blob archived and readable', () => {
172
+ it('blob in history is readable after archive()', async () => {
173
+ const ev = await makeEV();
174
+
175
+ const blobData = randomBytesOfLength(200 * 1024);
176
+ await ev.push(blobData);
177
+ await fillToHistory(ev); // blob + FILL_LARGE → history; FILL_SMALL → current
178
+
179
+ await ev.archive(); // history → archive; clamp sweeps current → history first
180
+ await ev.initialize();
181
+
182
+ expect(ev.archiveItemsCount).toBe(1);
183
+ expect(ev.historyItemsCount).toBe(0);
184
+
185
+ const back = await ev.at(0);
186
+ expect(equalUint8Arrays(back, blobData)).toBe(true);
187
+ }, TX_TIMEOUT);
188
+
189
+ it('items pushed after archive() are readable alongside archived blob', async () => {
190
+ const ev = await makeEV();
191
+
192
+ const blobData = randomBytesOfLength(200 * 1024);
193
+ await ev.push(blobData);
194
+ await fillToHistory(ev);
195
+
196
+ await ev.archive();
197
+
198
+ const afterArchive = randomBytesOfLength(4 * 1024);
199
+ await ev.push(afterArchive);
200
+
201
+ await ev.initialize();
202
+
203
+ expect(equalUint8Arrays(await ev.at(0), blobData)).toBe(true);
204
+ expect(equalUint8Arrays(await ev.at(ev.length - 1), afterArchive)).toBe(true);
205
+ }, TX_TIMEOUT * 2);
206
+ });
207
+
208
+ // ─── archive + burn ───────────────────────────────────────────────────────────
209
+
210
+ describe('blob archive burn lifecycle', () => {
211
+ it('burned archived blob throws on at(), remaining items are readable', async () => {
212
+ const ev = await makeEV();
213
+
214
+ const blobData = randomBytesOfLength(200 * 1024);
215
+ const afterData = randomBytesOfLength(4 * 1024);
216
+
217
+ await ev.push(blobData);
218
+ await fillToHistory(ev); // blob + FILL_LARGE → history; FILL_SMALL → current
219
+ await ev.archive(); // archive() clamps first → everything into archive
220
+ await ev.push(afterData);
221
+
222
+ await ev.burnArchive();
223
+ await ev.initialize();
224
+
225
+ expect(ev.burnedArchiveCount).toBe(1);
226
+ expect(ev.length).toBeGreaterThan(0);
227
+
228
+ // all archived items throw
229
+ const burnedCount = ev.archivedFromLength;
230
+ for (let i = 0; i < burnedCount; i++) {
231
+ await expect(ev.at(i)).rejects.toThrow();
232
+ }
233
+
234
+ // item pushed after archive is still readable
235
+ expect(equalUint8Arrays(await ev.at(ev.length - 1), afterData)).toBe(true);
236
+ }, TX_TIMEOUT);
237
+
238
+ it('multiple archive/burn cycles with blobs accumulate archivedFromLength', async () => {
239
+ const ev = await makeEV();
240
+
241
+ const blob1 = randomBytesOfLength(130 * 1024);
242
+ const blob2 = randomBytesOfLength(130 * 1024);
243
+
244
+ // cycle 1
245
+ await ev.push(blob1);
246
+ await fillToHistory(ev);
247
+ await ev.archive();
248
+ await ev.burnArchive();
249
+ await ev.initialize();
250
+
251
+ const burned1 = ev.archivedFromLength;
252
+ expect(ev.burnedArchiveCount).toBe(1);
253
+ expect(burned1).toBeGreaterThan(0);
254
+
255
+ for (let i = 0; i < burned1; i++) {
256
+ await expect(ev.at(i)).rejects.toThrow();
257
+ }
258
+
259
+ // cycle 2
260
+ await ev.push(blob2);
261
+ await fillToHistory(ev);
262
+ await ev.archive();
263
+ await ev.burnArchive();
264
+ await ev.initialize();
265
+
266
+ const burned2 = ev.archivedFromLength;
267
+ expect(ev.burnedArchiveCount).toBe(2);
268
+ expect(burned2).toBeGreaterThan(burned1);
269
+
270
+ for (let i = burned1; i < burned2; i++) {
271
+ await expect(ev.at(i)).rejects.toThrow();
272
+ }
273
+ }, TX_TIMEOUT);
274
+ });
275
+
276
+ // ─── re-read by ID ────────────────────────────────────────────────────────────
277
+
278
+ describe('re-reading vector by ID with blob in history', () => {
279
+ it('fresh instance resolves blob from history correctly', async () => {
280
+ const ev = await makeEV();
281
+
282
+ const blobData = randomBytesOfLength(200 * 1024);
283
+ await ev.push(blobData);
284
+ await fillToHistory(ev);
285
+
286
+ const fresh = new EndlessVector({
287
+ suiClient: suiMaster.client,
288
+ id: ev.id,
289
+ packageId,
290
+ walrusClient,
291
+ aggregatorUrl: walrusServer?.url,
292
+ senderAddress: suiMaster.address,
293
+ signAndExecuteTransaction: makeSignAndExecute(),
294
+ });
295
+ await fresh.initialize();
296
+
297
+ expect(fresh.historyItemsCount).toBe(1);
298
+ const back = await fresh.at(0);
299
+ expect(equalUint8Arrays(back, blobData)).toBe(true);
300
+ }, TX_TIMEOUT * 3);
301
+ });
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Integration test: exercises ev.push() with both small and large payloads.
3
+ * Small arrays (≤ 120 KB) go through a regular Move tx; larger ones are
4
+ * transparently routed to Walrus via walrus.pushBlob().
5
+ *
6
+ * Run: pnpm test:walrus-blobs-sdk (or pnpm test to run all suites)
7
+ */
8
+
9
+ import { beforeAll, afterAll, describe, it, expect } from 'vitest';
10
+ import { EndlessVector } from '../index.js';
11
+ import { randomBytesOfLength } from './helpers.js';
12
+ import { setupEndlessVectorLocalnet, teardownEndlessVectorLocalnet } from './fixture.js';
13
+
14
+ const TX_TIMEOUT = 60_000;
15
+
16
+ let suiMaster;
17
+ let walrusServer;
18
+ let walrusClient;
19
+ let packageId;
20
+
21
+ /** @type {EndlessVector} */
22
+ let ev;
23
+
24
+ // ─── setup ────────────────────────────────────────────────────────────────────
25
+
26
+ beforeAll(async () => {
27
+ ({ suiMaster, walrusServer, walrusClient, packageId } = await setupEndlessVectorLocalnet());
28
+ });
29
+
30
+ afterAll(async () => {
31
+ await teardownEndlessVectorLocalnet();
32
+ });
33
+
34
+ // ─── helpers ──────────────────────────────────────────────────────────────────
35
+
36
+ function makeSignAndExecute() {
37
+ return async (tx) => {
38
+ const result = await suiMaster.signAndExecuteTransaction({
39
+ transaction: tx,
40
+ requestType: 'WaitForLocalExecution',
41
+ });
42
+ return result.digest;
43
+ };
44
+ }
45
+
46
+ async function makeEV({ usePublisherUrl = false } = {}) {
47
+ return EndlessVector.create({
48
+ suiClient: suiMaster.client,
49
+ packageId,
50
+ walrusClient: usePublisherUrl ? undefined : walrusClient,
51
+ publisherUrl: usePublisherUrl ? walrusServer.url : undefined,
52
+ aggregatorUrl: walrusServer.url,
53
+ senderAddress: suiMaster.address,
54
+ signAndExecuteTransaction: makeSignAndExecute(),
55
+ });
56
+ }
57
+
58
+ // ─── deployment ───────────────────────────────────────────────────────────────
59
+
60
+ describe('deployment', () => {
61
+ it('publishes the combined package', () => {
62
+ expect(packageId).toMatch(/^0x[0-9a-f]+$/);
63
+ });
64
+
65
+ it('bootstraps the walrus committee (epoch == 1)', async () => {
66
+ const { walrusState } = await setupEndlessVectorLocalnet();
67
+ const epoch = await walrusState.systemEpoch();
68
+ expect(epoch).toBe(1);
69
+ });
70
+ });
71
+
72
+ // ─── create ───────────────────────────────────────────────────────────────────
73
+
74
+ describe('EndlessVector creation', () => {
75
+ it('creates an empty EndlessVector via SDK', async () => {
76
+ ev = await makeEV();
77
+ expect(ev.id).toMatch(/^0x[0-9a-f]+$/);
78
+ expect(ev.isWritable).toBe(true);
79
+ expect(ev.walrus).toBeDefined();
80
+ }, TX_TIMEOUT);
81
+ });
82
+
83
+ // ─── push via ev.push() ───────────────────────────────────────────────────────
84
+
85
+ describe('push items via ev.push()', () => {
86
+ it('pushes small items (≤ 120 KB) via regular tx', async () => {
87
+ const sizes = [512, 4 * 1024, 64 * 1024];
88
+
89
+ for (const size of sizes) {
90
+ const data = randomBytesOfLength(size);
91
+ const ok = await ev.push(data);
92
+ expect(ok).toBe(true);
93
+ console.log(`pushed ${size}B via tx`);
94
+ }
95
+
96
+ await ev.initialize();
97
+ expect(ev.length).toBe(3);
98
+ expect(ev.binaryLength).toBeGreaterThan(0);
99
+ }, TX_TIMEOUT);
100
+
101
+ it('pushes a large item (> 120 KB) via walrus fallback', async () => {
102
+ const data = randomBytesOfLength(200 * 1024);
103
+ const ok = await ev.push(data);
104
+ expect(ok).toBe(true);
105
+ console.log(`pushed 200 KB via walrus fallback`);
106
+
107
+ await ev.initialize();
108
+ expect(ev.length).toBe(4);
109
+ }, TX_TIMEOUT);
110
+
111
+ it('pushes 2 more small items sequentially', async () => {
112
+ await ev.push(randomBytesOfLength(1024));
113
+ await ev.push(randomBytesOfLength(2048));
114
+
115
+ await ev.initialize();
116
+ expect(ev.length).toBe(6);
117
+ }, TX_TIMEOUT);
118
+ });
119
+
120
+ // ─── round-trip reads ─────────────────────────────────────────────────────────
121
+
122
+ describe('round-trip reads', () => {
123
+ /** @type {EndlessVector} */
124
+ let evRoundTrip;
125
+
126
+ it('creates a fresh vector for round-trip tests (publisherUrl path)', async () => {
127
+ evRoundTrip = await makeEV({ usePublisherUrl: true });
128
+ expect(evRoundTrip.id).toMatch(/^0x[0-9a-f]+$/);
129
+ }, TX_TIMEOUT);
130
+
131
+ it('reads back small item pushed via tx', async () => {
132
+ const data = randomBytesOfLength(512);
133
+ await evRoundTrip.push(data);
134
+
135
+ await evRoundTrip.initialize();
136
+ const back = await evRoundTrip.at(evRoundTrip.length - 1);
137
+ expect(back).toEqual(data);
138
+ }, TX_TIMEOUT);
139
+
140
+ it('reads back a large item pushed via walrus fallback (publisherUrl)', async () => {
141
+ const data = randomBytesOfLength(200 * 1024);
142
+ await evRoundTrip.push(data);
143
+
144
+ await evRoundTrip.initialize();
145
+ const back = await evRoundTrip.at(evRoundTrip.length - 1);
146
+ expect(back).toEqual(data);
147
+ }, TX_TIMEOUT);
148
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "moduleResolution": "bundler",
4
+ "module": "ESNext",
5
+ "target": "ESNext",
6
+ "noEmit": true,
7
+ "allowJs": true,
8
+ "checkJs": false,
9
+ "strict": false,
10
+ "skipLibCheck": true
11
+ },
12
+ "include": ["index.d.ts"]
13
+ }
@@ -0,0 +1,16 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ testTimeout: 300_000,
6
+ hookTimeout: 300_000,
7
+ fileParallelism: false,
8
+ pool: 'forks',
9
+ // singleFork + isolate:false keeps every test file in the same Node process
10
+ // so the fixture's module-level cached Promise is shared — the validator
11
+ // and package deploy happen exactly once for the whole run.
12
+ isolate: false,
13
+ poolOptions: { forks: { singleFork: true } },
14
+ include: ['test/**/*.test.js'],
15
+ },
16
+ });