# `nupp.tasks`
An application task scope.
`nupp.suspension`'s combinators own a family that is complete when the call is
written: `all` cannot be handed a fourth body once it is running. `create` makes
a coroutine that inherits a handler and says nothing about its result, failure
or lifetime. Neither is what a server, a loading pipeline or a scene needs,
which is a place to put children discovered over time and one answer to what
happens when the body returns, a child fails, or the whole thing is cancelled.
`nupp.workers` has that shape already for CPU work, and its terminal cleanup
cannot suspend, so leaving its scope blocks the thread until unawaited children
finish. Correct for a worker scope, wrong inside a frame. A task scope owns one
lazily and closes it through the suspension-aware path before the task scope
returns.
The scheduling contract is with the host, not with each child. A host sees one
aggregate and decides when it runs; this decides which of its children run then,
in FIFO order, up to a bounded number of activations per host turn. Nesting
divides that bound rather than multiplying it, because the token belongs to the
turn rather than to a scope.
```nupp
local greeting = ""
with scope = nupp.tasks.open() do
const hello = scope:spawn(function(): string
return "hello"
end)
const world = scope:spawn(function(): string
return "world"
end)
greeting = hello:await() .. " " .. world:await()
end
assert(greeting == "hello world")
```
A scope is opened with `open`, held by a `with`, and settled when the block ends:
every child has run, been cancelled, or unwound by then. `open` takes a `limit`,
which parks `spawn` and `fork` while that many children are live, and a `deadline`.
A running child cooperates by calling `checkpoint`, so it can stop promptly when the
scope is cancelled.
```nupp
local total = 0
with scope = nupp.tasks.open(deadline = 1000) do
const sum = scope:spawnNamed("sum values", function(): integer
local answer = 0
for value = 1, 1000 do
nupp.tasks.checkpoint()
answer = answer + value
end
return answer
end)
total = sum:await()
end
assert(total == 500500)
```
## Types
### `Cancellation` _record_
```nupp
record tasks.Cancellation
operation: string
reason: string?
end
```
What a cancelled task raises.
Nominal rather than a string, so `isCancelled` recognizes it without matching
text, and so a program that catches everything still sees something it can ask
about. `tostring` renders it because an uncaught one reaches a human.
#### Fields
##### `operation`
```nupp
operation: string
```
What was cancelled, as it was named when it was started.
##### `reason`
```nupp
reason: string?
```
Why, where a caller supplied a reason.
### `Scope` _record_
```nupp
record tasks.Scope
spawn: function(
borrows self: tasks.Scope,
takes body: F,
...: unpackof Parameters(F)
): tasks.Task borrows (self)
spawnNamed: function(borrows self: tasks.Scope, name: string, takes body: F): tasks.Task borrows (self)
fork: function(
borrows self: tasks.Scope,
F,
...: unpackof nupp.runtime.services.workers.Submitted(F)
): tasks.Task borrows (self)
cancel: function(borrows self: tasks.Scope, reason: string?): nil
end
```
The scope a `run` body is handed.
Not affine, and deliberately: `run` owns the extent, so there is no obligation
for a body to discharge and no way for one to end the scope early. What the body
can do is add children to it and reach the worker scope it owns.
#### Methods
##### `spawn`
```nupp
spawn: function(
borrows self: tasks.Scope,
takes body: F,
...: unpackof Parameters(F)
): tasks.Task borrows (self)
```
Starts a child under this scope: `scope:spawn(arguments..., f)`.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `borrows self` | `tasks.Scope` | |
| `takes body` | `F` | |
| `...` | `unpackof Parameters(F)` | |
###### Returns
| Type | Description |
| --- | --- |
| `tasks.Task\ borrows (self)` | |
##### `spawnNamed`
```nupp
spawnNamed: function(borrows self: tasks.Scope, name: string, takes body: F): tasks.Task borrows (self)
```
Starts a named child under this scope.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `borrows self` | `tasks.Scope` | |
| `name` | `string` | |
| `takes body` | `F` | |
###### Returns
| Type | Description |
| --- | --- |
| `tasks.Task\ borrows (self)` | |
##### `fork`
```nupp
fork: function(
borrows self: tasks.Scope,
F,
...: unpackof nupp.runtime.services.workers.Submitted(F)
): tasks.Task borrows (self)
```
Starts a child on a worker lane: `scope:fork(arguments..., f)`.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `borrows self` | `tasks.Scope` | |
| `?` | `F` | |
| `...` | `unpackof nupp.runtime.services.workers.Submitted(F)` | |
###### Returns
| Type | Description |
| --- | --- |
| `tasks.Task\ borrows (self)` | |
##### `cancel`
```nupp
cancel: function(borrows self: tasks.Scope, reason: string?): nil
```
Requests cancellation of every child.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `borrows self` | `tasks.Scope` | |
| `reason` | `string?` | |
###### Returns
| Type | Description |
| --- | --- |
| `nil` | |
### `Task` _type_
```nupp
type tasks.Task = TaskType(F)
```
The handle one `spawn` answers.
Its `await` result pack is the body's, which is why this is derived from the
function type rather than declared once over `any`.
#### Type parameters
| Name | Description |
| --- | --- |
| `F` | |
## Functions
### `ForkMT.await` _function_
```nupp
function ForkMT.await(self: any)
```
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `self` | `any` | |
### `ForkMT.cancel` _function_
```nupp
function ForkMT.cancel(self: any, reason: string?): boolean
```
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `self` | `any` | |
| `reason` | `string?` | |
#### Returns
| Type | Description |
| --- | --- |
| `boolean` | |
### `ForkMT.isDone` _function_
```nupp
function ForkMT.isDone(self: any): boolean
```
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `self` | `any` | |
#### Returns
| Type | Description |
| --- | --- |
| `boolean` | |
### `ForkMT.status` _function_
```nupp
function ForkMT.status(self: any): string
```
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `self` | `any` | |
#### Returns
| Type | Description |
| --- | --- |
| `string` | |
### `TaskMT.await` _function_
```nupp
function TaskMT.await(self: any)
```
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `self` | `any` | |
### `TaskMT.cancel` _function_
```nupp
function TaskMT.cancel(self: any, reason: string?): boolean
```
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `self` | `any` | |
| `reason` | `string?` | |
#### Returns
| Type | Description |
| --- | --- |
| `boolean` | |
### `TaskMT.isDone` _function_
```nupp
function TaskMT.isDone(self: any): boolean
```
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `self` | `any` | |
#### Returns
| Type | Description |
| --- | --- |
| `boolean` | |
### `TaskMT.status` _function_
```nupp
function TaskMT.status(self: any): string
```
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `self` | `any` | |
#### Returns
| Type | Description |
| --- | --- |
| `string` | |
### `tasks.checkpoint` _function_
```nupp
function tasks.checkpoint(): nil
```
Raises where the current task has been cancelled or has run out of time.
The one authored cancellation point, and the only thing that reaches a body
computing without parking. It never suspends, so a `nosuspend` region and a
worker lane can both call it, and outside any task it does nothing.
```nupp
for index = 1, #items do
tasks.checkpoint()
consume(items[index])
end
```
#### Returns
| Type | Description |
| --- | --- |
| `nil` | |
#### Raises
- the cancellation, where one has been requested
### `tasks.deadline` _function_
```nupp
function tasks.deadline(): number?
```
The effective deadline of the current task, or nil where there is none.
So a body can size its work rather than discover the bound by being cancelled
part way through it.
#### Returns
| Type | Description |
| --- | --- |
| `number?` | an absolute monotonic reading, as `nupp.time.now` answers |
### `tasks.gather` _function_
```nupp
function tasks.gather(bodies: {function(): T}): {T?}, {any}
```
Runs every body concurrently and answers what each of them did, failures included.
The fail-soft family. Both arrays are indexed as `bodies` was, and exactly one of
them holds an entry per branch, so a caller who has to see every outcome sees them
beside each other. A scope is the fail-fast answer to the same question: use one
where the first failure should end the rest.
```nupp
const values, errors = nupp.tasks.gather({
function(): string return fetch(primary) end,
function(): string return fetch(mirror) end,
})
```
#### Type parameters
| Name | Description |
| --- | --- |
| `T` | |
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `bodies` | `{function(): T}` | what to run |
#### Returns
| Type | Description |
| --- | --- |
| `{T?}` | each body's value, where it returned |
| `{any}` | each body's error, where it raised |
#### Raises
- the enclosing deadline, where one passed before the family settled
### `tasks.isCancelled` _function_
```nupp
function tasks.isCancelled(value: any): boolean
```
Whether a caught value is a task cancellation.
The one question a `pcall` around task work has to be able to ask, because
cancellation is not a failure and should usually be re-raised rather than
reported.
```nupp
const ok, problem = pcall(work)
if not ok and not tasks.isCancelled(problem) then
report(problem)
end
```
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `value` | `any` | whatever was caught |
#### Returns
| Type | Description |
| --- | --- |
| `boolean` | whether this is a cancellation |
### `tasks.open` _function_
```nupp
function tasks.open(limit: integer?, deadline: number?): affine(tasks.Scope, tasks.settle)
```
Opens a scope, to be held by a `with`.
The block is the scope's body. Children started in it with `spawn` and `fork` are
its family, and leaving the block -- normally, by `break` or `return`, or by an
error -- settles them: every one has run, been cancelled, or unwound before the
block is left. A child's failure is the scope's from the moment it happens, cancels
its siblings, and is raised where the block is left, whether or not anything
awaited that child.
With a `limit`, `spawn` and `fork` park while that many children are live, so a
loop that fans out over a source is bounded by the loop itself: nothing is pulled
from the source until there is room to run it. With a `deadline`, in milliseconds
of the monotonic clock, expiry requests ordinary cancellation. A scope opened
inside another takes the earlier of the two deadlines: a child may bound itself
more tightly than its parent did, and may not extend what its parent already
promised.
Both are named arguments: `open()`, `open(limit = 8)`, `open(deadline = 500)`, or
`open(limit = 8, deadline = 500)`.
```nupp
const sizes: {integer} = {}
with scope = nupp.tasks.open(limit = 8) do
for index, url in ipairs(urls) do
scope:spawn(sizes, index, url, storeSize)
end
end
```
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `limit` | `integer?` | how many children may be live at once, or nil for no bound |
| `deadline` | `number?` | how long the whole scope may take, in milliseconds, or nil |
#### Returns
| Type | Description |
| --- | --- |
| `affine(tasks.Scope, tasks.settle)` | the scope, settled when its `with` ends |
#### Raises
- where the limit is not a positive integer or the deadline is not a finite non-negative number
### `tasks.settle` _function_
```nupp
function tasks.settle(takes scope: tasks.Scope): nil
```
Settles an opened scope, which is what leaving its `with` does.
Every child has run, been cancelled, or unwound when this returns, and the worker
scope, where one was opened, has been closed through the suspension-aware path.
It is a settling terminal: it parks until that is so, and so is refused inside a
`nosuspend` region. Idempotent, so a scope settled by hand before its block ends
settles once.
A failure the block itself raises does not cancel the children: they run to
completion before it propagates. Call `cancel` first where that is not wanted.
```nupp
with scope = nupp.tasks.open() do
scope:spawn(function(): nil work() end)
end
```
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `takes scope` | `tasks.Scope` | the scope `open` answered |
#### Returns
| Type | Description |
| --- | --- |
| `nil` | |
#### Raises
- the first failure a child had, or the cancellation a deadline caused