are-engine-core 1.0.0 → 1.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/README.md +86 -87
- package/dist/are-core.js +15 -15
- package/dist/are-core.mjs +358 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,40 +1,40 @@
|
|
|
1
|
-
# are-core — Action Rule Event Engine
|
|
1
|
+
# are-engine-core — Action Rule Event Engine
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Zero dependency, lightweight event-rule-action engine.
|
|
4
4
|
|
|
5
|
-
Browser, Node.js, React, Vue, React Native, Electron —
|
|
5
|
+
Browser, Node.js, React, Vue, React Native, Electron — works in any JS/TS environment.
|
|
6
6
|
|
|
7
|
-
>
|
|
8
|
-
>
|
|
7
|
+
> This package is the JavaScript/TypeScript port of the C# [ARE.Core](../ARE.Core/) engine.
|
|
8
|
+
> Same architecture, same API, same behavior.
|
|
9
9
|
|
|
10
10
|
---
|
|
11
11
|
|
|
12
|
-
##
|
|
12
|
+
## Installation
|
|
13
13
|
|
|
14
14
|
```bash
|
|
15
|
-
npm install are-core
|
|
15
|
+
npm install are-engine-core
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
No build step required. TypeScript type definitions are included out of the box.
|
|
19
19
|
|
|
20
20
|
---
|
|
21
21
|
|
|
22
|
-
##
|
|
22
|
+
## Quick Start
|
|
23
23
|
|
|
24
24
|
```javascript
|
|
25
|
-
const { AreEngine, Rule } = require('are-core');
|
|
26
|
-
//
|
|
27
|
-
// import { AreEngine, Rule } from 'are-core';
|
|
25
|
+
const { AreEngine, Rule } = require('are-engine-core');
|
|
26
|
+
// or
|
|
27
|
+
// import { AreEngine, Rule } from 'are-engine-core';
|
|
28
28
|
|
|
29
|
-
// 1)
|
|
29
|
+
// 1) Create the engine
|
|
30
30
|
const engine = new AreEngine();
|
|
31
31
|
|
|
32
|
-
// 2)
|
|
32
|
+
// 2) Register an action
|
|
33
33
|
engine.registerAction('send_email', async (ctx, s) => {
|
|
34
|
-
console.log('Email
|
|
34
|
+
console.log('Email sent:', s.get('template'));
|
|
35
35
|
});
|
|
36
36
|
|
|
37
|
-
// 3)
|
|
37
|
+
// 3) Define a rule
|
|
38
38
|
engine.addRule(
|
|
39
39
|
Rule.create('vip_order')
|
|
40
40
|
.on('order.created')
|
|
@@ -42,23 +42,23 @@ engine.addRule(
|
|
|
42
42
|
.then('send_email', s => s.set('template', 'vip_welcome'))
|
|
43
43
|
);
|
|
44
44
|
|
|
45
|
-
// 4)
|
|
45
|
+
// 4) Fire an event
|
|
46
46
|
await engine.fire('order.created', e => e.set('total', 7500));
|
|
47
|
-
//
|
|
47
|
+
// Output: Email sent: vip_welcome
|
|
48
48
|
```
|
|
49
49
|
|
|
50
50
|
---
|
|
51
51
|
|
|
52
|
-
##
|
|
52
|
+
## Detailed Usage
|
|
53
53
|
|
|
54
|
-
### Action
|
|
54
|
+
### Defining an Action — Object-Based
|
|
55
55
|
|
|
56
56
|
```javascript
|
|
57
57
|
const damageAction = {
|
|
58
58
|
actionType: 'damage',
|
|
59
59
|
execute: async (ctx, settings) => {
|
|
60
60
|
const amount = settings.get('amount');
|
|
61
|
-
console.log(`${amount}
|
|
61
|
+
console.log(`${amount} damage dealt!`);
|
|
62
62
|
ctx.set('lastDamage', amount);
|
|
63
63
|
}
|
|
64
64
|
};
|
|
@@ -66,7 +66,7 @@ const damageAction = {
|
|
|
66
66
|
engine.registerAction(damageAction);
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
-
### Action
|
|
69
|
+
### Defining an Action — Inline
|
|
70
70
|
|
|
71
71
|
```javascript
|
|
72
72
|
engine.registerAction('log', async (ctx, s) => {
|
|
@@ -74,7 +74,7 @@ engine.registerAction('log', async (ctx, s) => {
|
|
|
74
74
|
});
|
|
75
75
|
```
|
|
76
76
|
|
|
77
|
-
###
|
|
77
|
+
### Defining a Rule — Fluent Builder
|
|
78
78
|
|
|
79
79
|
```javascript
|
|
80
80
|
engine.addRule(
|
|
@@ -90,63 +90,63 @@ engine.addRule(
|
|
|
90
90
|
);
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
-
###
|
|
93
|
+
### Listening to Multiple Events
|
|
94
94
|
|
|
95
95
|
```javascript
|
|
96
96
|
Rule.create('license_warning')
|
|
97
97
|
.on('app.started', 'license.checked')
|
|
98
98
|
.when('expiring', (evt) => (evt.data.days_remaining ?? 999) <= 7)
|
|
99
99
|
.then('show_notification', s => s
|
|
100
|
-
.set('title', '
|
|
101
|
-
.set('message', '7
|
|
100
|
+
.set('title', 'License Warning')
|
|
101
|
+
.set('message', '7 days remaining!'))
|
|
102
102
|
```
|
|
103
103
|
|
|
104
|
-
###
|
|
104
|
+
### Condition Types
|
|
105
105
|
|
|
106
106
|
```javascript
|
|
107
|
-
//
|
|
107
|
+
// Field comparison (declarative)
|
|
108
108
|
.whenEquals('status', 'active')
|
|
109
109
|
.whenGreaterThan('score', 100)
|
|
110
110
|
.whenLessThan('stock', 10)
|
|
111
111
|
.whenField('category', CompareOp.Contains, 'premium')
|
|
112
112
|
.whenField('role', CompareOp.In, ['admin', 'moderator'])
|
|
113
113
|
|
|
114
|
-
// Lambda (
|
|
114
|
+
// Lambda (flexible)
|
|
115
115
|
.when('custom_check', (evt, ctx) => {
|
|
116
116
|
return evt.data.total > 1000 && ctx.get('user_type') === 'vip';
|
|
117
117
|
})
|
|
118
118
|
```
|
|
119
119
|
|
|
120
|
-
### MatchMode —
|
|
120
|
+
### MatchMode — Condition Matching Modes
|
|
121
121
|
|
|
122
122
|
```javascript
|
|
123
|
-
const { MatchMode } = require('are-core');
|
|
123
|
+
const { MatchMode } = require('are-engine-core');
|
|
124
124
|
|
|
125
|
-
//
|
|
125
|
+
// All conditions must be true (AND) — default
|
|
126
126
|
.withMatchMode(MatchMode.All)
|
|
127
127
|
|
|
128
|
-
//
|
|
128
|
+
// At least one condition must be true (OR)
|
|
129
129
|
.withMatchMode(MatchMode.Any)
|
|
130
130
|
|
|
131
|
-
//
|
|
131
|
+
// No conditions should be true (NOT)
|
|
132
132
|
.withMatchMode(MatchMode.None)
|
|
133
133
|
|
|
134
|
-
//
|
|
134
|
+
// Exactly one condition must be true
|
|
135
135
|
.withMatchMode(MatchMode.ExactlyOne)
|
|
136
136
|
```
|
|
137
137
|
|
|
138
138
|
### Middleware
|
|
139
139
|
|
|
140
140
|
```javascript
|
|
141
|
-
//
|
|
141
|
+
// Logging middleware
|
|
142
142
|
engine.use(0, async (ctx, next) => {
|
|
143
|
-
console.log('Event
|
|
143
|
+
console.log('Event started:', ctx.currentEvent.eventType);
|
|
144
144
|
const start = Date.now();
|
|
145
145
|
await next();
|
|
146
|
-
console.log('Event
|
|
146
|
+
console.log('Event completed:', (Date.now() - start) + 'ms');
|
|
147
147
|
});
|
|
148
148
|
|
|
149
|
-
// Auth
|
|
149
|
+
// Auth middleware
|
|
150
150
|
engine.use(-10, async (ctx, next) => {
|
|
151
151
|
if (!ctx.get('isAuthenticated')) {
|
|
152
152
|
ctx.stopPipeline = true;
|
|
@@ -156,27 +156,27 @@ engine.use(-10, async (ctx, next) => {
|
|
|
156
156
|
});
|
|
157
157
|
```
|
|
158
158
|
|
|
159
|
-
###
|
|
159
|
+
### Direct Listener (Without Rules)
|
|
160
160
|
|
|
161
161
|
```javascript
|
|
162
162
|
engine.on('order.created', async (evt, ctx) => {
|
|
163
|
-
console.log('
|
|
163
|
+
console.log('Order received:', evt.data.order_id);
|
|
164
164
|
});
|
|
165
165
|
```
|
|
166
166
|
|
|
167
|
-
###
|
|
167
|
+
### Dynamic Rule Management
|
|
168
168
|
|
|
169
169
|
```javascript
|
|
170
|
-
//
|
|
170
|
+
// Individual rules
|
|
171
171
|
engine.disableRule('seasonal_discount');
|
|
172
172
|
engine.enableRule('seasonal_discount');
|
|
173
173
|
engine.removeRule('old_rule');
|
|
174
174
|
|
|
175
|
-
//
|
|
175
|
+
// Entire groups
|
|
176
176
|
engine.disableGroup('marketing');
|
|
177
177
|
engine.enableGroup('marketing');
|
|
178
178
|
|
|
179
|
-
//
|
|
179
|
+
// Add a new rule at runtime
|
|
180
180
|
engine.addRule(
|
|
181
181
|
Rule.create('flash_sale')
|
|
182
182
|
.inGroup('marketing')
|
|
@@ -186,17 +186,17 @@ engine.addRule(
|
|
|
186
186
|
);
|
|
187
187
|
```
|
|
188
188
|
|
|
189
|
-
###
|
|
189
|
+
### Flow Control
|
|
190
190
|
|
|
191
191
|
```javascript
|
|
192
|
-
//
|
|
192
|
+
// Stop the entire pipeline (remaining rules will not execute)
|
|
193
193
|
engine.registerAction('validate', async (ctx) => {
|
|
194
194
|
if (!ctx.currentEvent.data.valid) {
|
|
195
195
|
ctx.stopPipeline = true;
|
|
196
196
|
}
|
|
197
197
|
});
|
|
198
198
|
|
|
199
|
-
//
|
|
199
|
+
// Skip only the remaining actions of the current rule
|
|
200
200
|
engine.registerAction('conditional_skip', async (ctx) => {
|
|
201
201
|
if (someCondition) {
|
|
202
202
|
ctx.skipRemainingActions = true;
|
|
@@ -204,7 +204,7 @@ engine.registerAction('conditional_skip', async (ctx) => {
|
|
|
204
204
|
});
|
|
205
205
|
```
|
|
206
206
|
|
|
207
|
-
### Context —
|
|
207
|
+
### Context — Sharing Data Between Actions
|
|
208
208
|
|
|
209
209
|
```javascript
|
|
210
210
|
engine.registerAction('calculate', async (ctx) => {
|
|
@@ -216,35 +216,35 @@ engine.registerAction('apply_tax', async (ctx) => {
|
|
|
216
216
|
ctx.set('totalWithTax', total * 1.18);
|
|
217
217
|
});
|
|
218
218
|
|
|
219
|
-
//
|
|
219
|
+
// When both run sequentially in the same event, they share data via context
|
|
220
220
|
```
|
|
221
221
|
|
|
222
|
-
###
|
|
222
|
+
### Reading Results
|
|
223
223
|
|
|
224
224
|
```javascript
|
|
225
225
|
const result = await engine.fire('order.created', e => e.set('total', 7500));
|
|
226
226
|
|
|
227
|
-
console.log('
|
|
228
|
-
console.log('
|
|
229
|
-
console.log('Pipeline
|
|
230
|
-
console.log('
|
|
227
|
+
console.log('Fired:', result.firedRules.length);
|
|
228
|
+
console.log('Skipped:', result.skippedRules.length);
|
|
229
|
+
console.log('Pipeline stopped:', result.pipelineStopped);
|
|
230
|
+
console.log('Duration:', result.duration + 'ms');
|
|
231
231
|
|
|
232
232
|
result.firedRules.forEach(r => {
|
|
233
233
|
console.log(` ${r.ruleId} → ${r.executedActions.join(', ')}`);
|
|
234
234
|
});
|
|
235
235
|
|
|
236
236
|
result.skippedRules.forEach(r => {
|
|
237
|
-
console.log(` ${r.ruleId} →
|
|
237
|
+
console.log(` ${r.ruleId} → failed: ${r.failedConditions.join(', ')}`);
|
|
238
238
|
});
|
|
239
239
|
```
|
|
240
240
|
|
|
241
241
|
---
|
|
242
242
|
|
|
243
|
-
## React
|
|
243
|
+
## React Usage
|
|
244
244
|
|
|
245
245
|
```jsx
|
|
246
|
-
import { AreEngine, Rule, GameEvent, AreContext } from 'are-core';
|
|
247
|
-
import { useRef
|
|
246
|
+
import { AreEngine, Rule, GameEvent, AreContext } from 'are-engine-core';
|
|
247
|
+
import { useRef } from 'react';
|
|
248
248
|
|
|
249
249
|
function useAreEngine(setup) {
|
|
250
250
|
const engineRef = useRef(null);
|
|
@@ -263,7 +263,7 @@ function useAreEngine(setup) {
|
|
|
263
263
|
return { fire, engine: engineRef.current };
|
|
264
264
|
}
|
|
265
265
|
|
|
266
|
-
//
|
|
266
|
+
// Usage
|
|
267
267
|
function App() {
|
|
268
268
|
const { fire } = useAreEngine((engine) => {
|
|
269
269
|
engine.registerAction('toast', async (ctx, s) => {
|
|
@@ -274,45 +274,45 @@ function App() {
|
|
|
274
274
|
Rule.create('big_order')
|
|
275
275
|
.on('cart.checkout')
|
|
276
276
|
.whenGreaterThan('total', 500)
|
|
277
|
-
.then('toast', s => s.set('message', '
|
|
277
|
+
.then('toast', s => s.set('message', 'Free shipping!'))
|
|
278
278
|
);
|
|
279
279
|
});
|
|
280
280
|
|
|
281
|
-
return <button onClick={() => fire('cart.checkout', { total: 700 })}
|
|
281
|
+
return <button onClick={() => fire('cart.checkout', { total: 700 })}>Checkout</button>;
|
|
282
282
|
}
|
|
283
283
|
```
|
|
284
284
|
|
|
285
285
|
---
|
|
286
286
|
|
|
287
|
-
## Export
|
|
287
|
+
## Export List
|
|
288
288
|
|
|
289
289
|
```javascript
|
|
290
290
|
const {
|
|
291
|
-
AreEngine, //
|
|
292
|
-
AreContext, //
|
|
293
|
-
GameEvent, //
|
|
294
|
-
Rule, // Fluent
|
|
295
|
-
ActionSettings, // Action
|
|
296
|
-
FieldCondition, //
|
|
291
|
+
AreEngine, // Core engine
|
|
292
|
+
AreContext, // Shared data bag
|
|
293
|
+
GameEvent, // Default event implementation
|
|
294
|
+
Rule, // Fluent rule builder
|
|
295
|
+
ActionSettings, // Action parameters
|
|
296
|
+
FieldCondition, // Field comparison condition
|
|
297
297
|
MatchMode, // All, Any, None, ExactlyOne
|
|
298
|
-
CompareOp, // Equal, GreaterThan, Contains, In
|
|
299
|
-
} = require('are-core');
|
|
298
|
+
CompareOp, // Equal, GreaterThan, Contains, In, etc.
|
|
299
|
+
} = require('are-engine-core');
|
|
300
300
|
```
|
|
301
301
|
|
|
302
302
|
---
|
|
303
303
|
|
|
304
|
-
## TypeScript
|
|
304
|
+
## TypeScript Support
|
|
305
305
|
|
|
306
|
-
|
|
306
|
+
Type definitions (`are-core.d.ts`) are included in the package. No additional setup required.
|
|
307
307
|
|
|
308
308
|
```typescript
|
|
309
|
-
import { AreEngine, Rule, IAction, IEvent, AreContext, ActionSettings } from 'are-core';
|
|
309
|
+
import { AreEngine, Rule, IAction, IEvent, AreContext, ActionSettings } from 'are-engine-core';
|
|
310
310
|
|
|
311
|
-
//
|
|
311
|
+
// Define a custom action type
|
|
312
312
|
const myAction: IAction = {
|
|
313
313
|
actionType: 'my_action',
|
|
314
|
-
execute: async (ctx: AreContext, settings: ActionSettings): Promise => {
|
|
315
|
-
const value = settings.get('key');
|
|
314
|
+
execute: async (ctx: AreContext, settings: ActionSettings): Promise<void> => {
|
|
315
|
+
const value = settings.get<string>('key');
|
|
316
316
|
ctx.set('result', value);
|
|
317
317
|
}
|
|
318
318
|
};
|
|
@@ -320,34 +320,33 @@ const myAction: IAction = {
|
|
|
320
320
|
|
|
321
321
|
---
|
|
322
322
|
|
|
323
|
-
##
|
|
323
|
+
## Import Patterns
|
|
324
324
|
|
|
325
325
|
```javascript
|
|
326
326
|
// CommonJS (Node.js, Electron)
|
|
327
|
-
const { AreEngine, Rule } = require('are-core');
|
|
327
|
+
const { AreEngine, Rule } = require('are-engine-core');
|
|
328
328
|
|
|
329
329
|
// ES Module (React, Vue, Angular, Vite, Next.js)
|
|
330
|
-
import { AreEngine, Rule } from 'are-core';
|
|
330
|
+
import { AreEngine, Rule } from 'are-engine-core';
|
|
331
331
|
|
|
332
332
|
// Script tag (browser - global)
|
|
333
|
-
// dist/are-core.js
|
|
334
|
-
|
|
333
|
+
// You can use the dist/are-core.js file directly
|
|
335
334
|
```
|
|
336
335
|
|
|
337
336
|
---
|
|
338
337
|
|
|
339
|
-
##
|
|
338
|
+
## Tests
|
|
340
339
|
|
|
341
340
|
```bash
|
|
342
341
|
npm test
|
|
343
|
-
#
|
|
342
|
+
# or
|
|
344
343
|
node test/test.js
|
|
345
344
|
```
|
|
346
345
|
|
|
347
|
-
17
|
|
346
|
+
17 tests covering: engine, conditions, MatchMode, middleware, pipeline control, group management, and context sharing.
|
|
348
347
|
|
|
349
348
|
---
|
|
350
349
|
|
|
351
|
-
##
|
|
350
|
+
## License
|
|
352
351
|
|
|
353
|
-
MIT
|
|
352
|
+
MIT
|
package/dist/are-core.js
CHANGED
|
@@ -165,7 +165,7 @@ class AreEngine {
|
|
|
165
165
|
this.onLog = null;
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
-
// --
|
|
168
|
+
// -- Registration --
|
|
169
169
|
|
|
170
170
|
registerAction(actionTypeOrObj, handler) {
|
|
171
171
|
if (typeof actionTypeOrObj === 'string') {
|
|
@@ -195,7 +195,7 @@ class AreEngine {
|
|
|
195
195
|
return this;
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
-
// --
|
|
198
|
+
// -- Rule Management --
|
|
199
199
|
|
|
200
200
|
enableRule(id) { var r = this._rules.find(function (r) { return r.ruleId === id; }); if (r) r.isEnabled = true; return this; }
|
|
201
201
|
disableRule(id) { var r = this._rules.find(function (r) { return r.ruleId === id; }); if (r) r.isEnabled = false; return this; }
|
|
@@ -239,23 +239,23 @@ class AreEngine {
|
|
|
239
239
|
var self = this;
|
|
240
240
|
|
|
241
241
|
var coreProcess = async function () {
|
|
242
|
-
// 1)
|
|
242
|
+
// 1) Direct listeners
|
|
243
243
|
var listeners = self._listeners.get(evt.eventType) || [];
|
|
244
244
|
for (var i = 0; i < listeners.length; i++) {
|
|
245
245
|
if (ctx.stopPipeline) break;
|
|
246
246
|
await listeners[i](evt, ctx);
|
|
247
247
|
}
|
|
248
248
|
|
|
249
|
-
// 2)
|
|
249
|
+
// 2) Matching rules (by priority)
|
|
250
250
|
var matching = self._rules
|
|
251
251
|
.filter(function (r) { return r.isEnabled && r.eventTypes.indexOf(evt.eventType) !== -1; })
|
|
252
252
|
.sort(function (a, b) { return b.priority - a.priority; });
|
|
253
253
|
|
|
254
|
-
self._log('[ARE] Event \'' + evt.eventType + '\' → ' + matching.length + '
|
|
254
|
+
self._log('[ARE] Event \'' + evt.eventType + '\' → ' + matching.length + ' candidate rules');
|
|
255
255
|
|
|
256
256
|
for (var j = 0; j < matching.length; j++) {
|
|
257
257
|
if (ctx.stopPipeline) {
|
|
258
|
-
self._log('[ARE] Pipeline
|
|
258
|
+
self._log('[ARE] Pipeline stopped');
|
|
259
259
|
break;
|
|
260
260
|
}
|
|
261
261
|
var rule = matching[j];
|
|
@@ -268,7 +268,7 @@ class AreEngine {
|
|
|
268
268
|
}
|
|
269
269
|
};
|
|
270
270
|
|
|
271
|
-
// Middleware
|
|
271
|
+
// Middleware chain
|
|
272
272
|
var pipeline = coreProcess;
|
|
273
273
|
for (var i = this._middlewares.length - 1; i >= 0; i--) {
|
|
274
274
|
(function (mw, next) {
|
|
@@ -280,21 +280,21 @@ class AreEngine {
|
|
|
280
280
|
|
|
281
281
|
result.pipelineStopped = ctx.stopPipeline;
|
|
282
282
|
result.duration = Date.now() - start;
|
|
283
|
-
this._log('[ARE] Event \'' + evt.eventType + '\'
|
|
284
|
-
result.firedRules.length + '
|
|
285
|
-
result.skippedRules.length + '
|
|
283
|
+
this._log('[ARE] Event \'' + evt.eventType + '\' completed: ' +
|
|
284
|
+
result.firedRules.length + ' fired, ' +
|
|
285
|
+
result.skippedRules.length + ' skipped, ' +
|
|
286
286
|
result.duration + 'ms');
|
|
287
287
|
return result;
|
|
288
288
|
}
|
|
289
289
|
|
|
290
|
-
// --
|
|
290
|
+
// -- Internal --
|
|
291
291
|
|
|
292
292
|
async _evaluateAndExecute(rule, evt, ctx) {
|
|
293
293
|
var failedConditions = [];
|
|
294
294
|
var conditionsMet = this._evaluateConditions(rule, evt, ctx, failedConditions);
|
|
295
295
|
|
|
296
296
|
if (!conditionsMet) {
|
|
297
|
-
this._log('[ARE]
|
|
297
|
+
this._log('[ARE] Rule \'' + rule.ruleId + '\' → conditions not met [' + failedConditions.join(', ') + ']');
|
|
298
298
|
return { ruleId: rule.ruleId, conditionsMet: false, executedActions: [], failedConditions: failedConditions };
|
|
299
299
|
}
|
|
300
300
|
|
|
@@ -307,16 +307,16 @@ class AreEngine {
|
|
|
307
307
|
var action = this._actions.get(binding.actionType);
|
|
308
308
|
|
|
309
309
|
if (!action) {
|
|
310
|
-
this._log('[ARE]
|
|
310
|
+
this._log('[ARE] Action \'' + binding.actionType + '\' not found!');
|
|
311
311
|
continue;
|
|
312
312
|
}
|
|
313
313
|
|
|
314
314
|
try {
|
|
315
|
-
this._log('[ARE] →
|
|
315
|
+
this._log('[ARE] → Executing action \'' + binding.actionType + '\'');
|
|
316
316
|
await action.execute(ctx, binding.settings);
|
|
317
317
|
executedActions.push(binding.actionType);
|
|
318
318
|
} catch (err) {
|
|
319
|
-
this._log('[ARE]
|
|
319
|
+
this._log('[ARE] Action \'' + binding.actionType + '\' error: ' + err.message);
|
|
320
320
|
break;
|
|
321
321
|
}
|
|
322
322
|
}
|
package/dist/are-core.mjs
CHANGED
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// ARE.Core - Action Rule Event Engine (ES Module)
|
|
3
|
+
// Zero dependency - Browser, Node.js, React Native, Electron
|
|
4
|
+
// ============================================================================
|
|
5
|
+
|
|
6
|
+
// ── Enums ──
|
|
7
|
+
|
|
8
|
+
const MatchMode = Object.freeze({
|
|
9
|
+
All: 'all',
|
|
10
|
+
Any: 'any',
|
|
11
|
+
None: 'none',
|
|
12
|
+
ExactlyOne: 'exactlyOne',
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const CompareOp = Object.freeze({
|
|
16
|
+
Equal: 'eq',
|
|
17
|
+
NotEqual: 'neq',
|
|
18
|
+
GreaterThan: 'gt',
|
|
19
|
+
GreaterOrEqual: 'gte',
|
|
20
|
+
LessThan: 'lt',
|
|
21
|
+
LessOrEqual: 'lte',
|
|
22
|
+
Contains: 'contains',
|
|
23
|
+
StartsWith: 'startsWith',
|
|
24
|
+
In: 'in',
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// ── AreContext ──
|
|
28
|
+
|
|
29
|
+
class AreContext {
|
|
30
|
+
constructor() {
|
|
31
|
+
this._data = new Map();
|
|
32
|
+
this.currentEvent = null;
|
|
33
|
+
this.currentRule = null;
|
|
34
|
+
this.stopPipeline = false;
|
|
35
|
+
this.skipRemainingActions = false;
|
|
36
|
+
}
|
|
37
|
+
set(key, value) { this._data.set(key, value); }
|
|
38
|
+
get(key) { return this._data.get(key); }
|
|
39
|
+
has(key) { return this._data.has(key); }
|
|
40
|
+
remove(key) { this._data.delete(key); }
|
|
41
|
+
clear() { this._data.clear(); }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ── ActionSettings ──
|
|
45
|
+
|
|
46
|
+
class ActionSettings {
|
|
47
|
+
constructor() { this._values = {}; }
|
|
48
|
+
set(key, value) { this._values[key] = value; return this; }
|
|
49
|
+
get(key) { return this._values[key]; }
|
|
50
|
+
has(key) { return key in this._values; }
|
|
51
|
+
all() { return Object.assign({}, this._values); }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ── GameEvent ──
|
|
55
|
+
|
|
56
|
+
class GameEvent {
|
|
57
|
+
constructor(eventType) {
|
|
58
|
+
this.eventType = eventType;
|
|
59
|
+
this.data = {};
|
|
60
|
+
this.timestamp = new Date();
|
|
61
|
+
}
|
|
62
|
+
set(key, value) { this.data[key] = value; return this; }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── FieldCondition ──
|
|
66
|
+
|
|
67
|
+
class FieldCondition {
|
|
68
|
+
constructor(fieldName, operator, expected) {
|
|
69
|
+
this.name = fieldName + ' ' + operator + ' ' + expected;
|
|
70
|
+
this.fieldName = fieldName;
|
|
71
|
+
this.operator = operator;
|
|
72
|
+
this.expected = expected;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
evaluate(evt, _context) {
|
|
76
|
+
var actual = evt.data[this.fieldName];
|
|
77
|
+
if (actual === undefined) return false;
|
|
78
|
+
|
|
79
|
+
switch (this.operator) {
|
|
80
|
+
case CompareOp.Equal: return actual === this.expected;
|
|
81
|
+
case CompareOp.NotEqual: return actual !== this.expected;
|
|
82
|
+
case CompareOp.GreaterThan: return actual > this.expected;
|
|
83
|
+
case CompareOp.GreaterOrEqual: return actual >= this.expected;
|
|
84
|
+
case CompareOp.LessThan: return actual < this.expected;
|
|
85
|
+
case CompareOp.LessOrEqual: return actual <= this.expected;
|
|
86
|
+
case CompareOp.Contains: return String(actual).includes(String(this.expected));
|
|
87
|
+
case CompareOp.StartsWith: return String(actual).startsWith(String(this.expected));
|
|
88
|
+
case CompareOp.In: return Array.isArray(this.expected) && this.expected.includes(actual);
|
|
89
|
+
default: return false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ── Rule (Fluent Builder) ──
|
|
95
|
+
|
|
96
|
+
class Rule {
|
|
97
|
+
constructor(ruleId) {
|
|
98
|
+
this.ruleId = ruleId;
|
|
99
|
+
this.group = null;
|
|
100
|
+
this.priority = 0;
|
|
101
|
+
this.isEnabled = true;
|
|
102
|
+
this.eventTypes = [];
|
|
103
|
+
this.conditions = [];
|
|
104
|
+
this.matchMode = MatchMode.All;
|
|
105
|
+
this.actions = [];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
static create(ruleId) { return new Rule(ruleId); }
|
|
109
|
+
|
|
110
|
+
inGroup(group) { this.group = group; return this; }
|
|
111
|
+
withPriority(p) { this.priority = p; return this; }
|
|
112
|
+
enable() { this.isEnabled = true; return this; }
|
|
113
|
+
disable() { this.isEnabled = false; return this; }
|
|
114
|
+
|
|
115
|
+
on() {
|
|
116
|
+
for (var i = 0; i < arguments.length; i++) {
|
|
117
|
+
var et = arguments[i];
|
|
118
|
+
if (this.eventTypes.indexOf(et) === -1) this.eventTypes.push(et);
|
|
119
|
+
}
|
|
120
|
+
return this;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
withMatchMode(mode) { this.matchMode = mode; return this; }
|
|
124
|
+
|
|
125
|
+
when(nameOrCondition, predicate) {
|
|
126
|
+
if (typeof nameOrCondition === 'object' && nameOrCondition.evaluate) {
|
|
127
|
+
this.conditions.push(nameOrCondition);
|
|
128
|
+
} else {
|
|
129
|
+
this.conditions.push({ name: nameOrCondition, evaluate: predicate });
|
|
130
|
+
}
|
|
131
|
+
return this;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
whenField(field, op, expected) {
|
|
135
|
+
this.conditions.push(new FieldCondition(field, op, expected));
|
|
136
|
+
return this;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
whenEquals(field, value) { return this.whenField(field, CompareOp.Equal, value); }
|
|
140
|
+
whenGreaterThan(field, value) { return this.whenField(field, CompareOp.GreaterThan, value); }
|
|
141
|
+
whenLessThan(field, value) { return this.whenField(field, CompareOp.LessThan, value); }
|
|
142
|
+
|
|
143
|
+
then(actionType, configure, order) {
|
|
144
|
+
var settings = new ActionSettings();
|
|
145
|
+
if (typeof configure === 'function') {
|
|
146
|
+
configure(settings);
|
|
147
|
+
} else if (typeof configure === 'number') {
|
|
148
|
+
order = configure;
|
|
149
|
+
}
|
|
150
|
+
this.actions.push({ actionType: actionType, settings: settings, order: order || 0 });
|
|
151
|
+
return this;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ── AreEngine ──
|
|
156
|
+
|
|
157
|
+
class AreEngine {
|
|
158
|
+
constructor() {
|
|
159
|
+
this._actions = new Map();
|
|
160
|
+
this._rules = [];
|
|
161
|
+
this._middlewares = [];
|
|
162
|
+
this._listeners = new Map();
|
|
163
|
+
this.onLog = null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// -- Registration --
|
|
167
|
+
|
|
168
|
+
registerAction(actionTypeOrObj, handler) {
|
|
169
|
+
if (typeof actionTypeOrObj === 'string') {
|
|
170
|
+
this._actions.set(actionTypeOrObj, { actionType: actionTypeOrObj, execute: handler });
|
|
171
|
+
} else {
|
|
172
|
+
this._actions.set(actionTypeOrObj.actionType, actionTypeOrObj);
|
|
173
|
+
}
|
|
174
|
+
return this;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
addRule(rule) { this._rules.push(rule); return this; }
|
|
178
|
+
|
|
179
|
+
addRules() {
|
|
180
|
+
for (var i = 0; i < arguments.length; i++) this._rules.push(arguments[i]);
|
|
181
|
+
return this;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
use(order, handler) {
|
|
185
|
+
this._middlewares.push({ order: order, process: handler });
|
|
186
|
+
this._middlewares.sort(function (a, b) { return a.order - b.order; });
|
|
187
|
+
return this;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
on(eventType, handler) {
|
|
191
|
+
if (!this._listeners.has(eventType)) this._listeners.set(eventType, []);
|
|
192
|
+
this._listeners.get(eventType).push(handler);
|
|
193
|
+
return this;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// -- Rule Management --
|
|
197
|
+
|
|
198
|
+
enableRule(id) { var r = this._rules.find(function (r) { return r.ruleId === id; }); if (r) r.isEnabled = true; return this; }
|
|
199
|
+
disableRule(id) { var r = this._rules.find(function (r) { return r.ruleId === id; }); if (r) r.isEnabled = false; return this; }
|
|
200
|
+
removeRule(id) { this._rules = this._rules.filter(function (r) { return r.ruleId !== id; }); return this; }
|
|
201
|
+
|
|
202
|
+
enableGroup(g) {
|
|
203
|
+
this._rules.forEach(function (r) { if (r.group === g) r.isEnabled = true; });
|
|
204
|
+
return this;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
disableGroup(g) {
|
|
208
|
+
this._rules.forEach(function (r) { if (r.group === g) r.isEnabled = false; });
|
|
209
|
+
return this;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// -- Event Firing --
|
|
213
|
+
|
|
214
|
+
async fire(eventTypeOrEvent, configure, context) {
|
|
215
|
+
var evt;
|
|
216
|
+
if (typeof eventTypeOrEvent === 'string') {
|
|
217
|
+
evt = new GameEvent(eventTypeOrEvent);
|
|
218
|
+
if (typeof configure === 'function') configure(evt);
|
|
219
|
+
} else {
|
|
220
|
+
evt = eventTypeOrEvent;
|
|
221
|
+
if (configure instanceof AreContext) context = configure;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
var start = Date.now();
|
|
225
|
+
var ctx = context || new AreContext();
|
|
226
|
+
ctx.currentEvent = evt;
|
|
227
|
+
ctx.stopPipeline = false;
|
|
228
|
+
|
|
229
|
+
var result = {
|
|
230
|
+
event: evt,
|
|
231
|
+
firedRules: [],
|
|
232
|
+
skippedRules: [],
|
|
233
|
+
pipelineStopped: false,
|
|
234
|
+
duration: 0
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
var self = this;
|
|
238
|
+
|
|
239
|
+
var coreProcess = async function () {
|
|
240
|
+
// 1) Direct listeners
|
|
241
|
+
var listeners = self._listeners.get(evt.eventType) || [];
|
|
242
|
+
for (var i = 0; i < listeners.length; i++) {
|
|
243
|
+
if (ctx.stopPipeline) break;
|
|
244
|
+
await listeners[i](evt, ctx);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// 2) Matching rules (by priority)
|
|
248
|
+
var matching = self._rules
|
|
249
|
+
.filter(function (r) { return r.isEnabled && r.eventTypes.indexOf(evt.eventType) !== -1; })
|
|
250
|
+
.sort(function (a, b) { return b.priority - a.priority; });
|
|
251
|
+
|
|
252
|
+
self._log('[ARE] Event \'' + evt.eventType + '\' → ' + matching.length + ' candidate rules');
|
|
253
|
+
|
|
254
|
+
for (var j = 0; j < matching.length; j++) {
|
|
255
|
+
if (ctx.stopPipeline) {
|
|
256
|
+
self._log('[ARE] Pipeline stopped');
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
var rule = matching[j];
|
|
260
|
+
ctx.currentRule = rule;
|
|
261
|
+
ctx.skipRemainingActions = false;
|
|
262
|
+
|
|
263
|
+
var rr = await self._evaluateAndExecute(rule, evt, ctx);
|
|
264
|
+
if (rr.conditionsMet) result.firedRules.push(rr);
|
|
265
|
+
else result.skippedRules.push(rr);
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
// Middleware chain
|
|
270
|
+
var pipeline = coreProcess;
|
|
271
|
+
for (var i = this._middlewares.length - 1; i >= 0; i--) {
|
|
272
|
+
(function (mw, next) {
|
|
273
|
+
pipeline = function () { return mw.process(ctx, next); };
|
|
274
|
+
})(this._middlewares[i], pipeline);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
await pipeline();
|
|
278
|
+
|
|
279
|
+
result.pipelineStopped = ctx.stopPipeline;
|
|
280
|
+
result.duration = Date.now() - start;
|
|
281
|
+
this._log('[ARE] Event \'' + evt.eventType + '\' completed: ' +
|
|
282
|
+
result.firedRules.length + ' fired, ' +
|
|
283
|
+
result.skippedRules.length + ' skipped, ' +
|
|
284
|
+
result.duration + 'ms');
|
|
285
|
+
return result;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// -- Internal --
|
|
289
|
+
|
|
290
|
+
async _evaluateAndExecute(rule, evt, ctx) {
|
|
291
|
+
var failedConditions = [];
|
|
292
|
+
var conditionsMet = this._evaluateConditions(rule, evt, ctx, failedConditions);
|
|
293
|
+
|
|
294
|
+
if (!conditionsMet) {
|
|
295
|
+
this._log('[ARE] Rule \'' + rule.ruleId + '\' → conditions not met [' + failedConditions.join(', ') + ']');
|
|
296
|
+
return { ruleId: rule.ruleId, conditionsMet: false, executedActions: [], failedConditions: failedConditions };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
var executedActions = [];
|
|
300
|
+
var ordered = rule.actions.slice().sort(function (a, b) { return a.order - b.order; });
|
|
301
|
+
|
|
302
|
+
for (var i = 0; i < ordered.length; i++) {
|
|
303
|
+
if (ctx.skipRemainingActions || ctx.stopPipeline) break;
|
|
304
|
+
var binding = ordered[i];
|
|
305
|
+
var action = this._actions.get(binding.actionType);
|
|
306
|
+
|
|
307
|
+
if (!action) {
|
|
308
|
+
this._log('[ARE] Action \'' + binding.actionType + '\' not found!');
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
try {
|
|
313
|
+
this._log('[ARE] → Executing action \'' + binding.actionType + '\'');
|
|
314
|
+
await action.execute(ctx, binding.settings);
|
|
315
|
+
executedActions.push(binding.actionType);
|
|
316
|
+
} catch (err) {
|
|
317
|
+
this._log('[ARE] Action \'' + binding.actionType + '\' error: ' + err.message);
|
|
318
|
+
break;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return { ruleId: rule.ruleId, conditionsMet: true, executedActions: executedActions, failedConditions: [] };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
_evaluateConditions(rule, evt, ctx, failed) {
|
|
326
|
+
if (rule.conditions.length === 0) return true;
|
|
327
|
+
|
|
328
|
+
var results = [];
|
|
329
|
+
for (var i = 0; i < rule.conditions.length; i++) {
|
|
330
|
+
var c = rule.conditions[i];
|
|
331
|
+
var r = c.evaluate(evt, ctx);
|
|
332
|
+
if (!r) failed.push(c.name);
|
|
333
|
+
results.push(r);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
switch (rule.matchMode) {
|
|
337
|
+
case MatchMode.All: return results.every(function (r) { return r; });
|
|
338
|
+
case MatchMode.Any: return results.some(function (r) { return r; });
|
|
339
|
+
case MatchMode.None: return results.every(function (r) { return !r; });
|
|
340
|
+
case MatchMode.ExactlyOne: return results.filter(function (r) { return r; }).length === 1;
|
|
341
|
+
default: return false;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
_log(msg) { if (this.onLog) this.onLog(msg); }
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// ── Export (ESM) ──
|
|
349
|
+
export {
|
|
350
|
+
AreEngine,
|
|
351
|
+
AreContext,
|
|
352
|
+
GameEvent,
|
|
353
|
+
Rule,
|
|
354
|
+
ActionSettings,
|
|
355
|
+
FieldCondition,
|
|
356
|
+
MatchMode,
|
|
357
|
+
CompareOp
|
|
358
|
+
};
|
package/package.json
CHANGED