@lowdefy/engine 5.6.0 → 6.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/Actions.js +288 -2
- package/dist/Block.js +4 -1
- package/dist/WebSockets.js +242 -0
- package/dist/actions/createPublish.js +23 -0
- package/dist/actions/createSubscribe.js +25 -0
- package/dist/actions/createUnsubscribe.js +22 -0
- package/dist/actions/getActionMethods.js +6 -0
- package/dist/getContext.js +27 -4
- package/dist/index.js +2 -1
- package/dist/resolveTarget.js +26 -13
- package/dist/stopChain.js +41 -0
- package/package.json +10 -10
package/dist/Actions.js
CHANGED
|
@@ -15,6 +15,15 @@
|
|
|
15
15
|
*/ import { ActionError, ConfigError, UserError } from '@lowdefy/errors';
|
|
16
16
|
import { type } from '@lowdefy/helpers';
|
|
17
17
|
import getActionMethods from './actions/getActionMethods.js';
|
|
18
|
+
import { isStopChain } from './stopChain.js';
|
|
19
|
+
const CONTROL_KEYS = [
|
|
20
|
+
':if',
|
|
21
|
+
':switch',
|
|
22
|
+
':return'
|
|
23
|
+
];
|
|
24
|
+
function isControl(item) {
|
|
25
|
+
return type.isObject(item) && CONTROL_KEYS.some((key)=>key in item);
|
|
26
|
+
}
|
|
18
27
|
let Actions = class Actions {
|
|
19
28
|
logActionError({ error, action }) {
|
|
20
29
|
const handleError = this.context._internal.lowdefy._internal.handleError;
|
|
@@ -52,10 +61,38 @@ let Actions = class Actions {
|
|
|
52
61
|
this.logActionError(err);
|
|
53
62
|
}
|
|
54
63
|
}
|
|
55
|
-
|
|
56
|
-
|
|
64
|
+
// Returns true when a ':return' control or an action ended the list, so callers can
|
|
65
|
+
// end the event.
|
|
66
|
+
async callActionLoop({ actions, arrayIndices, block, controls, counters, event, progress, responses }) {
|
|
67
|
+
for (const [position, action] of actions.entries()){
|
|
68
|
+
if (isControl(action)) {
|
|
69
|
+
const returned = await this.callControl({
|
|
70
|
+
arrayIndices,
|
|
71
|
+
block,
|
|
72
|
+
control: action,
|
|
73
|
+
controls,
|
|
74
|
+
counters,
|
|
75
|
+
event,
|
|
76
|
+
progress,
|
|
77
|
+
responses
|
|
78
|
+
});
|
|
79
|
+
if (returned === true) {
|
|
80
|
+
this.recordSkippedActions({
|
|
81
|
+
actions: actions.slice(position + 1),
|
|
82
|
+
counters,
|
|
83
|
+
responses
|
|
84
|
+
});
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const index = counters.action;
|
|
90
|
+
counters.action += 1;
|
|
57
91
|
try {
|
|
58
92
|
if (action.async === true) {
|
|
93
|
+
// Fire and forget - the response is never awaited here, so an 'async: true'
|
|
94
|
+
// action cannot end the chain even if it returns a stopChain marker. The
|
|
95
|
+
// following steps have already run by the time it resolves.
|
|
59
96
|
this.callAsyncAction({
|
|
60
97
|
action,
|
|
61
98
|
arrayIndices,
|
|
@@ -76,6 +113,18 @@ let Actions = class Actions {
|
|
|
76
113
|
responses
|
|
77
114
|
});
|
|
78
115
|
responses[action.id] = response;
|
|
116
|
+
// An action that navigated the browser away ends the chain, reported as a
|
|
117
|
+
// success. Same halt the ':return' control uses above - remaining steps are
|
|
118
|
+
// recorded as skipped, and callActions is never entered through its catch,
|
|
119
|
+
// so 'catch:' actions do not run and no error message displays.
|
|
120
|
+
if (response.stoppedChain === true) {
|
|
121
|
+
this.recordSkippedActions({
|
|
122
|
+
actions: actions.slice(position + 1),
|
|
123
|
+
counters,
|
|
124
|
+
responses
|
|
125
|
+
});
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
79
128
|
}
|
|
80
129
|
} catch (err) {
|
|
81
130
|
// err is already {error, action, index} from callAction
|
|
@@ -83,26 +132,242 @@ let Actions = class Actions {
|
|
|
83
132
|
throw err;
|
|
84
133
|
}
|
|
85
134
|
}
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
async callControl({ arrayIndices, block, control, controls, counters, event, progress, responses }) {
|
|
138
|
+
const index = counters.control;
|
|
139
|
+
counters.control += 1;
|
|
140
|
+
if (':return' in control) {
|
|
141
|
+
const value = this.evaluateControlValue({
|
|
142
|
+
arrayIndices,
|
|
143
|
+
block,
|
|
144
|
+
event,
|
|
145
|
+
input: control[':return'],
|
|
146
|
+
node: control,
|
|
147
|
+
responses
|
|
148
|
+
});
|
|
149
|
+
controls.push({
|
|
150
|
+
index,
|
|
151
|
+
type: ':return',
|
|
152
|
+
taken: value
|
|
153
|
+
});
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
if (':if' in control) {
|
|
157
|
+
const condition = this.evaluateControlValue({
|
|
158
|
+
arrayIndices,
|
|
159
|
+
block,
|
|
160
|
+
event,
|
|
161
|
+
input: control[':if'],
|
|
162
|
+
node: control,
|
|
163
|
+
responses
|
|
164
|
+
});
|
|
165
|
+
// JS truthiness, matching the routine ':if' - not skip's strict === true.
|
|
166
|
+
if (condition) {
|
|
167
|
+
controls.push({
|
|
168
|
+
index,
|
|
169
|
+
type: ':if',
|
|
170
|
+
taken: 'then'
|
|
171
|
+
});
|
|
172
|
+
const returned = await this.callActionLoop({
|
|
173
|
+
actions: control[':then'],
|
|
174
|
+
arrayIndices,
|
|
175
|
+
block,
|
|
176
|
+
controls,
|
|
177
|
+
counters,
|
|
178
|
+
event,
|
|
179
|
+
progress,
|
|
180
|
+
responses
|
|
181
|
+
});
|
|
182
|
+
this.recordSkippedActions({
|
|
183
|
+
actions: control[':else'] ?? [],
|
|
184
|
+
counters,
|
|
185
|
+
responses
|
|
186
|
+
});
|
|
187
|
+
return returned;
|
|
188
|
+
}
|
|
189
|
+
controls.push({
|
|
190
|
+
index,
|
|
191
|
+
type: ':if',
|
|
192
|
+
taken: 'else'
|
|
193
|
+
});
|
|
194
|
+
this.recordSkippedActions({
|
|
195
|
+
actions: control[':then'],
|
|
196
|
+
counters,
|
|
197
|
+
responses
|
|
198
|
+
});
|
|
199
|
+
return this.callActionLoop({
|
|
200
|
+
actions: control[':else'] ?? [],
|
|
201
|
+
arrayIndices,
|
|
202
|
+
block,
|
|
203
|
+
controls,
|
|
204
|
+
counters,
|
|
205
|
+
event,
|
|
206
|
+
progress,
|
|
207
|
+
responses
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
// ':switch' - cases are evaluated in order and lazily: the first truthy ':case' wins,
|
|
211
|
+
// later cases are never evaluated, matching the routine ':switch'.
|
|
212
|
+
let matched = false;
|
|
213
|
+
let returned = false;
|
|
214
|
+
for (const [casePosition, caseObject] of control[':switch'].entries()){
|
|
215
|
+
if (!matched) {
|
|
216
|
+
const condition = this.evaluateControlValue({
|
|
217
|
+
arrayIndices,
|
|
218
|
+
block,
|
|
219
|
+
event,
|
|
220
|
+
input: caseObject[':case'],
|
|
221
|
+
node: caseObject,
|
|
222
|
+
responses
|
|
223
|
+
});
|
|
224
|
+
if (condition) {
|
|
225
|
+
matched = true;
|
|
226
|
+
controls.push({
|
|
227
|
+
index,
|
|
228
|
+
type: ':switch',
|
|
229
|
+
taken: casePosition
|
|
230
|
+
});
|
|
231
|
+
returned = await this.callActionLoop({
|
|
232
|
+
actions: caseObject[':then'],
|
|
233
|
+
arrayIndices,
|
|
234
|
+
block,
|
|
235
|
+
controls,
|
|
236
|
+
counters,
|
|
237
|
+
event,
|
|
238
|
+
progress,
|
|
239
|
+
responses
|
|
240
|
+
});
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
this.recordSkippedActions({
|
|
245
|
+
actions: caseObject[':then'],
|
|
246
|
+
counters,
|
|
247
|
+
responses
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
if (matched) {
|
|
251
|
+
this.recordSkippedActions({
|
|
252
|
+
actions: control[':default'] ?? [],
|
|
253
|
+
counters,
|
|
254
|
+
responses
|
|
255
|
+
});
|
|
256
|
+
return returned;
|
|
257
|
+
}
|
|
258
|
+
controls.push({
|
|
259
|
+
index,
|
|
260
|
+
type: ':switch',
|
|
261
|
+
taken: 'default'
|
|
262
|
+
});
|
|
263
|
+
return this.callActionLoop({
|
|
264
|
+
actions: control[':default'] ?? [],
|
|
265
|
+
arrayIndices,
|
|
266
|
+
block,
|
|
267
|
+
controls,
|
|
268
|
+
counters,
|
|
269
|
+
event,
|
|
270
|
+
progress,
|
|
271
|
+
responses
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
evaluateControlValue({ arrayIndices, block, event, input, node, responses }) {
|
|
275
|
+
const { output, errors: parserErrors } = this.context._internal.parser.parse({
|
|
276
|
+
actions: responses,
|
|
277
|
+
event,
|
|
278
|
+
arrayIndices,
|
|
279
|
+
input,
|
|
280
|
+
location: block.blockId
|
|
281
|
+
});
|
|
282
|
+
if (parserErrors.length > 0) {
|
|
283
|
+
const error = parserErrors[0];
|
|
284
|
+
// Report against the nearest node's '~k' when the operator carries none.
|
|
285
|
+
if (type.isNone(error.configKey)) {
|
|
286
|
+
error.configKey = node['~k'];
|
|
287
|
+
}
|
|
288
|
+
// Controls are anonymous - no responses entry, so only {error} is thrown.
|
|
289
|
+
throw {
|
|
290
|
+
error
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
return output;
|
|
294
|
+
}
|
|
295
|
+
// Records actions the chain does not execute for a control-flow reason as skipped,
|
|
296
|
+
// without parsing their operators. Controls record no responses entry, but still
|
|
297
|
+
// consume a control index so reached controls keep their depth-first numbering.
|
|
298
|
+
recordSkippedActions({ actions, counters, responses }) {
|
|
299
|
+
for (const action of actions){
|
|
300
|
+
if (isControl(action)) {
|
|
301
|
+
counters.control += 1;
|
|
302
|
+
if (':if' in action) {
|
|
303
|
+
this.recordSkippedActions({
|
|
304
|
+
actions: action[':then'],
|
|
305
|
+
counters,
|
|
306
|
+
responses
|
|
307
|
+
});
|
|
308
|
+
this.recordSkippedActions({
|
|
309
|
+
actions: action[':else'] ?? [],
|
|
310
|
+
counters,
|
|
311
|
+
responses
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
if (':switch' in action) {
|
|
315
|
+
action[':switch'].forEach((caseObject)=>{
|
|
316
|
+
this.recordSkippedActions({
|
|
317
|
+
actions: caseObject[':then'],
|
|
318
|
+
counters,
|
|
319
|
+
responses
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
this.recordSkippedActions({
|
|
323
|
+
actions: action[':default'] ?? [],
|
|
324
|
+
counters,
|
|
325
|
+
responses
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
responses[action.id] = {
|
|
331
|
+
type: action.type,
|
|
332
|
+
skipped: true,
|
|
333
|
+
index: counters.action
|
|
334
|
+
};
|
|
335
|
+
counters.action += 1;
|
|
336
|
+
}
|
|
86
337
|
}
|
|
87
338
|
async callActions({ actions, arrayIndices, block, catchActions, event, eventName, progress }) {
|
|
88
339
|
const startTimestamp = new Date();
|
|
89
340
|
const responses = {};
|
|
341
|
+
// Only events with controls gain a 'controls' array - flat chains keep their result shape.
|
|
342
|
+
const hasControls = actions.some(isControl) || catchActions.some(isControl);
|
|
343
|
+
const controls = hasControls ? [] : undefined;
|
|
344
|
+
const counters = {
|
|
345
|
+
action: 0,
|
|
346
|
+
control: 0
|
|
347
|
+
};
|
|
90
348
|
try {
|
|
91
349
|
await this.callActionLoop({
|
|
92
350
|
actions,
|
|
93
351
|
arrayIndices,
|
|
94
352
|
block,
|
|
353
|
+
controls,
|
|
354
|
+
counters,
|
|
95
355
|
event,
|
|
96
356
|
responses,
|
|
97
357
|
progress
|
|
98
358
|
});
|
|
99
359
|
} catch (error) {
|
|
100
360
|
this.logActionError(error);
|
|
361
|
+
// Catch actions restart action numbering, matching flat-chain history; the control
|
|
362
|
+
// counter continues so every control entry keeps a unique index within the event.
|
|
363
|
+
counters.action = 0;
|
|
101
364
|
try {
|
|
102
365
|
await this.callActionLoop({
|
|
103
366
|
actions: catchActions,
|
|
104
367
|
arrayIndices,
|
|
105
368
|
block,
|
|
369
|
+
controls,
|
|
370
|
+
counters,
|
|
106
371
|
event,
|
|
107
372
|
responses,
|
|
108
373
|
progress
|
|
@@ -112,6 +377,9 @@ let Actions = class Actions {
|
|
|
112
377
|
return {
|
|
113
378
|
blockId: block.blockId,
|
|
114
379
|
bounced: false,
|
|
380
|
+
...controls && {
|
|
381
|
+
controls
|
|
382
|
+
},
|
|
115
383
|
endTimestamp: new Date(),
|
|
116
384
|
error,
|
|
117
385
|
errorCatch,
|
|
@@ -125,6 +393,9 @@ let Actions = class Actions {
|
|
|
125
393
|
return {
|
|
126
394
|
blockId: block.blockId,
|
|
127
395
|
bounced: false,
|
|
396
|
+
...controls && {
|
|
397
|
+
controls
|
|
398
|
+
},
|
|
128
399
|
endTimestamp: new Date(),
|
|
129
400
|
error,
|
|
130
401
|
event,
|
|
@@ -137,6 +408,9 @@ let Actions = class Actions {
|
|
|
137
408
|
return {
|
|
138
409
|
blockId: block.blockId,
|
|
139
410
|
bounced: false,
|
|
411
|
+
...controls && {
|
|
412
|
+
controls
|
|
413
|
+
},
|
|
140
414
|
endTimestamp: new Date(),
|
|
141
415
|
event,
|
|
142
416
|
eventName,
|
|
@@ -251,6 +525,17 @@ let Actions = class Actions {
|
|
|
251
525
|
message: messages.success,
|
|
252
526
|
status: 'success'
|
|
253
527
|
});
|
|
528
|
+
// Unwrap here, after the success message: an action that ends the chain is a
|
|
529
|
+
// success like any other. Only the inner value reaches '_actions.<id>.response',
|
|
530
|
+
// so the marker never becomes app-visible state.
|
|
531
|
+
if (isStopChain(response)) {
|
|
532
|
+
return {
|
|
533
|
+
type: action.type,
|
|
534
|
+
response: response.response,
|
|
535
|
+
index,
|
|
536
|
+
stoppedChain: true
|
|
537
|
+
};
|
|
538
|
+
}
|
|
254
539
|
return {
|
|
255
540
|
type: action.type,
|
|
256
541
|
response,
|
|
@@ -273,6 +558,7 @@ let Actions = class Actions {
|
|
|
273
558
|
this.callAction = this.callAction.bind(this);
|
|
274
559
|
this.callActionLoop = this.callActionLoop.bind(this);
|
|
275
560
|
this.callActions = this.callActions.bind(this);
|
|
561
|
+
this.callControl = this.callControl.bind(this);
|
|
276
562
|
this.displayMessage = this.displayMessage.bind(this);
|
|
277
563
|
this.logActionError = this.logActionError.bind(this);
|
|
278
564
|
this.actions = context._internal.lowdefy._internal.actions;
|
package/dist/Block.js
CHANGED
|
@@ -442,7 +442,10 @@ let Block = class Block {
|
|
|
442
442
|
value: type.isNone(this.value) ? null : this.value,
|
|
443
443
|
visible: this.visibleEval.output
|
|
444
444
|
};
|
|
445
|
-
|
|
445
|
+
// Updaters register per context — a context under construction has none,
|
|
446
|
+
// so construction-time evals never setState mounted components from a
|
|
447
|
+
// previous context.
|
|
448
|
+
this.context._internal.updaters[this.id]?.();
|
|
446
449
|
});
|
|
447
450
|
const { id, blockId, class: blockClass, events, layout, loading, properties, required, skeleton, style, validate, visible, type: blockType, slots } = blockConfig;
|
|
448
451
|
this.context = context;
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import { serializer, type } from '@lowdefy/helpers';
|
|
16
|
+
import Events from './Events.js';
|
|
17
|
+
const DEFAULT_MAX_MESSAGES = 100;
|
|
18
|
+
const DEFAULT_THROTTLE_RENDER = 250;
|
|
19
|
+
const MIN_THROTTLE_RENDER = 100;
|
|
20
|
+
let WebSockets = class WebSockets {
|
|
21
|
+
client() {
|
|
22
|
+
return this.context._internal.lowdefy._internal.websocketClient;
|
|
23
|
+
}
|
|
24
|
+
initChannelState(websocketId) {
|
|
25
|
+
this.context.websockets[websocketId] = {
|
|
26
|
+
connected: false,
|
|
27
|
+
error: null,
|
|
28
|
+
lastMessage: null,
|
|
29
|
+
messageCount: 0,
|
|
30
|
+
messages: []
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
getEvents(websocketId, config) {
|
|
34
|
+
if (!this.subscriptionEvents[websocketId]) {
|
|
35
|
+
this.subscriptionEvents[websocketId] = new Events({
|
|
36
|
+
arrayIndices: [],
|
|
37
|
+
block: {
|
|
38
|
+
blockId: `subscription:${websocketId}`,
|
|
39
|
+
events: config.events ?? {}
|
|
40
|
+
},
|
|
41
|
+
context: this.context
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return this.subscriptionEvents[websocketId];
|
|
45
|
+
}
|
|
46
|
+
flush({ websocketId, maxMessages }) {
|
|
47
|
+
const buffer = this.buffers[websocketId];
|
|
48
|
+
const channel = this.context.websockets[websocketId];
|
|
49
|
+
if (!buffer || buffer.length === 0 || !channel) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const batch = buffer.splice(0, buffer.length);
|
|
53
|
+
channel.messages.push(...batch);
|
|
54
|
+
if (channel.messages.length > maxMessages) {
|
|
55
|
+
channel.messages.splice(0, channel.messages.length - maxMessages);
|
|
56
|
+
}
|
|
57
|
+
channel.lastMessage = batch[batch.length - 1];
|
|
58
|
+
channel.messageCount += batch.length;
|
|
59
|
+
this.context._internal.update();
|
|
60
|
+
this.subscriptionEvents[websocketId]?.triggerEvent({
|
|
61
|
+
name: 'onMessage',
|
|
62
|
+
event: {
|
|
63
|
+
messages: batch
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
handleMessage({ websocketId, maxMessages, serializedPayload, throttleMs }) {
|
|
68
|
+
if (!this.active.has(websocketId)) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const data = serializer.deserialize(serializedPayload)?.data;
|
|
72
|
+
this.buffers[websocketId].push(data);
|
|
73
|
+
// Leading-edge throttle: the first message in a window renders
|
|
74
|
+
// immediately, the rest batch until the window closes.
|
|
75
|
+
if (!this.flushTimers[websocketId]) {
|
|
76
|
+
this.flush({
|
|
77
|
+
websocketId,
|
|
78
|
+
maxMessages
|
|
79
|
+
});
|
|
80
|
+
const arm = ()=>{
|
|
81
|
+
this.flushTimers[websocketId] = setTimeout(()=>{
|
|
82
|
+
if ((this.buffers[websocketId] ?? []).length > 0) {
|
|
83
|
+
this.flush({
|
|
84
|
+
websocketId,
|
|
85
|
+
maxMessages
|
|
86
|
+
});
|
|
87
|
+
arm();
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
this.flushTimers[websocketId] = null;
|
|
91
|
+
}, throttleMs);
|
|
92
|
+
};
|
|
93
|
+
arm();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async subscribe({ actions, arrayIndices, event, websocketId }) {
|
|
97
|
+
if (!type.isString(websocketId)) {
|
|
98
|
+
throw new Error('Subscribe requires a websocketId.');
|
|
99
|
+
}
|
|
100
|
+
if (this.active.has(websocketId)) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const config = this.subscriptionConfig[websocketId] ?? {
|
|
104
|
+
client: {},
|
|
105
|
+
events: {},
|
|
106
|
+
payload: {},
|
|
107
|
+
websocketId
|
|
108
|
+
};
|
|
109
|
+
if (!this.context.websockets[websocketId]) {
|
|
110
|
+
this.initChannelState(websocketId);
|
|
111
|
+
}
|
|
112
|
+
const { output: payload, errors: parserErrors } = this.context._internal.parser.parse({
|
|
113
|
+
actions,
|
|
114
|
+
arrayIndices,
|
|
115
|
+
event,
|
|
116
|
+
input: config.payload ?? {},
|
|
117
|
+
location: `subscription:${websocketId}`
|
|
118
|
+
});
|
|
119
|
+
if (parserErrors.length > 0) {
|
|
120
|
+
throw parserErrors[0];
|
|
121
|
+
}
|
|
122
|
+
const events = this.getEvents(websocketId, config);
|
|
123
|
+
const channel = this.context.websockets[websocketId];
|
|
124
|
+
const throttleMs = Math.max(config.client?.throttleRender ?? DEFAULT_THROTTLE_RENDER, MIN_THROTTLE_RENDER);
|
|
125
|
+
const maxMessages = config.client?.maxMessages ?? DEFAULT_MAX_MESSAGES;
|
|
126
|
+
this.active.add(websocketId);
|
|
127
|
+
this.buffers[websocketId] = [];
|
|
128
|
+
try {
|
|
129
|
+
await this.client().subscribe({
|
|
130
|
+
websocketId,
|
|
131
|
+
payload: serializer.serialize(payload),
|
|
132
|
+
handlers: {
|
|
133
|
+
onConnected: ()=>{
|
|
134
|
+
channel.connected = true;
|
|
135
|
+
channel.error = null;
|
|
136
|
+
this.context._internal.update();
|
|
137
|
+
events.triggerEvent({
|
|
138
|
+
name: 'onSubscribe',
|
|
139
|
+
event: {}
|
|
140
|
+
});
|
|
141
|
+
},
|
|
142
|
+
onDisconnected: ()=>{
|
|
143
|
+
channel.connected = false;
|
|
144
|
+
this.context._internal.update();
|
|
145
|
+
},
|
|
146
|
+
onError: (message)=>{
|
|
147
|
+
channel.error = {
|
|
148
|
+
message
|
|
149
|
+
};
|
|
150
|
+
this.context._internal.update();
|
|
151
|
+
events.triggerEvent({
|
|
152
|
+
name: 'onError',
|
|
153
|
+
event: {
|
|
154
|
+
message
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
},
|
|
158
|
+
onMessage: (serializedPayload)=>{
|
|
159
|
+
this.handleMessage({
|
|
160
|
+
websocketId,
|
|
161
|
+
maxMessages,
|
|
162
|
+
serializedPayload,
|
|
163
|
+
throttleMs
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
} catch (error) {
|
|
169
|
+
this.active.delete(websocketId);
|
|
170
|
+
channel.error = {
|
|
171
|
+
message: error.message
|
|
172
|
+
};
|
|
173
|
+
this.context._internal.update();
|
|
174
|
+
throw error;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
subscribeAll() {
|
|
178
|
+
Object.keys(this.subscriptionConfig).forEach((websocketId)=>{
|
|
179
|
+
this.subscribe({
|
|
180
|
+
websocketId
|
|
181
|
+
}).catch((error)=>{
|
|
182
|
+
this.context._internal.lowdefy._internal.handleError(error);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
unsubscribe({ websocketId }) {
|
|
187
|
+
if (!type.isString(websocketId)) {
|
|
188
|
+
throw new Error('Unsubscribe requires a websocketId.');
|
|
189
|
+
}
|
|
190
|
+
if (!this.active.has(websocketId)) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
this.active.delete(websocketId);
|
|
194
|
+
if (this.flushTimers[websocketId]) {
|
|
195
|
+
clearTimeout(this.flushTimers[websocketId]);
|
|
196
|
+
this.flushTimers[websocketId] = null;
|
|
197
|
+
}
|
|
198
|
+
this.buffers[websocketId] = [];
|
|
199
|
+
this.client().unsubscribe({
|
|
200
|
+
websocketId
|
|
201
|
+
});
|
|
202
|
+
this.initChannelState(websocketId);
|
|
203
|
+
this.context._internal.update();
|
|
204
|
+
}
|
|
205
|
+
unsubscribeAll() {
|
|
206
|
+
[
|
|
207
|
+
...this.active
|
|
208
|
+
].forEach((websocketId)=>{
|
|
209
|
+
this.unsubscribe({
|
|
210
|
+
websocketId
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
async publish({ payload, websocketId }) {
|
|
215
|
+
if (!type.isString(websocketId)) {
|
|
216
|
+
throw new Error('Publish requires a websocketId.');
|
|
217
|
+
}
|
|
218
|
+
await this.client().publish({
|
|
219
|
+
websocketId,
|
|
220
|
+
payload: serializer.serialize(payload ?? {})
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
constructor(context){
|
|
224
|
+
this.context = context;
|
|
225
|
+
this.subscriptionConfig = {};
|
|
226
|
+
this.subscriptionEvents = {};
|
|
227
|
+
this.buffers = {};
|
|
228
|
+
this.flushTimers = {};
|
|
229
|
+
this.active = new Set();
|
|
230
|
+
this.publish = this.publish.bind(this);
|
|
231
|
+
this.subscribe = this.subscribe.bind(this);
|
|
232
|
+
this.subscribeAll = this.subscribeAll.bind(this);
|
|
233
|
+
this.unsubscribe = this.unsubscribe.bind(this);
|
|
234
|
+
this.unsubscribeAll = this.unsubscribeAll.bind(this);
|
|
235
|
+
this.context.websockets = {};
|
|
236
|
+
(this.context._internal.rootBlock.subscriptions ?? []).forEach((subscription)=>{
|
|
237
|
+
this.subscriptionConfig[subscription.websocketId] = subscription;
|
|
238
|
+
this.initChannelState(subscription.websocketId);
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
export default WebSockets;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ function createPublish({ context }) {
|
|
16
|
+
return function publish({ payload, websocketId }) {
|
|
17
|
+
return context._internal.WebSockets.publish({
|
|
18
|
+
payload,
|
|
19
|
+
websocketId
|
|
20
|
+
});
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export default createPublish;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ function createSubscribe({ actions, arrayIndices, context, event }) {
|
|
16
|
+
return function subscribe({ websocketId }) {
|
|
17
|
+
return context._internal.WebSockets.subscribe({
|
|
18
|
+
actions,
|
|
19
|
+
arrayIndices,
|
|
20
|
+
event,
|
|
21
|
+
websocketId
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export default createSubscribe;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ function createUnsubscribe({ context }) {
|
|
16
|
+
return function unsubscribe({ websocketId }) {
|
|
17
|
+
return context._internal.WebSockets.unsubscribe({
|
|
18
|
+
websocketId
|
|
19
|
+
});
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export default createUnsubscribe;
|
|
@@ -29,12 +29,15 @@ import createLink from './createLink.js';
|
|
|
29
29
|
import createLogin from './createLogin.js';
|
|
30
30
|
import createLogout from './createLogout.js';
|
|
31
31
|
import createDisplayMessage from './createDisplayMessage.js';
|
|
32
|
+
import createPublish from './createPublish.js';
|
|
32
33
|
import createRequest from './createRequest.js';
|
|
33
34
|
import createReset from './createReset.js';
|
|
34
35
|
import createResetValidation from './createResetValidation.js';
|
|
35
36
|
import createSetGlobal from './createSetGlobal.js';
|
|
36
37
|
import createSetState from './createSetState.js';
|
|
38
|
+
import createSubscribe from './createSubscribe.js';
|
|
37
39
|
import createTranslate from './createTranslate.js';
|
|
40
|
+
import createUnsubscribe from './createUnsubscribe.js';
|
|
38
41
|
import createUpdateSession from './createUpdateSession.js';
|
|
39
42
|
import createValidate from './createValidate.js';
|
|
40
43
|
function getActionMethods(props) {
|
|
@@ -56,12 +59,15 @@ function getActionMethods(props) {
|
|
|
56
59
|
link: createLink(props),
|
|
57
60
|
login: createLogin(props),
|
|
58
61
|
logout: createLogout(props),
|
|
62
|
+
publish: createPublish(props),
|
|
59
63
|
request: createRequest(props),
|
|
60
64
|
reset: createReset(props),
|
|
61
65
|
resetValidation: createResetValidation(props),
|
|
62
66
|
setGlobal: createSetGlobal(props),
|
|
63
67
|
setState: createSetState(props),
|
|
68
|
+
subscribe: createSubscribe(props),
|
|
64
69
|
translate: createTranslate(props),
|
|
70
|
+
unsubscribe: createUnsubscribe(props),
|
|
65
71
|
updateSession: createUpdateSession(props),
|
|
66
72
|
validate: createValidate(props)
|
|
67
73
|
};
|
package/dist/getContext.js
CHANGED
|
@@ -17,8 +17,9 @@ import Actions from './Actions.js';
|
|
|
17
17
|
import Slots from './Slots.js';
|
|
18
18
|
import Requests from './Requests.js';
|
|
19
19
|
import State from './State.js';
|
|
20
|
+
import WebSockets from './WebSockets.js';
|
|
20
21
|
const blockData = (config)=>{
|
|
21
|
-
const { slots, blockId, blocks, events, field, id, layout, pageId, properties, requests, required, style, type, validate, visible } = config;
|
|
22
|
+
const { slots, blockId, blocks, events, field, id, layout, pageId, properties, requests, required, style, subscriptions, type, validate, visible } = config;
|
|
22
23
|
const result = {
|
|
23
24
|
slots,
|
|
24
25
|
blockId,
|
|
@@ -32,6 +33,7 @@ const blockData = (config)=>{
|
|
|
32
33
|
requests,
|
|
33
34
|
required,
|
|
34
35
|
style,
|
|
36
|
+
subscriptions,
|
|
35
37
|
type,
|
|
36
38
|
validate,
|
|
37
39
|
visible
|
|
@@ -55,12 +57,24 @@ function getContext({ config, jsMap = {}, lowdefy, resetContext = {
|
|
|
55
57
|
throw new Error('A page must be provided to get context.');
|
|
56
58
|
}
|
|
57
59
|
const { id } = config;
|
|
58
|
-
|
|
60
|
+
// Dynamic pages are server-resolved per request — a context memoized across
|
|
61
|
+
// navigations would render the previous request's content. Rebuild when a
|
|
62
|
+
// new config object arrives (a fresh fetch), but stay memoized across
|
|
63
|
+
// re-renders of the same config: getContext runs in the render body, so
|
|
64
|
+
// rebuilding per render would loop.
|
|
65
|
+
const sameDynamicConfig = config.dynamic !== true || lowdefy.contexts[id]?._internal.pageConfig === config;
|
|
66
|
+
if (lowdefy.contexts[id] && !resetContext.reset && sameDynamicConfig) {
|
|
59
67
|
// memoize context if already created, eg between page transitions, unless the reset flag is raised
|
|
60
68
|
lowdefy.contexts[id]._internal.update();
|
|
61
69
|
return lowdefy.contexts[id];
|
|
62
70
|
}
|
|
63
|
-
|
|
71
|
+
// Lower the context reset flag — only when raised: setReset is a React
|
|
72
|
+
// state setter on the Reload component, and getContext runs in the render
|
|
73
|
+
// body, so skip the redundant cross-component setState on rebuilds where
|
|
74
|
+
// the flag is already down.
|
|
75
|
+
if (resetContext.reset) {
|
|
76
|
+
resetContext.setReset(false);
|
|
77
|
+
}
|
|
64
78
|
if (!lowdefy.inputs[id]) {
|
|
65
79
|
lowdefy.inputs[id] = {};
|
|
66
80
|
}
|
|
@@ -73,8 +87,16 @@ function getContext({ config, jsMap = {}, lowdefy, resetContext = {
|
|
|
73
87
|
state: {},
|
|
74
88
|
_internal: {
|
|
75
89
|
lowdefy,
|
|
90
|
+
// Config object reference for dynamic page memoization — identity marks
|
|
91
|
+
// which fetch this context was built from.
|
|
92
|
+
pageConfig: config,
|
|
76
93
|
rootBlock: blockData(config),
|
|
77
|
-
update: ()=>{}
|
|
94
|
+
update: ()=>{},
|
|
95
|
+
// React updaters register here per block id when the context's Block
|
|
96
|
+
// components mount — scoped per context so rebuilding over a live
|
|
97
|
+
// context (dynamic page navigation, reset) never notifies the previous
|
|
98
|
+
// context's still-mounted components.
|
|
99
|
+
updaters: {}
|
|
78
100
|
}
|
|
79
101
|
};
|
|
80
102
|
const _internal = ctx._internal;
|
|
@@ -85,6 +107,7 @@ function getContext({ config, jsMap = {}, lowdefy, resetContext = {
|
|
|
85
107
|
_internal.State = new State(ctx);
|
|
86
108
|
_internal.Actions = new Actions(ctx);
|
|
87
109
|
_internal.Requests = new Requests(ctx);
|
|
110
|
+
_internal.WebSockets = new WebSockets(ctx);
|
|
88
111
|
_internal.RootSlots = new Slots({
|
|
89
112
|
slots: {
|
|
90
113
|
root: {
|
package/dist/index.js
CHANGED
|
@@ -21,5 +21,6 @@ import getHomePathname from './getHomePathname.js';
|
|
|
21
21
|
import Requests from './Requests.js';
|
|
22
22
|
import resolveTarget from './resolveTarget.js';
|
|
23
23
|
import State from './State.js';
|
|
24
|
-
|
|
24
|
+
import stopChain from './stopChain.js';
|
|
25
|
+
export { Actions, Slots, createLink, Events, getHomePathname, Requests, resolveTarget, State, stopChain };
|
|
25
26
|
export default getContext;
|
package/dist/resolveTarget.js
CHANGED
|
@@ -14,6 +14,14 @@
|
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/ import { type, urlQuery as urlQueryFn } from '@lowdefy/helpers';
|
|
16
16
|
import getHomePathname from './getHomePathname.js';
|
|
17
|
+
// The target's own urlQuery combines with any query the url string already
|
|
18
|
+
// carries, matching the grammar semantics createLink resolved before.
|
|
19
|
+
function combineQuery(ownQuery, query) {
|
|
20
|
+
return [
|
|
21
|
+
ownQuery,
|
|
22
|
+
query
|
|
23
|
+
].filter((part)=>part !== '').join('&');
|
|
24
|
+
}
|
|
17
25
|
// Classifies a `url` grammar value into a page or external target. basePath is
|
|
18
26
|
// stripped here, never applied - the single application boundary is createUrl.
|
|
19
27
|
function classifyUrl({ lowdefy, url, query }) {
|
|
@@ -24,16 +32,10 @@ function classifyUrl({ lowdefy, url, query }) {
|
|
|
24
32
|
const questionMark = url.indexOf('?');
|
|
25
33
|
const pathname = questionMark === -1 ? url : url.slice(0, questionMark);
|
|
26
34
|
const ownQuery = questionMark === -1 ? '' : url.slice(questionMark + 1);
|
|
27
|
-
// The target's own urlQuery combines with any query the string carries,
|
|
28
|
-
// matching the grammar semantics createLink resolves today.
|
|
29
|
-
const combined = [
|
|
30
|
-
ownQuery,
|
|
31
|
-
query
|
|
32
|
-
].filter((part)=>part !== '').join('&');
|
|
33
35
|
return {
|
|
34
36
|
kind: 'page',
|
|
35
37
|
pathname,
|
|
36
|
-
query:
|
|
38
|
+
query: combineQuery(ownQuery, query)
|
|
37
39
|
};
|
|
38
40
|
}
|
|
39
41
|
// A colon-less value like `example.com` is a schemeless hostname, not a path -
|
|
@@ -59,19 +61,30 @@ function classifyUrl({ lowdefy, url, query }) {
|
|
|
59
61
|
return {
|
|
60
62
|
kind: 'page',
|
|
61
63
|
pathname,
|
|
62
|
-
query: parsed.search.replace(/^\?/, '')
|
|
64
|
+
query: combineQuery(parsed.search.replace(/^\?/, ''), query)
|
|
63
65
|
};
|
|
64
66
|
}
|
|
65
67
|
// Same origin but outside basePath (a marketing page at the origin root while
|
|
66
68
|
// the app lives at `/app`) is a whole URL - routing it would 404 in `/app`.
|
|
67
|
-
return {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
};
|
|
69
|
+
return externalTarget({
|
|
70
|
+
parsed,
|
|
71
|
+
query
|
|
72
|
+
});
|
|
71
73
|
}
|
|
74
|
+
return externalTarget({
|
|
75
|
+
parsed,
|
|
76
|
+
query
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
// An external target is handed on as one finished href, so the target's own
|
|
80
|
+
// urlQuery has to be folded in here - the consumer has no separate query to
|
|
81
|
+
// append once the value is a whole URL.
|
|
82
|
+
function externalTarget({ parsed, query }) {
|
|
83
|
+
const search = combineQuery(parsed.search.replace(/^\?/, ''), query);
|
|
84
|
+
const href = `${parsed.origin}${parsed.pathname}${search === '' ? '' : `?${search}`}${parsed.hash}`;
|
|
72
85
|
return {
|
|
73
86
|
kind: 'external',
|
|
74
|
-
href
|
|
87
|
+
href
|
|
75
88
|
};
|
|
76
89
|
}
|
|
77
90
|
// The single resolver of the navigation grammar { home, pageId, url, urlQuery }
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import { type } from '@lowdefy/helpers';
|
|
16
|
+
// A marker an action returns to end its event chain, reported as a success
|
|
17
|
+
// rather than an error. The only callers are engine actions that have
|
|
18
|
+
// navigated the browser away, where every remaining step in the chain would
|
|
19
|
+
// act on a page that is being replaced - and one of those steps competing
|
|
20
|
+
// with the navigation is the defect this exists to close.
|
|
21
|
+
//
|
|
22
|
+
// Not an app-facing control: ':return' is how app config ends a chain, and it
|
|
23
|
+
// is unchanged. There is deliberately no CONTROL_KEYS entry here.
|
|
24
|
+
//
|
|
25
|
+
// A symbol, not a string key: the wrapper is only ever inspected by callAction
|
|
26
|
+
// one frame later, and a symbol cannot collide with a field of an action's own
|
|
27
|
+
// response the way a string marker could. It also cannot survive JSON
|
|
28
|
+
// serialization, which is correct - this is an in-process control signal,
|
|
29
|
+
// never something to persist or send over the wire.
|
|
30
|
+
const STOP_CHAIN = Symbol('lowdefyStopChain');
|
|
31
|
+
function stopChain(response) {
|
|
32
|
+
return {
|
|
33
|
+
[STOP_CHAIN]: true,
|
|
34
|
+
response
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function isStopChain(value) {
|
|
38
|
+
return type.isObject(value) && value[STOP_CHAIN] === true;
|
|
39
|
+
}
|
|
40
|
+
export { STOP_CHAIN, isStopChain };
|
|
41
|
+
export default stopChain;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lowdefy/engine",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.0.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"description": "",
|
|
6
6
|
"homepage": "https://lowdefy.com",
|
|
@@ -30,18 +30,18 @@
|
|
|
30
30
|
"dist/*"
|
|
31
31
|
],
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@lowdefy/errors": "
|
|
34
|
-
"@lowdefy/helpers": "
|
|
35
|
-
"@lowdefy/operators": "
|
|
33
|
+
"@lowdefy/errors": "6.0.0",
|
|
34
|
+
"@lowdefy/helpers": "6.0.0",
|
|
35
|
+
"@lowdefy/operators": "6.0.0"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@jest/globals": "28.1.3",
|
|
39
|
-
"@lowdefy/actions-core": "
|
|
40
|
-
"@lowdefy/build": "
|
|
41
|
-
"@lowdefy/operators-js": "
|
|
42
|
-
"@lowdefy/operators-mql": "
|
|
43
|
-
"@swc/cli": "0.8.
|
|
44
|
-
"@swc/core": "1.15.
|
|
39
|
+
"@lowdefy/actions-core": "6.0.0",
|
|
40
|
+
"@lowdefy/build": "6.0.0",
|
|
41
|
+
"@lowdefy/operators-js": "6.0.0",
|
|
42
|
+
"@lowdefy/operators-mql": "6.0.0",
|
|
43
|
+
"@swc/cli": "0.8.1",
|
|
44
|
+
"@swc/core": "1.15.32",
|
|
45
45
|
"@swc/jest": "0.2.39",
|
|
46
46
|
"jest": "28.1.3"
|
|
47
47
|
},
|