workflow 4.2.0-beta.76 → 4.2.0-beta.78
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/docs/api-reference/workflow-ai/durable-agent.mdx +86 -1
- package/docs/api-reference/workflow-api/get-world.mdx +51 -0
- package/docs/api-reference/workflow-api/start.mdx +5 -0
- package/docs/api-reference/workflow-next/with-workflow.mdx +51 -0
- package/docs/changelog/meta.json +1 -1
- package/docs/changelog/resilient-start.mdx +327 -0
- package/docs/deploying/building-a-world.mdx +16 -6
- package/docs/deploying/index.mdx +2 -2
- package/docs/deploying/world/vercel-world.mdx +30 -1
- package/docs/errors/hook-conflict.mdx +1 -1
- package/docs/errors/node-js-module-in-workflow.mdx +1 -1
- package/docs/errors/start-invalid-workflow-function.mdx +83 -58
- package/docs/foundations/starting-workflows.mdx +1 -1
- package/docs/foundations/streaming.mdx +1 -1
- package/docs/getting-started/astro.mdx +17 -0
- package/docs/getting-started/express.mdx +17 -0
- package/docs/getting-started/fastify.mdx +17 -0
- package/docs/getting-started/hono.mdx +17 -0
- package/docs/getting-started/nestjs.mdx +83 -5
- package/docs/getting-started/next.mdx +15 -0
- package/docs/getting-started/nitro.mdx +17 -0
- package/docs/getting-started/nuxt.mdx +17 -0
- package/docs/getting-started/sveltekit.mdx +17 -0
- package/docs/getting-started/vite.mdx +17 -0
- package/docs/how-it-works/encryption.mdx +39 -2
- package/docs/how-it-works/event-sourcing.mdx +19 -2
- package/docs/how-it-works/framework-integrations.mdx +68 -11
- package/docs/observability/index.mdx +1 -1
- package/docs/testing/index.mdx +1 -1
- package/docs/testing/server-based.mdx +59 -16
- package/package.json +10 -10
|
@@ -82,12 +82,49 @@ Without `--decrypt`, encrypted fields display as `🔒 Encrypted` placeholders.
|
|
|
82
82
|
|
|
83
83
|
## Custom World Implementations
|
|
84
84
|
|
|
85
|
-
The core runtime encrypts data automatically when the `World` implementation provides a `getEncryptionKeyForRun()` method.
|
|
85
|
+
The core runtime encrypts data automatically when the `World` implementation provides a `getEncryptionKeyForRun()` method. The core runtime can call this method in two forms:
|
|
86
|
+
|
|
87
|
+
{/* @skip-typecheck - interface signature, not runnable code */}
|
|
88
|
+
```typescript
|
|
89
|
+
getEncryptionKeyForRun?(run: WorkflowRun): Promise<Uint8Array | undefined>;
|
|
90
|
+
getEncryptionKeyForRun?(
|
|
91
|
+
runId: string,
|
|
92
|
+
context?: Record<string, unknown>
|
|
93
|
+
): Promise<Uint8Array | undefined>;
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Use `getEncryptionKeyForRun(run)` when the run entity already exists. Use `getEncryptionKeyForRun(runId, context?)` in runtime paths like `start()` where the run has not been created yet but the world may still need context such as `deploymentId`.
|
|
86
97
|
|
|
87
98
|
To add encryption support to a custom `World`:
|
|
88
99
|
|
|
89
|
-
1. Implement `getEncryptionKeyForRun(
|
|
100
|
+
1. Implement `getEncryptionKeyForRun()` on your `World` class, handling both call shapes
|
|
90
101
|
2. Return the raw 32-byte key as a `Uint8Array` — the core runtime uses it for AES-256-GCM operations
|
|
91
102
|
3. Ensure the same key is returned for the same run ID across invocations (for decryption during replay)
|
|
92
103
|
|
|
104
|
+
```typescript
|
|
105
|
+
import type { WorkflowRun, World } from "@workflow/world";
|
|
106
|
+
|
|
107
|
+
export const getEncryptionKeyForRun: World["getEncryptionKeyForRun"] = async (
|
|
108
|
+
run,
|
|
109
|
+
context
|
|
110
|
+
) => {
|
|
111
|
+
const runId = typeof run === "string" ? run : run.runId;
|
|
112
|
+
const deploymentId =
|
|
113
|
+
typeof run === "string"
|
|
114
|
+
? (context?.deploymentId as string | undefined)
|
|
115
|
+
: run.deploymentId;
|
|
116
|
+
|
|
117
|
+
return await lookupRunKey(runId, deploymentId);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
async function lookupRunKey(
|
|
121
|
+
runId: string,
|
|
122
|
+
deploymentId?: string
|
|
123
|
+
): Promise<Uint8Array | undefined> {
|
|
124
|
+
// Look up or derive the encryption key for this run
|
|
125
|
+
// Return undefined to skip encryption
|
|
126
|
+
return new Uint8Array(32);
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
93
130
|
The [Vercel World](/docs/deploying/world/vercel-world) implementation uses HKDF derivation from a deployment-scoped key, but any consistent key management scheme will work.
|
|
@@ -127,7 +127,7 @@ flowchart TD
|
|
|
127
127
|
|
|
128
128
|
Unlike other entities, hooks don't have a `status` field—the states above are conceptual. An "active" hook is one that exists in storage, while "disposed" means the hook has been deleted. When a `hook_disposed` event is created, the hook record is removed rather than updated.
|
|
129
129
|
|
|
130
|
-
While a hook is active, its token is reserved and cannot be used by other workflows. If a workflow attempts to create a hook with a token that is already in use by another active hook, a `hook_conflict` event is recorded instead of `hook_created`. This causes the hook's promise to reject with a `
|
|
130
|
+
While a hook is active, its token is reserved and cannot be used by other workflows. If a workflow attempts to create a hook with a token that is already in use by another active hook, a `hook_conflict` event is recorded instead of `hook_created`. This causes the hook's promise to reject with a `HookConflictError`, which you can detect with `HookConflictError.is(error)`. See the [hook-conflict error](/docs/errors/hook-conflict) documentation for more details.
|
|
131
131
|
|
|
132
132
|
When a hook is disposed (either explicitly or when its workflow completes), the token is released and can be claimed by future workflows. Hooks are automatically disposed when a workflow reaches a terminal state (`completed`, `failed`, or `cancelled`). The `hook_disposed` event is only needed for explicit disposal before workflow completion.
|
|
133
133
|
|
|
@@ -188,7 +188,7 @@ Events are categorized by the entity type they affect. Each event contains metad
|
|
|
188
188
|
| Event | Description |
|
|
189
189
|
|-------|-------------|
|
|
190
190
|
| `hook_created` | Creates a new hook in `active` state. Contains the hook token and optional metadata. |
|
|
191
|
-
| `hook_conflict` | Records that hook creation failed because the token is already in use by another active hook. The hook is not created, and the
|
|
191
|
+
| `hook_conflict` | Records that hook creation failed because the token is already in use by another active hook. The hook is not created, and awaiting the hook will reject with a `HookConflictError`. |
|
|
192
192
|
| `hook_received` | Records that a payload was delivered to the hook. The hook remains `active` and can receive more payloads. |
|
|
193
193
|
| `hook_disposed` | Deletes the hook from storage (conceptually transitioning to `disposed` state). The token is released for reuse by future workflows. |
|
|
194
194
|
|
|
@@ -241,6 +241,23 @@ This correlation enables:
|
|
|
241
241
|
- Building timelines of entity lifecycle transitions
|
|
242
242
|
- Debugging by tracing the complete history of any entity
|
|
243
243
|
|
|
244
|
+
### Request ID Correlation
|
|
245
|
+
|
|
246
|
+
Some `World` implementations also attach a `requestId` to events for platform-log correlation. This is different from `correlationId`:
|
|
247
|
+
|
|
248
|
+
- `correlationId` links together events for the same entity lifecycle
|
|
249
|
+
- `requestId` tells you which inbound platform request created or updated the event
|
|
250
|
+
|
|
251
|
+
On Vercel, `requestId` is the platform request ID when available. Other worlds are not expected to provide a `requestId`.
|
|
252
|
+
|
|
253
|
+
```json
|
|
254
|
+
{
|
|
255
|
+
"eventType": "step_started",
|
|
256
|
+
"correlationId": "step_01JQEXAMPLE1234567890",
|
|
257
|
+
"requestId": "iad1::abc123-1712345678901-xyz987"
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
244
261
|
## Entity IDs
|
|
245
262
|
|
|
246
263
|
All entities in the Workflow SDK use a consistent ID format: a 4-character prefix followed by an underscore and a [ULID](https://github.com/ulid/spec) (Universally Unique Lexicographically Sortable Identifier).
|
|
@@ -261,6 +261,35 @@ class MyFrameworkBuilder extends BaseBuilder {
|
|
|
261
261
|
|
|
262
262
|
If your framework supports virtual server routes and dev mode watching, make sure to adapt accordingly. Please open a PR to the Workflow SDK if the base builder class is missing necessary functionality.
|
|
263
263
|
|
|
264
|
+
### Monorepos and Workspace Imports
|
|
265
|
+
|
|
266
|
+
If your framework integration lives in a subdirectory and your workflows import code from sibling workspace packages, pass `projectRoot` to `BaseBuilder`. Use the smallest directory that contains every workspace package imported by your workflows.
|
|
267
|
+
|
|
268
|
+
{/* @skip-typecheck: @workflow/cli internal module */}
|
|
269
|
+
```typescript title="my-framework-builder.ts" lineNumbers
|
|
270
|
+
import { BaseBuilder } from "@workflow/cli/dist/lib/builders/base-builder";
|
|
271
|
+
|
|
272
|
+
class MyFrameworkBuilder extends BaseBuilder {
|
|
273
|
+
constructor(options: {
|
|
274
|
+
rootDir: string;
|
|
275
|
+
workspaceRoot?: string;
|
|
276
|
+
dev: boolean;
|
|
277
|
+
}) {
|
|
278
|
+
super({
|
|
279
|
+
dirs: ["workflows"],
|
|
280
|
+
workingDir: options.rootDir,
|
|
281
|
+
projectRoot: options.workspaceRoot ?? options.rootDir, // [!code highlight]
|
|
282
|
+
watch: options.dev,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
override async build(): Promise<void> {
|
|
287
|
+
const inputFiles = await this.getInputFiles();
|
|
288
|
+
// ...
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
```
|
|
292
|
+
|
|
264
293
|
Hook into your framework's build:
|
|
265
294
|
|
|
266
295
|
{/* @skip-typecheck: incomplete code sample */}
|
|
@@ -346,22 +375,50 @@ In the future, the Workflow SDK will emit more routes under the `.well-known/wor
|
|
|
346
375
|
|
|
347
376
|
## Security
|
|
348
377
|
|
|
349
|
-
|
|
378
|
+
The workflow and step handler endpoints are invoked by the world's queuing infrastructure, not by end users. How they're secured depends on which world you're deploying to.
|
|
379
|
+
|
|
380
|
+
### Vercel (`@workflow/world-vercel`)
|
|
381
|
+
|
|
382
|
+
On Vercel, workflow handler functions are not accessible through public endpoints. Handlers use the same [consumer function security](https://vercel.com/docs/queues/concepts#consumer-function-security) mechanism that secures [Vercel Queues](https://vercel.com/docs/queues) consumers.
|
|
383
|
+
|
|
384
|
+
During the build step, the Workflow SDK automatically configures each handler as a queue consumer by writing `experimentalTriggers` to the function's `.vc-config.json`:
|
|
385
|
+
|
|
386
|
+
```json title=".vc-config.json (generated by Workflow SDK)"
|
|
387
|
+
{
|
|
388
|
+
"experimentalTriggers": [
|
|
389
|
+
{
|
|
390
|
+
"type": "queue/v2beta",
|
|
391
|
+
"topic": "__wkf_step_*",
|
|
392
|
+
"consumer": "default",
|
|
393
|
+
"retryAfterSeconds": 5,
|
|
394
|
+
"initialDelaySeconds": 0
|
|
395
|
+
}
|
|
396
|
+
]
|
|
397
|
+
}
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
Two queue topics are created per deployment:
|
|
350
402
|
|
|
351
|
-
|
|
403
|
+
| Handler | Topic | Description |
|
|
404
|
+
| --- | --- | --- |
|
|
405
|
+
| `step.func` | `__wkf_step_*` | Step execution (long-running, `maxDuration: max`) |
|
|
406
|
+
| `flow.func` | `__wkf_workflow_*` | Workflow orchestration (`maxDuration: 60`) |
|
|
407
|
+
|
|
408
|
+
If you're building a framework integration that targets Vercel, you should write these triggers into the `.vc-config.json` for each generated function. The `STEP_QUEUE_TRIGGER` and `WORKFLOW_QUEUE_TRIGGER` constants are exported from `@workflow/builders` for this purpose:
|
|
409
|
+
|
|
410
|
+
```typescript
|
|
411
|
+
import { STEP_QUEUE_TRIGGER, WORKFLOW_QUEUE_TRIGGER } from "@workflow/builders";
|
|
412
|
+
```
|
|
352
413
|
|
|
353
|
-
**Vercel (`@workflow/world-vercel`):**
|
|
354
414
|
|
|
355
|
-
|
|
356
|
-
- Handlers receive only a message ID that must be retrieved from Vercel's backend
|
|
357
|
-
- Impossible to craft custom payloads without valid queue-issued message IDs
|
|
415
|
+
### Custom implementations
|
|
358
416
|
|
|
359
|
-
|
|
417
|
+
For self-hosted or non-Vercel deployments, you are responsible for securing the handler endpoints:
|
|
360
418
|
|
|
361
|
-
-
|
|
362
|
-
-
|
|
363
|
-
-
|
|
364
|
-
- Rate limiting and request validation
|
|
419
|
+
- **Framework middleware** — Add authentication (API keys, JWT, OIDC) in front of the `/.well-known/workflow/v1/*` routes
|
|
420
|
+
- **Network-level security** — Deploy handlers behind a VPC, private network, or firewall rules so only your queue infrastructure can reach them
|
|
421
|
+
- **Rate limiting** — Add request validation and rate limiting to prevent abuse
|
|
365
422
|
|
|
366
423
|
Learn more about [building custom Worlds](/docs/deploying/building-a-world).
|
|
367
424
|
|
|
@@ -18,7 +18,7 @@ Workflow SDK provides powerful tools to inspect, monitor, and debug your workflo
|
|
|
18
18
|
npx workflow
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
The CLI comes pre-installed with the Workflow SDK and registers the `workflow` command. If the `workflow` package is not already installed, `npx workflow` will
|
|
21
|
+
The CLI comes pre-installed with the Workflow SDK and registers the `workflow` command. If the `workflow` package is not already installed, `npx workflow` will download and run the CLI temporarily, or use the local installed version if available.
|
|
22
22
|
|
|
23
23
|
Get started inspecting your local workflows:
|
|
24
24
|
|
package/docs/testing/index.mdx
CHANGED
|
@@ -371,7 +371,7 @@ Or use the CLI to inspect runs in the terminal:
|
|
|
371
371
|
npx workflow inspect runs
|
|
372
372
|
|
|
373
373
|
# Inspect a specific run
|
|
374
|
-
npx workflow inspect
|
|
374
|
+
npx workflow inspect run <run-id>
|
|
375
375
|
```
|
|
376
376
|
|
|
377
377
|
The Web UI shows each step, its inputs and outputs, retry attempts, hook state, and timing. This is especially useful for diagnosing issues with hooks that were not resumed, steps that failed unexpectedly, or workflows that timed out.
|
|
@@ -48,8 +48,24 @@ import type { ChildProcess } from "node:child_process";
|
|
|
48
48
|
let server: ChildProcess | null = null;
|
|
49
49
|
const PORT = "4000";
|
|
50
50
|
|
|
51
|
+
function emitSetupLog(event: string, fields: Record<string, unknown> = {}) {
|
|
52
|
+
console.log(
|
|
53
|
+
JSON.stringify({
|
|
54
|
+
scope: "workflow-server-test",
|
|
55
|
+
event,
|
|
56
|
+
port: PORT,
|
|
57
|
+
...fields,
|
|
58
|
+
})
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
51
62
|
export async function setup() { // [!code highlight]
|
|
52
|
-
|
|
63
|
+
const stdout: string[] = [];
|
|
64
|
+
const stderr: string[] = [];
|
|
65
|
+
|
|
66
|
+
emitSetupLog("server_starting", {
|
|
67
|
+
command: `npx nitro dev --port ${PORT}`,
|
|
68
|
+
});
|
|
53
69
|
|
|
54
70
|
server = spawn("npx", ["nitro", "dev", "--port", PORT], {
|
|
55
71
|
stdio: "pipe",
|
|
@@ -57,13 +73,14 @@ export async function setup() { // [!code highlight]
|
|
|
57
73
|
env: process.env,
|
|
58
74
|
});
|
|
59
75
|
|
|
60
|
-
// Wait for the server to be ready
|
|
61
76
|
const ready = await new Promise<boolean>((resolve) => {
|
|
62
77
|
const timeout = setTimeout(() => resolve(false), 15_000);
|
|
63
78
|
|
|
64
79
|
server?.stdout?.on("data", (data) => {
|
|
65
80
|
const output = data.toString();
|
|
66
|
-
|
|
81
|
+
stdout.push(output);
|
|
82
|
+
emitSetupLog("server_stdout", { message: output.trim() });
|
|
83
|
+
|
|
67
84
|
if (output.includes("listening") || output.includes("ready")) {
|
|
68
85
|
clearTimeout(timeout);
|
|
69
86
|
resolve(true);
|
|
@@ -71,40 +88,66 @@ export async function setup() { // [!code highlight]
|
|
|
71
88
|
});
|
|
72
89
|
|
|
73
90
|
server?.stderr?.on("data", (data) => {
|
|
74
|
-
|
|
91
|
+
const output = data.toString();
|
|
92
|
+
stderr.push(output);
|
|
93
|
+
emitSetupLog("server_stderr", { message: output.trim() });
|
|
75
94
|
});
|
|
76
95
|
|
|
77
96
|
server?.on("error", (error) => {
|
|
78
|
-
|
|
97
|
+
emitSetupLog("server_process_error", {
|
|
98
|
+
name: error.name,
|
|
99
|
+
message: error.message,
|
|
100
|
+
});
|
|
79
101
|
clearTimeout(timeout);
|
|
80
102
|
resolve(false);
|
|
81
103
|
});
|
|
104
|
+
|
|
105
|
+
server?.on("exit", (code, signal) => {
|
|
106
|
+
emitSetupLog("server_exit", { code, signal });
|
|
107
|
+
});
|
|
82
108
|
});
|
|
83
109
|
|
|
84
110
|
if (!ready) {
|
|
85
|
-
|
|
111
|
+
const recentStdout = stdout.join("").trim().slice(-2000);
|
|
112
|
+
const recentStderr = stderr.join("").trim().slice(-2000);
|
|
113
|
+
|
|
114
|
+
throw new Error(
|
|
115
|
+
[
|
|
116
|
+
"Server failed to start within 15 seconds.",
|
|
117
|
+
`Command: npx nitro dev --port ${PORT}`,
|
|
118
|
+
`WORKFLOW_LOCAL_BASE_URL: http://localhost:${PORT}`,
|
|
119
|
+
`Recent stdout:\n${recentStdout || "(empty)"}`,
|
|
120
|
+
`Recent stderr:\n${recentStderr || "(empty)"}`,
|
|
121
|
+
].join("\n\n")
|
|
122
|
+
);
|
|
86
123
|
}
|
|
87
124
|
|
|
88
|
-
await delay(2_000);
|
|
125
|
+
await delay(2_000);
|
|
89
126
|
|
|
90
|
-
// Point the workflow runtime at the local server
|
|
91
127
|
process.env.WORKFLOW_LOCAL_BASE_URL = `http://localhost:${PORT}`; // [!code highlight]
|
|
92
128
|
|
|
93
|
-
|
|
129
|
+
emitSetupLog("server_ready", {
|
|
130
|
+
baseUrl: process.env.WORKFLOW_LOCAL_BASE_URL,
|
|
131
|
+
});
|
|
94
132
|
}
|
|
95
133
|
|
|
96
134
|
export async function teardown() { // [!code highlight]
|
|
97
|
-
if (server)
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
135
|
+
if (!server) return;
|
|
136
|
+
|
|
137
|
+
emitSetupLog("server_stopping");
|
|
138
|
+
|
|
139
|
+
server.kill("SIGTERM");
|
|
140
|
+
await delay(1_000);
|
|
141
|
+
|
|
142
|
+
if (!server.killed) {
|
|
143
|
+
emitSetupLog("server_force_kill");
|
|
144
|
+
server.kill("SIGKILL");
|
|
104
145
|
}
|
|
105
146
|
}
|
|
106
147
|
```
|
|
107
148
|
|
|
149
|
+
These JSON log lines are intentional. They give CI jobs, local tooling, and agents stable events to watch for (`server_starting`, `server_stdout`, `server_stderr`, `server_ready`, `server_exit`), and the thrown timeout error includes the command, expected `WORKFLOW_LOCAL_BASE_URL`, and buffered stdout/stderr so a failed setup is actionable without interactive debugging.
|
|
150
|
+
|
|
108
151
|
The setup script sets `WORKFLOW_LOCAL_BASE_URL` so the workflow runtime sends step execution requests to the running server.
|
|
109
152
|
|
|
110
153
|
<Callout type="info">
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "workflow",
|
|
3
|
-
"version": "4.2.0-beta.
|
|
3
|
+
"version": "4.2.0-beta.78",
|
|
4
4
|
"description": "Workflow SDK - Build durable, resilient, and observable workflows",
|
|
5
5
|
"main": "dist/typescript-plugin.cjs",
|
|
6
6
|
"type": "module",
|
|
@@ -57,18 +57,18 @@
|
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
59
|
"ms": "2.1.3",
|
|
60
|
-
"@workflow/astro": "4.0.0-beta.
|
|
61
|
-
"@workflow/cli": "4.2.0-beta.
|
|
62
|
-
"@workflow/core": "4.2.0-beta.
|
|
60
|
+
"@workflow/astro": "4.0.0-beta.52",
|
|
61
|
+
"@workflow/cli": "4.2.0-beta.78",
|
|
62
|
+
"@workflow/core": "4.2.0-beta.78",
|
|
63
63
|
"@workflow/errors": "4.1.0-beta.20",
|
|
64
64
|
"@workflow/typescript-plugin": "4.0.1-beta.5",
|
|
65
65
|
"@workflow/utils": "4.1.0-beta.13",
|
|
66
|
-
"@workflow/next": "4.0.1-beta.
|
|
67
|
-
"@workflow/nest": "0.0.0-beta.
|
|
68
|
-
"@workflow/nitro": "4.0.1-beta.
|
|
69
|
-
"@workflow/nuxt": "4.0.1-beta.
|
|
70
|
-
"@workflow/sveltekit": "4.0.0-beta.
|
|
71
|
-
"@workflow/rollup": "4.0.0-beta.
|
|
66
|
+
"@workflow/next": "4.0.1-beta.74",
|
|
67
|
+
"@workflow/nest": "0.0.0-beta.27",
|
|
68
|
+
"@workflow/nitro": "4.0.1-beta.73",
|
|
69
|
+
"@workflow/nuxt": "4.0.1-beta.62",
|
|
70
|
+
"@workflow/sveltekit": "4.0.0-beta.67",
|
|
71
|
+
"@workflow/rollup": "4.0.0-beta.35"
|
|
72
72
|
},
|
|
73
73
|
"devDependencies": {
|
|
74
74
|
"@types/ms": "2.1.0",
|