@hotmeshio/hotmesh 0.0.10 → 0.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -4
- package/build/modules/errors.d.ts +17 -1
- package/build/modules/errors.js +29 -1
- package/build/package.json +2 -1
- package/build/services/activities/activity.d.ts +1 -0
- package/build/services/activities/activity.js +13 -2
- package/build/services/activities/cycle.js +6 -1
- package/build/services/activities/trigger.js +2 -3
- package/build/services/collator/index.d.ts +8 -0
- package/build/services/collator/index.js +11 -1
- package/build/services/durable/factory.d.ts +18 -1
- package/build/services/durable/factory.js +46 -4
- package/build/services/durable/handle.js +25 -7
- package/build/services/durable/worker.d.ts +3 -3
- package/build/services/durable/worker.js +16 -10
- package/build/services/durable/workflow.js +1 -1
- package/build/services/pipe/functions/math.d.ts +4 -0
- package/build/services/pipe/functions/math.js +73 -0
- package/build/services/pipe/functions/number.d.ts +0 -4
- package/build/services/pipe/functions/number.js +0 -73
- package/build/services/signaler/stream.js +6 -3
- package/build/types/durable.d.ts +7 -2
- package/build/types/index.d.ts +1 -0
- package/modules/errors.ts +42 -1
- package/package.json +2 -1
- package/services/activities/activity.ts +14 -2
- package/services/activities/cycle.ts +6 -1
- package/services/activities/trigger.ts +2 -3
- package/services/collator/index.ts +12 -1
- package/services/durable/factory.ts +46 -4
- package/services/durable/handle.ts +23 -8
- package/services/durable/worker.ts +27 -12
- package/services/durable/workflow.ts +1 -1
- package/services/pipe/functions/math.ts +74 -0
- package/services/pipe/functions/number.ts +0 -75
- package/services/signaler/stream.ts +6 -3
- package/types/durable.ts +11 -4
- package/types/index.ts +15 -0
- package/build/services/dimension/index.d.ts +0 -29
- package/build/services/dimension/index.js +0 -35
- package/services/dimension/README.md +0 -73
- package/services/dimension/index.ts +0 -39
|
@@ -70,81 +70,6 @@ class NumberHandler {
|
|
|
70
70
|
round(input: number): number {
|
|
71
71
|
return Math.round(input);
|
|
72
72
|
}
|
|
73
|
-
|
|
74
|
-
add(...operands: (number | number[])[]): number {
|
|
75
|
-
// @ts-ignore
|
|
76
|
-
return operands.reduce((a: number, b: number | number[]) => {
|
|
77
|
-
if (Array.isArray(b)) {
|
|
78
|
-
return a + this.add(...b);
|
|
79
|
-
} else {
|
|
80
|
-
return a + b;
|
|
81
|
-
}
|
|
82
|
-
}, 0);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
subtract(...operands: (number | number[])[]): number {
|
|
86
|
-
if (operands.length === 0) {
|
|
87
|
-
throw new Error('At least one operand is required.');
|
|
88
|
-
}
|
|
89
|
-
let flatOperands: number[] = [];
|
|
90
|
-
operands.forEach((op: number | number[]) => {
|
|
91
|
-
if (Array.isArray(op)) {
|
|
92
|
-
flatOperands = [...flatOperands, ...op];
|
|
93
|
-
} else {
|
|
94
|
-
flatOperands.push(op);
|
|
95
|
-
}
|
|
96
|
-
});
|
|
97
|
-
if (flatOperands.length === 0) {
|
|
98
|
-
throw new Error('At least one operand is required after flattening.');
|
|
99
|
-
}
|
|
100
|
-
const result = flatOperands.reduce((a: number, b: number, i: number) => {
|
|
101
|
-
return i === 0 ? a : a - b;
|
|
102
|
-
});
|
|
103
|
-
return result;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
multiply(...operands: (number | number[])[]): number {
|
|
108
|
-
if (operands.length === 0) {
|
|
109
|
-
throw new Error('At least one operand is required.');
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// @ts-ignore
|
|
113
|
-
return operands.reduce((a: number, b: number | number[]) => {
|
|
114
|
-
if (Array.isArray(b)) {
|
|
115
|
-
return a * this.multiply(...b);
|
|
116
|
-
} else {
|
|
117
|
-
return a * b;
|
|
118
|
-
}
|
|
119
|
-
}, 1);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
divide(...operands: (number | number[])[]): number {
|
|
123
|
-
if (operands.length === 0) {
|
|
124
|
-
throw new Error('At least one operand is required.');
|
|
125
|
-
}
|
|
126
|
-
let flatOperands: number[] = [];
|
|
127
|
-
operands.forEach((op: number | number[]) => {
|
|
128
|
-
if (Array.isArray(op)) {
|
|
129
|
-
flatOperands = [...flatOperands, ...op];
|
|
130
|
-
} else {
|
|
131
|
-
flatOperands.push(op);
|
|
132
|
-
}
|
|
133
|
-
});
|
|
134
|
-
if (flatOperands.length === 0) {
|
|
135
|
-
throw new Error('At least one operand is required after flattening.');
|
|
136
|
-
}
|
|
137
|
-
const result = flatOperands.reduce((a: number, b: number, i: number) => {
|
|
138
|
-
if (b === 0) {
|
|
139
|
-
return NaN;
|
|
140
|
-
}
|
|
141
|
-
return i === 0 ? a : a / b;
|
|
142
|
-
});
|
|
143
|
-
if (isNaN(result)) {
|
|
144
|
-
return NaN;
|
|
145
|
-
}
|
|
146
|
-
return result;
|
|
147
|
-
}
|
|
148
73
|
}
|
|
149
74
|
|
|
150
75
|
export { NumberHandler };
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
StreamStatus
|
|
17
17
|
} from '../../types/stream';
|
|
18
18
|
|
|
19
|
-
const MAX_RETRIES =
|
|
19
|
+
const MAX_RETRIES = 3; //local retry; 10, 100, 1000ms
|
|
20
20
|
const MAX_TIMEOUT_MS = 60000;
|
|
21
21
|
const GRADUATED_INTERVAL_MS = 5000;
|
|
22
22
|
const BLOCK_DURATION = 15000; //Set to `15` so SIGINT/SIGTERM can interrupt; set to `0` to BLOCK indefinitely
|
|
@@ -189,8 +189,11 @@ class StreamSignaler {
|
|
|
189
189
|
const policy = policies?.[errorCode];
|
|
190
190
|
const maxRetries = policy?.[0];
|
|
191
191
|
const tryCount = Math.min(input.metadata.try || 0, MAX_RETRIES);
|
|
192
|
-
|
|
193
|
-
|
|
192
|
+
//only possible values for maxRetries are 1, 2, 3
|
|
193
|
+
//only possible values for tryCount are 0, 1, 2
|
|
194
|
+
if (maxRetries > tryCount) {
|
|
195
|
+
// 10ms, 100ms, or 1000ms delays between system retries
|
|
196
|
+
return[true, Math.pow(10, tryCount + 1)];
|
|
194
197
|
}
|
|
195
198
|
return [false, 0];
|
|
196
199
|
}
|
package/types/durable.ts
CHANGED
|
@@ -9,7 +9,7 @@ type WorkflowOptions = {
|
|
|
9
9
|
workflowSpan?: string;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
type
|
|
12
|
+
type ActivityWorkflowDataType = {
|
|
13
13
|
activityName: string;
|
|
14
14
|
arguments: any[];
|
|
15
15
|
workflowId: string;
|
|
@@ -41,7 +41,13 @@ type WorkerConfig = {
|
|
|
41
41
|
connection: Connection;
|
|
42
42
|
namespace?: string; //`appid` in the YAML (e.g, 'default')
|
|
43
43
|
taskQueue: string; //`subscribes` in the YAML (e.g, 'hello-world')
|
|
44
|
-
workflow: Function //target function to run
|
|
44
|
+
workflow: Function; //target function to run
|
|
45
|
+
options?: WorkerOptions;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
type WorkerOptions = {
|
|
49
|
+
maxSystemRetries?: number; //1-3 (10ms, 100ms, 1_000ms)
|
|
50
|
+
backoffExponent?: number; //2-10ish
|
|
45
51
|
}
|
|
46
52
|
|
|
47
53
|
type ContextType = {
|
|
@@ -67,7 +73,7 @@ type ActivityConfig = {
|
|
|
67
73
|
|
|
68
74
|
export {
|
|
69
75
|
ActivityConfig,
|
|
70
|
-
|
|
76
|
+
ActivityWorkflowDataType,
|
|
71
77
|
ClientConfig,
|
|
72
78
|
ContextType,
|
|
73
79
|
ConnectionConfig,
|
|
@@ -76,6 +82,7 @@ export {
|
|
|
76
82
|
ProxyType,
|
|
77
83
|
Registry,
|
|
78
84
|
WorkerConfig,
|
|
85
|
+
WorkerOptions,
|
|
79
86
|
WorkflowDataType,
|
|
80
87
|
WorkflowOptions,
|
|
81
|
-
};
|
|
88
|
+
};
|
package/types/index.ts
CHANGED
|
@@ -26,6 +26,21 @@ export { CacheMode } from './cache';
|
|
|
26
26
|
export {
|
|
27
27
|
CollationFaultType,
|
|
28
28
|
CollationStage } from './collator';
|
|
29
|
+
export {
|
|
30
|
+
ActivityConfig,
|
|
31
|
+
ActivityWorkflowDataType,
|
|
32
|
+
ClientConfig,
|
|
33
|
+
ContextType,
|
|
34
|
+
ConnectionConfig,
|
|
35
|
+
Connection,
|
|
36
|
+
NativeConnection,
|
|
37
|
+
ProxyType,
|
|
38
|
+
Registry,
|
|
39
|
+
WorkerConfig,
|
|
40
|
+
WorkerOptions,
|
|
41
|
+
WorkflowDataType,
|
|
42
|
+
WorkflowOptions,
|
|
43
|
+
}from './durable'
|
|
29
44
|
export {
|
|
30
45
|
HookCondition,
|
|
31
46
|
HookConditions,
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { HotMeshGraph } from '../../types/hotmesh';
|
|
2
|
-
declare class DimensionService {
|
|
3
|
-
static targetLength: number;
|
|
4
|
-
/**
|
|
5
|
-
* entry point for compiler-type activities. This is called by the compiler
|
|
6
|
-
* to bind the sorted activity IDs to the trigger activity. These are then used
|
|
7
|
-
* at runtime by the activities to track job/activity status.
|
|
8
|
-
* @param graphs
|
|
9
|
-
*/
|
|
10
|
-
static compile(graphs: HotMeshGraph[]): void;
|
|
11
|
-
/**
|
|
12
|
-
* All activities exist on a dimensional plane. Zero
|
|
13
|
-
* is the default and is implied if no dimension is
|
|
14
|
-
* present in the hash item key. EVERY value in the
|
|
15
|
-
* job ledger is dimensionalized even if the dimension
|
|
16
|
-
* is not present. The key, `AaA`, might not contain
|
|
17
|
-
* a dimensional index, but it is still implicitly
|
|
18
|
-
* dimensionalized as `AaA,0` (assuming a trigger).
|
|
19
|
-
* A value of `AxY,0,0,0,0,1,0,0` would reflect that
|
|
20
|
-
* an ancestor activity was dimensionalized beyond
|
|
21
|
-
* the default. The dimensional string must
|
|
22
|
-
* be included if not zero. There is likely a preceding
|
|
23
|
-
* sibling dimension, so it would not need to include
|
|
24
|
-
* the suffix, so these addresses are equivalent:
|
|
25
|
-
* `AxY,0,0,0,0,0,0,0` == `AxY` for said sibling.
|
|
26
|
-
*/
|
|
27
|
-
static getSeed(index?: number): string;
|
|
28
|
-
}
|
|
29
|
-
export { DimensionService };
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.DimensionService = void 0;
|
|
4
|
-
class DimensionService {
|
|
5
|
-
/**
|
|
6
|
-
* entry point for compiler-type activities. This is called by the compiler
|
|
7
|
-
* to bind the sorted activity IDs to the trigger activity. These are then used
|
|
8
|
-
* at runtime by the activities to track job/activity status.
|
|
9
|
-
* @param graphs
|
|
10
|
-
*/
|
|
11
|
-
static compile(graphs) {
|
|
12
|
-
}
|
|
13
|
-
/**
|
|
14
|
-
* All activities exist on a dimensional plane. Zero
|
|
15
|
-
* is the default and is implied if no dimension is
|
|
16
|
-
* present in the hash item key. EVERY value in the
|
|
17
|
-
* job ledger is dimensionalized even if the dimension
|
|
18
|
-
* is not present. The key, `AaA`, might not contain
|
|
19
|
-
* a dimensional index, but it is still implicitly
|
|
20
|
-
* dimensionalized as `AaA,0` (assuming a trigger).
|
|
21
|
-
* A value of `AxY,0,0,0,0,1,0,0` would reflect that
|
|
22
|
-
* an ancestor activity was dimensionalized beyond
|
|
23
|
-
* the default. The dimensional string must
|
|
24
|
-
* be included if not zero. There is likely a preceding
|
|
25
|
-
* sibling dimension, so it would not need to include
|
|
26
|
-
* the suffix, so these addresses are equivalent:
|
|
27
|
-
* `AxY,0,0,0,0,0,0,0` == `AxY` for said sibling.
|
|
28
|
-
*/
|
|
29
|
-
static getSeed(index = 0) {
|
|
30
|
-
return `,${index}`;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
exports.DimensionService = DimensionService;
|
|
34
|
-
//max int digit count that supports `hincrby`
|
|
35
|
-
DimensionService.targetLength = 15;
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
# Optimizing Cycle Management Through Shared Collation State
|
|
2
|
-
Activity state is managed using a 15-digit integer. For all non-trigger activities, this value is initialized by the parent activity/preceding in the DAG. (Triggers are initialized in a 'completed' state, since their execution only reflects the completion of the Leg 2 Duplex.)
|
|
3
|
-
|
|
4
|
-
>Presetting the value for subsequent generations is critical to establishing return receipt checks, so that the system can guarantee durability and idempotency in the result of system failure during handoff.
|
|
5
|
-
|
|
6
|
-
The first three digits of the 15-digit integer track and convey the activity lifecycle status. Supporting 3 states using any 3 integers is the only critical requirement for the chosen integers. As long as the backend system supports decrement and increment commands that return the modified integer value, the system can durably track state using the strategy described here. In the reference implementation, the digit `9` is “pending” and `8` is “complete”. Additional digits can be employed to convey additional states.
|
|
7
|
-
|
|
8
|
-
The remaining 12 digits offer 1 million distinct dimensional threads for activity expansion. Dimensional Threads isolate and track those activities in the workflow that run in a cycle. They ensure that no naming collisions occur, even if the same activity is run multiple times. Each time duplex leg 2 of an activity returns with its payload, it can traverse the primary execution tree that remains (the remaining nodes in the graph); however, if the message includes a ‘pending’ status, it means that the channel will remain open, necessitating a dimensional execution thread be added to the flow, so that subsequent incoming messages can be tracked. This pattern likewise exists for `iterator` and `mark/goto` activities in that every adjacent activity that follows in the DAG will be uniquely addressed using a sequential dimensional thread that reflects its location in the collection being iterated.
|
|
9
|
-
|
|
10
|
-
In the following series, [S] represents interactions with the STREAM while [D] represents interactions with the backend DATA STORE. The job begins with the trigger ( the entry point for the rooted tree). Every activity runs as a duplexed exchange with the request and response channels fully decoupled.
|
|
11
|
-
|
|
12
|
-
The trigger is unique, however, in that it does not execute Leg 1. This is because the outside caller is responsible for Leg 1, including generating the input data to be sent to the trigger to kick off the workflow.
|
|
13
|
-
|
|
14
|
-
TRIGGER LEG 2
|
|
15
|
-
|
|
16
|
-
```bash
|
|
17
|
-
[D] a A | SET IF UNIQUE <JOBID> (ENSURE UNIQUE JOB ID)
|
|
18
|
-
888000001000001 [D] a B | SET TRIGGER STATE (SET TO 888000001000001)
|
|
19
|
-
[S] b C | [SPAWN ADJACENT (CHILD) NODES]
|
|
20
|
-
999000000000000 [D] a E | PRESET SPAWNED ADJACENT NODE(S) ACTIVITY STATE
|
|
21
|
-
F => RETURN JOB ID TO CALLER
|
|
22
|
-
WORKER ACTIVITY LEG 1
|
|
23
|
-
[S] a a | [DEQUEUE ACTIVITY]
|
|
24
|
-
999000000000000 *ACTIVITY STATUS WAS PRESET BY THE PARENT NODE*
|
|
25
|
-
^-------------- [D] b c | UPDATE ACTIVITY STATUS (DECREMENT to 899*)
|
|
26
|
-
=> FIX or CANCEL `IF < /^8\d+$/` [ACK/DELETE]
|
|
27
|
-
[S] c d | [ENQUEUE WORKER REQUEST]
|
|
28
|
-
^------------- [D] b e | UPDATE ACTIVITY STATUS (DECREMENT to 889*)
|
|
29
|
-
[S] a f | [ACK/DELETE]
|
|
30
|
-
WORKER (SYNCHRONOUS | VARIANT 1 OF 2)
|
|
31
|
-
[S] a g | [DEQUEUE WORKER REQUEST]
|
|
32
|
-
^^^------------ [D] b h | QUERY ACTIVITY Status (CHECK IF JOB ACTIVE)
|
|
33
|
-
=> CANCEL `IF != /^889\d+$/` => [ACK/DELETE]
|
|
34
|
-
X X => EXEC WORKER
|
|
35
|
-
[S] d j | [ENQUEUE WORKER RESPONSE]
|
|
36
|
-
[S] a k | [ACK/DELETE]
|
|
37
|
-
WORKER (ASYNCHRONOUS | VARIANT 2 OF 2)
|
|
38
|
-
[S] a g | [DEQUEUE WORKER REQUEST]
|
|
39
|
-
^^^------------ [D] b h | QUERY ACTIVITY Status (CHECK IF JOB ACTIVE)
|
|
40
|
-
=> CANCEL `IF != /^889\d+$/` => [ACK/DELETE]
|
|
41
|
-
X X => SAVE JOB METADATA | START WORKER
|
|
42
|
-
[S] a k | [ACK/DELETE]
|
|
43
|
-
=> => =>
|
|
44
|
-
X X => => => STOP WORKER (ALL DONE)
|
|
45
|
-
[S] d j | [ENQUEUE WORKER RESPONSE]
|
|
46
|
-
WORKER ACTIVITY LEG 2 (PROCESS PENDING | VARIANT 1 of 3)
|
|
47
|
-
[S] a l | [DEQUEUE WORKER RESPONSE]
|
|
48
|
-
^^^^^^------ [D] b m | INCREMENT DIMENSIONAL THREAD ENTRY COUNT by 1
|
|
49
|
-
=> CANCEL `IF != /^889\d+$/` => [ACK/DELETE]
|
|
50
|
-
[S] c n | [ENQUEUE ADJACENT (CHILD) NODES]
|
|
51
|
-
^^^^^^ [D] b o | INCREMENT DIMENSIONAL THREAD EXIT COUNT by 1
|
|
52
|
-
999000000000000 [D] b q | PRESET SPAWNED ADJACENT NODE(S) STATE
|
|
53
|
-
[S] a s | [ACK/DELETE]
|
|
54
|
-
WORKER ACTIVITY LEG 2 (PROCESS SUCCESS [OR CAUGHT ERROR] | VARIANT 2 of 3)
|
|
55
|
-
[S] a l | [DEQUEUE WORKER RESPONSE]
|
|
56
|
-
^^^^^^------ [D] b m | INCREMENT DIMENSIONAL THREAD ENTRY COUNT by 1
|
|
57
|
-
=> CANCEL `IF != /^889\d+$/` => [ACK/DELETE]
|
|
58
|
-
[S] c n | [ENQUEUE ADJACENT (CHILD) NODES]
|
|
59
|
-
^^^^^^ [D] b o | INCREMENT DIMENSIONAL THREAD EXIT COUNT by 1
|
|
60
|
-
^------------ [D] b p | UPDATE ACTIVITY STATUS (DECREMENT to 888*)
|
|
61
|
-
999000000000000 [D] b q | PRESET SPAWNED ADJACENT NODE(S) STATE
|
|
62
|
-
[S] d r | [ENQUEUE `JOB CLEANUP TASKS` IF JOB STATE = 0]
|
|
63
|
-
[S] a s | [ACK/DELETE]
|
|
64
|
-
WORKER ACTIVITY LEG 2 (PROCESS UNCAUGHT ERROR | VARIANT 3 of 3)
|
|
65
|
-
[S] a l | [DEQUEUE WORKER RESPONSE]
|
|
66
|
-
^^^^^^------ [D] b m | INCREMENT DIMENSIONAL THREAD ENTRY COUNT by 1
|
|
67
|
-
=> CANCEL `IF != /^\d{2}9\d+$/` => [ACK/DELETE]
|
|
68
|
-
^------------ [D] b p | UPDATE ACTIVITY STATUS (DECREMENT to 887*)
|
|
69
|
-
[S] d r | [ENQUEUE `JOB CLEANUP TASKS`]
|
|
70
|
-
[S] a s | [ACK/DELETE]
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
Streams are used when executing an activity (such as transitioning to a child activity) as they guarantee that the child activity will be fully created and initialized before the request is marked for deletion. Even if the system has a catastrophic failure, the chain of custody can be guaranteed through the use of streams when the system comes online. If, for example, the system crashed during activity processing, the event history will reveal exactly where in the process it occurred, so that it is possible to restore state while still guaranteeing idempotency.
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { HotMeshGraph } from '../../types/hotmesh';
|
|
2
|
-
|
|
3
|
-
class DimensionService {
|
|
4
|
-
|
|
5
|
-
//max int digit count that supports `hincrby`
|
|
6
|
-
static targetLength = 15;
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* entry point for compiler-type activities. This is called by the compiler
|
|
10
|
-
* to bind the sorted activity IDs to the trigger activity. These are then used
|
|
11
|
-
* at runtime by the activities to track job/activity status.
|
|
12
|
-
* @param graphs
|
|
13
|
-
*/
|
|
14
|
-
static compile(graphs: HotMeshGraph[]) {
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* All activities exist on a dimensional plane. Zero
|
|
20
|
-
* is the default and is implied if no dimension is
|
|
21
|
-
* present in the hash item key. EVERY value in the
|
|
22
|
-
* job ledger is dimensionalized even if the dimension
|
|
23
|
-
* is not present. The key, `AaA`, might not contain
|
|
24
|
-
* a dimensional index, but it is still implicitly
|
|
25
|
-
* dimensionalized as `AaA,0` (assuming a trigger).
|
|
26
|
-
* A value of `AxY,0,0,0,0,1,0,0` would reflect that
|
|
27
|
-
* an ancestor activity was dimensionalized beyond
|
|
28
|
-
* the default. The dimensional string must
|
|
29
|
-
* be included if not zero. There is likely a preceding
|
|
30
|
-
* sibling dimension, so it would not need to include
|
|
31
|
-
* the suffix, so these addresses are equivalent:
|
|
32
|
-
* `AxY,0,0,0,0,0,0,0` == `AxY` for said sibling.
|
|
33
|
-
*/
|
|
34
|
-
static getSeed(index = 0): string {
|
|
35
|
-
return `,${index}`;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export { DimensionService };
|