@fizzyflow/endless-vector 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.
@@ -0,0 +1,862 @@
1
+ import EndlessVectorHistory from './EndlessVectorHistory.js';
2
+ import EndlessVectorArchive from './EndlessVectorArchive.js';
3
+ import { Transaction } from '@mysten/sui/transactions';
4
+ import { bcs } from '@mysten/sui/bcs';
5
+ import ids from './ids.js';
6
+
7
+ /**
8
+ * @typedef {import('@mysten/sui/client').SuiClient} SuiClient
9
+ * @typedef {import('@mysten/sui/client').GetObjectParams} GetObjectParams
10
+ * @typedef {import('@mysten/sui/client').GetDynamicFieldsParams} GetDynamicFieldsParams
11
+ */
12
+
13
+ /**
14
+ * Should accept Transaction as parameter and return executed transaction digest
15
+ * @callback CustomSignAndExecuteTransactionFunction
16
+ * @param {Transaction} tx
17
+ * @returns {Promise<string>}
18
+ */
19
+
20
+ /**
21
+ * Represents an endless vector data structure that can grow beyond Sui object size limits
22
+ * by storing overflow data in history items. Provides seamless access to all elements regardless
23
+ * of whether they're stored in the current object or historical segments.
24
+ */
25
+ export default class EndlessVector {
26
+ /**
27
+ * Creates a new EndlessVector instance.
28
+ * @param {Object} params - Configuration parameters
29
+ * @param {SuiClient} [params.suiClient] - Sui client instance for blockchain interactions
30
+ * @param {string} [params.id] - ID or address of the EndlessVector on the Sui blockchain
31
+ *
32
+ * @param {?string} [params.packageId] - Adds write capability if provided, ID of the Move package containing the EndlessVector module or 'mainnet', 'testnet' to use known IDs
33
+ * @param {?CustomSignAndExecuteTransactionFunction} [params.signAndExecuteTransaction] - Adds write capability if provided, function should accept Sui transaction, sign and submit it to the blockchain and return its digest
34
+ */
35
+ constructor(params = {}) {
36
+ /** @type {SuiClient} */
37
+ this.suiClient = params.suiClient;
38
+ /** @type {string} */
39
+ this.id = params.id;
40
+
41
+ /** @type {number} */
42
+ this.binaryLength = 0;
43
+ /** @type {number} */
44
+ this.length = 0;
45
+
46
+ /** @type {number} */
47
+ this.historyItemsCount = 0; // data from EndlessVector object fields, how many history items loaded
48
+ /** @type {string} */
49
+ this.historyTableId = null; // id of the dynamic field table that contains history items of this EndlessVector
50
+ /** @type {Object<number, EndlessVectorHistory>} */
51
+ this._history = {}; // EndlessVectorHistory instances
52
+
53
+ /** @type {boolean} */
54
+ this.firstItemIsFromPreviousHistory = false; // from EndlessVector.fields.
55
+
56
+ /** @type {Array<Uint8Array>} */
57
+ this._items = []; // items in current EndlessVector object,
58
+
59
+ /** @type {string} */
60
+ this.archiveTableId = null; // id of the dynamic field table that contains archives of this EndlessVector
61
+
62
+ /** @type {number} */
63
+ this.archiveItemsCount = 0; // how many archives exist, from EndlessVector.fields.archive_items_count
64
+ /** @type {Object<number, EndlessVectorArchive>} */
65
+ this._archive = {}; // EndlessVectorArchive instances, key is archive index
66
+ /** @type {number} */
67
+ this.archivedAtLength = 0; // how many items have been archived in total, from EndlessVector.fields.
68
+ /** @type {number} */
69
+ this.archivedFromLength = 0; // from EndlessVector.fields.archived_from_length
70
+ /** @type {number} */
71
+ this.burnedArchiveCount = 0; // from EndlessVector.fields.burned_archive_count
72
+
73
+ /** @type {boolean} */
74
+ this._isInitialized = false;
75
+
76
+ /** @type {?string} */
77
+ this._packageId = params.packageId || null;
78
+
79
+ if (this._packageId == 'mainnet' || !this._packageId && this.suiClient?.network == 'mainnet') {
80
+ this._packageId = ids['mainnet'].packageId;
81
+ } else if (this._packageId == 'testnet' || !this._packageId && this.suiClient?.network == 'testnet') {
82
+ this._packageId = ids['testnet'].packageId;
83
+ }
84
+
85
+ /** @type {?CustomSignAndExecuteTransactionFunction} */
86
+ this._signAndExecuteTransaction = params.signAndExecuteTransaction || null;
87
+ }
88
+
89
+ /**
90
+ * Static factory method to create a new empty EndlessVector on the blockchain.
91
+ * Creates a new EndlessVector object via the Move contract and returns a wrapped instance.
92
+ *
93
+ * @param {Object} params - Configuration parameters
94
+ * @param {SuiClient} params.suiClient - Sui client instance for blockchain interactions
95
+ * @param {string} params.packageId - ID of the Move package containing the EndlessVector module
96
+ * @param {CustomSignAndExecuteTransactionFunction} params.signAndExecuteTransaction - Function to sign and execute transactions
97
+ * @param {?Array<Uint8Array>} [params.items] - Optional array of Uint8Array items to initialize the vector with. If provided, uses empty_entry_and_push, otherwise uses empty_entry
98
+ * @param {?Object} [params.gasCoin] - Optional gas coin object reference {objectId: string, digest: string, version: string} to use for transaction payment
99
+ * @param {?Object} [params.options] - Optional transaction parameters
100
+ * @param {?Number} [params.options.timeout] - Transaction confirmation timeout in ms, default 30000
101
+ * @param {?Number} [params.options.pollIntervalMs] - Poll interval in ms, default 1000
102
+ * @returns {Promise<EndlessVector>} A new EndlessVector instance
103
+ * @throws {Error} If the transaction fails or no EndlessVector object is created
104
+ */
105
+ static async create(params) {
106
+ const { suiClient, packageId, signAndExecuteTransaction, items, gasCoin, options = {} } = params;
107
+
108
+ if (!suiClient) {
109
+ throw new Error('suiClient is required');
110
+ }
111
+ if (!packageId) {
112
+ throw new Error('packageId is required');
113
+ }
114
+ if (!signAndExecuteTransaction) {
115
+ throw new Error('signAndExecuteTransaction is required');
116
+ }
117
+
118
+ // Create transaction to call empty_entry
119
+ const tx = new Transaction();
120
+
121
+ if (gasCoin) {
122
+ tx.setGasPayment([gasCoin]);
123
+ }
124
+
125
+ if (items && Array.isArray(items) && items.length) {
126
+ tx.moveCall({
127
+ target: `${packageId}::endless_vector::empty_entry_and_push`,
128
+ arguments: [tx.pure(bcs.vector(bcs.vector(bcs.u8())).serialize(items))],
129
+ });
130
+ } else {
131
+ tx.moveCall({
132
+ target: `${packageId}::endless_vector::empty_entry`,
133
+ arguments: [],
134
+ });
135
+ }
136
+
137
+ // Execute transaction
138
+ const digest = await signAndExecuteTransaction(tx);
139
+
140
+ // Wait for transaction to complete
141
+ const transactionBlockResponse = await suiClient.waitForTransaction({
142
+ digest: digest,
143
+ timeout: options.timeout || 30000,
144
+ pollIntervalMs: options.pollIntervalMs || 1000,
145
+ options: {
146
+ showEffects: true,
147
+ showObjectChanges: true,
148
+ },
149
+ });
150
+
151
+ if (transactionBlockResponse?.effects?.status?.status !== 'success') {
152
+ throw new Error('Transaction failed to create EndlessVector');
153
+ }
154
+
155
+ // Find the created EndlessVector object
156
+ const objectChanges = transactionBlockResponse.objectChanges || [];
157
+ const createdVector = objectChanges.find(
158
+ change => change.type === 'created' &&
159
+ change.objectType &&
160
+ change.objectType.includes('endless_vector::EndlessVector')
161
+ );
162
+
163
+ if (!createdVector || !createdVector.objectId) {
164
+ throw new Error('Failed to find created EndlessVector object in transaction response');
165
+ }
166
+
167
+ // Create and return the EndlessVector instance
168
+ return new EndlessVector({
169
+ suiClient,
170
+ id: createdVector.objectId,
171
+ packageId,
172
+ signAndExecuteTransaction,
173
+ });
174
+ }
175
+
176
+ get isWritable() {
177
+ return !!(this._packageId && this._signAndExecuteTransaction);
178
+ }
179
+
180
+ /**
181
+ * Creates a transaction to push new byte arrays to the EndlessVector.
182
+ * Note: this method only creates the transaction, it does not sign or execute it.
183
+
184
+ * @param {Uint8Array} arr - Array of byte arrays to push
185
+ * @returns {Transaction} The transaction object to be signed and executed
186
+ */
187
+ getPushTransaction(arr, txToAppendTo = null) {
188
+ if (!this._packageId) {
189
+ throw new Error('packageId is required to compose push transaction');
190
+ }
191
+
192
+ let tx = txToAppendTo;
193
+ if (!tx) {
194
+ tx = new Transaction();
195
+ }
196
+
197
+ const maxArgLength = 12 * 1024;
198
+ if (arr.length < maxArgLength) {
199
+ tx.moveCall({
200
+ target: `${this._packageId}::endless_vector::push_back`,
201
+ arguments: [
202
+ tx.object(this.id),
203
+ tx.pure(bcs.vector(bcs.u8()).serialize(arr)),
204
+ ],
205
+ });
206
+ } else if (arr.length <= 10 * maxArgLength) {
207
+ const N = 10;
208
+ const chunks = [];
209
+ for (let i = 0; i < N; i++) {
210
+ const start = i * maxArgLength;
211
+ const end = start + maxArgLength;
212
+
213
+ if (start < arr.length) {
214
+ chunks.push(arr.slice(start, Math.min(end, arr.length)));
215
+ } else {
216
+ chunks.push(new Uint8Array()); // empty chunk
217
+ }
218
+ }
219
+ const args = [tx.object(this.id)];
220
+ for (let i = 0; i < N; i++) {
221
+ args.push(tx.pure(bcs.vector(bcs.u8()).serialize(chunks[i])));
222
+ }
223
+ tx.moveCall({
224
+ target: `${this._packageId}::endless_vector::compose_and_push_back`,
225
+ arguments: args,
226
+ });
227
+ } else {
228
+ throw new Error('Array too large, max '+(10*maxArgLength)+' bytes supported per single tx');
229
+ }
230
+ return tx;
231
+ }
232
+
233
+
234
+ /**
235
+ * Pushes new byte array to the EndlessVector, creating and executing the necessary transaction.
236
+ * Requires the instance to be writable (packageId and signAndExecuteTransaction must be provided).
237
+ * @throws {Error} If the instance is not writable or if the transaction fails
238
+ *
239
+ * @param {Uint8Array} arr - Byte array to push
240
+ * @param {?Object} params - Configuration parameters
241
+ * @param {?Number} [params.timeout] - wait for transaction confirmation timeout in ms, default 30000
242
+ * @param {?Number} [params.pollIntervalMs] - wait for transaction confirmation poll interval in ms, default 1000
243
+ * @return {Promise<boolean>} True if the push was successful
244
+ */
245
+ async push(arr, params = {}) {
246
+ if (!this.isWritable) {
247
+ throw new Error('EndlessVector is not writable, packageId and signAndExecuteTransaction are required');
248
+ }
249
+ const tx = this.getPushTransaction(arr);
250
+ const digest = await this._signAndExecuteTransaction(tx);
251
+
252
+ const transactionBlockResponse = await this.suiClient.waitForTransaction({
253
+ digest: digest,
254
+ timeout: params.timeout || 30000,
255
+ pollIntervalMs: params.pollIntervalMs || 1000,
256
+ options: { showEffects: true },
257
+ });
258
+ if (transactionBlockResponse?.effects?.status?.status !== 'success') {
259
+ throw new Error('Transaction failed');
260
+ }
261
+
262
+ this.reInitialize(); // force re-initialization to load new data
263
+
264
+ return true;
265
+ }
266
+
267
+ /**
268
+ * Creates a transaction to concatenate EndlessVector(s) into this one.
269
+ * The other EndlessVector(s) will be consumed (destroyed) in the process.
270
+ * Note: this method only creates the transaction, it does not sign or execute it.
271
+ *
272
+ * @param {string|EndlessVector|Array<string|EndlessVector>} other - The ID of the EndlessVector to concatenate, an EndlessVector instance, or an array of IDs/instances
273
+ * @param {Transaction} [txToAppendTo=null] - Optional transaction to append to
274
+ * @returns {Transaction} The transaction object to be signed and executed
275
+ * @throws {Error} If packageId is not set or if the other vector has archived items
276
+ */
277
+ getConcatTransaction(other, txToAppendTo = null) {
278
+ if (!this._packageId) {
279
+ throw new Error('packageId is required to compose concat transaction');
280
+ }
281
+
282
+ let tx = txToAppendTo;
283
+ if (!tx) {
284
+ tx = new Transaction();
285
+ }
286
+
287
+ // Check if other is an array - if so, use append, otherwise use concat
288
+ if (Array.isArray(other)) {
289
+ // Extract IDs from EndlessVector instances or use as strings
290
+ const otherIds = other.map(item =>
291
+ (typeof item === 'object' && item.id) ? item.id : item
292
+ );
293
+
294
+ // Create a vector of objects to pass to the Move function
295
+ const objectRefs = otherIds.map(id => tx.object(id));
296
+
297
+ tx.moveCall({
298
+ target: `${this._packageId}::endless_vector::append`,
299
+ arguments: [
300
+ tx.object(this.id),
301
+ tx.makeMoveVec({ elements: objectRefs }),
302
+ ],
303
+ });
304
+ } else {
305
+ // Extract ID if other is an EndlessVector instance, otherwise use as string
306
+ const otherEndlessVectorId = (typeof other === 'object' && other.id) ? other.id : other;
307
+
308
+ tx.moveCall({
309
+ target: `${this._packageId}::endless_vector::concat`,
310
+ arguments: [
311
+ tx.object(this.id),
312
+ tx.object(otherEndlessVectorId),
313
+ ],
314
+ });
315
+ }
316
+
317
+ return tx;
318
+ }
319
+
320
+ /**
321
+ * Concatenates EndlessVector(s) into this one, creating and executing the necessary transaction.
322
+ * The other EndlessVector(s) will be consumed (destroyed) in the process.
323
+ * Requires the instance to be writable (packageId and signAndExecuteTransaction must be provided).
324
+ *
325
+ * @param {string|EndlessVector|Array<string|EndlessVector>} other - The ID of the EndlessVector to concatenate, an EndlessVector instance, or an array of IDs/instances to append
326
+ * @param {?Object} params - Configuration parameters
327
+ * @param {?Number} [params.timeout] - wait for transaction confirmation timeout in ms, default 30000
328
+ * @param {?Number} [params.pollIntervalMs] - wait for transaction confirmation poll interval in ms, default 1000
329
+ * @return {Promise<boolean>} True if the concat was successful
330
+ * @throws {Error} If the instance is not writable, if the transaction fails, or if any vector has archived items
331
+ */
332
+ async concat(other, params = {}) {
333
+ if (!this.isWritable) {
334
+ throw new Error('EndlessVector is not writable, packageId and signAndExecuteTransaction are required');
335
+ }
336
+
337
+ const tx = this.getConcatTransaction(other);
338
+ const digest = await this._signAndExecuteTransaction(tx);
339
+
340
+ const transactionBlockResponse = await this.suiClient.waitForTransaction({
341
+ digest: digest,
342
+ timeout: params.timeout || 30000,
343
+ pollIntervalMs: params.pollIntervalMs || 1000,
344
+ options: { showEffects: true },
345
+ });
346
+ if (transactionBlockResponse?.effects?.status?.status !== 'success') {
347
+ throw new Error('Transaction failed');
348
+ }
349
+
350
+ this.reInitialize(); // force re-initialization to load new data
351
+
352
+ return true;
353
+ }
354
+
355
+ /**
356
+ * Gets the first index that is stored in the current EndlessVector object (not in history items).
357
+ * @returns {number} The index where current items begin
358
+ */
359
+ get firstNotHistoryIndex() {
360
+ if (this.length === 0) {
361
+ return 0;
362
+ }
363
+ if (this.historyItemsCount === 0 && this.archiveItemsCount === 0) {
364
+ return 0;
365
+ }
366
+ if (this.firstItemIsFromPreviousHistory) {
367
+ return this.length - (this._items.length - 1);
368
+ }
369
+ return this.length - this._items.length;
370
+ }
371
+
372
+ /**
373
+ * Forces re-initialization of the EndlessVector to reload data from the blockchain.
374
+ */
375
+ reInitialize() {
376
+ this._isInitialized = false; // force re-initialization to load new data
377
+ this._items = []; // clear current items cache
378
+ }
379
+
380
+ /**
381
+ * Initializes the EndlessVector by loading data from the Sui blockchain.
382
+ * Fetches the main object data and all associated history items.
383
+ * @returns {Promise<void>}
384
+ * @throws {Error} If suiClient or id is not provided
385
+ */
386
+ async initialize() {
387
+ if (this._isInitialized) {
388
+ return;
389
+ }
390
+ if (!this.suiClient) {
391
+ throw new Error('suiClient is required');
392
+ }
393
+ if (!this.id) {
394
+ throw new Error('id is required');
395
+ }
396
+
397
+ // prevent multiple concurrent initializations
398
+ if (this.__initializationPromise) {
399
+ return await this.__initializationPromise;
400
+ }
401
+
402
+ this.__initializationPromiseResolver = null;
403
+ this.__initializationPromise = new Promise((res)=>{ this.__initializationPromiseResolver = res; });
404
+
405
+ /** @type {GetObjectParams} */
406
+ const getObjectParams = {
407
+ id: this.id,
408
+ options: {
409
+ showContent: true,
410
+ },
411
+ };
412
+ const endlessVectorObjectResponse = await this.suiClient.getObject(getObjectParams);
413
+ const endlessVectorObject = endlessVectorObjectResponse.data?.content?.fields;
414
+
415
+ this.binaryLength = parseInt(endlessVectorObject?.binary_length || 0);
416
+ this.length = parseInt(endlessVectorObject?.length || 0);
417
+ this.historyItemsCount = parseInt(endlessVectorObject?.history_items_count || 0);
418
+ this.firstItemIsFromPreviousHistory = (endlessVectorObject?.first_item_is_from_previous_history) || false;
419
+ this.archivedAtLength = parseInt(endlessVectorObject?.archived_at_length || 0);
420
+ this.archiveItemsCount = parseInt(endlessVectorObject?.archive_items_count || 0);
421
+
422
+ this.archivedFromLength = parseInt(endlessVectorObject?.archived_from_length || 0);
423
+ this.burnedArchiveCount = parseInt(endlessVectorObject?.burned_archive_count || 0);
424
+
425
+ this._items = [];
426
+ if (endlessVectorObject?.items && endlessVectorObject.items.length) {
427
+ for (const item of endlessVectorObject.items) {
428
+ this._items.push(new Uint8Array(item));
429
+ }
430
+ }
431
+
432
+ this.archiveTableId = endlessVectorObject?.archive?.fields?.id?.id;
433
+ this.historyTableId = endlessVectorObject?.history?.fields?.id?.id;
434
+
435
+ this._isInitialized = true;
436
+ this.__initializationPromiseResolver();
437
+
438
+ delete this.__initializationPromise;
439
+ delete this.__initializationPromiseResolver;
440
+ }
441
+
442
+
443
+
444
+ /**
445
+ * Gets a history item by its index, loading it from the blockchain if needed.
446
+ * @param {number|string} historyIndex - The index of the history item to retrieve
447
+ * @returns {Promise<EndlessVectorHistory>} The history item at the specified index
448
+ * @throws {Error} If historyTableId is not set or history item not found
449
+ */
450
+ async getHistory(historyIndex) {
451
+ // @todo: check by historyItemsCount
452
+
453
+ const historyIndexInt = parseInt(historyIndex);
454
+
455
+ if (this._history[historyIndexInt]) {
456
+ await this._history[historyIndexInt].initialize();
457
+ return this._history[historyIndexInt];
458
+ }
459
+
460
+ // go throught the dynamic fields of the history table to find the id of the needed history item
461
+ if (!this.historyTableId) {
462
+ throw new Error('historyTableId is not set');
463
+ }
464
+
465
+ /** @type {GetDynamicFieldsParams} */
466
+ const getDynamicFieldsParams = {
467
+ parentId: this.historyTableId,
468
+ options: {
469
+ showContent: true,
470
+ showType: true,
471
+ },
472
+ };
473
+
474
+ let resp = null;
475
+ let haveToLookMore = true;
476
+
477
+ do {
478
+ resp = await this.suiClient.getDynamicFields(getDynamicFieldsParams);
479
+ if (resp && resp.data && resp.data.length) {
480
+ for (const df of resp.data) {
481
+ if (df?.objectId) {
482
+ const itemHistoryIndex = parseInt(df.name.value);
483
+ const endlessVectorHistory = new EndlessVectorHistory({
484
+ suiClient: this.suiClient,
485
+ id: df.objectId,
486
+ index: itemHistoryIndex,
487
+ endlessVector: this,
488
+ });
489
+ this._history[itemHistoryIndex] = endlessVectorHistory;
490
+ if (itemHistoryIndex === historyIndexInt) {
491
+ haveToLookMore = false;
492
+ }
493
+ }
494
+ }
495
+ getDynamicFieldsParams.cursor = resp.nextCursor;
496
+ }
497
+ } while (resp?.hasNextPage && haveToLookMore);
498
+
499
+ if (!this._history[historyIndexInt]) {
500
+ throw new Error(`History not found for index ${historyIndexInt}`);
501
+ }
502
+
503
+ await this._history[historyIndexInt].initialize();
504
+
505
+ return this._history[historyIndexInt];
506
+ }
507
+
508
+
509
+ /**
510
+ * Gets an archive item by its index, loading it from the blockchain if needed.
511
+ * @param {number|string} archiveIndex - The index of the archive item to retrieve
512
+ * @returns {Promise<EndlessVectorArchive>} The archive item at the specified index
513
+ * @throws {Error} If archiveTableId is not set or archive item not found
514
+ */
515
+ async getArchive(archiveIndex) {
516
+ const archiveIndexInt = parseInt(archiveIndex);
517
+
518
+ if (this._archive[archiveIndexInt]) {
519
+ await this._archive[archiveIndexInt].initialize();
520
+ return this._archive[archiveIndexInt];
521
+ }
522
+
523
+ // go throught the dynamic fields of the archive table to find the id of the needed archive
524
+ if (!this.archiveTableId) {
525
+ throw new Error('archiveTableId is not set');
526
+ }
527
+
528
+ /** @type {GetDynamicFieldsParams} */
529
+ const getDynamicFieldsParams = {
530
+ parentId: this.archiveTableId,
531
+ options: {
532
+ showContent: true,
533
+ showType: true,
534
+ },
535
+ };
536
+
537
+ let resp = null;
538
+ let haveToLookMore = true;
539
+
540
+ do {
541
+ resp = await this.suiClient.getDynamicFields(getDynamicFieldsParams);
542
+ if (resp && resp.data && resp.data.length) {
543
+ for (const df of resp.data) {
544
+ if (df?.objectId) {
545
+ const itemArchiveIndex = parseInt(df.name.value);
546
+ const endlessVectorArchive = new EndlessVectorArchive({
547
+ suiClient: this.suiClient,
548
+ id: df.objectId,
549
+ index: itemArchiveIndex,
550
+ endlessVector: this,
551
+ });
552
+ this._archive[itemArchiveIndex] = endlessVectorArchive;
553
+ if (itemArchiveIndex === archiveIndexInt) {
554
+ haveToLookMore = false;
555
+ }
556
+ }
557
+ }
558
+ getDynamicFieldsParams.cursor = resp.nextCursor;
559
+ }
560
+ } while (resp?.hasNextPage && haveToLookMore);
561
+
562
+ if (!this._archive[archiveIndexInt]) {
563
+ throw new Error(`Archive not found for index ${archiveIndexInt}`);
564
+ }
565
+
566
+ await this._archive[archiveIndexInt].initialize();
567
+
568
+ return this._archive[archiveIndexInt];
569
+ }
570
+
571
+ /**
572
+ * Loads multiple history items in a single batch request for efficiency.
573
+ * Uses multiGetObjects to fetch multiple history items in a single blockchain call.
574
+ * @param {Array<EndlessVectorHistory>} historyItems - Array of history items to load
575
+ * @returns {Promise<void>}
576
+ */
577
+ async loadHistoryItemsBunch(historyItems) {
578
+ const ids = historyItems.map(hi => hi.id);
579
+ let results = [];
580
+ try {
581
+ results = await this.suiClient.multiGetObjects({
582
+ ids: ids,
583
+ options: { showContent: true, },
584
+ });
585
+ } catch(e) {
586
+ console.error(e);
587
+ }
588
+
589
+ if (results && results.length) {
590
+ for (const res of results) {
591
+ const fields = res?.data?.content?.fields?.value?.fields;
592
+ const id = res?.data?.content?.fields?.id?.id;
593
+
594
+ historyItems.forEach(hi => {
595
+ if (hi.id === id) {
596
+ hi.setFields(fields);
597
+ }
598
+ });
599
+ }
600
+ }
601
+ }
602
+
603
+ /**
604
+ * Loads a single history item, batching requests for efficiency.
605
+ * Uses a batching mechanism to group multiple requests within a short time window.
606
+ * Automatically batches up to 50 items per request with a 30ms timeout.
607
+ * @param {EndlessVectorHistory} historyItem - The history item to load
608
+ * @returns {Promise<EndlessVectorHistory>} The loaded history item
609
+ */
610
+ async loadHistoryItem(historyItem) {
611
+ const maxWaitForBunchTimeMs = 30;
612
+
613
+ if (historyItem.isReady()) {
614
+ return historyItem;
615
+ }
616
+
617
+ if (!this.__historyItemLoaderBunches) {
618
+ this.__historyItemLoaderBunches = [];
619
+ this.__historyItemsAlreadyAskedToLoad = {};
620
+ }
621
+
622
+ let lastBunch = null;
623
+ if (this.__historyItemLoaderBunches.length) {
624
+ lastBunch = this.__historyItemLoaderBunches[this.__historyItemLoaderBunches.length - 1];
625
+ if (lastBunch.started) {
626
+ // that bunch is already started to load, so we need a new one
627
+ lastBunch = null;
628
+ }
629
+ if (lastBunch && lastBunch.historyItems.length == 50) {
630
+ // max count per bunch reached, we need a new one
631
+ lastBunch = null;
632
+ }
633
+ }
634
+
635
+ const doLoad = async() => {
636
+ clearTimeout(lastBunch.timeout);
637
+ if (lastBunch.historyItems.length < 50) {
638
+ // try to add more items to the bunch
639
+ for (const ind in this._history) {
640
+ if (this.__historyItemsAlreadyAskedToLoad[this._history[ind].id]) {
641
+ continue;
642
+ }
643
+ if (lastBunch.historyItems.length == 50) {
644
+ break;
645
+ }
646
+ lastBunch.historyItems.push(this._history[ind]);
647
+ this.__historyItemsAlreadyAskedToLoad[this._history[ind].id] = true;
648
+ }
649
+ }
650
+ lastBunch.started = true;
651
+ await this.loadHistoryItemsBunch(lastBunch.historyItems);
652
+ lastBunch.promiseResolver();
653
+ };
654
+
655
+ if (!lastBunch) {
656
+ lastBunch = {
657
+ historyItems: [],
658
+ askedAt: Date.now(),
659
+ started: false,
660
+ promise: null,
661
+ promiseResolver: null,
662
+ };
663
+ lastBunch.promise = new Promise((resolve) => {
664
+ lastBunch.promiseResolver = resolve;
665
+ });
666
+ lastBunch.timeout = setTimeout( async() => {
667
+ doLoad();
668
+ }, maxWaitForBunchTimeMs);
669
+ this.__historyItemLoaderBunches.push(lastBunch);
670
+ }
671
+
672
+ lastBunch.historyItems.push(historyItem);
673
+ this.__historyItemsAlreadyAskedToLoad[historyItem.id] = true;
674
+
675
+ await lastBunch.promise;
676
+
677
+ return historyItem;
678
+ }
679
+
680
+ /**
681
+ * Loads multiple archive items in a single batch request for efficiency.
682
+ * Uses multiGetObjects to fetch multiple archive items in a single blockchain call.
683
+ * @param {Array<EndlessVectorArchive>} archiveItems - Array of archive items to load
684
+ * @returns {Promise<void>}
685
+ */
686
+ async loadArchiveItemsBunch(archiveItems) {
687
+ const ids = archiveItems.map(ai => ai.id);
688
+ let results = [];
689
+ try {
690
+ results = await this.suiClient.multiGetObjects({
691
+ ids: ids,
692
+ options: { showContent: true, },
693
+ });
694
+ } catch(e) {
695
+ console.error(e);
696
+ }
697
+
698
+ if (results && results.length) {
699
+ for (const res of results) {
700
+ const fields = res?.data?.content?.fields?.value?.fields;
701
+ const id = res?.data?.content?.fields?.id?.id;
702
+
703
+ archiveItems.forEach(ai => {
704
+ if (ai.id === id) {
705
+ ai.setFields(fields);
706
+ }
707
+ });
708
+ }
709
+ }
710
+ }
711
+
712
+ /**
713
+ * Loads a single archive item, batching requests for efficiency.
714
+ * Uses a batching mechanism to group multiple requests within a short time window.
715
+ * Automatically batches up to 50 items per request with a 30ms timeout.
716
+ * @param {EndlessVectorArchive} archiveItem - The archive item to load
717
+ * @returns {Promise<EndlessVectorArchive>} The loaded archive item
718
+ */
719
+ async loadArchiveItem(archiveItem) {
720
+ const maxWaitForBunchTimeMs = 30;
721
+
722
+ if (archiveItem.isReady()) {
723
+ return archiveItem;
724
+ }
725
+
726
+ if (!this.__archiveItemLoaderBunches) {
727
+ this.__archiveItemLoaderBunches = [];
728
+ this.__archiveItemsAlreadyAskedToLoad = {};
729
+ }
730
+
731
+ let lastBunch = null;
732
+ if (this.__archiveItemLoaderBunches.length) {
733
+ lastBunch = this.__archiveItemLoaderBunches[this.__archiveItemLoaderBunches.length - 1];
734
+ if (lastBunch.started) {
735
+ // that bunch is already started to load, so we need a new one
736
+ lastBunch = null;
737
+ }
738
+ if (lastBunch &&lastBunch.archiveItems.length == 50) {
739
+ // max count per bunch reached, we need a new one
740
+ lastBunch = null;
741
+ }
742
+ }
743
+
744
+ const doLoad = async() => {
745
+ clearTimeout(lastBunch.timeout);
746
+ if (lastBunch.archiveItems.length < 50) {
747
+ // try to add more items to the bunch
748
+ for (const ind in this._archive) {
749
+ if (this.__archiveItemsAlreadyAskedToLoad[this._archive[ind].id]) {
750
+ continue;
751
+ }
752
+ if (lastBunch.archiveItems.length == 50) {
753
+ break;
754
+ }
755
+ lastBunch.archiveItems.push(this._archive[ind]);
756
+ this.__archiveItemsAlreadyAskedToLoad[this._archive[ind].id] = true;
757
+ }
758
+ }
759
+ lastBunch.started = true;
760
+ await this.loadArchiveItemsBunch(lastBunch.archiveItems);
761
+ lastBunch.promiseResolver();
762
+ };
763
+
764
+ if (!lastBunch) {
765
+ lastBunch = {
766
+ archiveItems: [],
767
+ askedAt: Date.now(),
768
+ started: false,
769
+ promise: null,
770
+ promiseResolver: null,
771
+ };
772
+ lastBunch.promise = new Promise((resolve) => {
773
+ lastBunch.promiseResolver = resolve;
774
+ });
775
+ lastBunch.timeout = setTimeout( async() => {
776
+ doLoad();
777
+ }, maxWaitForBunchTimeMs);
778
+ this.__archiveItemLoaderBunches.push(lastBunch);
779
+ }
780
+
781
+ lastBunch.archiveItems.push(archiveItem);
782
+ this.__archiveItemsAlreadyAskedToLoad[archiveItem.id] = true;
783
+
784
+ await lastBunch.promise;
785
+
786
+ return archiveItem;
787
+ }
788
+
789
+
790
+
791
+ /**
792
+ * Retrieves the byte array at the specified index from either current items or history.
793
+ * @param {number} i - The index to retrieve
794
+ * @returns {Promise<Uint8Array>} The byte array at the specified index
795
+ * @throws {Error} If the index is out of range or cannot be found
796
+ */
797
+ async at(i) {
798
+ await this.initialize();
799
+
800
+ if (i < 0 || i >= this.length) {
801
+ throw new Error('at() is out of range. Current length: ' + this.length + ', requested index: ' + i);
802
+ }
803
+
804
+ if (i < this.firstNotHistoryIndex) {
805
+ if (i < this.archivedAtLength) {
806
+ // in archive
807
+ if (i < this.archivedFromLength) {
808
+ throw new Error('at() is out of range, this part of archive has been burned');
809
+ }
810
+
811
+ for (let j = this.archiveItemsCount - 1; j >= 0; j--) {
812
+ // reverse order, so we can burn ealier archives
813
+ // @todo: binary search?
814
+ const archiveItem = await this.getArchive(j);
815
+ if (archiveItem.startsAt <= i && i <= archiveItem.endsAt) {
816
+ return await archiveItem.at(i);
817
+ }
818
+ }
819
+ } else {
820
+ // find history item that contains i
821
+ for (let j = 0; j < this.historyItemsCount; j++) {
822
+ const historyItem = await this.getHistory(j); // @todo: binary search?
823
+ if (historyItem.startsAt <= i && i <= historyItem.endsAt) {
824
+ return await historyItem.at(i);
825
+ }
826
+ }
827
+ }
828
+ } else {
829
+ // in current items
830
+ if (this.firstItemIsFromPreviousHistory) {
831
+ const indexInItems = i - this.firstNotHistoryIndex + 1;
832
+ return this._items[indexInItems];
833
+ } else {
834
+ const indexInItems = i - this.firstNotHistoryIndex;
835
+ return this._items[indexInItems];
836
+ }
837
+ }
838
+
839
+
840
+ throw new Error('at() could not find history item for index ' + i);
841
+ }
842
+
843
+ /**
844
+ * Gets suffix bytes from a history item at the specified index.
845
+ * Used for combining data across history item boundaries. Returns the suffix bytes
846
+ * stored in the first item of the specified history item, or from the current vector's
847
+ * first item if accessing the boundary between history and current items.
848
+ * @param {number} i - The history item index
849
+ * @returns {Promise<Uint8Array|undefined>} The suffix bytes, or undefined if not found
850
+ */
851
+ async getSuffixFromHistoryItemOfIndex(i) {
852
+ if (i < this.historyItemsCount) {
853
+ const historyItem = await this.getHistory(i);
854
+ if (historyItem) {
855
+ return historyItem.getSuffixStoredBytes();
856
+ }
857
+ } else if (this._history[i - 1] && this.firstItemIsFromPreviousHistory) {
858
+ // if there is no such history item, but previous exists, then suffix is the first item of the EndlessVector object items itself
859
+ return this._items[0];
860
+ }
861
+ }
862
+ }