@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
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import EndlessVectorHistory from './EndlessVectorHistory.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {import('@mysten/sui/client').SuiClient} SuiClient
|
|
5
|
+
* @typedef {import('@mysten/sui/client').GetDynamicFieldsParams} GetDynamicFieldsParams
|
|
6
|
+
* @typedef {import('./EndlessVector.js').default} EndlessVector
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export default class EndlessVectorArchive {
|
|
10
|
+
/**
|
|
11
|
+
* Creates a new EndlessVectorArchive instance.
|
|
12
|
+
* @param {Object} params - Configuration parameters
|
|
13
|
+
* @param {SuiClient} [params.suiClient] - Sui client instance for blockchain interactions
|
|
14
|
+
* @param {string} [params.id] - ID or address of the EndlessVectorArchive on the Sui blockchain
|
|
15
|
+
* @param {number} [params.index=0] - Index position of this archive item in the sequence
|
|
16
|
+
* @param {EndlessVector} [params.endlessVector] - Reference to the parent EndlessVector instance
|
|
17
|
+
* @param {Object} [params.fields] - Raw field data from the blockchain object
|
|
18
|
+
*/
|
|
19
|
+
constructor(params = {}) {
|
|
20
|
+
/** @type {SuiClient} */
|
|
21
|
+
this.suiClient = params.suiClient;
|
|
22
|
+
/** @type {string} */
|
|
23
|
+
this.id = params.id;
|
|
24
|
+
/** @type {number} */
|
|
25
|
+
this.index = params.index || 0;
|
|
26
|
+
/** @type {?EndlessVector} */
|
|
27
|
+
this._endlessVector = params.endlessVector || null; // parent instance of EndlessVector class
|
|
28
|
+
|
|
29
|
+
/** @type {string} */
|
|
30
|
+
this.historyTableId = null; // id of the dynamic field table that contains history items of this EndlessVector
|
|
31
|
+
/** @type {number} */
|
|
32
|
+
this.historyItemsCount = 0; // data from EndlessVectorArchive object .history field
|
|
33
|
+
this._history = {}; // EndlessVectorHistory instances
|
|
34
|
+
|
|
35
|
+
/** @type {Array<Uint8Array>} */
|
|
36
|
+
this._items = []; // final items in the archived vector
|
|
37
|
+
|
|
38
|
+
/** @type {boolean} */
|
|
39
|
+
this._isInitialized = false;
|
|
40
|
+
|
|
41
|
+
this._fields = params.fields || null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Sets the fields data for this archive item. Called by loader of EndlessVector.
|
|
46
|
+
* @param {Object} fields - The fields data from the blockchain object
|
|
47
|
+
*/
|
|
48
|
+
setFields(fields) {
|
|
49
|
+
this._fields = fields;
|
|
50
|
+
this.historyTableId = fields?.history?.fields?.id?.id;
|
|
51
|
+
this.historyItemsCount = parseInt(fields?.history?.fields?.size || '0');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Checks if this archive item has been initialized and is ready for use.
|
|
56
|
+
* @returns {boolean} True if the archive item is initialized
|
|
57
|
+
*/
|
|
58
|
+
isReady() {
|
|
59
|
+
return this._isInitialized;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Gets the total number of items stored in this archive.
|
|
64
|
+
* @returns {number} The length of the archived vector segment
|
|
65
|
+
*/
|
|
66
|
+
get length() {
|
|
67
|
+
if (this._fields && this._fields.length) {
|
|
68
|
+
return parseInt(this._fields.length);
|
|
69
|
+
}
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Gets the first index position that this archive covers.
|
|
75
|
+
* @returns {number} The starting index (inclusive)
|
|
76
|
+
*/
|
|
77
|
+
get startsAt() {
|
|
78
|
+
return this.endsAt - this.length;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Gets the last index position that this archive covers.
|
|
83
|
+
* @returns {number|undefined} The ending index (inclusive), or undefined if not available
|
|
84
|
+
*/
|
|
85
|
+
get endsAt() {
|
|
86
|
+
if (this._fields && this._fields.archived_at_length) {
|
|
87
|
+
return parseInt(this._fields.archived_at_length) - 1;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Initializes this archive item by loading its data from the blockchain.
|
|
93
|
+
* Uses promise-based synchronization to prevent multiple concurrent initializations.
|
|
94
|
+
* @returns {Promise<boolean>} True when initialization is complete
|
|
95
|
+
*/
|
|
96
|
+
async initialize() {
|
|
97
|
+
if (this._isInitialized) {
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
if (this.__initializationPromise) {
|
|
101
|
+
return await this.__initializationPromise;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
this.__initializationPromiseResolver = null;
|
|
105
|
+
this.__initializationPromise = new Promise((res)=>{ this.__initializationPromiseResolver = res; });
|
|
106
|
+
|
|
107
|
+
await this._endlessVector.loadArchiveItem(this);
|
|
108
|
+
|
|
109
|
+
this._isInitialized = true;
|
|
110
|
+
this.__initializationPromiseResolver();
|
|
111
|
+
|
|
112
|
+
delete this.__initializationPromise;
|
|
113
|
+
delete this.__initializationPromiseResolver;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Gets a history item within this archive by its index.
|
|
118
|
+
* @param {number|string} historyIndex - The index of the history item to retrieve
|
|
119
|
+
* @returns {Promise<EndlessVectorHistory>} The history item at the specified index
|
|
120
|
+
* @throws {Error} If historyTableId is not set or history item not found
|
|
121
|
+
*/
|
|
122
|
+
async getHistory(historyIndex) {
|
|
123
|
+
// @todo: check by historyItemsCount
|
|
124
|
+
|
|
125
|
+
const historyIndexInt = parseInt(historyIndex);
|
|
126
|
+
|
|
127
|
+
if (this._history[historyIndexInt]) {
|
|
128
|
+
await this._history[historyIndexInt].initialize();
|
|
129
|
+
return this._history[historyIndexInt];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// go throught the dynamic fields of the history table to find the id of the needed history item
|
|
133
|
+
if (!this.historyTableId) {
|
|
134
|
+
throw new Error('historyTableId is not set');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** @type {GetDynamicFieldsParams} */
|
|
138
|
+
const getDynamicFieldsParams = {
|
|
139
|
+
parentId: this.historyTableId,
|
|
140
|
+
options: {
|
|
141
|
+
showContent: true,
|
|
142
|
+
showType: true,
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
let resp = null;
|
|
147
|
+
let haveToLookMore = true;
|
|
148
|
+
|
|
149
|
+
do {
|
|
150
|
+
resp = await this.suiClient.getDynamicFields(getDynamicFieldsParams);
|
|
151
|
+
if (resp && resp.data && resp.data.length) {
|
|
152
|
+
for (const df of resp.data) {
|
|
153
|
+
if (df?.objectId) {
|
|
154
|
+
const itemHistoryIndex = parseInt(df.name.value);
|
|
155
|
+
const endlessVectorHistory = new EndlessVectorHistory({
|
|
156
|
+
suiClient: this.suiClient,
|
|
157
|
+
id: df.objectId,
|
|
158
|
+
index: itemHistoryIndex,
|
|
159
|
+
endlessVector: this._endlessVector,
|
|
160
|
+
endlessVectorArchive: this,
|
|
161
|
+
});
|
|
162
|
+
this._history[itemHistoryIndex] = endlessVectorHistory;
|
|
163
|
+
if (itemHistoryIndex === historyIndexInt) {
|
|
164
|
+
haveToLookMore = false;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
getDynamicFieldsParams.cursor = resp.nextCursor;
|
|
169
|
+
}
|
|
170
|
+
} while (resp?.hasNextPage && haveToLookMore);
|
|
171
|
+
|
|
172
|
+
if (!this._history[historyIndexInt]) {
|
|
173
|
+
throw new Error(`History not found for index ${historyIndexInt}`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
await this._history[historyIndexInt].initialize();
|
|
177
|
+
|
|
178
|
+
return this._history[historyIndexInt];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Retrieves the byte array at the specified index within this archive.
|
|
183
|
+
* @param {number} i - The index to retrieve
|
|
184
|
+
* @returns {Promise<Uint8Array>} The byte array at the specified index
|
|
185
|
+
* @throws {Error} If the index is out of range for this archive item
|
|
186
|
+
*/
|
|
187
|
+
async at(i) {
|
|
188
|
+
if (i <= this.endsAt) {
|
|
189
|
+
for (let j = 0; j < this.historyItemsCount; j++) {
|
|
190
|
+
const historyItem = await this.getHistory(j);
|
|
191
|
+
if (j == 0 && i < historyItem.startsAt) {
|
|
192
|
+
throw new Error('at() is out of range for this archive item');
|
|
193
|
+
}
|
|
194
|
+
if (historyItem.startsAt <= i && i <= historyItem.endsAt) {
|
|
195
|
+
return historyItem.at(i);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
throw new Error('at() is out of range for this history item');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Gets suffix bytes from a history item at the specified index within this archive.
|
|
205
|
+
* @param {number} i - The history item index
|
|
206
|
+
* @returns {Promise<Uint8Array>} The suffix bytes from the history item
|
|
207
|
+
* @throws {Error} If the index is out of range for this archive item
|
|
208
|
+
*/
|
|
209
|
+
async getSuffixFromHistoryItemOfIndex(i) {
|
|
210
|
+
if (i < this.historyItemsCount) {
|
|
211
|
+
const historyItem = await this.getHistory(i);
|
|
212
|
+
if (historyItem) {
|
|
213
|
+
return historyItem.getSuffixStoredBytes();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
throw new Error('getSuffixFromHistoryItemOfIndex() is out of range for this archive item');
|
|
218
|
+
}
|
|
219
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
* @typedef {import('@mysten/sui/client').SuiClient} SuiClient
|
|
4
|
+
* @typedef {import('./EndlessVector.js').default} EndlessVector
|
|
5
|
+
* @typedef {import('./EndlessVectorArchive.js').default} EndlessVectorArchive
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Represents a history item in an EndlessVector, managing a segment of the vector's data.
|
|
10
|
+
* Each history item stores a portion of the vector's elements and maintains metadata
|
|
11
|
+
* about its position and relationships with adjacent history items.
|
|
12
|
+
*/
|
|
13
|
+
export default class EndlessVectorHistory {
|
|
14
|
+
/**
|
|
15
|
+
* Creates a new EndlessVectorHistory instance.
|
|
16
|
+
* @param {Object} params - Configuration parameters
|
|
17
|
+
* @param {SuiClient} [params.suiClient] - Sui client instance for blockchain interactions
|
|
18
|
+
* @param {string} [params.id] - Unique identifier for this history item
|
|
19
|
+
* @param {number} [params.index=0] - Index position of this history item in the sequence
|
|
20
|
+
* @param {?Object} [params.fields] - Raw field data from the blockchain object
|
|
21
|
+
* @param {EndlessVector} [params.endlessVector] - Reference to the parent EndlessVector instance
|
|
22
|
+
* @param {?EndlessVectorArchive} [params.endlessVectorArchive] - Reference to the parent EndlessVectorArchive instance
|
|
23
|
+
*/
|
|
24
|
+
constructor(params = {}) {
|
|
25
|
+
/** @type {SuiClient} */
|
|
26
|
+
this.suiClient = params.suiClient;
|
|
27
|
+
/** @type {string} */
|
|
28
|
+
this.id = params.id;
|
|
29
|
+
/** @type {number} */
|
|
30
|
+
this.index = params.index || 0;
|
|
31
|
+
|
|
32
|
+
/** @type {?Object} */
|
|
33
|
+
this._fields = params.fields || null;
|
|
34
|
+
/** @type {EndlessVector} */
|
|
35
|
+
this._endlessVector = params.endlessVector || null; // parent instance of EndlessVector class
|
|
36
|
+
/** @type {?EndlessVectorArchive} */
|
|
37
|
+
this._endlessVectorArchive = params.endlessVectorArchive || null; // parent instance of EndlessVectorArchive class
|
|
38
|
+
|
|
39
|
+
/** @type {boolean} */
|
|
40
|
+
this._isInitialized = false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Sets the fields data for this history item. Called by loader of EndlessVector.
|
|
45
|
+
* @param {Object} fields - The fields data from the blockchain object
|
|
46
|
+
*/
|
|
47
|
+
setFields(fields) {
|
|
48
|
+
this._fields = fields;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Checks if this history item has been initialized and is ready for use.
|
|
53
|
+
* @returns {boolean} True if the history item is initialized
|
|
54
|
+
*/
|
|
55
|
+
isReady() {
|
|
56
|
+
return this._isInitialized;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Initializes this history item by loading its data from the blockchain.
|
|
61
|
+
* Uses promise-based synchronization to prevent multiple concurrent initializations.
|
|
62
|
+
* @returns {Promise<boolean>} True when initialization is complete
|
|
63
|
+
*/
|
|
64
|
+
async initialize() {
|
|
65
|
+
if (this._isInitialized) {
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
if (this.__initializationPromise) {
|
|
69
|
+
return await this.__initializationPromise;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
this.__initializationPromiseResolver = null;
|
|
73
|
+
this.__initializationPromise = new Promise((res)=>{ this.__initializationPromiseResolver = res; });
|
|
74
|
+
|
|
75
|
+
await this._endlessVector.loadHistoryItem(this);
|
|
76
|
+
|
|
77
|
+
this._isInitialized = true;
|
|
78
|
+
this.__initializationPromiseResolver();
|
|
79
|
+
|
|
80
|
+
delete this.__initializationPromise;
|
|
81
|
+
delete this.__initializationPromiseResolver;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Gets the last index position that this history item covers.
|
|
86
|
+
* @returns {number|undefined} The ending index (inclusive), or undefined if not available
|
|
87
|
+
*/
|
|
88
|
+
get endsAt() {
|
|
89
|
+
if (this._fields && this._fields.saved_at_length) {
|
|
90
|
+
return parseInt(this._fields.saved_at_length) - 1;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Indicates whether the first item in this history contains suffix bytes that should be
|
|
96
|
+
* added to the last item from the previous history segment.
|
|
97
|
+
* @returns {boolean} True if the first item contains suffix bytes for the previous history
|
|
98
|
+
*/
|
|
99
|
+
get firstItemIsFromPreviousHistory() {
|
|
100
|
+
if (this._fields && this._fields.first_item_is_from_previous_history) {
|
|
101
|
+
return this._fields.first_item_is_from_previous_history;
|
|
102
|
+
}
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Gets the first index position that this history item covers.
|
|
108
|
+
* Calculation adjusts for whether the first item contains suffix bytes for the previous history.
|
|
109
|
+
* @returns {number} The starting index (inclusive)
|
|
110
|
+
*/
|
|
111
|
+
get startsAt() {
|
|
112
|
+
let innerItemsCount = 0;
|
|
113
|
+
if (this._fields && this._fields.items && this._fields.items.length) {
|
|
114
|
+
innerItemsCount = this._fields.items.length;
|
|
115
|
+
}
|
|
116
|
+
if (this.firstItemIsFromPreviousHistory) {
|
|
117
|
+
return this.endsAt - innerItemsCount + 2;
|
|
118
|
+
}
|
|
119
|
+
return this.endsAt - innerItemsCount + 1;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Gets the number of bytes from the next history item that should be appended
|
|
124
|
+
* to the last item in this history segment. This should equal the byte length
|
|
125
|
+
* of the first item of the next history.
|
|
126
|
+
* @returns {number} Number of bytes to append from the next history item
|
|
127
|
+
*/
|
|
128
|
+
get followedByNextBytes() {
|
|
129
|
+
if (this._fields && this._fields.followed_by_next_bytes) {
|
|
130
|
+
return parseInt(this._fields.followed_by_next_bytes);
|
|
131
|
+
}
|
|
132
|
+
return 0;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Retrieves the byte array at the specified index within this history segment.
|
|
137
|
+
* Handles combining items with suffix bytes from the next history when needed.
|
|
138
|
+
* @param {number} i - The index to retrieve
|
|
139
|
+
* @returns {Promise<Uint8Array>} The byte array at the specified index
|
|
140
|
+
* @throws {Error} If the index is out of range for this history item
|
|
141
|
+
*/
|
|
142
|
+
async at(i) {
|
|
143
|
+
if (this.startsAt <= i && i <= this.endsAt) {
|
|
144
|
+
let indexInItems = i - this.startsAt;
|
|
145
|
+
if (this.firstItemIsFromPreviousHistory) {
|
|
146
|
+
indexInItems = i - this.startsAt + 1;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (indexInItems < (this._fields.items.length - 1)) {
|
|
150
|
+
return new Uint8Array(this._fields.items[indexInItems]);
|
|
151
|
+
} else if (indexInItems === (this._fields.items.length - 1)) {
|
|
152
|
+
if (this.followedByNextBytes) {
|
|
153
|
+
// if this item is child of archive, get suffix from next item of archive, otherwise from endless vector
|
|
154
|
+
const suffix = this._endlessVectorArchive ?
|
|
155
|
+
(await this._endlessVectorArchive.getSuffixFromHistoryItemOfIndex(this.index + 1)) :
|
|
156
|
+
(await this._endlessVector.getSuffixFromHistoryItemOfIndex(this.index + 1));
|
|
157
|
+
|
|
158
|
+
if (suffix.length !== this.followedByNextBytes) {
|
|
159
|
+
throw new Error('suffix bytes length mismatch');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const current = new Uint8Array(this._fields.items[indexInItems]);
|
|
163
|
+
const combined = new Uint8Array(current.length + suffix.length);
|
|
164
|
+
combined.set(current);
|
|
165
|
+
combined.set(suffix, current.length);
|
|
166
|
+
return combined;
|
|
167
|
+
} else {
|
|
168
|
+
return new Uint8Array(this._fields.items[indexInItems]);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
throw new Error('at() is out of range for this history item');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Gets the suffix bytes stored in this history item that should be appended
|
|
178
|
+
* to the last item of the previous history segment.
|
|
179
|
+
* @returns {Uint8Array} The suffix bytes, or empty array if none stored
|
|
180
|
+
*/
|
|
181
|
+
getSuffixStoredBytes() {
|
|
182
|
+
if (this.firstItemIsFromPreviousHistory) {
|
|
183
|
+
return new Uint8Array(this._fields.items[0]);
|
|
184
|
+
}
|
|
185
|
+
return new Uint8Array();
|
|
186
|
+
}
|
|
187
|
+
}
|