@lowdefy/engine 5.5.1 → 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 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
- async callActionLoop({ actions, arrayIndices, block, event, progress, responses }) {
56
- for (const [index, action] of actions.entries()){
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
@@ -247,6 +247,9 @@ let Block = class Block {
247
247
  if (beforeVisible !== this.visibleEval.output) {
248
248
  repeat.value = true;
249
249
  }
250
+ if (this.isList() && !this.isVisible()) {
251
+ this.captureHiddenValue();
252
+ }
250
253
  if (this.visibleEval.output !== false) {
251
254
  this.propertiesEval = this.parse(this.properties);
252
255
  this.requiredEval = this.parse(this.required);
@@ -347,8 +350,23 @@ let Block = class Block {
347
350
  visibleEval: this.visibleEval
348
351
  });
349
352
  });
353
+ _define_property(this, "captureHiddenValue", ()=>{
354
+ const stateValue = get(this.context.state, this.blockId);
355
+ if (type.isUndefined(stateValue)) return;
356
+ this.hiddenValue = serializer.copy(stateValue);
357
+ });
358
+ _define_property(this, "restoreHiddenValue", ()=>{
359
+ if (type.isUndefined(this.hiddenValue)) return;
360
+ if (type.isUndefined(get(this.context.state, this.blockId))) {
361
+ this.context._internal.State.set(this.blockId, this.hiddenValue);
362
+ }
363
+ this.hiddenValue = undefined;
364
+ });
350
365
  _define_property(this, "updateState", (toSet)=>{
351
366
  if (!this.isVisible()) return;
367
+ if (this.isList()) {
368
+ this.restoreHiddenValue();
369
+ }
352
370
  if (this.isContainer() || this.isList()) {
353
371
  if (this.subSlots && this.subSlots.length > 0) {
354
372
  this.loopSubSlots((subSlotsClass)=>subSlotsClass.updateState());
@@ -424,7 +442,10 @@ let Block = class Block {
424
442
  value: type.isNone(this.value) ? null : this.value,
425
443
  visible: this.visibleEval.output
426
444
  };
427
- this.context._internal.lowdefy._internal.updateBlock(this.id);
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]?.();
428
449
  });
429
450
  const { id, blockId, class: blockClass, events, layout, loading, properties, required, skeleton, style, validate, visible, type: blockType, slots } = blockConfig;
430
451
  this.context = context;
package/dist/State.js CHANGED
@@ -12,7 +12,7 @@
12
12
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
- */ import { unset, get, serializer, set, swap, type } from '@lowdefy/helpers';
15
+ */ import { unset, get, joinPath, serializer, set, splitPath, swap, type } from '@lowdefy/helpers';
16
16
  let State = class State {
17
17
  resetState() {
18
18
  Object.keys(this.context.state).forEach((key)=>{
@@ -35,9 +35,9 @@ let State = class State {
35
35
  del(field) {
36
36
  unset(this.context.state, field);
37
37
  // remove all empty objects from state as an effect of deleted values
38
- const fields = field.split('.');
38
+ const fields = splitPath(field);
39
39
  if (fields.length > 1) {
40
- const parent = fields.slice(0, fields.length - 1).join('.');
40
+ const parent = joinPath(fields.slice(0, -1));
41
41
  const parentValue = get(this.context.state, parent);
42
42
  if (type.isObject(parentValue) && Object.keys(parentValue).length === 0) {
43
43
  this.del(parent);
@@ -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
  };
@@ -12,7 +12,7 @@
12
12
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
- */ import { applyArrayIndices, get, serializer, type } from '@lowdefy/helpers';
15
+ */ import { ReservedKeyError, applyArrayIndices, get, serializer, type } from '@lowdefy/helpers';
16
16
  const getFromObject = ({ arrayIndices, location, method, object, params })=>{
17
17
  if (params === true) params = {
18
18
  all: true
@@ -23,20 +23,26 @@ const getFromObject = ({ arrayIndices, location, method, object, params })=>{
23
23
  if (!type.isObject(params)) {
24
24
  throw new Error(`Method Error: ${method} params must be of type string, integer, boolean or object at ${location}.`);
25
25
  }
26
- if (params.key === null) return get(params, 'default', {
26
+ const defaultValue = get(params, 'default', {
27
27
  default: null,
28
28
  copy: true
29
29
  });
30
+ if (params.key === null) return defaultValue;
30
31
  if (params.all === true) return serializer.copy(object);
31
32
  if (!type.isString(params.key) && !type.isInt(params.key)) {
32
33
  throw new Error(`Method Error: ${method} params.key must be of type string or integer at ${location}.`);
33
34
  }
34
- return get(object, applyArrayIndices(arrayIndices, params.key), {
35
- default: get(params, 'default', {
36
- default: null,
35
+ try {
36
+ return get(object, applyArrayIndices(arrayIndices, params.key), {
37
+ default: defaultValue,
37
38
  copy: true
38
- }),
39
- copy: true
40
- });
39
+ });
40
+ } catch (error) {
41
+ // Tier 2: a runtime read of an app-developer keypath. There is no loud failure that helps here —
42
+ // a thrown error in the browser is worse than the default, and the reserved rule's job (refusing
43
+ // the read) is already done. Author-written identifiers fail loudly at build instead.
44
+ if (error instanceof ReservedKeyError) return defaultValue;
45
+ throw error;
46
+ }
41
47
  };
42
48
  export default getFromObject;
@@ -12,60 +12,68 @@
12
12
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
- */ import { type, urlQuery as urlQueryFn } from '@lowdefy/helpers';
15
+ */ import { type } from '@lowdefy/helpers';
16
+ import resolveTarget from './resolveTarget.js';
16
17
  function createLink({ backLink, disabledLink, lowdefy, newOriginLink, noLink, sameOriginLink }) {
17
18
  function link(props) {
18
19
  if (props.disabled === true) {
19
20
  return disabledLink(props);
20
21
  }
21
- if ([
22
- !props.pageId,
23
- !props.back,
24
- !props.home,
25
- !props.href,
26
- !props.url
27
- ].filter((v)=>!v).length > 1) {
28
- throw new Error(`Invalid Link: To avoid ambiguity, only one of 'back', 'home', 'href', 'pageId' or 'url' can be defined.`);
29
- }
22
+ // back has no pathname to resolve and cannot carry input or urlQuery.
30
23
  if (props.back === true) {
31
- // Cannot set input or urlQuery on back
32
24
  return backLink(props);
33
25
  }
34
- const query = type.isNone(props.urlQuery) ? '' : `${urlQueryFn.stringify(props.urlQuery)}`;
35
- if (props.home === true) {
36
- const pathname = `/${lowdefy.home.configured ? '' : lowdefy.home.pageId}`;
37
- return sameOriginLink({
38
- ...props,
39
- pathname,
40
- query,
41
- setInput: ()=>{
42
- lowdefy.inputs[`page:${lowdefy.home.pageId}`] = props.input ?? {};
43
- }
44
- });
45
- }
46
- if (type.isString(props.pageId)) {
47
- return sameOriginLink({
48
- ...props,
49
- pathname: `/${props.pageId}`,
50
- query,
51
- setInput: ()=>{
52
- lowdefy.inputs[`page:${props.pageId}`] = props.input ?? {};
53
- }
54
- });
55
- }
26
+ // href is an HTML-attribute passthrough the <Link> component reads, not a
27
+ // navigation target, so it never enters the grammar resolver.
56
28
  if (type.isString(props.href)) {
57
29
  return newOriginLink(props);
58
30
  }
59
- if (type.isString(props.url)) {
60
- const protocol = props.url.includes(':') ? '' : 'https://';
31
+ const target = resolveTarget({
32
+ lowdefy,
33
+ target: {
34
+ home: props.home,
35
+ pageId: props.pageId,
36
+ url: props.url,
37
+ urlQuery: props.urlQuery
38
+ }
39
+ });
40
+ if (type.isNone(target)) {
41
+ return noLink(props);
42
+ }
43
+ if (target.kind === 'external') {
44
+ // The resolver's href is the whole URL with any query already folded in,
45
+ // so it is passed as the url prop the callback reads with an empty query.
61
46
  return newOriginLink({
62
47
  ...props,
63
- url: `${protocol}${props.url}`,
64
- query
48
+ url: target.href,
49
+ query: ''
65
50
  });
66
51
  }
67
- return noLink(props);
52
+ return sameOriginLink({
53
+ ...props,
54
+ pathname: target.pathname,
55
+ query: target.query,
56
+ setInput: getSetInput({
57
+ lowdefy,
58
+ props
59
+ })
60
+ });
68
61
  }
69
62
  return link;
70
63
  }
64
+ // A page-kind url names no page, so it seeds no input - writing
65
+ // inputs['page:undefined'] is the bug family a no-op setInput avoids.
66
+ function getSetInput({ lowdefy, props }) {
67
+ if (props.home === true) {
68
+ return ()=>{
69
+ lowdefy.inputs[`page:${lowdefy.home.pageId}`] = props.input ?? {};
70
+ };
71
+ }
72
+ if (type.isString(props.pageId)) {
73
+ return ()=>{
74
+ lowdefy.inputs[`page:${props.pageId}`] = props.input ?? {};
75
+ };
76
+ }
77
+ return ()=>{};
78
+ }
71
79
  export default createLink;
@@ -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
- if (lowdefy.contexts[id] && !resetContext.reset) {
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
- resetContext.setReset(false); // lower context reset flag.
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: {
@@ -0,0 +1,34 @@
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
+ // The single reading of `lowdefy.home` for every 'home' target - the Link
17
+ // grammar in createLink and the post-auth callbackUrl ladder in the client both
18
+ // resolve through here. Returns undefined when the app names no home page:
19
+ // getHomeAndMenus resolves pageId to null for an app with no homePageId whose
20
+ // authorized menu yields no link, and each caller decides what no-home means
21
+ // for it (an invalid link, no callback target) rather than interpolating the
22
+ // missing value into a path.
23
+ function getHomePathname({ lowdefy }) {
24
+ // A configured homePageId is served at the app root - the server resolves `/`
25
+ // to that page, so the pageId is deliberately not in the path.
26
+ if (lowdefy.home?.configured === true) {
27
+ return '/';
28
+ }
29
+ if (type.isString(lowdefy.home?.pageId)) {
30
+ return `/${lowdefy.home.pageId}`;
31
+ }
32
+ return undefined;
33
+ }
34
+ export default getHomePathname;
package/dist/index.js CHANGED
@@ -17,7 +17,10 @@ import Slots from './Slots.js';
17
17
  import createLink from './createLink.js';
18
18
  import Events from './Events.js';
19
19
  import getContext from './getContext.js';
20
+ import getHomePathname from './getHomePathname.js';
20
21
  import Requests from './Requests.js';
22
+ import resolveTarget from './resolveTarget.js';
21
23
  import State from './State.js';
22
- export { Actions, Slots, createLink, Events, Requests, State };
24
+ import stopChain from './stopChain.js';
25
+ export { Actions, Slots, createLink, Events, getHomePathname, Requests, resolveTarget, State, stopChain };
23
26
  export default getContext;
@@ -0,0 +1,139 @@
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, urlQuery as urlQueryFn } from '@lowdefy/helpers';
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
+ }
25
+ // Classifies a `url` grammar value into a page or external target. basePath is
26
+ // stripped here, never applied - the single application boundary is createUrl.
27
+ function classifyUrl({ lowdefy, url, query }) {
28
+ // The leading-slash test runs before the colon-less test: `/2fa` is an
29
+ // app-relative page and colon-less, and the colon-less branch would wrongly
30
+ // give it an `https://` scheme and parse it as an off-app origin.
31
+ if (url.startsWith('/')) {
32
+ const questionMark = url.indexOf('?');
33
+ const pathname = questionMark === -1 ? url : url.slice(0, questionMark);
34
+ const ownQuery = questionMark === -1 ? '' : url.slice(questionMark + 1);
35
+ return {
36
+ kind: 'page',
37
+ pathname,
38
+ query: combineQuery(ownQuery, query)
39
+ };
40
+ }
41
+ // A colon-less value like `example.com` is a schemeless hostname, not a path -
42
+ // prepend `https://` so the URL parser reads it as an absolute URL rather than
43
+ // the app-relative path `/example.com`. Confined to here by the leading-slash
44
+ // test above, so a colon-bearing path like `/path:1` never reaches it.
45
+ const value = url.includes(':') ? url : `https://${url}`;
46
+ const origin = lowdefy._internal?.globals?.window?.location?.origin;
47
+ // No window (SSR, tests): a `url` that reaches origin classification cannot be
48
+ // placed, so it resolves to nothing rather than dereferencing a missing window.
49
+ if (type.isNone(origin)) {
50
+ return undefined;
51
+ }
52
+ const parsed = new URL(value, origin);
53
+ const basePath = lowdefy.basePath ?? '';
54
+ if (parsed.origin === origin) {
55
+ const insideBasePath = basePath === '' || parsed.pathname.startsWith(basePath);
56
+ if (insideBasePath) {
57
+ // Strip basePath so the router does not re-apply it: an absolute
58
+ // `https://myapp.com/app/reports` under basePath `/app` already carries the
59
+ // prefix, and without stripping router.push would push `/app/app/reports`.
60
+ const pathname = parsed.pathname.startsWith(basePath) ? parsed.pathname.slice(basePath.length) : parsed.pathname;
61
+ return {
62
+ kind: 'page',
63
+ pathname,
64
+ query: combineQuery(parsed.search.replace(/^\?/, ''), query)
65
+ };
66
+ }
67
+ // Same origin but outside basePath (a marketing page at the origin root while
68
+ // the app lives at `/app`) is a whole URL - routing it would 404 in `/app`.
69
+ return externalTarget({
70
+ parsed,
71
+ query
72
+ });
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}`;
85
+ return {
86
+ kind: 'external',
87
+ href
88
+ };
89
+ }
90
+ // The single resolver of the navigation grammar { home, pageId, url, urlQuery }
91
+ // for every reader. Returns a discriminated, un-prefixed target - never a string,
92
+ // never basePath-prefixed - so the page/external distinction is data the consumer
93
+ // reads rather than a shape it guesses from a leading slash.
94
+ function resolveTarget({ lowdefy, target, name = 'Link' }) {
95
+ if (!type.isObject(target)) {
96
+ return undefined;
97
+ }
98
+ const { home, pageId, url, urlQuery } = target;
99
+ const defined = [
100
+ home,
101
+ pageId,
102
+ url
103
+ ].filter((value)=>value);
104
+ if (defined.length > 1) {
105
+ throw new Error(`Invalid ${name}: To avoid ambiguity, only one of 'home', 'pageId' or 'url' can be defined.`);
106
+ }
107
+ const query = type.isNone(urlQuery) ? '' : `${urlQueryFn.stringify(urlQuery)}`;
108
+ if (home === true) {
109
+ const pathname = getHomePathname({
110
+ lowdefy
111
+ });
112
+ // An app whose home config names no page has no resolvable home - propagate
113
+ // getHomePathname's undefined rather than building the literal "/undefined".
114
+ if (type.isNone(pathname)) {
115
+ return undefined;
116
+ }
117
+ return {
118
+ kind: 'page',
119
+ pathname,
120
+ query
121
+ };
122
+ }
123
+ if (type.isString(pageId)) {
124
+ return {
125
+ kind: 'page',
126
+ pathname: `/${pageId}`,
127
+ query
128
+ };
129
+ }
130
+ if (type.isString(url)) {
131
+ return classifyUrl({
132
+ lowdefy,
133
+ url,
134
+ query
135
+ });
136
+ }
137
+ return undefined;
138
+ }
139
+ export default resolveTarget;
@@ -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": "5.5.1",
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": "5.5.1",
34
- "@lowdefy/helpers": "5.5.1",
35
- "@lowdefy/operators": "5.5.1"
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": "5.5.1",
40
- "@lowdefy/build": "5.5.1",
41
- "@lowdefy/operators-js": "5.5.1",
42
- "@lowdefy/operators-mql": "5.5.1",
43
- "@swc/cli": "0.8.0",
44
- "@swc/core": "1.15.18",
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
  },