@enfocussw/switch-scripting-context 0.1.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/CHANGELOG.md +504 -0
- package/README.md +192 -0
- package/bin/cli.js +8 -0
- package/dist/init.d.ts +78 -0
- package/dist/init.js +894 -0
- package/docs/switch-api/api-versions.md +127 -0
- package/docs/switch-api/connection.md +82 -0
- package/docs/switch-api/document-classes.md +189 -0
- package/docs/switch-api/entry-points.md +185 -0
- package/docs/switch-api/enums.md +181 -0
- package/docs/switch-api/execution-environment.md +143 -0
- package/docs/switch-api/flow-element.md +143 -0
- package/docs/switch-api/http.md +96 -0
- package/docs/switch-api/job-patterns.md +238 -0
- package/docs/switch-api/job.md +187 -0
- package/docs/switch-api/logging.md +117 -0
- package/docs/switch-api/switch.md +210 -0
- package/docs/switch-appstore/app-guidelines.md +281 -0
- package/docs/switch-appstore/app-manual.md +84 -0
- package/docs/switch-appstore/app-store-listing.md +69 -0
- package/docs/switch-appstore/app-store-submission.md +83 -0
- package/docs/switch-project/debugging.md +61 -0
- package/docs/switch-project/logs-and-dataroot.md +80 -0
- package/docs/switch-project/node-versions.md +87 -0
- package/docs/switch-project/project-planning.md +149 -0
- package/docs/switch-project/property-documentation.md +75 -0
- package/docs/switch-project/property-editors.md +249 -0
- package/docs/switch-project/script-declaration.md +407 -0
- package/docs/switch-project/script-structure.md +157 -0
- package/docs/switch-project/tooling.md +165 -0
- package/docs/switch-project/vscode.md +90 -0
- package/docs/switch-scripting.md +70 -0
- package/package.json +65 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: job-patterns
|
|
3
|
+
category: switch-api
|
|
4
|
+
order: 16
|
|
5
|
+
summary: "`Job` **rules and gotchas**: file access semantics, temp cleanup, which `sendTo*()` suits which connection type, dataset/child ordering, locking shared global data, executor limits"
|
|
6
|
+
triggers:
|
|
7
|
+
- "Writing or reviewing job-handling code. Pair with `job.md` when you also need signatures"
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Job Patterns and Behavioral Rules
|
|
11
|
+
|
|
12
|
+
Common behavioral rules for file access, routing, temp file cleanup, and child jobs. These are the most frequent sources of bugs.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
<!-- docs-index:contents begin -->
|
|
17
|
+
## Contents
|
|
18
|
+
|
|
19
|
+
- File access semantics
|
|
20
|
+
- Temp file cleanup
|
|
21
|
+
- Platform-independent paths
|
|
22
|
+
- Sending jobs
|
|
23
|
+
- Dataset writes must precede child job creation
|
|
24
|
+
- processLater
|
|
25
|
+
- Shared state in global data
|
|
26
|
+
- Executor cleanup thresholds
|
|
27
|
+
- Driving a third-party CLI application
|
|
28
|
+
<!-- docs-index:contents end -->
|
|
29
|
+
|
|
30
|
+
## File access semantics
|
|
31
|
+
|
|
32
|
+
`job.get(accessLevel)` and `job.getDataset(name, accessLevel)` behave differently depending on the access level:
|
|
33
|
+
|
|
34
|
+
| Access level | What it returns | Modification |
|
|
35
|
+
|---|---|---|
|
|
36
|
+
| `AccessLevel.ReadOnly` | Path in the element's **input folder** | Throws if the file is modified |
|
|
37
|
+
| `AccessLevel.ReadWrite` | Path in a **temp location** (copy) | Auto-uploaded to the output folder when `sendTo*()` is called |
|
|
38
|
+
|
|
39
|
+
Always use `AccessLevel.ReadOnly` unless you need to modify the file content.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Temp file cleanup
|
|
44
|
+
|
|
45
|
+
Switch does **not** automatically clean up files you create or pass to job-creation methods. The script is responsible:
|
|
46
|
+
|
|
47
|
+
- Files/folders passed to `flowElement.createJob(path)` — delete after routing.
|
|
48
|
+
- Files/folders passed to `job.createChild(path)` — delete after routing.
|
|
49
|
+
- Files/folders passed to `job.createDataset(name, filePath, model)` — delete after routing.
|
|
50
|
+
|
|
51
|
+
Use `flowElement.createPathWithName()` to create temp files by default — its output lives in the executor's temp area, which Switch removes automatically on executor refresh. Even so, delete explicitly after routing rather than relying on that refresh (see below). `createPathWithName()` needs Switch 24.0+; on an older Switch, build a unique path under `os.tmpdir()` and delete it explicitly after routing.
|
|
52
|
+
|
|
53
|
+
If a temp file is created some other way (e.g. `fs`/`path` writing outside the temp area, or a library that writes its own scratch files), the same rule applies: the script must delete it explicitly. Don't rely on Switch's executor-refresh cleanup for files outside the temp area — it only removes what it put there, so anything else creates a permanent leak.
|
|
54
|
+
|
|
55
|
+
The [`tmp`](https://www.npmjs.com/package/tmp) npm package is common in scripts written by app creators, but it is **not recommended** here — prefer `flowElement.createPathWithName()` instead. If `tmp` is used anyway: its `setGracefulCleanup()` is not always reliable in the Switch script execution environment, so don't depend on it for cleanup — delete the file explicitly after routing regardless. Also pass `discardDescriptor: true` when creating a temp path (e.g. `tmp.fileSync({ discardDescriptor: true })`), or the open file descriptor can leak.
|
|
56
|
+
|
|
57
|
+
> Files accumulate between executor refreshes if not cleaned up. The executor is refreshed after 5 min idle, 5,000 tasks, 150 MB memory, or 1,024 open file handles — at which point accumulated files **created via `createPathWithName()`** are removed automatically, but do not rely on this.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Platform-independent paths
|
|
62
|
+
|
|
63
|
+
Switch Server runs on both Windows and macOS. **App guideline (mandatory for apps, recommended for
|
|
64
|
+
scripts):** a script package should work unmodified on either — paths returned by `job.get()`,
|
|
65
|
+
`flowElement.createPathWithName()`, and similar methods use the host OS's native separator, so avoid
|
|
66
|
+
hardcoding `/` or `\` when building or splitting a path, and don't assume one form when comparing or
|
|
67
|
+
matching paths. Use Node's `path` module (`path.join`, `path.resolve`, `path.sep`, `path.basename`,
|
|
68
|
+
etc.) instead of string concatenation.
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Sending jobs
|
|
73
|
+
|
|
74
|
+
After calling any `job.sendTo*()`:
|
|
75
|
+
- Only further `sendTo*()` calls and `job.fail()` are allowed on that job object.
|
|
76
|
+
- An **unmodified** incoming job can be sent to multiple connections (call `sendTo*()` multiple times).
|
|
77
|
+
- A **modified** job (ReadWrite access used) must use `job.createChild()` to produce additional copies for routing to other connections.
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
// Route unmodified job to two connections — OK
|
|
81
|
+
await job.sendToData(Connection.Level.Success);
|
|
82
|
+
await job.sendToData(Connection.Level.Warning); // only if job was not modified
|
|
83
|
+
|
|
84
|
+
// Route modified content to multiple connections — use createChild
|
|
85
|
+
const child = await job.createChild(tempPath);
|
|
86
|
+
await job.sendTo(conn1);
|
|
87
|
+
await child.sendTo(conn2);
|
|
88
|
+
// Clean up tempPath after routing
|
|
89
|
+
await fs.promises.rm(tempPath);
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**Consistency with declared connections** (applies to every script, not just apps):
|
|
93
|
+
- If `OutgoingConnections` is not `No`, the script must call some `sendTo*()` method at least once
|
|
94
|
+
for any job it actually routes. A purely timer/event-driven script that never touches a job
|
|
95
|
+
(polling an API, rotating logs, etc.) is a legitimate exception.
|
|
96
|
+
- If `OutgoingConnections="Unlimited"` (more than one outgoing connection allowed), never use
|
|
97
|
+
`job.sendToSingle()` — it only makes sense when exactly one outgoing connection exists.
|
|
98
|
+
- If no output is produced for a job, call `job.sendToNull()` rather than leaving it unrouted.
|
|
99
|
+
- `ConnectionType="Move"` (see [script-declaration.md](../switch-project/script-declaration.md#connectionfields)):
|
|
100
|
+
never use `job.sendToData()`/`job.sendToLog()` — those are for `TrafficLight` connections only. Use
|
|
101
|
+
`job.sendTo()`/`job.sendToSingle()`.
|
|
102
|
+
- `ConnectionType="TrafficLight"`: use only `job.sendToData()`/`job.sendToLog()` — never
|
|
103
|
+
`job.sendTo()`/`job.sendToSingle()`. Use `sendToData` when a Data Success/Warning/Error connection
|
|
104
|
+
is enabled in the declaration, and `sendToLog` when the corresponding Log connection is enabled.
|
|
105
|
+
Conversely, don't declare a Success/Warning/Error element the script never routes to — omit it from
|
|
106
|
+
`ConnectionFields` entirely (see the TrafficLight example in
|
|
107
|
+
[script-declaration.md](../switch-project/script-declaration.md#connectionfields)).
|
|
108
|
+
`sendToData(level)` fails the job if no connected outgoing data connection accepts that level,
|
|
109
|
+
and there is no supported way to check in advance which levels are wired up (see
|
|
110
|
+
[connection.md § Connection.Level enum](connection.md#connectionlevel-enum)). If routing at a
|
|
111
|
+
given level is optional rather than guaranteed by the flow design, expose an explicit custom
|
|
112
|
+
property that lets the flow author opt in or out, and call `job.sendToNull()` when opted out
|
|
113
|
+
instead of guessing whether `sendToData()` will succeed.
|
|
114
|
+
- Prefer `job.fail()`/`flowElement.failProcess()` for genuine processing errors (see
|
|
115
|
+
[logging.md](logging.md)) over silently routing a failed job to a generic error connection —
|
|
116
|
+
reserve an error `TrafficLight` connection for cases the flow author should be able to reroute
|
|
117
|
+
downstream, not as a substitute for `fail()`.
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Dataset writes must precede child job creation
|
|
122
|
+
|
|
123
|
+
**Known issue — pending a server-side fix; remove this section once fixed.** Applies to all current versions.
|
|
124
|
+
|
|
125
|
+
`job.createChild()` copies the parent's *current* datasets immediately (see [job.md](job.md#child-jobs)); `job.createDataset()` only registers a pending write, flushed to disk at the next `sendTo*()` call (see [job.md](job.md#datasets-metadata)). If a child is created *before* a dataset is written, the child inherits the job's existing dataset under that name instead. At flush time, the pending write fans out concurrently to the parent and to every child created so far, all reading from the same source file on disk — targets that already have a dataset by that name get the file **moved** into place, while targets that don't get it **copied**. When a mix of both happens against a single source file, the move wins the race and the copies fail with errors like `Could not place the file with the decoded data '...' into the datasets folder`, deterministically and on every retry (the source file is gone after the first attempt).
|
|
126
|
+
|
|
127
|
+
**Rule:** perform every dataset write on a job before creating any child of it. Where a script's structure naturally interleaves the two, iterate the dataset outputs first, then the child-job outputs, rather than handling each output in one pass.
|
|
128
|
+
|
|
129
|
+
Approaches that do not work:
|
|
130
|
+
- Deferring `createChild()` until after routing — the SDK rejects any of `createChild`, `setPriority`, `log`, `setPrivateData`, `listDatasets`, `removeDataset`, and others once a `sendTo*()` call has been made on the job, throwing `Method is not allowed at this time. Have you called any sendTo method already?`.
|
|
131
|
+
- Creating children first and only deferring *their* routing — a child is enlisted in the dataset flush at `createChild()` time, not when it's routed.
|
|
132
|
+
- Calling `removeDataset()` before `createDataset()` — this only clears the parent's record and cannot reach copies a child already inherited.
|
|
133
|
+
|
|
134
|
+
A dataset name that's genuinely new to the job never triggers this, since every target takes the copy path — the bug requires a pre-existing dataset of that name.
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## processLater
|
|
139
|
+
|
|
140
|
+
`job.processLater()` (Switch 22.1+) defers the job for the next `timerFired` invocation.
|
|
141
|
+
|
|
142
|
+
Constraints:
|
|
143
|
+
- Minimum deferral: **10 seconds** from the time `jobArrived` was called.
|
|
144
|
+
- Cannot be called on jobs created with `createJob()` or `createChild()`.
|
|
145
|
+
- Private data changes made before `processLater()` are preserved.
|
|
146
|
+
- New datasets created before `processLater()` are **discarded**.
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Shared state in global data
|
|
151
|
+
|
|
152
|
+
The deferred-processing pattern (register each job in global data in `jobArrived`, fetch and
|
|
153
|
+
process the registered jobs in `timerFired` with `flowElement.getJobs(ids)`) is a read-modify-write
|
|
154
|
+
of a shared tag. In `Concurrent` mode several `jobArrived` invocations run at once, so without a lock
|
|
155
|
+
two of them can read the same list and one overwrites the other's addition.
|
|
156
|
+
|
|
157
|
+
Lock the tag, change it, and write it back straight away. Do the slow work (fetching, processing and
|
|
158
|
+
sending jobs) after the lock is released, because every other invocation that reads the tag waits
|
|
159
|
+
for as long as the lock is held. The locking rules this relies on, including the one-lock-per-
|
|
160
|
+
invocation gotcha, are in [switch.md § Locking](switch.md#locking).
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
const TAG = 'pendingJobIds';
|
|
164
|
+
|
|
165
|
+
async function jobArrived(s: Switch, flowElement: FlowElement, job: Job): Promise<void> {
|
|
166
|
+
const stored = await s.getGlobalData(Scope.FlowElement, TAG, true);
|
|
167
|
+
let updated = stored;
|
|
168
|
+
try {
|
|
169
|
+
const ids: string[] = stored === '' ? [] : JSON.parse(stored);
|
|
170
|
+
if (!ids.includes(job.getId())) ids.push(job.getId());
|
|
171
|
+
updated = JSON.stringify(ids);
|
|
172
|
+
} finally {
|
|
173
|
+
await s.setGlobalData(Scope.FlowElement, TAG, updated); // releases the lock, also on error
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function timerFired(s: Switch, flowElement: FlowElement): Promise<void> {
|
|
178
|
+
// Take the list and empty it under the lock, then work on the jobs without holding it.
|
|
179
|
+
const stored = await s.getGlobalData(Scope.FlowElement, TAG, true);
|
|
180
|
+
await s.setGlobalData(Scope.FlowElement, TAG, '[]'); // releases the lock
|
|
181
|
+
const ids: string[] = stored === '' ? [] : JSON.parse(stored);
|
|
182
|
+
if (ids.length === 0) return;
|
|
183
|
+
const jobs = await flowElement.getJobs(ids);
|
|
184
|
+
// process and send each job
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
To track more than one tag, lock them together with the array form of `getGlobalData` and release
|
|
189
|
+
them with one array `setGlobalData`; a second locking call in the same invocation throws. Remove the
|
|
190
|
+
tag in `flowStopTriggered` (global data is never cleaned up automatically), and see the
|
|
191
|
+
[flow restart gotcha](entry-points.md#job-processing) for why `jobArrived` must also skip jobs that
|
|
192
|
+
are already registered.
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## Executor cleanup thresholds
|
|
197
|
+
|
|
198
|
+
The Node.js executor process is recycled when any of the following thresholds is reached:
|
|
199
|
+
|
|
200
|
+
| Threshold | Value |
|
|
201
|
+
|---|---|
|
|
202
|
+
| Idle time | 5 minutes |
|
|
203
|
+
| Tasks processed | 5,000 |
|
|
204
|
+
| Memory consumption | 150 MB |
|
|
205
|
+
| Open file handles | 1,024 |
|
|
206
|
+
|
|
207
|
+
On cleanup, any files/folders created in the temp area are removed. Always close file handles and database connections before the entry point returns.
|
|
208
|
+
|
|
209
|
+
See [execution-environment.md](execution-environment.md) for how this process model affects state persistence and error handling across job invocations.
|
|
210
|
+
|
|
211
|
+
## Driving a third-party CLI application
|
|
212
|
+
|
|
213
|
+
`findApplicationPath` and the `ApplicationPath` property are the legacy-scripting mechanism for
|
|
214
|
+
locating a third-party application; neither is available to Node.js scripts, and `ApplicationPath`
|
|
215
|
+
is a reserved name Switch silently drops from the declaration (see
|
|
216
|
+
[script-declaration.md](../switch-project/script-declaration.md#built-in-elementfields)). A Node.js script or
|
|
217
|
+
app that shells out to an external binary resolves the path itself, using the following pattern.
|
|
218
|
+
|
|
219
|
+
1. Declare an ordinary custom property for the path under a name of your own — `cliPath`, not
|
|
220
|
+
`ApplicationPath`.
|
|
221
|
+
2. Give it the editor chain `automatic;choosefile;sltextwithvar;scriptexp`, with
|
|
222
|
+
`Default="Automatic"` and `Subtype="automatic"`. The `automatic` literal editor is what lets the
|
|
223
|
+
user express "find it yourself" without typing a magic string, and `getPropertyStringValue()`
|
|
224
|
+
returns the literal `"Automatic"` for it — see
|
|
225
|
+
[property-editors.md § Literal editors](../switch-project/property-editors.md#literal-editors).
|
|
226
|
+
3. Resolve the path once per flow start, in `flowStartTriggered`: if the property reads
|
|
227
|
+
`Automatic`, run whatever discovery the application needs; otherwise take
|
|
228
|
+
`flowElement.getPropertyStringValue('cliPath')`. Verify the resulting path actually exists, then
|
|
229
|
+
store it in global data at `Scope.FlowElement`. If it doesn't exist, store an empty string
|
|
230
|
+
rather than leaving the tag unset.
|
|
231
|
+
4. In `jobArrived`/`timerFired`, read the path back from global data. An empty string means
|
|
232
|
+
discovery failed: call `flowElement.failProcess()` so the element goes into an error state,
|
|
233
|
+
instead of failing every individual job.
|
|
234
|
+
5. Remove the global data entry in `flowStopTriggered` — global data is never cleaned up
|
|
235
|
+
automatically (see [switch.md § Global data](switch.md#global-data)).
|
|
236
|
+
|
|
237
|
+
Doing discovery once at flow start rather than per job keeps a filesystem search off the job path,
|
|
238
|
+
and gives the operator a single clear element-level error when the application is missing.
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: job
|
|
3
|
+
category: switch-api
|
|
4
|
+
order: 5
|
|
5
|
+
summary: "`Job` **signatures**: routing, file access, child jobs, private data, datasets"
|
|
6
|
+
triggers:
|
|
7
|
+
- "Looking up what a `job.*` method takes, returns, or throws"
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Job Class
|
|
11
|
+
|
|
12
|
+
A `Job` represents a file or folder moving through the flow. It is passed to `jobArrived` and can be created via `flowElement.createJob()` or `job.createChild()`. Every job must be routed with a `sendTo*()` call or `fail()` before the entry point ends (or at a later time via `timerFired`).
|
|
13
|
+
> Jobs obtained via `flowElement.getJobs()` throw on `getPrivateData()`, `listDatasets()`, and `getDataset()` in the same entry point invocation — private data/metadata cannot be *read* back. `setPrivateData()`/`removePrivateData()` are not restricted this way.
|
|
14
|
+
|
|
15
|
+
<!-- docs-index:contents begin -->
|
|
16
|
+
## Contents
|
|
17
|
+
|
|
18
|
+
- Identity
|
|
19
|
+
- Priority
|
|
20
|
+
- File access
|
|
21
|
+
- Routing
|
|
22
|
+
- Failure & logging
|
|
23
|
+
- Child jobs
|
|
24
|
+
- Private data
|
|
25
|
+
- Datasets (metadata)
|
|
26
|
+
- Variables & structured data
|
|
27
|
+
<!-- docs-index:contents end -->
|
|
28
|
+
|
|
29
|
+
## Identity
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
job.getName(includeExtension?: boolean): string // default: true
|
|
33
|
+
```
|
|
34
|
+
Returns the filename without the internal ID prefix. Optionally strips the extension.
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
job.getId(): string
|
|
38
|
+
```
|
|
39
|
+
Returns the unique job ID (the prefix portion of the filename, without underscores).
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
job.isFile(): boolean
|
|
43
|
+
job.isFolder(): boolean
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Priority
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
// Switch 22.1+
|
|
50
|
+
job.getPriority(): number
|
|
51
|
+
job.setPriority(priority: number): void
|
|
52
|
+
```
|
|
53
|
+
Use `Priority.*` enum values for standard levels.
|
|
54
|
+
|
|
55
|
+
## File access
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
job.get(accessLevel: AccessLevel): Promise<string>
|
|
59
|
+
```
|
|
60
|
+
Returns the local filesystem path to the job. Use `AccessLevel.ReadOnly` to read, `AccessLevel.ReadWrite` if you will modify the file/folder. Modified content is automatically uploaded on the next `sendTo*()` call. Throws if `ReadOnly` is used but the file was modified.
|
|
61
|
+
|
|
62
|
+
## Routing
|
|
63
|
+
|
|
64
|
+
Every job must be routed exactly once. See [job-patterns.md § Sending jobs](job-patterns.md#sending-jobs) for which `sendTo*()` method is appropriate for a given `ConnectionType`/`OutgoingConnections` declaration.
|
|
65
|
+
|
|
66
|
+
**Routing is applied when the entry point returns.** Each `sendTo*()` call registers a routing action; Switch places all the jobs from one invocation in their output folders together, after it returns. If the entry point throws or is aborted, none of its routing actions reach Switch, including those for jobs routed earlier in the same call. A long `timerFired` that produces many jobs delivers them all at the end; to deliver them sooner, handle fewer items per call and let the timer fire again.
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
job.sendToNull(): Promise<void>
|
|
70
|
+
```
|
|
71
|
+
Discard the job (mark as completed with no output).
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
job.sendToSingle(newName?: string): Promise<void>
|
|
75
|
+
```
|
|
76
|
+
Send to the single outgoing move connection. Optionally rename.
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
job.sendTo(connection: Connection, newName?: string): Promise<void>
|
|
80
|
+
```
|
|
81
|
+
Send to a specific connection (any type). Get connections via `flowElement.getOutConnections()`.
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
job.sendToData(level: Connection.Level, newName?: string): Promise<void>
|
|
85
|
+
```
|
|
86
|
+
Send via a traffic light "data" connection at the specified level. Fails the job if no matching connection exists. **There is no supported way to check in advance whether a connection at that level exists** — don't call `connection.getPropertyStringValue("Success")`/`"Warning"`/`"Error"` expecting to detect this; that throws an invalid-tag error, since traffic light levels aren't a readable connection property. See [job-patterns.md § Sending jobs](job-patterns.md#sending-jobs) for the pattern (an explicit custom property plus `job.sendToNull()`).
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
job.sendToLog(level: Connection.Level, model: DatasetModel, newName?: string): Promise<void>
|
|
90
|
+
```
|
|
91
|
+
Send via a traffic light "log" connection. For "data with log" connections, attaches the job as a metadata dataset to data jobs routed via `sendToData`. Discards the job if no matching connection exists.
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
// Switch 22.1+
|
|
95
|
+
job.sendToChannel(channelId: string, newName?: string): Promise<void>
|
|
96
|
+
```
|
|
97
|
+
Send to a named channel. Unlike `sendToData`/`sendToLog`, this **throws** synchronously (it does not fail-and-route the job) if the channel has no active subscriber, or if `channelId`/`newName` is empty — catch it explicitly.
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
// Switch 22.1+
|
|
101
|
+
job.processLater(seconds?: number): Promise<void> // default: 300
|
|
102
|
+
```
|
|
103
|
+
Re-queue the job for `jobArrived` after a minimum delay (re-evaluates dynamic properties). Minimum delay: 10 seconds from `jobArrived` (a warning is logged if less). Cannot be called on newly created or child jobs. Content is not modified; newly created datasets are discarded. Private data changes are preserved.
|
|
104
|
+
|
|
105
|
+
## Failure & logging
|
|
106
|
+
|
|
107
|
+
See [logging.md](logging.md) for log level semantics and logging practice.
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
job.fail(message: string, messageParams?: (string | number | boolean)[]): void
|
|
111
|
+
```
|
|
112
|
+
Log a fatal error and move the job to Problem Jobs. Newly created jobs are not moved. Use `%1`, `%2`, etc. for substitution. When targeting Switch 20.x, always pass `messageParams`, even as `[]` (see [api-versions.md](api-versions.md#declaration-changes-to-existing-methods)).
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
job.log(level: LogLevel, message: string, messageParams?: (string | number | boolean)[]): Promise<void>
|
|
116
|
+
```
|
|
117
|
+
Log a message including job context. To log a literal `%`, pass it as a param: `job.log(LogLevel.Info, '%1', [message])`.
|
|
118
|
+
|
|
119
|
+
## Child jobs
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
job.createChild(path: string): Promise<Job>
|
|
123
|
+
```
|
|
124
|
+
Creates a new job inheriting the processing history, metadata, and private data of the parent. Takes effect immediately: the child's datasets are copied from the parent's *current* server-side state, so any pending `createDataset()` write not yet flushed by a `sendTo*()` call is not reflected in it. See [job-patterns.md](job-patterns.md#dataset-writes-must-precede-child-job-creation) — creating a child before writing a dataset is a common source of bugs. The caller is responsible for cleaning up the source file after routing (see [job-patterns.md](job-patterns.md#temp-file-cleanup)).
|
|
125
|
+
|
|
126
|
+
## Private data
|
|
127
|
+
|
|
128
|
+
Private data is arbitrary key/value storage attached to a job and passed along with it. `EnfocusSwitchPrivateDataTag` needs Switch 21.0+; before 21.0, store string values only (see [api-versions.md](api-versions.md#declaration-changes-to-existing-methods)). For each tag's string value (`"EnfocusSwitch.<name>"`) and the release that added it, see [enums.md § EnfocusSwitchPrivateDataTag](enums.md#enfocusswitchprivatedatatag).
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
job.getPrivateData(tag: string | EnfocusSwitchPrivateDataTag): Promise<any>
|
|
132
|
+
job.getPrivateData(tags?: (string | EnfocusSwitchPrivateDataTag)[]): Promise<{ tag: string, value: any }[]>
|
|
133
|
+
```
|
|
134
|
+
Read one tag (returns the value) or multiple tags / all tags (returns an array). Returns an empty string for missing tags.
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
job.setPrivateData(tag: string | EnfocusSwitchPrivateDataTag, value: any): Promise<void>
|
|
138
|
+
job.setPrivateData(privateData: { tag: string | EnfocusSwitchPrivateDataTag, value: any }[]): Promise<void>
|
|
139
|
+
```
|
|
140
|
+
Write one or multiple private data entries. Replaces existing values.
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
job.removePrivateData(tag: string | EnfocusSwitchPrivateDataTag): Promise<void>
|
|
144
|
+
job.removePrivateData(tags: (string | EnfocusSwitchPrivateDataTag)[]): Promise<void>
|
|
145
|
+
```
|
|
146
|
+
Delete one or multiple private data entries.
|
|
147
|
+
|
|
148
|
+
## Datasets (metadata)
|
|
149
|
+
|
|
150
|
+
Datasets are named files (XML, XMP, JDF, JSON, or Opaque) attached to a job. `DatasetModel.JSON` needs Switch 22.0+; read a JSON dataset back with `job.getJSONData()` (24.0+).
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
job.listDatasets(): Promise<{ name: string, model: DatasetModel, extension: string }[]>
|
|
154
|
+
```
|
|
155
|
+
List all datasets attached to the job. **Immediate**: reads server state at once, so a pending `createDataset()` write not yet flushed by `sendTo*()` will not appear.
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
job.createDataset(name: string, filePath: string, model: DatasetModel): Promise<void>
|
|
159
|
+
```
|
|
160
|
+
Attach a new dataset. **Deferred**: registers in-memory only; the file is not uploaded until the next `sendTo*()` call. Caller must clean up the source file after routing — see [job-patterns.md](job-patterns.md#temp-file-cleanup).
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
job.getDataset(name: string, accessLevel: AccessLevel): Promise<string>
|
|
164
|
+
```
|
|
165
|
+
Returns the local path to the dataset file. Prefer `AccessLevel.ReadOnly`.
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
job.removeDataset(name: string): Promise<void>
|
|
169
|
+
```
|
|
170
|
+
Remove a dataset by name. **Immediate**: updates server state at once. Throws if it does not exist.
|
|
171
|
+
|
|
172
|
+
## Variables & structured data
|
|
173
|
+
|
|
174
|
+
Everything in this section needs Switch 24.0+.
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
job.getVariableAsString(variable: string): Promise<string>
|
|
178
|
+
```
|
|
179
|
+
Returns the value of a Switch variable as a string.
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
job.getxmlData(path: any, xpathQuery: any): Promise<any>
|
|
183
|
+
job.getxmpData(path: any, xpath: any): Promise<any>
|
|
184
|
+
job.getJdfData(path: any, xpath: any): Promise<any>
|
|
185
|
+
job.getJSONData(metadata: any): Promise<any>
|
|
186
|
+
```
|
|
187
|
+
Convenience helpers to read XML/XMP/JDF/JSON data from a dataset path and return it as a parsed object. On error, these log the failure and resolve to `undefined` rather than throwing or rejecting — check the result rather than wrapping the call in `try`/`catch`.
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: logging
|
|
3
|
+
category: switch-api
|
|
4
|
+
order: 18
|
|
5
|
+
summary: "Log level semantics, logging practice, `console.log` limitation, common gotchas"
|
|
6
|
+
triggers:
|
|
7
|
+
- "Adding or reviewing `job.log()`/`flowElement.log()`/`job.fail()`/`failProcess()` calls"
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Logging Practices
|
|
11
|
+
|
|
12
|
+
Guidance for using `job.log()`, `flowElement.log()`, `job.fail()`, and `flowElement.failProcess()`
|
|
13
|
+
— see [job.md](job.md#failure--logging) and
|
|
14
|
+
[flow-element.md](flow-element.md#logging) for their signatures. `LogLevel` values are
|
|
15
|
+
listed in [enums.md](enums.md). Each log call has a small performance overhead, so where
|
|
16
|
+
and how often to log is a deliberate choice, not a default.
|
|
17
|
+
|
|
18
|
+
<!-- docs-index:contents begin -->
|
|
19
|
+
## Contents
|
|
20
|
+
|
|
21
|
+
- Log levels
|
|
22
|
+
- Don't add a verbosity property
|
|
23
|
+
- Logging in loops
|
|
24
|
+
- `console.log` does not work
|
|
25
|
+
- Don't double-log before a failure
|
|
26
|
+
- Message formatting and the `%` gotcha
|
|
27
|
+
- Signature gotcha: array vs. single value
|
|
28
|
+
- Placeholders instead of concatenation
|
|
29
|
+
- Message quality
|
|
30
|
+
- Log volume
|
|
31
|
+
<!-- docs-index:contents end -->
|
|
32
|
+
|
|
33
|
+
## Log levels
|
|
34
|
+
|
|
35
|
+
| Level | Use for |
|
|
36
|
+
|---|---|
|
|
37
|
+
| `Error` | The job/step failed or produced a wrong/unusable result. Often accompanies or precedes a hard stop, but can stand alone if the job continues in a degraded way. |
|
|
38
|
+
| `Warning` | Something unexpected happened but processing continued successfully (a fallback was used, an optional resource was missing). |
|
|
39
|
+
| `Info` | Normal, expected checkpoints worth surfacing to a flow operator without them needing debug mode (e.g. "processed 40 records", "output written to X"). |
|
|
40
|
+
| `Debug` | Verbose detail for diagnosing execution. Many users leave debug logging enabled in production to self-diagnose issues and to help app developers and Enfocus Support — leave meaningful checkpoints in, but trim noisy or no-longer-useful debug logs before shipping. **`Debug` messages only reach the log database if the Switch preference "Log debug messages" is turned on** (off by default) — see [logs-and-dataroot.md](../switch-project/logs-and-dataroot.md#retention-and-the-debug-level-gate). |
|
|
41
|
+
|
|
42
|
+
## Don't add a verbosity property
|
|
43
|
+
|
|
44
|
+
Switch already has the switch: `Debug` messages only reach the log database when the preference
|
|
45
|
+
"Log debug messages" is on. A script-level "log detail" or "verbose logging" property duplicates a
|
|
46
|
+
control the user already has, adds a row to the property pane, and (for an app) adds strings to
|
|
47
|
+
translate. Put verbose detail behind `LogLevel.Debug` and let the preference govern it.
|
|
48
|
+
|
|
49
|
+
## Logging in loops
|
|
50
|
+
|
|
51
|
+
Don't categorically avoid logging inside loops — per-item logs can matter (e.g. a per-job error).
|
|
52
|
+
But where multiple log calls can be combined without losing clarity, prefer a single summary over
|
|
53
|
+
one call per iteration, e.g. `"Found 12 matching jobs"` instead of one log line per job.
|
|
54
|
+
|
|
55
|
+
## `console.log` does not work
|
|
56
|
+
|
|
57
|
+
`console.log` only produces output when a debug session is attached (see
|
|
58
|
+
[debugging.md](../switch-project/debugging.md)); in a normal run it is not visible anywhere. Never rely on it
|
|
59
|
+
in production scripts — use `job.log()`/`flowElement.log()` for anything that should be visible in
|
|
60
|
+
the log/message pane.
|
|
61
|
+
|
|
62
|
+
## Don't double-log before a failure
|
|
63
|
+
|
|
64
|
+
`job.fail()` and `flowElement.failProcess()` already log their message as a fatal error. Don't call
|
|
65
|
+
`job.log(LogLevel.Error, ...)` / `flowElement.log(LogLevel.Error, ...)` with the same message
|
|
66
|
+
immediately before them — it duplicates the entry in the log. Use a separate `Error` log call only
|
|
67
|
+
when it conveys something distinct from the failure message.
|
|
68
|
+
|
|
69
|
+
## Message formatting and the `%` gotcha
|
|
70
|
+
|
|
71
|
+
Template literals are fine for ordinary log messages. But a string that may contain a literal `%`
|
|
72
|
+
character — commonly external/untrusted values such as cloud storage URLs — will error if
|
|
73
|
+
interpolated directly into the message, because it gets parsed as a `%1`-style substitution token.
|
|
74
|
+
Workaround: pass `'%1'` as the message and the value as the parameter, e.g.:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
job.log(LogLevel.Info, '%1', [urlThatMightContainPercent]);
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Signature gotcha: array vs. single value
|
|
81
|
+
|
|
82
|
+
`job.fail(message, messageParams?: (string | number | boolean)[])` takes an **array**.
|
|
83
|
+
`flowElement.failProcess(message, messageParam?: string | number | boolean)` takes a **single
|
|
84
|
+
value**, not an array. Passing an array to `failProcess` is a common mistake.
|
|
85
|
+
|
|
86
|
+
## Placeholders instead of concatenation
|
|
87
|
+
|
|
88
|
+
Build log messages with `%1`, `%2`, … placeholders and pass the dynamic values as
|
|
89
|
+
`messageParams`/`messageParam`, rather than concatenating or interpolating them into the message
|
|
90
|
+
string (beyond the literal-`%` escaping case above). A concatenated message can't be translated
|
|
91
|
+
correctly — word order and pluralization vary by language, so a translator needs the message
|
|
92
|
+
template and the substituted values kept separate. This matters even for a script that only ships in
|
|
93
|
+
English today if it might ever be localized later.
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
// Avoid — can't be translated correctly, and hits the literal-% gotcha if fileName contains one
|
|
97
|
+
job.log(LogLevel.Info, `Processed file ${fileName} in ${seconds}s`);
|
|
98
|
+
|
|
99
|
+
// Prefer
|
|
100
|
+
job.log(LogLevel.Info, 'Processed file %1 in %2s', [fileName, seconds]);
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Message quality
|
|
104
|
+
|
|
105
|
+
- Log messages are user-facing text — proofread them. A message with grammatical errors or typos
|
|
106
|
+
looks unfinished and undermines trust.
|
|
107
|
+
- Log an `Error` when a custom `validateProperties`/`validateConnectionProperties` check returns
|
|
108
|
+
`valid: false` for a tag, and when `getLibraryForProperty`/`getLibraryForConnectionProperty` would
|
|
109
|
+
otherwise return an empty list — see [entry-points.md § Property UI callbacks](entry-points.md#property-ui-callbacks).
|
|
110
|
+
Silently returning `false`/`[]` with no log line leaves the user without any explanation.
|
|
111
|
+
|
|
112
|
+
## Log volume
|
|
113
|
+
|
|
114
|
+
**App guideline (mandatory for apps, recommended for scripts):** don't over-log. Beyond the
|
|
115
|
+
[Logging in loops](#logging-in-loops) guidance, avoid `Info`/`Debug` calls that don't help a flow
|
|
116
|
+
operator or support engineer understand what happened — every extra message adds noise (and
|
|
117
|
+
translation burden, for an app) without adding signal.
|