@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.
- package/EndlessVector.js +862 -0
- package/EndlessVectorArchive.js +219 -0
- package/EndlessVectorHistory.js +187 -0
- package/README.md +326 -0
- package/ids.js +11 -0
- package/index.js +10 -0
- package/package.json +36 -0
- package/test/base.test.js +433 -0
- package/test/helpers.js +32 -0
package/EndlessVector.js
ADDED
|
@@ -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
|
+
}
|