s3db.js 10.0.18 → 11.0.0
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/s3db.cjs.js +606 -206
- package/dist/s3db.cjs.js.map +1 -1
- package/dist/s3db.d.ts +198 -2
- package/dist/s3db.es.js +606 -206
- package/dist/s3db.es.js.map +1 -1
- package/package.json +4 -2
- package/src/concerns/plugin-storage.js +443 -0
- package/src/database.class.js +48 -15
- package/src/plugins/audit.plugin.js +1 -1
- package/src/plugins/backup.plugin.js +1 -1
- package/src/plugins/cache.plugin.js +2 -6
- package/src/plugins/eventual-consistency/analytics.js +16 -4
- package/src/plugins/eventual-consistency/consolidation.js +18 -1
- package/src/plugins/eventual-consistency/index.js +4 -4
- package/src/plugins/eventual-consistency/{setup.js → install.js} +7 -6
- package/src/plugins/fulltext.plugin.js +3 -4
- package/src/plugins/metrics.plugin.js +10 -11
- package/src/plugins/plugin.class.js +79 -9
- package/src/plugins/queue-consumer.plugin.js +4 -3
- package/src/plugins/replicator.plugin.js +11 -13
- package/src/plugins/s3-queue.plugin.js +1 -1
- package/src/plugins/scheduler.plugin.js +8 -9
- package/src/plugins/state-machine.plugin.js +3 -4
- package/src/s3db.d.ts +198 -2
- package/src/plugins/eventual-consistency.plugin.js +0 -2559
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* @module eventual-consistency/
|
|
2
|
+
* Install logic for EventualConsistencyPlugin
|
|
3
|
+
* @module eventual-consistency/install
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import tryFn from "../../concerns/try-fn.js";
|
|
@@ -11,14 +11,14 @@ import { startConsolidationTimer } from "./consolidation.js";
|
|
|
11
11
|
import { startGarbageCollectionTimer } from "./garbage-collection.js";
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
14
|
+
* Install plugin for all configured resources
|
|
15
15
|
*
|
|
16
16
|
* @param {Object} database - Database instance
|
|
17
17
|
* @param {Map} fieldHandlers - Field handlers map
|
|
18
|
-
* @param {Function} completeFieldSetupFn - Function to complete setup for a field
|
|
18
|
+
* @param {Function} completeFieldSetupFn - Function to complete field setup for a field
|
|
19
19
|
* @param {Function} watchForResourceFn - Function to watch for resource creation
|
|
20
20
|
*/
|
|
21
|
-
export async function
|
|
21
|
+
export async function onInstall(database, fieldHandlers, completeFieldSetupFn, watchForResourceFn) {
|
|
22
22
|
// Iterate over all resource/field combinations
|
|
23
23
|
for (const [resourceName, resourceHandlers] of fieldHandlers) {
|
|
24
24
|
const targetResource = database.resources[resourceName];
|
|
@@ -70,7 +70,7 @@ export function watchForResource(resourceName, database, fieldHandlers, complete
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
/**
|
|
73
|
-
* Complete setup for a single field handler
|
|
73
|
+
* Complete field setup for a single field handler
|
|
74
74
|
*
|
|
75
75
|
* @param {Object} handler - Field handler
|
|
76
76
|
* @param {Object} database - Database instance
|
|
@@ -174,6 +174,7 @@ async function createAnalyticsResource(handler, database, resourceName, fieldNam
|
|
|
174
174
|
name: analyticsResourceName,
|
|
175
175
|
attributes: {
|
|
176
176
|
id: 'string|required',
|
|
177
|
+
field: 'string|required',
|
|
177
178
|
period: 'string|required',
|
|
178
179
|
cohort: 'string|required',
|
|
179
180
|
transactionCount: 'number|required',
|
|
@@ -13,11 +13,10 @@ export class FullTextPlugin extends Plugin {
|
|
|
13
13
|
this.indexes = new Map(); // In-memory index for simplicity
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
async
|
|
17
|
-
this.database = database;
|
|
16
|
+
async onInstall() {
|
|
18
17
|
|
|
19
18
|
// Create index resource if it doesn't exist
|
|
20
|
-
const [ok, err, indexResource] = await tryFn(() => database.createResource({
|
|
19
|
+
const [ok, err, indexResource] = await tryFn(() => this.database.createResource({
|
|
21
20
|
name: 'plg_fulltext_indexes',
|
|
22
21
|
attributes: {
|
|
23
22
|
id: 'string|required',
|
|
@@ -29,7 +28,7 @@ export class FullTextPlugin extends Plugin {
|
|
|
29
28
|
lastUpdated: 'string|required'
|
|
30
29
|
}
|
|
31
30
|
}));
|
|
32
|
-
this.indexResource = ok ? indexResource : database.resources.fulltext_indexes;
|
|
31
|
+
this.indexResource = ok ? indexResource : this.database.resources.fulltext_indexes;
|
|
33
32
|
|
|
34
33
|
// Load existing indexes
|
|
35
34
|
await this.loadIndexes();
|
|
@@ -31,12 +31,11 @@ export class MetricsPlugin extends Plugin {
|
|
|
31
31
|
this.flushTimer = null;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
async
|
|
35
|
-
this.database = database;
|
|
34
|
+
async onInstall() {
|
|
36
35
|
if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') return;
|
|
37
36
|
|
|
38
37
|
const [ok, err] = await tryFn(async () => {
|
|
39
|
-
const [ok1, err1, metricsResource] = await tryFn(() => database.createResource({
|
|
38
|
+
const [ok1, err1, metricsResource] = await tryFn(() => this.database.createResource({
|
|
40
39
|
name: 'plg_metrics',
|
|
41
40
|
attributes: {
|
|
42
41
|
id: 'string|required',
|
|
@@ -51,9 +50,9 @@ export class MetricsPlugin extends Plugin {
|
|
|
51
50
|
metadata: 'json'
|
|
52
51
|
}
|
|
53
52
|
}));
|
|
54
|
-
this.metricsResource = ok1 ? metricsResource : database.resources.plg_metrics;
|
|
53
|
+
this.metricsResource = ok1 ? metricsResource : this.database.resources.plg_metrics;
|
|
55
54
|
|
|
56
|
-
const [ok2, err2, errorsResource] = await tryFn(() => database.createResource({
|
|
55
|
+
const [ok2, err2, errorsResource] = await tryFn(() => this.database.createResource({
|
|
57
56
|
name: 'plg_error_logs',
|
|
58
57
|
attributes: {
|
|
59
58
|
id: 'string|required',
|
|
@@ -64,9 +63,9 @@ export class MetricsPlugin extends Plugin {
|
|
|
64
63
|
metadata: 'json'
|
|
65
64
|
}
|
|
66
65
|
}));
|
|
67
|
-
this.errorsResource = ok2 ? errorsResource : database.resources.plg_error_logs;
|
|
66
|
+
this.errorsResource = ok2 ? errorsResource : this.database.resources.plg_error_logs;
|
|
68
67
|
|
|
69
|
-
const [ok3, err3, performanceResource] = await tryFn(() => database.createResource({
|
|
68
|
+
const [ok3, err3, performanceResource] = await tryFn(() => this.database.createResource({
|
|
70
69
|
name: 'plg_performance_logs',
|
|
71
70
|
attributes: {
|
|
72
71
|
id: 'string|required',
|
|
@@ -77,13 +76,13 @@ export class MetricsPlugin extends Plugin {
|
|
|
77
76
|
metadata: 'json'
|
|
78
77
|
}
|
|
79
78
|
}));
|
|
80
|
-
this.performanceResource = ok3 ? performanceResource : database.resources.plg_performance_logs;
|
|
79
|
+
this.performanceResource = ok3 ? performanceResource : this.database.resources.plg_performance_logs;
|
|
81
80
|
});
|
|
82
81
|
if (!ok) {
|
|
83
82
|
// Resources might already exist
|
|
84
|
-
this.metricsResource = database.resources.plg_metrics;
|
|
85
|
-
this.errorsResource = database.resources.plg_error_logs;
|
|
86
|
-
this.performanceResource = database.resources.plg_performance_logs;
|
|
83
|
+
this.metricsResource = this.database.resources.plg_metrics;
|
|
84
|
+
this.errorsResource = this.database.resources.plg_error_logs;
|
|
85
|
+
this.performanceResource = this.database.resources.plg_performance_logs;
|
|
87
86
|
}
|
|
88
87
|
|
|
89
88
|
// Use database hooks for automatic resource discovery
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import EventEmitter from "events";
|
|
2
|
+
import { PluginStorage } from "../concerns/plugin-storage.js";
|
|
2
3
|
|
|
3
4
|
export class Plugin extends EventEmitter {
|
|
4
5
|
constructor(options = {}) {
|
|
@@ -6,13 +7,50 @@ export class Plugin extends EventEmitter {
|
|
|
6
7
|
this.name = this.constructor.name;
|
|
7
8
|
this.options = options;
|
|
8
9
|
this.hooks = new Map();
|
|
10
|
+
|
|
11
|
+
// Auto-generate slug from class name (CamelCase -> kebab-case)
|
|
12
|
+
// e.g., EventualConsistencyPlugin -> eventual-consistency-plugin
|
|
13
|
+
this.slug = options.slug || this._generateSlug();
|
|
14
|
+
|
|
15
|
+
// Storage instance (lazy-loaded)
|
|
16
|
+
this._storage = null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Generate kebab-case slug from class name
|
|
21
|
+
* @private
|
|
22
|
+
* @returns {string}
|
|
23
|
+
*/
|
|
24
|
+
_generateSlug() {
|
|
25
|
+
return this.name
|
|
26
|
+
.replace(/Plugin$/, '') // Remove "Plugin" suffix
|
|
27
|
+
.replace(/([a-z])([A-Z])/g, '$1-$2') // CamelCase -> kebab-case
|
|
28
|
+
.toLowerCase();
|
|
9
29
|
}
|
|
10
30
|
|
|
11
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Get PluginStorage instance (lazy-loaded)
|
|
33
|
+
* @returns {PluginStorage}
|
|
34
|
+
*/
|
|
35
|
+
getStorage() {
|
|
36
|
+
if (!this._storage) {
|
|
37
|
+
if (!this.database || !this.database.client) {
|
|
38
|
+
throw new Error('Plugin must be installed before accessing storage');
|
|
39
|
+
}
|
|
40
|
+
this._storage = new PluginStorage(this.database.client, this.slug);
|
|
41
|
+
}
|
|
42
|
+
return this._storage;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Install plugin
|
|
47
|
+
* @param {Database} database - Database instance
|
|
48
|
+
*/
|
|
49
|
+
async install(database) {
|
|
12
50
|
this.database = database;
|
|
13
|
-
this.
|
|
14
|
-
await this.
|
|
15
|
-
this.
|
|
51
|
+
this.beforeInstall();
|
|
52
|
+
await this.onInstall();
|
|
53
|
+
this.afterInstall();
|
|
16
54
|
}
|
|
17
55
|
|
|
18
56
|
async start() {
|
|
@@ -27,8 +65,28 @@ export class Plugin extends EventEmitter {
|
|
|
27
65
|
this.afterStop();
|
|
28
66
|
}
|
|
29
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Uninstall plugin and cleanup all data
|
|
70
|
+
* @param {Object} options - Uninstall options
|
|
71
|
+
* @param {boolean} options.purgeData - Delete all plugin data from S3 (default: false)
|
|
72
|
+
*/
|
|
73
|
+
async uninstall(options = {}) {
|
|
74
|
+
const { purgeData = false } = options;
|
|
75
|
+
|
|
76
|
+
this.beforeUninstall();
|
|
77
|
+
await this.onUninstall(options);
|
|
78
|
+
|
|
79
|
+
// Purge all plugin data if requested
|
|
80
|
+
if (purgeData && this._storage) {
|
|
81
|
+
const deleted = await this._storage.deleteAll();
|
|
82
|
+
this.emit('plugin.dataPurged', { deleted });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
this.afterUninstall();
|
|
86
|
+
}
|
|
87
|
+
|
|
30
88
|
// Override these methods in subclasses
|
|
31
|
-
async
|
|
89
|
+
async onInstall() {
|
|
32
90
|
// Override in subclasses
|
|
33
91
|
}
|
|
34
92
|
|
|
@@ -40,6 +98,10 @@ export class Plugin extends EventEmitter {
|
|
|
40
98
|
// Override in subclasses
|
|
41
99
|
}
|
|
42
100
|
|
|
101
|
+
async onUninstall(options) {
|
|
102
|
+
// Override in subclasses
|
|
103
|
+
}
|
|
104
|
+
|
|
43
105
|
// Hook management methods
|
|
44
106
|
addHook(resource, event, handler) {
|
|
45
107
|
if (!this.hooks.has(resource)) {
|
|
@@ -182,12 +244,12 @@ export class Plugin extends EventEmitter {
|
|
|
182
244
|
}
|
|
183
245
|
|
|
184
246
|
// Event emission methods
|
|
185
|
-
|
|
186
|
-
this.emit("plugin.
|
|
247
|
+
beforeInstall() {
|
|
248
|
+
this.emit("plugin.beforeInstall", new Date());
|
|
187
249
|
}
|
|
188
250
|
|
|
189
|
-
|
|
190
|
-
this.emit("plugin.
|
|
251
|
+
afterInstall() {
|
|
252
|
+
this.emit("plugin.afterInstall", new Date());
|
|
191
253
|
}
|
|
192
254
|
|
|
193
255
|
beforeStart() {
|
|
@@ -205,6 +267,14 @@ export class Plugin extends EventEmitter {
|
|
|
205
267
|
afterStop() {
|
|
206
268
|
this.emit("plugin.afterStop", new Date());
|
|
207
269
|
}
|
|
270
|
+
|
|
271
|
+
beforeUninstall() {
|
|
272
|
+
this.emit("plugin.beforeUninstall", new Date());
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
afterUninstall() {
|
|
276
|
+
this.emit("plugin.afterUninstall", new Date());
|
|
277
|
+
}
|
|
208
278
|
}
|
|
209
279
|
|
|
210
280
|
export default Plugin;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Plugin } from './plugin.class.js';
|
|
1
2
|
import { createConsumer } from './consumers/index.js';
|
|
2
3
|
import tryFn from "../concerns/try-fn.js";
|
|
3
4
|
|
|
@@ -20,16 +21,16 @@ import tryFn from "../concerns/try-fn.js";
|
|
|
20
21
|
// reconnectInterval: 2000,
|
|
21
22
|
// });
|
|
22
23
|
|
|
23
|
-
export class QueueConsumerPlugin {
|
|
24
|
+
export class QueueConsumerPlugin extends Plugin {
|
|
24
25
|
constructor(options = {}) {
|
|
26
|
+
super(options);
|
|
25
27
|
this.options = options;
|
|
26
28
|
// New pattern: consumers = [{ driver, config, consumers: [{ queueUrl, resources, ... }] }]
|
|
27
29
|
this.driversConfig = Array.isArray(options.consumers) ? options.consumers : [];
|
|
28
30
|
this.consumers = [];
|
|
29
31
|
}
|
|
30
32
|
|
|
31
|
-
async
|
|
32
|
-
this.database = database;
|
|
33
|
+
async onInstall() {
|
|
33
34
|
|
|
34
35
|
for (const driverDef of this.driversConfig) {
|
|
35
36
|
const { driver, config: driverConfig = {}, consumers: consumerDefs = [] } = driverDef;
|
|
@@ -236,12 +236,10 @@ export class ReplicatorPlugin extends Plugin {
|
|
|
236
236
|
this.eventListenersInstalled.add(resource.name);
|
|
237
237
|
}
|
|
238
238
|
|
|
239
|
-
async
|
|
240
|
-
this.database = database;
|
|
241
|
-
|
|
239
|
+
async onInstall() {
|
|
242
240
|
// Create replicator log resource if enabled
|
|
243
241
|
if (this.config.persistReplicatorLog) {
|
|
244
|
-
const [ok, err, logResource] = await tryFn(() => database.createResource({
|
|
242
|
+
const [ok, err, logResource] = await tryFn(() => this.database.createResource({
|
|
245
243
|
name: this.config.replicatorLogResource || 'plg_replicator_logs',
|
|
246
244
|
attributes: {
|
|
247
245
|
id: 'string|required',
|
|
@@ -253,24 +251,24 @@ export class ReplicatorPlugin extends Plugin {
|
|
|
253
251
|
},
|
|
254
252
|
behavior: 'truncate-data'
|
|
255
253
|
}));
|
|
256
|
-
|
|
254
|
+
|
|
257
255
|
if (ok) {
|
|
258
256
|
this.replicatorLogResource = logResource;
|
|
259
257
|
} else {
|
|
260
|
-
this.replicatorLogResource = database.resources[this.config.replicatorLogResource || 'plg_replicator_logs'];
|
|
258
|
+
this.replicatorLogResource = this.database.resources[this.config.replicatorLogResource || 'plg_replicator_logs'];
|
|
261
259
|
}
|
|
262
260
|
}
|
|
263
261
|
|
|
264
262
|
// Initialize replicators
|
|
265
|
-
await this.initializeReplicators(database);
|
|
266
|
-
|
|
263
|
+
await this.initializeReplicators(this.database);
|
|
264
|
+
|
|
267
265
|
// Use database hooks for automatic resource discovery
|
|
268
266
|
this.installDatabaseHooks();
|
|
269
|
-
|
|
267
|
+
|
|
270
268
|
// Install event listeners for existing resources
|
|
271
|
-
for (const resource of Object.values(database.resources)) {
|
|
269
|
+
for (const resource of Object.values(this.database.resources)) {
|
|
272
270
|
if (resource.name !== (this.config.replicatorLogResource || 'plg_replicator_logs')) {
|
|
273
|
-
this.installEventListeners(resource, database, this);
|
|
271
|
+
this.installEventListeners(resource, this.database, this);
|
|
274
272
|
}
|
|
275
273
|
}
|
|
276
274
|
}
|
|
@@ -334,8 +332,8 @@ export class ReplicatorPlugin extends Plugin {
|
|
|
334
332
|
}
|
|
335
333
|
|
|
336
334
|
async uploadMetadataFile(database) {
|
|
337
|
-
if (typeof database.uploadMetadataFile === 'function') {
|
|
338
|
-
await database.uploadMetadataFile();
|
|
335
|
+
if (typeof this.database.uploadMetadataFile === 'function') {
|
|
336
|
+
await this.database.uploadMetadataFile();
|
|
339
337
|
}
|
|
340
338
|
}
|
|
341
339
|
|
|
@@ -97,7 +97,7 @@ export class S3QueuePlugin extends Plugin {
|
|
|
97
97
|
this.lockCleanupInterval = null;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
async
|
|
100
|
+
async onInstall() {
|
|
101
101
|
// Get target resource
|
|
102
102
|
this.targetResource = this.database.resources[this.config.resource];
|
|
103
103
|
if (!this.targetResource) {
|
|
@@ -29,11 +29,11 @@ import { idGenerator } from "../concerns/id.js";
|
|
|
29
29
|
* schedule: '0 3 * * *',
|
|
30
30
|
* description: 'Clean up expired records',
|
|
31
31
|
* action: async (database, context) => {
|
|
32
|
-
* const expired = await database.resource('sessions')
|
|
32
|
+
* const expired = await this.database.resource('sessions')
|
|
33
33
|
* .list({ where: { expiresAt: { $lt: new Date() } } });
|
|
34
34
|
*
|
|
35
35
|
* for (const record of expired) {
|
|
36
|
-
* await database.resource('sessions').delete(record.id);
|
|
36
|
+
* await this.database.resource('sessions').delete(record.id);
|
|
37
37
|
* }
|
|
38
38
|
*
|
|
39
39
|
* return { deleted: expired.length };
|
|
@@ -48,8 +48,8 @@ import { idGenerator } from "../concerns/id.js";
|
|
|
48
48
|
* schedule: '0 9 * * MON',
|
|
49
49
|
* description: 'Generate weekly analytics report',
|
|
50
50
|
* action: async (database, context) => {
|
|
51
|
-
* const users = await database.resource('users').count();
|
|
52
|
-
* const orders = await database.resource('orders').count({
|
|
51
|
+
* const users = await this.database.resource('users').count();
|
|
52
|
+
* const orders = await this.database.resource('orders').count({
|
|
53
53
|
* where: {
|
|
54
54
|
* createdAt: {
|
|
55
55
|
* $gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
|
|
@@ -64,7 +64,7 @@ import { idGenerator } from "../concerns/id.js";
|
|
|
64
64
|
* createdAt: new Date().toISOString()
|
|
65
65
|
* };
|
|
66
66
|
*
|
|
67
|
-
* await database.resource('reports').insert(report);
|
|
67
|
+
* await this.database.resource('reports').insert(report);
|
|
68
68
|
* return report;
|
|
69
69
|
* }
|
|
70
70
|
* },
|
|
@@ -106,7 +106,7 @@ import { idGenerator } from "../concerns/id.js";
|
|
|
106
106
|
* const hourAgo = new Date(now.getTime() - 60 * 60 * 1000);
|
|
107
107
|
*
|
|
108
108
|
* // Aggregate metrics from the last hour
|
|
109
|
-
* const events = await database.resource('events').list({
|
|
109
|
+
* const events = await this.database.resource('events').list({
|
|
110
110
|
* where: {
|
|
111
111
|
* timestamp: {
|
|
112
112
|
* $gte: hourAgo.getTime(),
|
|
@@ -120,7 +120,7 @@ import { idGenerator } from "../concerns/id.js";
|
|
|
120
120
|
* return acc;
|
|
121
121
|
* }, {});
|
|
122
122
|
*
|
|
123
|
-
* await database.resource('hourly_metrics').insert({
|
|
123
|
+
* await this.database.resource('hourly_metrics').insert({
|
|
124
124
|
* hour: hourAgo.toISOString().slice(0, 13),
|
|
125
125
|
* metrics: aggregated,
|
|
126
126
|
* total: events.length,
|
|
@@ -217,8 +217,7 @@ export class SchedulerPlugin extends Plugin {
|
|
|
217
217
|
return true; // Simplified validation
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
-
async
|
|
221
|
-
this.database = database;
|
|
220
|
+
async onInstall() {
|
|
222
221
|
|
|
223
222
|
// Create lock resource for distributed locking
|
|
224
223
|
await this._createLockResource();
|
|
@@ -61,7 +61,7 @@ import tryFn from "../concerns/try-fn.js";
|
|
|
61
61
|
*
|
|
62
62
|
* actions: {
|
|
63
63
|
* onConfirmed: async (context, event, machine) => {
|
|
64
|
-
* await machine.database.resource('inventory').update(context.productId, {
|
|
64
|
+
* await machine.this.database.resource('inventory').update(context.productId, {
|
|
65
65
|
* quantity: { $decrement: context.quantity }
|
|
66
66
|
* });
|
|
67
67
|
* await machine.sendNotification(context.customerEmail, 'order_confirmed');
|
|
@@ -73,7 +73,7 @@ import tryFn from "../concerns/try-fn.js";
|
|
|
73
73
|
*
|
|
74
74
|
* guards: {
|
|
75
75
|
* canShip: async (context, event, machine) => {
|
|
76
|
-
* const inventory = await machine.database.resource('inventory').get(context.productId);
|
|
76
|
+
* const inventory = await machine.this.database.resource('inventory').get(context.productId);
|
|
77
77
|
* return inventory.quantity >= context.quantity;
|
|
78
78
|
* }
|
|
79
79
|
* },
|
|
@@ -138,8 +138,7 @@ export class StateMachinePlugin extends Plugin {
|
|
|
138
138
|
}
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
-
async
|
|
142
|
-
this.database = database;
|
|
141
|
+
async onInstall() {
|
|
143
142
|
|
|
144
143
|
// Create state storage resource if persistence is enabled
|
|
145
144
|
if (this.config.persistTransitions) {
|
package/src/s3db.d.ts
CHANGED
|
@@ -1069,11 +1069,207 @@ declare module 's3db.js' {
|
|
|
1069
1069
|
deleteBackup(backupId: string): Promise<void>;
|
|
1070
1070
|
}
|
|
1071
1071
|
|
|
1072
|
+
/** Eventual Consistency Plugin Config */
|
|
1073
|
+
export interface EventualConsistencyPluginConfig extends PluginConfig {
|
|
1074
|
+
/** Resource name to field names mapping (required) */
|
|
1075
|
+
resources: Record<string, string[]>;
|
|
1076
|
+
|
|
1077
|
+
/** Consolidation settings */
|
|
1078
|
+
consolidation?: {
|
|
1079
|
+
/** Consolidation mode: 'sync' or 'async' (default: 'async') */
|
|
1080
|
+
mode?: 'sync' | 'async';
|
|
1081
|
+
/** Consolidation interval in seconds (default: 300) */
|
|
1082
|
+
interval?: number;
|
|
1083
|
+
/** Consolidation concurrency (default: 5) */
|
|
1084
|
+
concurrency?: number;
|
|
1085
|
+
/** Consolidation window in hours (default: 24) */
|
|
1086
|
+
window?: number;
|
|
1087
|
+
/** Enable auto-consolidation (default: true) */
|
|
1088
|
+
auto?: boolean;
|
|
1089
|
+
};
|
|
1090
|
+
|
|
1091
|
+
/** Lock settings */
|
|
1092
|
+
locks?: {
|
|
1093
|
+
/** Lock timeout in seconds (default: 300) */
|
|
1094
|
+
timeout?: number;
|
|
1095
|
+
};
|
|
1096
|
+
|
|
1097
|
+
/** Garbage collection settings */
|
|
1098
|
+
garbageCollection?: {
|
|
1099
|
+
/** Transaction retention in days (default: 30) */
|
|
1100
|
+
retention?: number;
|
|
1101
|
+
/** GC interval in seconds (default: 86400) */
|
|
1102
|
+
interval?: number;
|
|
1103
|
+
};
|
|
1104
|
+
|
|
1105
|
+
/** Analytics settings */
|
|
1106
|
+
analytics?: {
|
|
1107
|
+
/** Enable analytics (default: false) */
|
|
1108
|
+
enabled?: boolean;
|
|
1109
|
+
/** Time periods to track (default: ['hour', 'day', 'month']) */
|
|
1110
|
+
periods?: Array<'hour' | 'day' | 'month'>;
|
|
1111
|
+
/** Metrics to track (default: ['count', 'sum', 'avg', 'min', 'max']) */
|
|
1112
|
+
metrics?: Array<'count' | 'sum' | 'avg' | 'min' | 'max'>;
|
|
1113
|
+
/** Rollup strategy (default: 'incremental') */
|
|
1114
|
+
rollupStrategy?: 'incremental' | 'full';
|
|
1115
|
+
/** Analytics retention in days (default: 365) */
|
|
1116
|
+
retentionDays?: number;
|
|
1117
|
+
};
|
|
1118
|
+
|
|
1119
|
+
/** Batch transaction settings */
|
|
1120
|
+
batch?: {
|
|
1121
|
+
/** Enable batch transactions (default: false) */
|
|
1122
|
+
enabled?: boolean;
|
|
1123
|
+
/** Batch size (default: 100) */
|
|
1124
|
+
size?: number;
|
|
1125
|
+
};
|
|
1126
|
+
|
|
1127
|
+
/** Late arrivals handling */
|
|
1128
|
+
lateArrivals?: {
|
|
1129
|
+
/** Strategy for late arrivals (default: 'warn') */
|
|
1130
|
+
strategy?: 'warn' | 'ignore' | 'error';
|
|
1131
|
+
};
|
|
1132
|
+
|
|
1133
|
+
/** Checkpoint settings */
|
|
1134
|
+
checkpoints?: {
|
|
1135
|
+
/** Enable checkpoints (default: true) */
|
|
1136
|
+
enabled?: boolean;
|
|
1137
|
+
/** Checkpoint strategy (default: 'hourly') */
|
|
1138
|
+
strategy?: 'hourly' | 'daily' | 'threshold';
|
|
1139
|
+
/** Checkpoint retention in days (default: 90) */
|
|
1140
|
+
retention?: number;
|
|
1141
|
+
/** Checkpoint threshold (default: 1000) */
|
|
1142
|
+
threshold?: number;
|
|
1143
|
+
/** Delete consolidated transactions (default: true) */
|
|
1144
|
+
deleteConsolidated?: boolean;
|
|
1145
|
+
/** Enable auto-checkpoint (default: true) */
|
|
1146
|
+
auto?: boolean;
|
|
1147
|
+
};
|
|
1148
|
+
|
|
1149
|
+
/** Cohort settings */
|
|
1150
|
+
cohort?: {
|
|
1151
|
+
/** Timezone for cohorts (default: UTC or TZ env var) */
|
|
1152
|
+
timezone?: string;
|
|
1153
|
+
};
|
|
1154
|
+
|
|
1155
|
+
/** Custom reducer function */
|
|
1156
|
+
reducer?: (transactions: any[]) => number;
|
|
1157
|
+
|
|
1158
|
+
/** Enable verbose logging (default: false) */
|
|
1159
|
+
verbose?: boolean;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
/** Analytics query options */
|
|
1163
|
+
export interface EventualConsistencyAnalyticsOptions {
|
|
1164
|
+
/** Period to query */
|
|
1165
|
+
period?: 'hour' | 'day' | 'month';
|
|
1166
|
+
/** Start date (YYYY-MM-DD or YYYY-MM-DD HH:00) */
|
|
1167
|
+
startDate?: string;
|
|
1168
|
+
/** End date (YYYY-MM-DD or YYYY-MM-DD HH:00) */
|
|
1169
|
+
endDate?: string;
|
|
1170
|
+
/** Single date (YYYY-MM-DD) */
|
|
1171
|
+
date?: string;
|
|
1172
|
+
/** Operation breakdown */
|
|
1173
|
+
breakdown?: 'operations';
|
|
1174
|
+
/** Fill gaps with zeros for continuous data (default: false) */
|
|
1175
|
+
fillGaps?: boolean;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
/** Top records query options */
|
|
1179
|
+
export interface EventualConsistencyTopRecordsOptions {
|
|
1180
|
+
/** Period to query */
|
|
1181
|
+
period?: 'hour' | 'day' | 'month';
|
|
1182
|
+
/** Date for the query */
|
|
1183
|
+
date?: string;
|
|
1184
|
+
/** Metric to sort by: 'transactionCount' or 'totalValue' */
|
|
1185
|
+
metric?: 'transactionCount' | 'totalValue';
|
|
1186
|
+
/** Limit results (default: 10) */
|
|
1187
|
+
limit?: number;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
/** Analytics result */
|
|
1191
|
+
export interface EventualConsistencyAnalyticsResult {
|
|
1192
|
+
/** Cohort identifier (date/hour/month string) */
|
|
1193
|
+
cohort: string;
|
|
1194
|
+
/** Number of transactions */
|
|
1195
|
+
count: number;
|
|
1196
|
+
/** Sum of values */
|
|
1197
|
+
sum: number;
|
|
1198
|
+
/** Average value */
|
|
1199
|
+
avg: number;
|
|
1200
|
+
/** Minimum value */
|
|
1201
|
+
min: number;
|
|
1202
|
+
/** Maximum value */
|
|
1203
|
+
max: number;
|
|
1204
|
+
/** Number of distinct records */
|
|
1205
|
+
recordCount: number;
|
|
1206
|
+
/** Breakdown by operation (if requested) */
|
|
1207
|
+
add?: { count: number; sum: number };
|
|
1208
|
+
sub?: { count: number; sum: number };
|
|
1209
|
+
set?: { count: number; sum: number };
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
/** Top record result */
|
|
1213
|
+
export interface EventualConsistencyTopRecordResult {
|
|
1214
|
+
/** Record ID */
|
|
1215
|
+
recordId: string;
|
|
1216
|
+
/** Number of transactions or total value */
|
|
1217
|
+
count: number;
|
|
1218
|
+
/** Total value */
|
|
1219
|
+
sum: number;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
/** Cohort information */
|
|
1223
|
+
export interface EventualConsistencyCohortInfo {
|
|
1224
|
+
/** Date in YYYY-MM-DD format */
|
|
1225
|
+
date: string;
|
|
1226
|
+
/** Hour in YYYY-MM-DD HH:00 format */
|
|
1227
|
+
hour: string;
|
|
1228
|
+
/** Month in YYYY-MM format */
|
|
1229
|
+
month: string;
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1072
1232
|
/** Eventual Consistency Plugin */
|
|
1073
1233
|
export class EventualConsistencyPlugin extends Plugin {
|
|
1074
|
-
constructor(config
|
|
1234
|
+
constructor(config: EventualConsistencyPluginConfig);
|
|
1235
|
+
|
|
1236
|
+
// Lifecycle methods
|
|
1075
1237
|
setup(database: Database): Promise<void>;
|
|
1076
|
-
|
|
1238
|
+
|
|
1239
|
+
// Analytics methods
|
|
1240
|
+
getAnalytics(resourceName: string, field: string, options?: EventualConsistencyAnalyticsOptions): Promise<EventualConsistencyAnalyticsResult[]>;
|
|
1241
|
+
getMonthByDay(resourceName: string, field: string, month: string, options?: EventualConsistencyAnalyticsOptions): Promise<EventualConsistencyAnalyticsResult[]>;
|
|
1242
|
+
getDayByHour(resourceName: string, field: string, date: string, options?: EventualConsistencyAnalyticsOptions): Promise<EventualConsistencyAnalyticsResult[]>;
|
|
1243
|
+
getLastNDays(resourceName: string, field: string, days?: number, options?: EventualConsistencyAnalyticsOptions): Promise<EventualConsistencyAnalyticsResult[]>;
|
|
1244
|
+
getYearByMonth(resourceName: string, field: string, year: number, options?: EventualConsistencyAnalyticsOptions): Promise<EventualConsistencyAnalyticsResult[]>;
|
|
1245
|
+
getMonthByHour(resourceName: string, field: string, month: string, options?: EventualConsistencyAnalyticsOptions): Promise<EventualConsistencyAnalyticsResult[]>;
|
|
1246
|
+
getTopRecords(resourceName: string, field: string, options?: EventualConsistencyTopRecordsOptions): Promise<EventualConsistencyTopRecordResult[]>;
|
|
1247
|
+
|
|
1248
|
+
// Utility methods
|
|
1249
|
+
getCohortInfo(date: Date): EventualConsistencyCohortInfo;
|
|
1250
|
+
createPartitionConfig(): any;
|
|
1251
|
+
createTransaction(handler: any, data: any): Promise<any>;
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
/** Resource extensions added by EventualConsistencyPlugin */
|
|
1255
|
+
export interface EventualConsistencyResourceExtensions {
|
|
1256
|
+
/** Set field value (replaces current value) */
|
|
1257
|
+
set(id: string, field: string, value: number): Promise<number>;
|
|
1258
|
+
|
|
1259
|
+
/** Increment field value */
|
|
1260
|
+
add(id: string, field: string, amount: number): Promise<number>;
|
|
1261
|
+
|
|
1262
|
+
/** Decrement field value */
|
|
1263
|
+
sub(id: string, field: string, amount: number): Promise<number>;
|
|
1264
|
+
|
|
1265
|
+
/** Manually trigger consolidation */
|
|
1266
|
+
consolidate(id: string, field: string): Promise<number>;
|
|
1267
|
+
|
|
1268
|
+
/** Get consolidated value without applying */
|
|
1269
|
+
getConsolidatedValue(id: string, field: string, options?: any): Promise<number>;
|
|
1270
|
+
|
|
1271
|
+
/** Recalculate from scratch (rebuilds from transaction log) */
|
|
1272
|
+
recalculate(id: string, field: string): Promise<number>;
|
|
1077
1273
|
}
|
|
1078
1274
|
|
|
1079
1275
|
/** Scheduler Plugin */
|