@ductape/sdk 0.0.4-v54 → 0.0.4-v56
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/api/urls.js +7 -7
- package/dist/brokers/brokers.service.d.ts +24 -0
- package/dist/brokers/brokers.service.js +72 -0
- package/dist/brokers/brokers.service.js.map +1 -1
- package/dist/graph/graphs.service.d.ts +1 -1
- package/dist/graph/graphs.service.js +1 -1
- package/dist/graph/graphs.service.js.map +1 -1
- package/dist/index.d.ts +542 -58
- package/dist/index.js +773 -97
- package/dist/index.js.map +1 -1
- package/dist/resilience/fallback.service.d.ts +60 -11
- package/dist/resilience/fallback.service.js +278 -23
- package/dist/resilience/fallback.service.js.map +1 -1
- package/dist/resilience/healthcheck.service.d.ts +48 -0
- package/dist/resilience/healthcheck.service.js +556 -1
- package/dist/resilience/healthcheck.service.js.map +1 -1
- package/dist/resilience/quota.service.d.ts +42 -53
- package/dist/resilience/quota.service.js +364 -369
- package/dist/resilience/quota.service.js.map +1 -1
- package/dist/resilience/resilience.service.js +38 -3
- package/dist/resilience/resilience.service.js.map +1 -1
- package/dist/resilience/types/index.d.ts +35 -1
- package/dist/resilience/types/index.js +3 -0
- package/dist/resilience/types/index.js.map +1 -1
- package/dist/storage/storage.service.d.ts +25 -0
- package/dist/storage/storage.service.js +39 -0
- package/dist/storage/storage.service.js.map +1 -1
- package/package.json +1 -1
|
@@ -3,11 +3,17 @@
|
|
|
3
3
|
* Quota Service
|
|
4
4
|
*
|
|
5
5
|
* Code-first API for defining and managing quotas with weighted distribution.
|
|
6
|
+
* Uses ProductBuilder for CRUD operations and ProcessorService for execution.
|
|
7
|
+
* Implements health-aware weighted selection with minimal latency.
|
|
6
8
|
*/
|
|
9
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
10
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
11
|
+
};
|
|
7
12
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
13
|
exports.QuotaService = exports.QuotaError = void 0;
|
|
9
14
|
const types_1 = require("./types");
|
|
10
|
-
const
|
|
15
|
+
const products_service_1 = __importDefault(require("../products/services/products.service"));
|
|
16
|
+
const processor_service_1 = __importDefault(require("../processor/services/processor.service"));
|
|
11
17
|
/**
|
|
12
18
|
* Error class for quota operations
|
|
13
19
|
*/
|
|
@@ -20,6 +26,97 @@ class QuotaError extends Error {
|
|
|
20
26
|
}
|
|
21
27
|
}
|
|
22
28
|
exports.QuotaError = QuotaError;
|
|
29
|
+
// ============================================================================
|
|
30
|
+
// Recording Builders (for code-first API)
|
|
31
|
+
// ============================================================================
|
|
32
|
+
class RecordingAppProviderBuilder {
|
|
33
|
+
constructor(parent, appTag) {
|
|
34
|
+
this.parent = parent;
|
|
35
|
+
this.appTag = appTag;
|
|
36
|
+
}
|
|
37
|
+
action(event) {
|
|
38
|
+
this.parent.setAppAction(this.appTag, event);
|
|
39
|
+
return this.parent;
|
|
40
|
+
}
|
|
41
|
+
weight(value) { return this.parent.weight(value); }
|
|
42
|
+
healthcheck(tag) { return this.parent.healthcheck(tag); }
|
|
43
|
+
app(tag) { return this.parent.app(tag); }
|
|
44
|
+
feature(tag) { return this.parent.feature(tag); }
|
|
45
|
+
database(tag) { return this.parent.database(tag); }
|
|
46
|
+
workflow(tag) { return this.parent.workflow(tag); }
|
|
47
|
+
notification(tag) { return this.parent.notification(tag); }
|
|
48
|
+
mapInput(mapper) { return this.parent.mapInput(mapper); }
|
|
49
|
+
mapOutput(mapper) { return this.parent.mapOutput(mapper); }
|
|
50
|
+
retries(count) { return this.parent.retries(count); }
|
|
51
|
+
checkInterval(ms) { return this.parent.checkInterval(ms); }
|
|
52
|
+
}
|
|
53
|
+
class RecordingDatabaseProviderBuilder {
|
|
54
|
+
constructor(parent, databaseTag) {
|
|
55
|
+
this.parent = parent;
|
|
56
|
+
this.databaseTag = databaseTag;
|
|
57
|
+
}
|
|
58
|
+
action(event) {
|
|
59
|
+
this.parent.setDatabaseAction(this.databaseTag, event);
|
|
60
|
+
return this.parent;
|
|
61
|
+
}
|
|
62
|
+
weight(value) { return this.parent.weight(value); }
|
|
63
|
+
healthcheck(tag) { return this.parent.healthcheck(tag); }
|
|
64
|
+
app(tag) { return this.parent.app(tag); }
|
|
65
|
+
feature(tag) { return this.parent.feature(tag); }
|
|
66
|
+
database(tag) { return this.parent.database(tag); }
|
|
67
|
+
workflow(tag) { return this.parent.workflow(tag); }
|
|
68
|
+
notification(tag) { return this.parent.notification(tag); }
|
|
69
|
+
mapInput(mapper) { return this.parent.mapInput(mapper); }
|
|
70
|
+
mapOutput(mapper) { return this.parent.mapOutput(mapper); }
|
|
71
|
+
retries(count) { return this.parent.retries(count); }
|
|
72
|
+
checkInterval(ms) { return this.parent.checkInterval(ms); }
|
|
73
|
+
}
|
|
74
|
+
class RecordingWorkflowProviderBuilder {
|
|
75
|
+
constructor(parent, workflowTag) {
|
|
76
|
+
this.parent = parent;
|
|
77
|
+
this.parent.setWorkflow(workflowTag);
|
|
78
|
+
}
|
|
79
|
+
input(data) {
|
|
80
|
+
this.parent.setWorkflowInput(data);
|
|
81
|
+
return this.parent;
|
|
82
|
+
}
|
|
83
|
+
weight(value) { return this.parent.weight(value); }
|
|
84
|
+
healthcheck(tag) { return this.parent.healthcheck(tag); }
|
|
85
|
+
app(tag) { return this.parent.app(tag); }
|
|
86
|
+
feature(tag) { return this.parent.feature(tag); }
|
|
87
|
+
database(tag) { return this.parent.database(tag); }
|
|
88
|
+
workflow(tag) { return this.parent.workflow(tag); }
|
|
89
|
+
notification(tag) { return this.parent.notification(tag); }
|
|
90
|
+
mapInput(mapper) { return this.parent.mapInput(mapper); }
|
|
91
|
+
mapOutput(mapper) { return this.parent.mapOutput(mapper); }
|
|
92
|
+
retries(count) { return this.parent.retries(count); }
|
|
93
|
+
checkInterval(ms) { return this.parent.checkInterval(ms); }
|
|
94
|
+
}
|
|
95
|
+
class RecordingNotificationProviderBuilder {
|
|
96
|
+
constructor(parent, notificationTag) {
|
|
97
|
+
this.parent = parent;
|
|
98
|
+
this.parent.setNotification(notificationTag);
|
|
99
|
+
}
|
|
100
|
+
message(messageTag) {
|
|
101
|
+
this.parent.setNotificationMessage(messageTag);
|
|
102
|
+
return this.parent;
|
|
103
|
+
}
|
|
104
|
+
event(eventTag) {
|
|
105
|
+
this.parent.setNotificationEvent(eventTag);
|
|
106
|
+
return this.parent;
|
|
107
|
+
}
|
|
108
|
+
weight(value) { return this.parent.weight(value); }
|
|
109
|
+
healthcheck(tag) { return this.parent.healthcheck(tag); }
|
|
110
|
+
app(tag) { return this.parent.app(tag); }
|
|
111
|
+
feature(tag) { return this.parent.feature(tag); }
|
|
112
|
+
database(tag) { return this.parent.database(tag); }
|
|
113
|
+
workflow(tag) { return this.parent.workflow(tag); }
|
|
114
|
+
notification(tag) { return this.parent.notification(tag); }
|
|
115
|
+
mapInput(mapper) { return this.parent.mapInput(mapper); }
|
|
116
|
+
mapOutput(mapper) { return this.parent.mapOutput(mapper); }
|
|
117
|
+
retries(count) { return this.parent.retries(count); }
|
|
118
|
+
checkInterval(ms) { return this.parent.checkInterval(ms); }
|
|
119
|
+
}
|
|
23
120
|
/**
|
|
24
121
|
* Provider builder that records provider configuration
|
|
25
122
|
*/
|
|
@@ -39,310 +136,75 @@ class RecordingProviderBuilder {
|
|
|
39
136
|
return this;
|
|
40
137
|
}
|
|
41
138
|
app(tag) {
|
|
42
|
-
this._type = types_1.FeatureEventTypes.ACTION;
|
|
43
139
|
this._app = tag;
|
|
44
|
-
return new RecordingAppProviderBuilder(this,
|
|
140
|
+
return new RecordingAppProviderBuilder(this, tag);
|
|
45
141
|
}
|
|
46
|
-
/**
|
|
47
|
-
* @deprecated Features are deprecated - use workflow() instead
|
|
48
|
-
*/
|
|
49
142
|
feature(tag) {
|
|
50
143
|
console.warn('feature() is deprecated. Use workflow() instead.');
|
|
51
|
-
this._type = types_1.FeatureEventTypes.ACTION;
|
|
52
144
|
this._event = tag;
|
|
53
145
|
this.finalize();
|
|
54
146
|
return this;
|
|
55
147
|
}
|
|
56
148
|
database(tag) {
|
|
57
|
-
this
|
|
58
|
-
this._database = tag;
|
|
59
|
-
return new RecordingDatabaseProviderBuilder(this, this._database);
|
|
149
|
+
return new RecordingDatabaseProviderBuilder(this, tag);
|
|
60
150
|
}
|
|
61
151
|
workflow(tag) {
|
|
62
|
-
this
|
|
63
|
-
this._workflow = tag;
|
|
64
|
-
return new RecordingWorkflowProviderBuilder(this, this._workflow);
|
|
152
|
+
return new RecordingWorkflowProviderBuilder(this, tag);
|
|
65
153
|
}
|
|
66
154
|
notification(tag) {
|
|
67
|
-
this
|
|
68
|
-
this._notification = tag;
|
|
69
|
-
return new RecordingNotificationProviderBuilder(this, this._notification);
|
|
155
|
+
return new RecordingNotificationProviderBuilder(this, tag);
|
|
70
156
|
}
|
|
71
|
-
mapInput(
|
|
72
|
-
this._inputMapper = mapper;
|
|
157
|
+
mapInput(_mapper) {
|
|
73
158
|
return this;
|
|
74
159
|
}
|
|
75
|
-
mapOutput(
|
|
76
|
-
this._outputMapper = mapper;
|
|
160
|
+
mapOutput(_mapper) {
|
|
77
161
|
return this;
|
|
78
162
|
}
|
|
79
163
|
retries(count) {
|
|
80
164
|
this._retries = count;
|
|
165
|
+
this.finalize();
|
|
81
166
|
return this;
|
|
82
167
|
}
|
|
83
168
|
checkInterval(ms) {
|
|
84
169
|
this._checkInterval = ms;
|
|
85
170
|
return this;
|
|
86
171
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
*/
|
|
90
|
-
setEvent(event) {
|
|
172
|
+
setAppAction(app, event) {
|
|
173
|
+
this._app = app;
|
|
91
174
|
this._event = event;
|
|
92
|
-
this.finalize();
|
|
93
175
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
this.
|
|
99
|
-
|
|
176
|
+
setDatabaseAction(_database, event) {
|
|
177
|
+
this._event = event;
|
|
178
|
+
}
|
|
179
|
+
setWorkflow(workflow) {
|
|
180
|
+
this._event = workflow;
|
|
181
|
+
}
|
|
182
|
+
setWorkflowInput(_input) { }
|
|
183
|
+
setNotification(_notification) { }
|
|
184
|
+
setNotificationMessage(_message) { }
|
|
185
|
+
setNotificationEvent(event) {
|
|
186
|
+
this._event = event;
|
|
100
187
|
}
|
|
101
|
-
/**
|
|
102
|
-
* Finalize and add provider to options
|
|
103
|
-
*/
|
|
104
188
|
finalize() {
|
|
105
|
-
// Generate input mapping from function
|
|
106
|
-
const inputSchema = this.generateInputSchema();
|
|
107
|
-
const outputSchema = this.generateOutputSchema();
|
|
108
189
|
const option = {
|
|
109
190
|
provider: this.providerName,
|
|
110
191
|
quota: this._weight,
|
|
111
|
-
|
|
112
|
-
type: this._type || types_1.FeatureEventTypes.ACTION,
|
|
113
|
-
app: this._app,
|
|
192
|
+
type: types_1.FeatureEventTypes.ACTION,
|
|
114
193
|
event: this._event || '',
|
|
115
|
-
input:
|
|
116
|
-
output:
|
|
194
|
+
input: {},
|
|
195
|
+
output: {},
|
|
117
196
|
retries: this._retries,
|
|
197
|
+
healthcheck: this._healthcheck,
|
|
118
198
|
check_interval: this._checkInterval,
|
|
119
199
|
};
|
|
120
|
-
this.
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Generate input schema from mapper function
|
|
124
|
-
*/
|
|
125
|
-
generateInputSchema() {
|
|
126
|
-
if (!this._inputMapper) {
|
|
127
|
-
return {};
|
|
200
|
+
if (this._app) {
|
|
201
|
+
option.app = this._app;
|
|
128
202
|
}
|
|
129
|
-
|
|
130
|
-
const proxyHandler = {
|
|
131
|
-
get: (_target, prop) => {
|
|
132
|
-
if (typeof prop === 'string') {
|
|
133
|
-
return `$Input{${prop}}`;
|
|
134
|
-
}
|
|
135
|
-
return undefined;
|
|
136
|
-
},
|
|
137
|
-
};
|
|
138
|
-
try {
|
|
139
|
-
const proxy = new Proxy({}, proxyHandler);
|
|
140
|
-
return this._inputMapper(proxy);
|
|
141
|
-
}
|
|
142
|
-
catch (_a) {
|
|
143
|
-
// If proxy fails, return empty schema
|
|
144
|
-
return {};
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
/**
|
|
148
|
-
* Generate output schema from mapper function
|
|
149
|
-
*/
|
|
150
|
-
generateOutputSchema() {
|
|
151
|
-
if (!this._outputMapper) {
|
|
152
|
-
return {};
|
|
153
|
-
}
|
|
154
|
-
// Create a proxy to capture property access patterns
|
|
155
|
-
const proxyHandler = {
|
|
156
|
-
get: (_target, prop) => {
|
|
157
|
-
if (typeof prop === 'string') {
|
|
158
|
-
return `$Response{${prop}}`;
|
|
159
|
-
}
|
|
160
|
-
return undefined;
|
|
161
|
-
},
|
|
162
|
-
};
|
|
163
|
-
try {
|
|
164
|
-
const proxy = new Proxy({}, proxyHandler);
|
|
165
|
-
return this._outputMapper(proxy);
|
|
166
|
-
}
|
|
167
|
-
catch (_a) {
|
|
168
|
-
return {};
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
class RecordingAppProviderBuilder {
|
|
173
|
-
constructor(parent, appTag) {
|
|
174
|
-
this.parent = parent;
|
|
175
|
-
this.appTag = appTag;
|
|
176
|
-
}
|
|
177
|
-
action(event) {
|
|
178
|
-
this.parent.setEvent(event);
|
|
179
|
-
return this.parent;
|
|
180
|
-
}
|
|
181
|
-
weight(value) {
|
|
182
|
-
return this.parent.weight(value);
|
|
183
|
-
}
|
|
184
|
-
healthcheck(tag) {
|
|
185
|
-
return this.parent.healthcheck(tag);
|
|
186
|
-
}
|
|
187
|
-
app(tag) {
|
|
188
|
-
return this.parent.app(tag);
|
|
189
|
-
}
|
|
190
|
-
feature(tag) {
|
|
191
|
-
return this.parent.feature(tag);
|
|
192
|
-
}
|
|
193
|
-
database(tag) {
|
|
194
|
-
return this.parent.database(tag);
|
|
195
|
-
}
|
|
196
|
-
workflow(tag) {
|
|
197
|
-
return this.parent.workflow(tag);
|
|
198
|
-
}
|
|
199
|
-
notification(tag) {
|
|
200
|
-
return this.parent.notification(tag);
|
|
201
|
-
}
|
|
202
|
-
mapInput(mapper) {
|
|
203
|
-
return this.parent.mapInput(mapper);
|
|
204
|
-
}
|
|
205
|
-
mapOutput(mapper) {
|
|
206
|
-
return this.parent.mapOutput(mapper);
|
|
207
|
-
}
|
|
208
|
-
retries(count) {
|
|
209
|
-
return this.parent.retries(count);
|
|
210
|
-
}
|
|
211
|
-
checkInterval(ms) {
|
|
212
|
-
return this.parent.checkInterval(ms);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
class RecordingDatabaseProviderBuilder {
|
|
216
|
-
constructor(parent, dbTag) {
|
|
217
|
-
this.parent = parent;
|
|
218
|
-
this.dbTag = dbTag;
|
|
219
|
-
}
|
|
220
|
-
action(event) {
|
|
221
|
-
this.parent.setEvent(event);
|
|
222
|
-
return this.parent;
|
|
223
|
-
}
|
|
224
|
-
weight(value) {
|
|
225
|
-
return this.parent.weight(value);
|
|
226
|
-
}
|
|
227
|
-
healthcheck(tag) {
|
|
228
|
-
return this.parent.healthcheck(tag);
|
|
229
|
-
}
|
|
230
|
-
app(tag) {
|
|
231
|
-
return this.parent.app(tag);
|
|
232
|
-
}
|
|
233
|
-
feature(tag) {
|
|
234
|
-
return this.parent.feature(tag);
|
|
235
|
-
}
|
|
236
|
-
database(tag) {
|
|
237
|
-
return this.parent.database(tag);
|
|
238
|
-
}
|
|
239
|
-
workflow(tag) {
|
|
240
|
-
return this.parent.workflow(tag);
|
|
241
|
-
}
|
|
242
|
-
notification(tag) {
|
|
243
|
-
return this.parent.notification(tag);
|
|
244
|
-
}
|
|
245
|
-
mapInput(mapper) {
|
|
246
|
-
return this.parent.mapInput(mapper);
|
|
247
|
-
}
|
|
248
|
-
mapOutput(mapper) {
|
|
249
|
-
return this.parent.mapOutput(mapper);
|
|
250
|
-
}
|
|
251
|
-
retries(count) {
|
|
252
|
-
return this.parent.retries(count);
|
|
253
|
-
}
|
|
254
|
-
checkInterval(ms) {
|
|
255
|
-
return this.parent.checkInterval(ms);
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
class RecordingWorkflowProviderBuilder {
|
|
259
|
-
constructor(parent, workflowTag) {
|
|
260
|
-
this.parent = parent;
|
|
261
|
-
this.workflowTag = workflowTag;
|
|
262
|
-
}
|
|
263
|
-
input(data) {
|
|
264
|
-
this.parent.setWorkflowInput(data);
|
|
265
|
-
return this.parent;
|
|
266
|
-
}
|
|
267
|
-
weight(value) {
|
|
268
|
-
return this.parent.weight(value);
|
|
269
|
-
}
|
|
270
|
-
healthcheck(tag) {
|
|
271
|
-
return this.parent.healthcheck(tag);
|
|
272
|
-
}
|
|
273
|
-
app(tag) {
|
|
274
|
-
return this.parent.app(tag);
|
|
275
|
-
}
|
|
276
|
-
feature(tag) {
|
|
277
|
-
return this.parent.feature(tag);
|
|
278
|
-
}
|
|
279
|
-
database(tag) {
|
|
280
|
-
return this.parent.database(tag);
|
|
281
|
-
}
|
|
282
|
-
workflow(tag) {
|
|
283
|
-
return this.parent.workflow(tag);
|
|
284
|
-
}
|
|
285
|
-
notification(tag) {
|
|
286
|
-
return this.parent.notification(tag);
|
|
287
|
-
}
|
|
288
|
-
mapInput(mapper) {
|
|
289
|
-
return this.parent.mapInput(mapper);
|
|
290
|
-
}
|
|
291
|
-
mapOutput(mapper) {
|
|
292
|
-
return this.parent.mapOutput(mapper);
|
|
293
|
-
}
|
|
294
|
-
retries(count) {
|
|
295
|
-
return this.parent.retries(count);
|
|
296
|
-
}
|
|
297
|
-
checkInterval(ms) {
|
|
298
|
-
return this.parent.checkInterval(ms);
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
class RecordingNotificationProviderBuilder {
|
|
302
|
-
constructor(parent, notificationTag) {
|
|
303
|
-
this.parent = parent;
|
|
304
|
-
this.notificationTag = notificationTag;
|
|
305
|
-
}
|
|
306
|
-
event(event) {
|
|
307
|
-
this.parent.setEvent(event);
|
|
308
|
-
return this.parent;
|
|
309
|
-
}
|
|
310
|
-
weight(value) {
|
|
311
|
-
return this.parent.weight(value);
|
|
312
|
-
}
|
|
313
|
-
healthcheck(tag) {
|
|
314
|
-
return this.parent.healthcheck(tag);
|
|
315
|
-
}
|
|
316
|
-
app(tag) {
|
|
317
|
-
return this.parent.app(tag);
|
|
318
|
-
}
|
|
319
|
-
feature(tag) {
|
|
320
|
-
return this.parent.feature(tag);
|
|
321
|
-
}
|
|
322
|
-
database(tag) {
|
|
323
|
-
return this.parent.database(tag);
|
|
324
|
-
}
|
|
325
|
-
workflow(tag) {
|
|
326
|
-
return this.parent.workflow(tag);
|
|
327
|
-
}
|
|
328
|
-
notification(tag) {
|
|
329
|
-
return this.parent.notification(tag);
|
|
330
|
-
}
|
|
331
|
-
mapInput(mapper) {
|
|
332
|
-
return this.parent.mapInput(mapper);
|
|
333
|
-
}
|
|
334
|
-
mapOutput(mapper) {
|
|
335
|
-
return this.parent.mapOutput(mapper);
|
|
336
|
-
}
|
|
337
|
-
retries(count) {
|
|
338
|
-
return this.parent.retries(count);
|
|
339
|
-
}
|
|
340
|
-
checkInterval(ms) {
|
|
341
|
-
return this.parent.checkInterval(ms);
|
|
203
|
+
this.options.push(option);
|
|
342
204
|
}
|
|
343
205
|
}
|
|
344
206
|
/**
|
|
345
|
-
* Recording context
|
|
207
|
+
* Recording context for quota configuration
|
|
346
208
|
*/
|
|
347
209
|
class RecordingQuotaContext {
|
|
348
210
|
constructor(inputSchema) {
|
|
@@ -350,106 +212,84 @@ class RecordingQuotaContext {
|
|
|
350
212
|
this._input = inputSchema;
|
|
351
213
|
}
|
|
352
214
|
get input() {
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
if (typeof prop === 'string') {
|
|
357
|
-
return `$Input{${prop}}`;
|
|
358
|
-
}
|
|
359
|
-
return undefined;
|
|
360
|
-
},
|
|
361
|
-
};
|
|
362
|
-
return new Proxy({}, proxyHandler);
|
|
215
|
+
return new Proxy({}, {
|
|
216
|
+
get: (_, prop) => typeof prop === 'string' ? `$Input{${prop}}` : undefined,
|
|
217
|
+
});
|
|
363
218
|
}
|
|
364
219
|
provider(name) {
|
|
365
220
|
return new RecordingProviderBuilder(name, this._options);
|
|
366
221
|
}
|
|
367
|
-
auth(field) {
|
|
368
|
-
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
return `$Secret{${key}}`;
|
|
372
|
-
}
|
|
373
|
-
variable(app, key) {
|
|
374
|
-
return `$Variable{${app}}{${key}}`;
|
|
375
|
-
}
|
|
376
|
-
constant(app, key) {
|
|
377
|
-
return `$Constant{${app}}{${key}}`;
|
|
378
|
-
}
|
|
379
|
-
/**
|
|
380
|
-
* Build the quota schema from recorded configuration
|
|
381
|
-
*/
|
|
222
|
+
auth(field) { return `$Auth{${field}}`; }
|
|
223
|
+
token(key) { return `$Secret{${key}}`; }
|
|
224
|
+
variable(app, key) { return `$Variable{${app}}{${key}}`; }
|
|
225
|
+
constant(app, key) { return `$Constant{${app}}{${key}}`; }
|
|
382
226
|
build(tag, name, description) {
|
|
383
227
|
if (this._options.length === 0) {
|
|
384
228
|
throw new QuotaError('At least one provider must be configured', 'NO_PROVIDERS_CONFIGURED');
|
|
385
229
|
}
|
|
386
|
-
const total_quota = this._options.reduce((sum, opt) => sum + opt.quota, 0);
|
|
387
230
|
return {
|
|
388
231
|
tag,
|
|
389
232
|
name,
|
|
390
233
|
description,
|
|
391
234
|
input: this._input,
|
|
392
|
-
total_quota,
|
|
235
|
+
total_quota: this._options.reduce((sum, opt) => sum + opt.quota, 0),
|
|
393
236
|
options: this._options,
|
|
394
237
|
};
|
|
395
238
|
}
|
|
396
|
-
get options() {
|
|
397
|
-
return this._options;
|
|
398
|
-
}
|
|
239
|
+
get options() { return this._options; }
|
|
399
240
|
}
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
* Provides code-first API for defining, creating, and managing quotas.
|
|
404
|
-
*/
|
|
241
|
+
// ============================================================================
|
|
242
|
+
// Quota Service - Optimized for Minimal Latency
|
|
243
|
+
// ============================================================================
|
|
405
244
|
class QuotaService {
|
|
406
245
|
constructor(config) {
|
|
407
|
-
this.
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
base_url: config.base_url,
|
|
411
|
-
});
|
|
246
|
+
this.productBuilders = new Map();
|
|
247
|
+
this.processorCache = new Map();
|
|
248
|
+
this.config = config;
|
|
412
249
|
}
|
|
413
250
|
/**
|
|
414
|
-
*
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
*
|
|
435
|
-
*
|
|
436
|
-
* ctx.provider('nexmo')
|
|
437
|
-
* .weight(30)
|
|
438
|
-
* .healthcheck('nexmo-health')
|
|
439
|
-
* .app('nexmo-app')
|
|
440
|
-
* .action('send-sms')
|
|
441
|
-
* .mapInput((input) => ({ body: { to: input.phone, text: input.message } }))
|
|
442
|
-
* .mapOutput((res) => ({ messageId: res['message-id'] }))
|
|
443
|
-
* .retries(2);
|
|
444
|
-
* },
|
|
445
|
-
* });
|
|
446
|
-
* ```
|
|
251
|
+
* Gets or creates a cached ProductBuilder
|
|
252
|
+
*/
|
|
253
|
+
async getProductBuilder(productTag) {
|
|
254
|
+
let builder = this.productBuilders.get(productTag);
|
|
255
|
+
if (!builder) {
|
|
256
|
+
builder = new products_service_1.default({
|
|
257
|
+
workspace_id: this.config.workspace_id,
|
|
258
|
+
public_key: this.config.public_key,
|
|
259
|
+
user_id: this.config.user_id,
|
|
260
|
+
token: this.config.token,
|
|
261
|
+
env_type: this.config.env_type,
|
|
262
|
+
redis_client: this.config.redis_client,
|
|
263
|
+
queues: this.config.queues,
|
|
264
|
+
});
|
|
265
|
+
await builder.initializeProductByTag(productTag);
|
|
266
|
+
this.productBuilders.set(productTag, builder);
|
|
267
|
+
}
|
|
268
|
+
return builder;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Gets or creates a cached ProcessorService for minimal latency
|
|
447
272
|
*/
|
|
273
|
+
getProcessor(productTag) {
|
|
274
|
+
let processor = this.processorCache.get(productTag);
|
|
275
|
+
if (!processor) {
|
|
276
|
+
processor = new processor_service_1.default({
|
|
277
|
+
workspace_id: this.config.workspace_id,
|
|
278
|
+
public_key: this.config.public_key,
|
|
279
|
+
user_id: this.config.user_id,
|
|
280
|
+
token: this.config.token,
|
|
281
|
+
env_type: this.config.env_type,
|
|
282
|
+
redis_client: this.config.redis_client,
|
|
283
|
+
queues: this.config.queues,
|
|
284
|
+
});
|
|
285
|
+
this.processorCache.set(productTag, processor);
|
|
286
|
+
}
|
|
287
|
+
return processor;
|
|
288
|
+
}
|
|
289
|
+
// ==================== Code-First API ====================
|
|
448
290
|
async define(options) {
|
|
449
291
|
const ctx = new RecordingQuotaContext(options.input);
|
|
450
|
-
// Execute the handler to record the configuration
|
|
451
292
|
await options.handler(ctx);
|
|
452
|
-
// Build the schema
|
|
453
293
|
const schema = ctx.build(options.tag, options.name, options.description);
|
|
454
294
|
const definedQuota = {
|
|
455
295
|
tag: options.tag,
|
|
@@ -458,60 +298,215 @@ class QuotaService {
|
|
|
458
298
|
input: options.input,
|
|
459
299
|
compile: () => schema,
|
|
460
300
|
};
|
|
461
|
-
// If product is specified, register immediately
|
|
462
301
|
if (options.product) {
|
|
463
302
|
await this.create(options.product, definedQuota);
|
|
464
303
|
}
|
|
465
304
|
return definedQuota;
|
|
466
305
|
}
|
|
467
|
-
|
|
468
|
-
* Create a quota from a defined quota or schema
|
|
469
|
-
*/
|
|
306
|
+
// ==================== CRUD via ProductBuilder ====================
|
|
470
307
|
async create(product, quota) {
|
|
471
308
|
const schema = 'compile' in quota ? quota.compile() : quota;
|
|
472
|
-
|
|
309
|
+
const builder = await this.getProductBuilder(product);
|
|
310
|
+
const productQuota = {
|
|
311
|
+
tag: schema.tag,
|
|
312
|
+
name: schema.name || schema.tag,
|
|
313
|
+
description: schema.description || '',
|
|
314
|
+
input: schema.input,
|
|
315
|
+
total_quota: schema.total_quota,
|
|
316
|
+
options: schema.options.map(opt => ({
|
|
317
|
+
app: opt.app,
|
|
318
|
+
type: types_1.FeatureEventTypes.ACTION,
|
|
319
|
+
event: opt.event,
|
|
320
|
+
input: opt.input || {},
|
|
321
|
+
output: opt.output || {},
|
|
322
|
+
retries: opt.retries || 2,
|
|
323
|
+
quota: opt.quota,
|
|
324
|
+
healthcheck: opt.healthcheck,
|
|
325
|
+
check_interval: opt.check_interval,
|
|
326
|
+
})),
|
|
327
|
+
};
|
|
328
|
+
await builder.createQuota(productQuota);
|
|
329
|
+
return await builder.fetchQuota(schema.tag);
|
|
473
330
|
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
const schema = ctx.build(tag, quota.name, quota.description);
|
|
482
|
-
return await this.apiService.updateQuota(product, tag, schema);
|
|
483
|
-
}
|
|
484
|
-
return await this.apiService.updateQuota(product, tag, quota);
|
|
331
|
+
async fetch(product, tag) {
|
|
332
|
+
const builder = await this.getProductBuilder(product);
|
|
333
|
+
return builder.fetchQuota(tag);
|
|
334
|
+
}
|
|
335
|
+
async fetchAll(product) {
|
|
336
|
+
const builder = await this.getProductBuilder(product);
|
|
337
|
+
return builder.fetchQuotas();
|
|
485
338
|
}
|
|
339
|
+
async update(product, tag, data) {
|
|
340
|
+
const builder = await this.getProductBuilder(product);
|
|
341
|
+
await builder.updateQuota(tag, data);
|
|
342
|
+
}
|
|
343
|
+
async delete(product, tag) {
|
|
344
|
+
const builder = await this.getProductBuilder(product);
|
|
345
|
+
await builder.deleteQuota(tag);
|
|
346
|
+
}
|
|
347
|
+
// ==================== Execution - Optimized for Minimal Latency ====================
|
|
486
348
|
/**
|
|
487
|
-
*
|
|
349
|
+
* Run a quota with health-aware weighted provider selection
|
|
350
|
+
*
|
|
351
|
+
* Optimization strategies:
|
|
352
|
+
* 1. Parallel health status lookups from Redis (O(1) per provider)
|
|
353
|
+
* 2. Cached processor instances to avoid re-initialization
|
|
354
|
+
* 3. Pre-computed weighted selection for O(n) provider selection
|
|
355
|
+
* 4. Exponential backoff only on failures
|
|
488
356
|
*/
|
|
489
|
-
async
|
|
490
|
-
|
|
357
|
+
async run(options) {
|
|
358
|
+
var _a;
|
|
359
|
+
const { product, env, tag, input } = options;
|
|
360
|
+
const startTime = Date.now();
|
|
361
|
+
// Fetch quota config
|
|
362
|
+
const quota = await this.fetch(product, tag);
|
|
363
|
+
if (!quota) {
|
|
364
|
+
throw new QuotaError(`Quota '${tag}' not found in product '${product}'`, 'QUOTA_NOT_FOUND');
|
|
365
|
+
}
|
|
366
|
+
if (!((_a = quota.options) === null || _a === void 0 ? void 0 : _a.length)) {
|
|
367
|
+
throw new QuotaError(`Quota '${tag}' has no providers configured`, 'NO_PROVIDERS');
|
|
368
|
+
}
|
|
369
|
+
const processor = this.getProcessor(product);
|
|
370
|
+
// Parallel health status lookup - minimal latency
|
|
371
|
+
const healthStatuses = await this.getHealthStatusesParallel(processor, product, env, quota.options);
|
|
372
|
+
// Fast weighted selection with health awareness
|
|
373
|
+
const { provider, wasHealthy } = this.selectHealthyProvider(quota.options, healthStatuses);
|
|
374
|
+
// Execute with retries
|
|
375
|
+
const result = await this.executeWithRetries(processor, provider, env, product, input);
|
|
376
|
+
return {
|
|
377
|
+
data: result.data,
|
|
378
|
+
provider: provider.app || provider.event,
|
|
379
|
+
latency: Date.now() - startTime,
|
|
380
|
+
wasHealthy,
|
|
381
|
+
retriesUsed: result.retriesUsed,
|
|
382
|
+
};
|
|
491
383
|
}
|
|
492
384
|
/**
|
|
493
|
-
*
|
|
385
|
+
* Parallel health status lookup from Redis - O(n) with parallel I/O
|
|
494
386
|
*/
|
|
495
|
-
async
|
|
496
|
-
|
|
387
|
+
async getHealthStatusesParallel(processor, product, env, options) {
|
|
388
|
+
const statuses = new Map();
|
|
389
|
+
// Collect healthcheck tags
|
|
390
|
+
const healthcheckTags = options
|
|
391
|
+
.filter(opt => opt.healthcheck)
|
|
392
|
+
.map(opt => opt.healthcheck);
|
|
393
|
+
if (healthcheckTags.length === 0) {
|
|
394
|
+
return statuses;
|
|
395
|
+
}
|
|
396
|
+
// Parallel Redis lookups
|
|
397
|
+
const results = await Promise.allSettled(healthcheckTags.map(async (tag) => {
|
|
398
|
+
try {
|
|
399
|
+
const cached = await processor.getHealthcheckStatusFromCache(product, `${tag}:${env}`);
|
|
400
|
+
return { tag, status: (cached === null || cached === void 0 ? void 0 : cached.status) || 'unknown', latency: cached === null || cached === void 0 ? void 0 : cached.latency };
|
|
401
|
+
}
|
|
402
|
+
catch (_a) {
|
|
403
|
+
return { tag, status: 'unknown' };
|
|
404
|
+
}
|
|
405
|
+
}));
|
|
406
|
+
for (const result of results) {
|
|
407
|
+
if (result.status === 'fulfilled') {
|
|
408
|
+
statuses.set(result.value.tag, {
|
|
409
|
+
status: result.value.status,
|
|
410
|
+
lastLatency: result.value.latency,
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return statuses;
|
|
497
415
|
}
|
|
498
416
|
/**
|
|
499
|
-
*
|
|
417
|
+
* Fast weighted selection with health-aware prioritization
|
|
500
418
|
*/
|
|
501
|
-
|
|
502
|
-
|
|
419
|
+
selectHealthyProvider(options, healthStatuses) {
|
|
420
|
+
// Categorize by health in single pass
|
|
421
|
+
const healthy = [];
|
|
422
|
+
const unknown = [];
|
|
423
|
+
const unhealthy = [];
|
|
424
|
+
for (const opt of options) {
|
|
425
|
+
if (!opt.healthcheck) {
|
|
426
|
+
healthy.push(opt); // No healthcheck = assume healthy
|
|
427
|
+
}
|
|
428
|
+
else {
|
|
429
|
+
const status = healthStatuses.get(opt.healthcheck);
|
|
430
|
+
if (!status || status.status === 'unknown') {
|
|
431
|
+
unknown.push(opt);
|
|
432
|
+
}
|
|
433
|
+
else if (status.status === 'available') {
|
|
434
|
+
healthy.push(opt);
|
|
435
|
+
}
|
|
436
|
+
else {
|
|
437
|
+
unhealthy.push(opt);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
// Priority: healthy > unknown > unhealthy
|
|
442
|
+
let candidates = healthy.length > 0 ? healthy : unknown.length > 0 ? unknown : unhealthy;
|
|
443
|
+
const wasHealthy = healthy.length > 0;
|
|
444
|
+
if (candidates.length === 0) {
|
|
445
|
+
throw new QuotaError('No providers available', 'NO_PROVIDERS_AVAILABLE');
|
|
446
|
+
}
|
|
447
|
+
// Weighted random selection - O(n)
|
|
448
|
+
const totalWeight = candidates.reduce((sum, opt) => sum + (opt.quota || 1), 0);
|
|
449
|
+
const random = Math.random() * totalWeight;
|
|
450
|
+
let cumulative = 0;
|
|
451
|
+
for (const opt of candidates) {
|
|
452
|
+
cumulative += opt.quota || 1;
|
|
453
|
+
if (random <= cumulative) {
|
|
454
|
+
return { provider: opt, wasHealthy };
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
return { provider: candidates[candidates.length - 1], wasHealthy };
|
|
503
458
|
}
|
|
504
459
|
/**
|
|
505
|
-
*
|
|
460
|
+
* Execute provider action with exponential backoff retries
|
|
506
461
|
*/
|
|
507
|
-
async
|
|
508
|
-
|
|
462
|
+
async executeWithRetries(processor, provider, env, product, input) {
|
|
463
|
+
const maxRetries = provider.retries || 2;
|
|
464
|
+
let lastError = null;
|
|
465
|
+
let retriesUsed = 0;
|
|
466
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
467
|
+
try {
|
|
468
|
+
// Execute app action via processor
|
|
469
|
+
const result = await processor.processAction({
|
|
470
|
+
env,
|
|
471
|
+
product,
|
|
472
|
+
app: provider.app,
|
|
473
|
+
action: provider.event,
|
|
474
|
+
input,
|
|
475
|
+
retries: 0, // We handle retries
|
|
476
|
+
});
|
|
477
|
+
return { data: result, retriesUsed };
|
|
478
|
+
}
|
|
479
|
+
catch (error) {
|
|
480
|
+
lastError = error;
|
|
481
|
+
retriesUsed++;
|
|
482
|
+
if (attempt < maxRetries) {
|
|
483
|
+
// Exponential backoff: 50ms, 100ms, 200ms (capped at 2s for low latency)
|
|
484
|
+
await new Promise(r => setTimeout(r, Math.min(50 * Math.pow(2, attempt), 2000)));
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
throw new QuotaError(`All ${maxRetries + 1} attempts failed. Last error: ${lastError === null || lastError === void 0 ? void 0 : lastError.message}`, 'ALL_RETRIES_FAILED', { lastError, retriesUsed });
|
|
509
489
|
}
|
|
510
490
|
/**
|
|
511
|
-
* Dispatch
|
|
491
|
+
* Dispatch quota for deferred execution via job queue
|
|
492
|
+
*
|
|
493
|
+
* Note: For immediate execution, use run() instead.
|
|
494
|
+
* Dispatch schedules the quota for background processing.
|
|
512
495
|
*/
|
|
513
496
|
async dispatch(options) {
|
|
514
|
-
|
|
497
|
+
const { product, tag } = options;
|
|
498
|
+
const jobId = `quota_${tag}_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
|
|
499
|
+
// For deferred execution, we run the quota asynchronously
|
|
500
|
+
// The job queue integration would be implemented based on your queue system
|
|
501
|
+
setImmediate(async () => {
|
|
502
|
+
try {
|
|
503
|
+
await this.run(options);
|
|
504
|
+
}
|
|
505
|
+
catch (error) {
|
|
506
|
+
console.error(`Deferred quota execution failed for ${product}:${tag}:`, error);
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
return { jobId };
|
|
515
510
|
}
|
|
516
511
|
}
|
|
517
512
|
exports.QuotaService = QuotaService;
|