@twin.org/entity-storage-connector-memory 0.0.3-next.8 → 0.9.0-next.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/dist/es/index.js +2 -1
- package/dist/es/index.js.map +1 -1
- package/dist/es/memoryEntityStorageConnector.js +365 -84
- package/dist/es/memoryEntityStorageConnector.js.map +1 -1
- package/dist/es/models/IMemoryEntityStorageConnectorConfig.js +4 -0
- package/dist/es/models/IMemoryEntityStorageConnectorConfig.js.map +1 -0
- package/dist/es/models/IMemoryEntityStorageConnectorConstructorOptions.js +0 -2
- package/dist/es/models/IMemoryEntityStorageConnectorConstructorOptions.js.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/memoryEntityStorageConnector.d.ts +84 -7
- package/dist/types/models/IMemoryEntityStorageConnectorConfig.d.ts +24 -0
- package/dist/types/models/IMemoryEntityStorageConnectorConstructorOptions.d.ts +5 -0
- package/docs/changelog.md +534 -50
- package/docs/reference/classes/MemoryEntityStorageConnector.md +319 -6
- package/docs/reference/index.md +1 -0
- package/docs/reference/interfaces/IMemoryEntityStorageConnectorConfig.md +48 -0
- package/docs/reference/interfaces/IMemoryEntityStorageConnectorConstructorOptions.md +8 -0
- package/locales/en.json +13 -1
- package/package.json +9 -8
package/dist/es/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
// Copyright
|
|
1
|
+
// Copyright 2026 IOTA Stiftung.
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0.
|
|
3
3
|
export * from "./memoryEntityStorageConnector.js";
|
|
4
|
+
export * from "./models/IMemoryEntityStorageConnectorConfig.js";
|
|
4
5
|
export * from "./models/IMemoryEntityStorageConnectorConstructorOptions.js";
|
|
5
6
|
//# sourceMappingURL=index.js.map
|
package/dist/es/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,gCAAgC;AAChC,uCAAuC;AACvC,cAAc,mCAAmC,CAAC;AAClD,cAAc,6DAA6D,CAAC","sourcesContent":["// Copyright
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,gCAAgC;AAChC,uCAAuC;AACvC,cAAc,mCAAmC,CAAC;AAClD,cAAc,iDAAiD,CAAC;AAChE,cAAc,6DAA6D,CAAC","sourcesContent":["// Copyright 2026 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nexport * from \"./memoryEntityStorageConnector.js\";\nexport * from \"./models/IMemoryEntityStorageConnectorConfig.js\";\nexport * from \"./models/IMemoryEntityStorageConnectorConstructorOptions.js\";\n"]}
|
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
// Copyright 2024 IOTA Stiftung.
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0.
|
|
3
3
|
import { ContextIdHelper, ContextIdStore } from "@twin.org/context";
|
|
4
|
-
import { Coerce, Guards, Is, ObjectHelper } from "@twin.org/core";
|
|
4
|
+
import { Coerce, ComponentFactory, Guards, HealthStatus, Is, Mutex, ObjectHelper, SharedObjectBuffer, Validation } from "@twin.org/core";
|
|
5
5
|
import { ComparisonOperator, EntityConditions, EntitySchemaFactory, EntitySchemaHelper, EntitySorter, LogicalOperator } from "@twin.org/entity";
|
|
6
|
+
import { EntityStorageHelper } from "@twin.org/entity-storage-models";
|
|
6
7
|
/**
|
|
7
|
-
* Class for performing entity storage operations in-memory.
|
|
8
|
+
* Class for performing entity storage operations in-memory backed by a shared object buffer.
|
|
9
|
+
*
|
|
10
|
+
* All reads and writes are serialised with a per-schema lock so that concurrent async
|
|
11
|
+
* access, including across worker threads, never produces torn or lost updates.
|
|
12
|
+
*
|
|
13
|
+
* All connector instances that share the same entity schema name share the same underlying
|
|
14
|
+
* buffer, making data written in one instance immediately visible in another, including
|
|
15
|
+
* across worker threads when the main thread forwards worker messages to the lock and
|
|
16
|
+
* buffer handlers.
|
|
8
17
|
*/
|
|
9
18
|
export class MemoryEntityStorageConnector {
|
|
10
19
|
/**
|
|
@@ -37,10 +46,25 @@ export class MemoryEntityStorageConnector {
|
|
|
37
46
|
*/
|
|
38
47
|
_primaryKey;
|
|
39
48
|
/**
|
|
40
|
-
* The storage
|
|
49
|
+
* The resolved storage key used as the shared buffer and lock key.
|
|
41
50
|
* @internal
|
|
42
51
|
*/
|
|
43
|
-
|
|
52
|
+
_storageKey;
|
|
53
|
+
/**
|
|
54
|
+
* Initial capacity hint in bytes for the shared entity buffer.
|
|
55
|
+
* @internal
|
|
56
|
+
*/
|
|
57
|
+
_initialCapacityBytes;
|
|
58
|
+
/**
|
|
59
|
+
* Maximum capacity in bytes for the shared entity buffer.
|
|
60
|
+
* @internal
|
|
61
|
+
*/
|
|
62
|
+
_maxCapacityBytes;
|
|
63
|
+
/**
|
|
64
|
+
* Milliseconds to wait for the directory lock before throwing.
|
|
65
|
+
* @internal
|
|
66
|
+
*/
|
|
67
|
+
_mutexTimeoutMs;
|
|
44
68
|
/**
|
|
45
69
|
* Create a new instance of MemoryEntityStorageConnector.
|
|
46
70
|
* @param options The options for the connector.
|
|
@@ -48,33 +72,15 @@ export class MemoryEntityStorageConnector {
|
|
|
48
72
|
constructor(options) {
|
|
49
73
|
Guards.object(MemoryEntityStorageConnector.CLASS_NAME, "options", options);
|
|
50
74
|
Guards.stringValue(MemoryEntityStorageConnector.CLASS_NAME, "options.entitySchema", options.entitySchema);
|
|
75
|
+
Guards.object(MemoryEntityStorageConnector.CLASS_NAME, "options.config", options.config);
|
|
76
|
+
Guards.stringValue(MemoryEntityStorageConnector.CLASS_NAME, "options.config.storageKey", options.config.storageKey);
|
|
51
77
|
this._entitySchema = EntitySchemaFactory.get(options.entitySchema);
|
|
78
|
+
this._storageKey = options.config.storageKey;
|
|
52
79
|
this._partitionContextIds = options.partitionContextIds;
|
|
53
80
|
this._primaryKey = EntitySchemaHelper.getPrimaryKey(this._entitySchema);
|
|
54
|
-
this.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
* Deep-clone condition tree and map `null` to `undefined` on Equals/NotEquals leaves
|
|
58
|
-
* so in-memory evaluation matches SQL-style "IS NULL" / "IS NOT NULL" semantics.
|
|
59
|
-
* @param condition The user-supplied condition (not mutated).
|
|
60
|
-
* @returns A clone safe to pass to {@link EntityConditions.check}.
|
|
61
|
-
* @internal
|
|
62
|
-
*/
|
|
63
|
-
static normalizeNullToUndefined(condition) {
|
|
64
|
-
if ("conditions" in condition) {
|
|
65
|
-
return {
|
|
66
|
-
...condition,
|
|
67
|
-
conditions: condition.conditions.map(c => MemoryEntityStorageConnector.normalizeNullToUndefined(c))
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
// In the non-group branch, `condition` is the leaf comparator.
|
|
71
|
-
const leaf = condition;
|
|
72
|
-
if ((leaf.comparison === ComparisonOperator.Equals ||
|
|
73
|
-
leaf.comparison === ComparisonOperator.NotEquals) &&
|
|
74
|
-
leaf.value === null) {
|
|
75
|
-
return { ...leaf, value: undefined };
|
|
76
|
-
}
|
|
77
|
-
return { ...leaf };
|
|
81
|
+
this._initialCapacityBytes = options.config?.initialCapacityBytes;
|
|
82
|
+
this._maxCapacityBytes = options.config?.maxCapacityBytes;
|
|
83
|
+
this._mutexTimeoutMs = Coerce.integer(options.config.mutexTimeoutMs);
|
|
78
84
|
}
|
|
79
85
|
/**
|
|
80
86
|
* Returns the class name of the component.
|
|
@@ -83,6 +89,20 @@ export class MemoryEntityStorageConnector {
|
|
|
83
89
|
className() {
|
|
84
90
|
return MemoryEntityStorageConnector.CLASS_NAME;
|
|
85
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* Returns the health status of the component.
|
|
94
|
+
* @returns The health status of the component.
|
|
95
|
+
*/
|
|
96
|
+
async health() {
|
|
97
|
+
return [
|
|
98
|
+
{
|
|
99
|
+
source: MemoryEntityStorageConnector.CLASS_NAME,
|
|
100
|
+
status: HealthStatus.Ok,
|
|
101
|
+
description: "healthDescription",
|
|
102
|
+
data: { entityType: this._storageKey }
|
|
103
|
+
}
|
|
104
|
+
];
|
|
105
|
+
}
|
|
86
106
|
/**
|
|
87
107
|
* Get the schema for the entities.
|
|
88
108
|
* @returns The schema for the entities.
|
|
@@ -90,6 +110,18 @@ export class MemoryEntityStorageConnector {
|
|
|
90
110
|
getSchema() {
|
|
91
111
|
return this._entitySchema;
|
|
92
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* Bootstrap the component by creating and initializing any resources it needs.
|
|
115
|
+
* @param nodeLoggingComponentType The node logging component type.
|
|
116
|
+
* @returns True if the bootstrapping process was successful.
|
|
117
|
+
*/
|
|
118
|
+
async bootstrap(nodeLoggingComponentType) {
|
|
119
|
+
await SharedObjectBuffer.create(this._storageKey, {
|
|
120
|
+
initialCapacityBytes: this._initialCapacityBytes,
|
|
121
|
+
maxCapacityBytes: this._maxCapacityBytes
|
|
122
|
+
});
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
93
125
|
/**
|
|
94
126
|
* Get an entity.
|
|
95
127
|
* @param id The id of the entity to get, or the index value if secondaryIndex is set.
|
|
@@ -108,40 +140,84 @@ export class MemoryEntityStorageConnector {
|
|
|
108
140
|
value: partitionKey
|
|
109
141
|
});
|
|
110
142
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
143
|
+
return this.withLock(entities => {
|
|
144
|
+
const index = this.findItem(entities, id, secondaryIndex, finalConditions);
|
|
145
|
+
const item = entities[index];
|
|
146
|
+
if (Is.objectValue(item)) {
|
|
147
|
+
return {
|
|
148
|
+
result: EntityStorageHelper.unPrepareEntity(item, [
|
|
149
|
+
MemoryEntityStorageConnector._PARTITION_KEY
|
|
150
|
+
])
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
return { result: undefined };
|
|
154
|
+
});
|
|
117
155
|
}
|
|
118
156
|
/**
|
|
119
157
|
* Set an entity.
|
|
120
158
|
* @param entity The entity to set.
|
|
121
159
|
* @param conditions The optional conditions to match for the entities.
|
|
122
|
-
* @returns
|
|
160
|
+
* @returns Resolves when the entity has been stored.
|
|
123
161
|
*/
|
|
124
162
|
async set(entity, conditions) {
|
|
125
163
|
Guards.object(MemoryEntityStorageConnector.CLASS_NAME, "entity", entity);
|
|
126
164
|
const contextIds = await ContextIdStore.getContextIds();
|
|
127
165
|
const partitionKey = ContextIdHelper.combinedContextKey(contextIds, this._partitionContextIds);
|
|
128
|
-
EntitySchemaHelper.validateEntity(entity, this.getSchema());
|
|
129
166
|
const finalConditions = conditions ?? [];
|
|
130
|
-
const
|
|
167
|
+
const prepared = EntityStorageHelper.prepareEntity(entity, this._entitySchema, Is.stringValue(partitionKey)
|
|
168
|
+
? [{ property: MemoryEntityStorageConnector._PARTITION_KEY, value: partitionKey }]
|
|
169
|
+
: undefined, { nullBehavior: "omit" });
|
|
131
170
|
if (Is.stringValue(partitionKey)) {
|
|
132
171
|
finalConditions.push({
|
|
133
172
|
property: MemoryEntityStorageConnector._PARTITION_KEY,
|
|
134
173
|
value: partitionKey
|
|
135
174
|
});
|
|
136
|
-
ObjectHelper.propertySet(finalEntity, MemoryEntityStorageConnector._PARTITION_KEY, partitionKey);
|
|
137
|
-
}
|
|
138
|
-
const existingIndex = this.findItem(finalEntity[this._primaryKey.property], undefined, finalConditions);
|
|
139
|
-
if (existingIndex >= 0) {
|
|
140
|
-
this._store[existingIndex] = finalEntity;
|
|
141
|
-
}
|
|
142
|
-
else {
|
|
143
|
-
this._store.push(finalEntity);
|
|
144
175
|
}
|
|
176
|
+
return this.withLock(entities => {
|
|
177
|
+
const existingIndex = this.findItem(entities, prepared[this._primaryKey.property], undefined, finalConditions);
|
|
178
|
+
if (existingIndex >= 0) {
|
|
179
|
+
entities[existingIndex] = prepared;
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
entities.push(prepared);
|
|
183
|
+
}
|
|
184
|
+
return { updated: entities, result: undefined };
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Set multiple entities in a batch.
|
|
189
|
+
* @param entities The entities to set.
|
|
190
|
+
* @returns Nothing.
|
|
191
|
+
*/
|
|
192
|
+
async setBatch(entities) {
|
|
193
|
+
Guards.arrayValue(MemoryEntityStorageConnector.CLASS_NAME, "entities", entities);
|
|
194
|
+
const contextIds = await ContextIdStore.getContextIds();
|
|
195
|
+
const partitionKey = ContextIdHelper.combinedContextKey(contextIds, this._partitionContextIds);
|
|
196
|
+
const preparedItems = entities.map(entity => EntityStorageHelper.prepareEntity(entity, this._entitySchema, Is.stringValue(partitionKey)
|
|
197
|
+
? [{ property: MemoryEntityStorageConnector._PARTITION_KEY, value: partitionKey }]
|
|
198
|
+
: undefined, { nullBehavior: "omit" }));
|
|
199
|
+
return this.withLock(store => {
|
|
200
|
+
const indexMap = new Map();
|
|
201
|
+
for (let i = 0; i < store.length; i++) {
|
|
202
|
+
const stored = store[i];
|
|
203
|
+
const storedPartition = ObjectHelper.propertyGet(stored, MemoryEntityStorageConnector._PARTITION_KEY);
|
|
204
|
+
if (!Is.stringValue(partitionKey) || storedPartition === partitionKey) {
|
|
205
|
+
indexMap.set(stored[this._primaryKey.property], i);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
for (const prepared of preparedItems) {
|
|
209
|
+
const id = prepared[this._primaryKey.property];
|
|
210
|
+
const existingIndex = indexMap.get(id);
|
|
211
|
+
if (existingIndex !== undefined) {
|
|
212
|
+
store[existingIndex] = prepared;
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
const newIndex = store.push(prepared) - 1;
|
|
216
|
+
indexMap.set(id, newIndex);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return { updated: store, result: undefined };
|
|
220
|
+
});
|
|
145
221
|
}
|
|
146
222
|
/**
|
|
147
223
|
* Remove the entity.
|
|
@@ -160,10 +236,13 @@ export class MemoryEntityStorageConnector {
|
|
|
160
236
|
value: partitionKey
|
|
161
237
|
});
|
|
162
238
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
239
|
+
return this.withLock(entities => {
|
|
240
|
+
const index = this.findItem(entities, id, undefined, finalConditions);
|
|
241
|
+
if (index >= 0) {
|
|
242
|
+
entities.splice(index, 1);
|
|
243
|
+
}
|
|
244
|
+
return { updated: entities, result: undefined };
|
|
245
|
+
});
|
|
167
246
|
}
|
|
168
247
|
/**
|
|
169
248
|
* Find all the entities which match the conditions.
|
|
@@ -178,7 +257,135 @@ export class MemoryEntityStorageConnector {
|
|
|
178
257
|
async query(conditions, sortProperties, properties, cursor, limit) {
|
|
179
258
|
const contextIds = await ContextIdStore.getContextIds();
|
|
180
259
|
const partitionKey = ContextIdHelper.combinedContextKey(contextIds, this._partitionContextIds);
|
|
181
|
-
|
|
260
|
+
EntityStorageHelper.validateSortProperties(this._entitySchema, sortProperties);
|
|
261
|
+
EntityStorageHelper.validateProperties(this._entitySchema, properties);
|
|
262
|
+
if (!Is.empty(limit)) {
|
|
263
|
+
const validationFailures = [];
|
|
264
|
+
Validation.integer("limit", limit, validationFailures, undefined, { minValue: 1 });
|
|
265
|
+
Validation.asValidationError(MemoryEntityStorageConnector.CLASS_NAME, "query", validationFailures);
|
|
266
|
+
}
|
|
267
|
+
return this.withLock(store => {
|
|
268
|
+
let allEntities = store.slice();
|
|
269
|
+
const finalConditions = {
|
|
270
|
+
conditions: [],
|
|
271
|
+
logicalOperator: LogicalOperator.And
|
|
272
|
+
};
|
|
273
|
+
if (Is.stringValue(partitionKey)) {
|
|
274
|
+
finalConditions.conditions.push({
|
|
275
|
+
property: MemoryEntityStorageConnector._PARTITION_KEY,
|
|
276
|
+
comparison: ComparisonOperator.Equals,
|
|
277
|
+
value: partitionKey
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
if (!Is.empty(conditions)) {
|
|
281
|
+
finalConditions.conditions.push(EntityStorageHelper.normalizeConditionValues(conditions));
|
|
282
|
+
}
|
|
283
|
+
const resultEntities = [];
|
|
284
|
+
const finalLimit = limit ?? MemoryEntityStorageConnector._DEFAULT_LIMIT;
|
|
285
|
+
let nextCursor;
|
|
286
|
+
if (allEntities.length > 0) {
|
|
287
|
+
const finalSortKeys = EntitySchemaHelper.buildSortProperties(this._entitySchema, sortProperties);
|
|
288
|
+
allEntities = EntitySorter.sort(allEntities, finalSortKeys);
|
|
289
|
+
const startIndex = Coerce.number(cursor) ?? 0;
|
|
290
|
+
for (let i = startIndex; i < allEntities.length; i++) {
|
|
291
|
+
if (EntityConditions.check(allEntities[i], finalConditions) &&
|
|
292
|
+
resultEntities.length < finalLimit) {
|
|
293
|
+
const entity = Is.arrayValue(properties)
|
|
294
|
+
? ObjectHelper.pick(allEntities[i], properties)
|
|
295
|
+
: allEntities[i];
|
|
296
|
+
resultEntities.push(EntityStorageHelper.unPrepareEntity(entity, [
|
|
297
|
+
MemoryEntityStorageConnector._PARTITION_KEY
|
|
298
|
+
]));
|
|
299
|
+
if (resultEntities.length >= finalLimit) {
|
|
300
|
+
if (i < allEntities.length - 1) {
|
|
301
|
+
nextCursor = (i + 1).toString();
|
|
302
|
+
}
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return { result: { entities: resultEntities, cursor: nextCursor } };
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Remove all entities from the storage.
|
|
313
|
+
* @returns Nothing.
|
|
314
|
+
*/
|
|
315
|
+
async empty() {
|
|
316
|
+
const contextIds = await ContextIdStore.getContextIds();
|
|
317
|
+
const partitionKey = ContextIdHelper.combinedContextKey(contextIds, this._partitionContextIds);
|
|
318
|
+
return this.withLock(entities => {
|
|
319
|
+
if (Is.stringValue(partitionKey)) {
|
|
320
|
+
const filtered = entities.filter(item => ObjectHelper.propertyGet(item, MemoryEntityStorageConnector._PARTITION_KEY) !==
|
|
321
|
+
partitionKey);
|
|
322
|
+
return { updated: filtered, result: undefined };
|
|
323
|
+
}
|
|
324
|
+
return { updated: [], result: undefined };
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Remove multiple entities by id.
|
|
329
|
+
* @param ids The ids of the entities to remove.
|
|
330
|
+
* @returns Nothing.
|
|
331
|
+
*/
|
|
332
|
+
async removeBatch(ids) {
|
|
333
|
+
Guards.arrayValue(MemoryEntityStorageConnector.CLASS_NAME, "ids", ids);
|
|
334
|
+
const contextIds = await ContextIdStore.getContextIds();
|
|
335
|
+
const partitionKey = ContextIdHelper.combinedContextKey(contextIds, this._partitionContextIds);
|
|
336
|
+
const finalConditions = [];
|
|
337
|
+
if (Is.stringValue(partitionKey)) {
|
|
338
|
+
finalConditions.push({
|
|
339
|
+
property: MemoryEntityStorageConnector._PARTITION_KEY,
|
|
340
|
+
value: partitionKey
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
return this.withLock(entities => {
|
|
344
|
+
for (const id of ids) {
|
|
345
|
+
const index = this.findItem(entities, id, undefined, finalConditions);
|
|
346
|
+
if (index >= 0) {
|
|
347
|
+
entities.splice(index, 1);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return { updated: entities, result: undefined };
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Teardown the storage by clearing the underlying shared buffer for this schema.
|
|
355
|
+
* @param nodeLoggingComponentType The node logging component type.
|
|
356
|
+
* @returns True if the teardown process was successful.
|
|
357
|
+
*/
|
|
358
|
+
async teardown(nodeLoggingComponentType) {
|
|
359
|
+
const nodeLogging = ComponentFactory.getIfExists(nodeLoggingComponentType);
|
|
360
|
+
await nodeLogging?.log({
|
|
361
|
+
level: "info",
|
|
362
|
+
source: MemoryEntityStorageConnector.CLASS_NAME,
|
|
363
|
+
ts: Date.now(),
|
|
364
|
+
message: "storeTearingDown"
|
|
365
|
+
});
|
|
366
|
+
await Mutex.lock(this._storageKey, { throwOnTimeout: true, timeoutMs: this._mutexTimeoutMs });
|
|
367
|
+
try {
|
|
368
|
+
SharedObjectBuffer.remove(this._storageKey);
|
|
369
|
+
}
|
|
370
|
+
finally {
|
|
371
|
+
Mutex.unlock(this._storageKey);
|
|
372
|
+
}
|
|
373
|
+
await nodeLogging?.log({
|
|
374
|
+
level: "info",
|
|
375
|
+
source: MemoryEntityStorageConnector.CLASS_NAME,
|
|
376
|
+
ts: Date.now(),
|
|
377
|
+
message: "storeTornDown"
|
|
378
|
+
});
|
|
379
|
+
return true;
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Count all the entities which match the conditions.
|
|
383
|
+
* @param conditions The optional conditions to match for the entities.
|
|
384
|
+
* @returns The total count of entities in the storage.
|
|
385
|
+
*/
|
|
386
|
+
async count(conditions) {
|
|
387
|
+
const contextIds = await ContextIdStore.getContextIds();
|
|
388
|
+
const partitionKey = ContextIdHelper.combinedContextKey(contextIds, this._partitionContextIds);
|
|
182
389
|
const finalConditions = {
|
|
183
390
|
conditions: [],
|
|
184
391
|
logicalOperator: LogicalOperator.And
|
|
@@ -191,51 +398,126 @@ export class MemoryEntityStorageConnector {
|
|
|
191
398
|
});
|
|
192
399
|
}
|
|
193
400
|
if (!Is.empty(conditions)) {
|
|
194
|
-
finalConditions.conditions.push(
|
|
401
|
+
finalConditions.conditions.push(EntityStorageHelper.normalizeConditionValues(conditions));
|
|
195
402
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
403
|
+
return this.withLock(entities => {
|
|
404
|
+
if (finalConditions.conditions.length === 0) {
|
|
405
|
+
return { result: entities.length };
|
|
406
|
+
}
|
|
407
|
+
return {
|
|
408
|
+
result: entities.filter(item => EntityConditions.check(item, finalConditions)).length
|
|
409
|
+
};
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Get all entities in the memory store.
|
|
414
|
+
* @returns All stored entities with partition keys removed.
|
|
415
|
+
*/
|
|
416
|
+
async getStore() {
|
|
417
|
+
return this.withLock(entities => ({
|
|
418
|
+
result: entities.map(item => EntityStorageHelper.unPrepareEntity(item, [MemoryEntityStorageConnector._PARTITION_KEY]))
|
|
419
|
+
}));
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Get a unique list of all the context ids from the storage.
|
|
423
|
+
* @returns The list of unique context ids.
|
|
424
|
+
*/
|
|
425
|
+
async getPartitionContextIds() {
|
|
426
|
+
return this.withLock(entities => {
|
|
427
|
+
const contextIds = {};
|
|
428
|
+
for (const entity of entities) {
|
|
429
|
+
const partitionId = ObjectHelper.propertyGet(entity, MemoryEntityStorageConnector._PARTITION_KEY);
|
|
430
|
+
if (Is.stringValue(partitionId)) {
|
|
431
|
+
contextIds[partitionId] = ContextIdHelper.shortSplit(this._partitionContextIds ?? [], partitionId);
|
|
215
432
|
}
|
|
216
433
|
}
|
|
434
|
+
return { result: Object.values(contextIds) };
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Create the target connector for performing the migration it will use a temporary storage location.
|
|
439
|
+
* @param newEntitySchema The name of the new entity schema to create the connector for.
|
|
440
|
+
* @returns Connector for performing the migration.
|
|
441
|
+
*/
|
|
442
|
+
async createTargetConnector(newEntitySchema) {
|
|
443
|
+
// Resolve the target schema name the same way _storageKey is resolved in the constructor.
|
|
444
|
+
const targetSchemaEntry = EntitySchemaFactory.get(newEntitySchema);
|
|
445
|
+
const targetSchemaName = targetSchemaEntry.type ?? newEntitySchema;
|
|
446
|
+
// When migrating to a different schema, wipe the target buffer so that every
|
|
447
|
+
// migration starts from an empty store regardless of any previous connector
|
|
448
|
+
// instances that shared the same schema name.
|
|
449
|
+
if (targetSchemaName !== this._storageKey) {
|
|
450
|
+
await Mutex.lock(targetSchemaName, { throwOnTimeout: true, timeoutMs: this._mutexTimeoutMs });
|
|
451
|
+
try {
|
|
452
|
+
SharedObjectBuffer.remove(targetSchemaName);
|
|
453
|
+
}
|
|
454
|
+
finally {
|
|
455
|
+
Mutex.unlock(targetSchemaName);
|
|
456
|
+
}
|
|
217
457
|
}
|
|
218
|
-
return {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
458
|
+
return new MemoryEntityStorageConnector({
|
|
459
|
+
entitySchema: newEntitySchema,
|
|
460
|
+
partitionContextIds: this._partitionContextIds,
|
|
461
|
+
config: {
|
|
462
|
+
storageKey: this._storageKey,
|
|
463
|
+
initialCapacityBytes: this._initialCapacityBytes,
|
|
464
|
+
maxCapacityBytes: this._maxCapacityBytes
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Finalize the migration by tearing down the old connector and replacing it with the target connector.
|
|
470
|
+
* @param targetConnector The target connector to finalize the migration with.
|
|
471
|
+
* @param options The options to control how the migration is finalized.
|
|
472
|
+
* @param loggingComponentType The optional component type to use for logging the migration progress.
|
|
473
|
+
* @returns A promise that resolves when the migration is finalized.
|
|
474
|
+
*/
|
|
475
|
+
async finalizeMigration(targetConnector, options, loggingComponentType) {
|
|
476
|
+
return targetConnector;
|
|
222
477
|
}
|
|
223
478
|
/**
|
|
224
|
-
*
|
|
225
|
-
* @
|
|
479
|
+
* Cleanup the migration if a migration fails or needs to be aborted.
|
|
480
|
+
* @param targetConnector The target connector to cleanup the migration with.
|
|
481
|
+
* @param options The options to control how the migration is cleaned up.
|
|
482
|
+
* @param loggingComponentType The optional component type to use for logging the migration progress.
|
|
483
|
+
* @returns A promise that resolves when the migration is cleaned up.
|
|
226
484
|
*/
|
|
227
|
-
|
|
228
|
-
|
|
485
|
+
async cleanupMigration(targetConnector, options, loggingComponentType) { }
|
|
486
|
+
/**
|
|
487
|
+
* Acquires the schema-keyed lock, runs fn with the current entity array, optionally
|
|
488
|
+
* writes back a modified array, then releases the lock.
|
|
489
|
+
* @param fn The synchronous function to run while the lock is held.
|
|
490
|
+
* @returns The result produced by fn.
|
|
491
|
+
* @internal
|
|
492
|
+
*/
|
|
493
|
+
async withLock(fn) {
|
|
494
|
+
await Mutex.lock(this._storageKey, { throwOnTimeout: true, timeoutMs: this._mutexTimeoutMs });
|
|
495
|
+
try {
|
|
496
|
+
await SharedObjectBuffer.create(this._storageKey, {
|
|
497
|
+
initialCapacityBytes: this._initialCapacityBytes,
|
|
498
|
+
maxCapacityBytes: this._maxCapacityBytes
|
|
499
|
+
});
|
|
500
|
+
const entities = (await SharedObjectBuffer.read(this._storageKey)) ?? [];
|
|
501
|
+
const outcome = fn(entities);
|
|
502
|
+
if (outcome.updated !== undefined) {
|
|
503
|
+
await SharedObjectBuffer.write(this._storageKey, outcome.updated);
|
|
504
|
+
}
|
|
505
|
+
return outcome.result;
|
|
506
|
+
}
|
|
507
|
+
finally {
|
|
508
|
+
Mutex.unlock(this._storageKey);
|
|
509
|
+
}
|
|
229
510
|
}
|
|
230
511
|
/**
|
|
231
|
-
* Find the item in the
|
|
512
|
+
* Find the item in the provided entity array.
|
|
513
|
+
* @param entities The current entity array.
|
|
232
514
|
* @param id The id to search for.
|
|
233
515
|
* @param secondaryIndex The secondary index to search for.
|
|
234
516
|
* @param conditions The optional conditions to match for the entities.
|
|
235
517
|
* @returns The index of the item if found or -1.
|
|
236
518
|
* @internal
|
|
237
519
|
*/
|
|
238
|
-
findItem(id, secondaryIndex, conditions) {
|
|
520
|
+
findItem(entities, id, secondaryIndex, conditions) {
|
|
239
521
|
const finalConditions = [];
|
|
240
522
|
if (!Is.empty(secondaryIndex)) {
|
|
241
523
|
finalConditions.push({
|
|
@@ -245,7 +527,6 @@ export class MemoryEntityStorageConnector {
|
|
|
245
527
|
});
|
|
246
528
|
}
|
|
247
529
|
if (Is.arrayValue(conditions)) {
|
|
248
|
-
// If we haven't added a secondary index condition we need to add the primary key condition.
|
|
249
530
|
if (finalConditions.length === 0) {
|
|
250
531
|
finalConditions.push({
|
|
251
532
|
property: this._primaryKey.property,
|
|
@@ -260,14 +541,14 @@ export class MemoryEntityStorageConnector {
|
|
|
260
541
|
})));
|
|
261
542
|
}
|
|
262
543
|
if (finalConditions.length > 0) {
|
|
263
|
-
for (let i = 0; i <
|
|
264
|
-
if (EntityConditions.check(
|
|
544
|
+
for (let i = 0; i < entities.length; i++) {
|
|
545
|
+
if (EntityConditions.check(entities[i], { conditions: finalConditions })) {
|
|
265
546
|
return i;
|
|
266
547
|
}
|
|
267
548
|
}
|
|
268
549
|
}
|
|
269
550
|
else {
|
|
270
|
-
return
|
|
551
|
+
return entities.findIndex(e => e[this._primaryKey.property] === id);
|
|
271
552
|
}
|
|
272
553
|
return -1;
|
|
273
554
|
}
|