@acodeninja/persist 3.3.1 → 3.3.2-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/docs/transactions.md +17 -1
- package/package.json +4 -1
- package/src/Connection.js +10 -2
- package/src/engine/storage/StorageEngine.js +25 -24
package/docs/transactions.md
CHANGED
|
@@ -15,8 +15,24 @@ export class Tag extends Persist.Model {
|
|
|
15
15
|
static tag = Persist.Property.String.required;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
const transaction = connection.
|
|
18
|
+
const transaction = connection.transaction();
|
|
19
19
|
|
|
20
20
|
await transaction.put(new Tag({tag: 'documentation'}));
|
|
21
21
|
await transaction.commit();
|
|
22
22
|
```
|
|
23
|
+
|
|
24
|
+
Nothing reaches storage until `commit()`. Reads inside a transaction go straight
|
|
25
|
+
to the engine, so they see the data as it was before the transaction started.
|
|
26
|
+
|
|
27
|
+
### Write ordering
|
|
28
|
+
|
|
29
|
+
Both `put` and `delete` order their writes so that a reader never sees an index
|
|
30
|
+
entry without the model it points at:
|
|
31
|
+
|
|
32
|
+
- `put` writes the model, then the indexes that list it. An index that has not
|
|
33
|
+
caught up hides a model that is stored, so it reads as missing.
|
|
34
|
+
- `delete` writes the indexes, then deletes the model. A model nothing lists is
|
|
35
|
+
unreachable, so it costs storage.
|
|
36
|
+
|
|
37
|
+
The inverse of either leaves an index pointing at a model that is not there,
|
|
38
|
+
which fails every read of that id.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@acodeninja/persist",
|
|
3
|
-
"version": "3.3.1",
|
|
3
|
+
"version": "3.3.2-next.1",
|
|
4
4
|
"description": "A JSON based data modelling and persistence module with alternate storage mechanisms.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -39,6 +39,9 @@
|
|
|
39
39
|
"@eslint/js": "^10.0.1",
|
|
40
40
|
"@jest/globals": "^30.4.1",
|
|
41
41
|
"@semantic-release/commit-analyzer": "^13.0.1",
|
|
42
|
+
"@semantic-release/github": "^12.0.10",
|
|
43
|
+
"@semantic-release/npm": "^13.2.0",
|
|
44
|
+
"@semantic-release/release-notes-generator": "^14.1.1",
|
|
42
45
|
"eslint": "^10.9.1",
|
|
43
46
|
"globals": "^17.11.0",
|
|
44
47
|
"husky": "^9.1.7",
|
package/src/Connection.js
CHANGED
|
@@ -290,8 +290,12 @@ export default class Connection {
|
|
|
290
290
|
|
|
291
291
|
await processModel(model);
|
|
292
292
|
|
|
293
|
+
// Models before the indexes that list them. Written the other way round,
|
|
294
|
+
// a reader that queries an index between the two writes finds an id whose
|
|
295
|
+
// model is not there yet, and a get on it fails.
|
|
296
|
+
await Promise.all(modelsToPut.map(m => this.#storage.putModel(m.toData())));
|
|
297
|
+
|
|
293
298
|
await Promise.all([
|
|
294
|
-
Promise.all(modelsToPut.map(m => this.#storage.putModel(m.toData()))),
|
|
295
299
|
Promise.all(Object.entries(modelsToReindex).map(async ([constructorName, models]) => {
|
|
296
300
|
const modelConstructor = this.#models.get(constructorName);
|
|
297
301
|
const index = await this.#storage.getIndex(modelConstructor);
|
|
@@ -446,9 +450,11 @@ export default class Connection {
|
|
|
446
450
|
state.updateIndex(indexName, index);
|
|
447
451
|
}
|
|
448
452
|
|
|
453
|
+
// Indexes before the models they stop listing. Deleted first, an index
|
|
454
|
+
// left pointing at a model that is gone breaks every read of it, where a
|
|
455
|
+
// model no index lists is merely unreachable.
|
|
449
456
|
await Promise.all([
|
|
450
457
|
Promise.all([...modelsToUpdate].map(id => this.#storage.putModel(state.modelCache.get(id).toData()))),
|
|
451
|
-
Promise.all([...modelsToDelete].map(id => this.#storage.deleteModel(id))),
|
|
452
458
|
Promise.all(state
|
|
453
459
|
.getTaintedIndexes()
|
|
454
460
|
.entries()
|
|
@@ -464,6 +470,8 @@ export default class Connection {
|
|
|
464
470
|
),
|
|
465
471
|
),
|
|
466
472
|
]);
|
|
473
|
+
|
|
474
|
+
await Promise.all([...modelsToDelete].map(id => this.#storage.deleteModel(id)));
|
|
467
475
|
}
|
|
468
476
|
|
|
469
477
|
/**
|
|
@@ -160,34 +160,35 @@ export class Operation {
|
|
|
160
160
|
}
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
const QUEUED_OPERATIONS = ['putModel', 'deleteModel', 'putIndex', 'putSearchIndex'];
|
|
164
|
+
|
|
163
165
|
/**
|
|
166
|
+
* Wrap a storage engine so that writes queue as operations and reads pass through.
|
|
164
167
|
*
|
|
165
|
-
* @param {Array<Operation>}
|
|
168
|
+
* @param {Array<Operation>} operations
|
|
166
169
|
* @param {StorageEngine} engine
|
|
167
170
|
* @return {StorageEngine}
|
|
168
171
|
*/
|
|
169
172
|
export function CreateTransactionalStorageEngine(operations, engine) {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
};
|
|
191
|
-
|
|
192
|
-
return transactionalEngine;
|
|
173
|
+
return new Proxy(engine, {
|
|
174
|
+
get(target, property) {
|
|
175
|
+
if (QUEUED_OPERATIONS.includes(property))
|
|
176
|
+
return (...args) => {
|
|
177
|
+
operations.push(new Operation(property, ...args));
|
|
178
|
+
return Promise.resolve();
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const value = Reflect.get(target, property, target);
|
|
182
|
+
|
|
183
|
+
// Methods are bound to the engine rather than to the wrapper. An
|
|
184
|
+
// engine holding its state in private class members cannot run its
|
|
185
|
+
// own methods with anything else as the receiver, and every read
|
|
186
|
+
// made through the transaction threw "Receiver must be an instance
|
|
187
|
+
// of". The class itself is left alone, because binding it renames it
|
|
188
|
+
// to "bound <Engine>" and the engine's errors quote that name.
|
|
189
|
+
return typeof value === 'function' && property !== 'constructor'
|
|
190
|
+
? value.bind(target)
|
|
191
|
+
: value;
|
|
192
|
+
},
|
|
193
|
+
});
|
|
193
194
|
}
|