@zio.dev/zio-blocks 0.0.28 → 0.0.30
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/guides/compile-time-resource-safety-with-scope.md +997 -0
- package/guides/query-dsl-extending.md +1 -1
- package/guides/query-dsl-fluent-builder.md +203 -203
- package/guides/query-dsl-reified-optics.md +1 -1
- package/guides/query-dsl-sql.md +1 -1
- package/guides/zio-schema-migration.md +6 -6
- package/index.md +68 -16
- package/package.json +1 -1
- package/path-interpolator.md +70 -9
- package/reference/allows.md +96 -0
- package/reference/codec.md +8 -8
- package/reference/combinators.md +345 -0
- package/reference/context.md +639 -67
- package/reference/docs.md +1 -1
- package/reference/dynamic-schema.md +39 -42
- package/reference/http-model.md +1716 -0
- package/reference/json-differ.md +320 -0
- package/reference/json-patch.md +1 -1
- package/reference/media-type.md +2 -2
- package/reference/resource-management/defer-handle.md +246 -0
- package/reference/resource-management/finalization.md +286 -0
- package/reference/resource-management/finalizer.md +167 -0
- package/reference/resource-management/index.md +44 -0
- package/reference/resource-management/resource.md +1607 -0
- package/reference/resource-management/scope.md +3026 -0
- package/reference/resource-management/unscoped.md +125 -0
- package/reference/resource-management/wire.md +878 -0
- package/reference/schema-evolution/as.md +4 -4
- package/reference/schema-evolution/into.md +2 -2
- package/reference/schema-expr.md +2 -2
- package/reference/streams.md +989 -0
- package/reference/type-class-derivation.md +31 -31
- package/ringbuffer.md +249 -0
- package/sidebars.js +19 -2
- package/scope.md +0 -1423
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: json-differ
|
|
3
|
+
title: "JsonDiffer"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
`JsonDiffer` is a **diff algorithm for JSON values** that computes the minimal [`JsonPatch`](./json-patch.md) transforming one `Json` value into another. It is the foundation of `JsonPatch.diff`.
|
|
7
|
+
|
|
8
|
+
```scala
|
|
9
|
+
object JsonDiffer {
|
|
10
|
+
def diff(source: Json, target: Json): JsonPatch
|
|
11
|
+
}
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Overview
|
|
15
|
+
|
|
16
|
+
`JsonDiffer` solves a fundamental problem: given two `Json` values, what is the most compact representation of the changes from one to the other?
|
|
17
|
+
|
|
18
|
+
Instead of transmitting or storing the entire new value, `JsonDiffer` emits a `JsonPatch` containing only the differences. For each type of change — numeric updates, string mutations, array reordering, field additions — it selects the most space-efficient representation:
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
Original JSON Target JSON
|
|
22
|
+
┌──────────────────┐ ┌──────────────────┐
|
|
23
|
+
│ { │ │ { │
|
|
24
|
+
│ "name": "Alice"│ │ "name": "Alice"│
|
|
25
|
+
│ "age": 25 │─────→│ "age": 26 │
|
|
26
|
+
│ "city": "NYC" │ diff │ "city": "NYC" │
|
|
27
|
+
│ } │ │ } │
|
|
28
|
+
└──────────────────┘ └──────────────────┘
|
|
29
|
+
│
|
|
30
|
+
▼
|
|
31
|
+
JsonPatch {
|
|
32
|
+
ObjectEdit(
|
|
33
|
+
Modify("age", NumberDelta(1))
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The core guarantee: `JsonDiffer.diff(source, target).apply(source) == Right(target)` always holds.
|
|
39
|
+
|
|
40
|
+
## Motivation
|
|
41
|
+
|
|
42
|
+
Transmitting or storing entire JSON documents wastes bandwidth, disk space, and network latency when only a fraction of the data changes. `JsonDiffer` enables:
|
|
43
|
+
|
|
44
|
+
- **Efficient data synchronization** — send only what changed, not the entire document
|
|
45
|
+
- **Change tracking and audit logs** — record every mutation as a first-class value
|
|
46
|
+
- **Optimistic concurrency control** — detect conflicts by analyzing patches rather than full documents
|
|
47
|
+
- **Replay and time-travel debugging** — reconstruct any historical state by applying patches in sequence
|
|
48
|
+
- **Compression** — patches are often 10–100× smaller than the full target value
|
|
49
|
+
|
|
50
|
+
## The Diffing Algorithm
|
|
51
|
+
|
|
52
|
+
`JsonDiffer.diff` adapts its strategy per `Json` type, choosing the most compact representation for each kind of change.
|
|
53
|
+
|
|
54
|
+
### Type Mismatches
|
|
55
|
+
|
|
56
|
+
When the types differ (e.g., number to string, object to array), `JsonDiffer` emits `Op.Set` to replace the value entirely. This is the only compact choice when the types diverge:
|
|
57
|
+
|
|
58
|
+
```scala
|
|
59
|
+
import zio.blocks.schema.json.{Json, JsonDiffer}
|
|
60
|
+
|
|
61
|
+
val numberToString = JsonDiffer.diff(Json.Number(42), Json.String("hello"))
|
|
62
|
+
val objectToArray = JsonDiffer.diff(
|
|
63
|
+
Json.Object("x" -> Json.Number(1)),
|
|
64
|
+
Json.Array(Json.Number(1), Json.Number(2))
|
|
65
|
+
)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Both patches consist of a single `Set` operation:
|
|
69
|
+
|
|
70
|
+
```scala
|
|
71
|
+
import zio.blocks.schema.json.JsonPatch
|
|
72
|
+
|
|
73
|
+
numberToString.apply(Json.Number(42))
|
|
74
|
+
objectToArray.apply(Json.Object("x" -> Json.Number(1)))
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Numbers
|
|
78
|
+
|
|
79
|
+
For numeric changes, `JsonDiffer` emits `NumberDelta` — representing the difference between old and new values as a delta rather than replacing the entire number:
|
|
80
|
+
|
|
81
|
+
```scala
|
|
82
|
+
import zio.blocks.schema.json.{Json, JsonDiffer}
|
|
83
|
+
|
|
84
|
+
val increment = JsonDiffer.diff(Json.Number(100), Json.Number(105))
|
|
85
|
+
val decrement = JsonDiffer.diff(Json.Number(50), Json.Number(48))
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The patches store deltas, not full new values:
|
|
89
|
+
|
|
90
|
+
```scala
|
|
91
|
+
import zio.blocks.schema.json.JsonPatch
|
|
92
|
+
|
|
93
|
+
increment.apply(Json.Number(100))
|
|
94
|
+
decrement.apply(Json.Number(50))
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Strings
|
|
98
|
+
|
|
99
|
+
For strings, `JsonDiffer` chooses between two strategies:
|
|
100
|
+
|
|
101
|
+
1. **LCS (Longest Common Subsequence) edit operations** — if the edits are more compact than the full new string.
|
|
102
|
+
2. **Full replacement with `Set`** — if the string has changed so much that storing the entire new value is smaller.
|
|
103
|
+
|
|
104
|
+
The algorithm computes common characters, then emits `Insert` and `Delete` operations (with `Append` and `Modify` existing as supported patch ops but not currently produced by `JsonDiffer`). It only emits the edits if their byte size is smaller than the new string's length:
|
|
105
|
+
|
|
106
|
+
```scala
|
|
107
|
+
import zio.blocks.schema.json.{Json, JsonDiffer}
|
|
108
|
+
|
|
109
|
+
// Prefix insertion — common suffix preserved
|
|
110
|
+
val addPrefix = JsonDiffer.diff(Json.String("world"), Json.String("hello world"))
|
|
111
|
+
|
|
112
|
+
// Complete replacement — almost no common subsequence
|
|
113
|
+
val replacement = JsonDiffer.diff(Json.String("abc"), Json.String("xyz"))
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
The first patch uses `StringEdit` (compact), the second uses `Set` (more efficient):
|
|
117
|
+
|
|
118
|
+
```scala
|
|
119
|
+
addPrefix.apply(Json.String("world"))
|
|
120
|
+
replacement.apply(Json.String("abc"))
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Arrays
|
|
124
|
+
|
|
125
|
+
For arrays, `JsonDiffer` uses **LCS-aligned insert, delete, and append operations** to transform the old array into the new one. It aligns common elements and generates the minimum sequence of mutations:
|
|
126
|
+
|
|
127
|
+
```scala
|
|
128
|
+
import zio.blocks.schema.json.{Json, JsonDiffer}
|
|
129
|
+
|
|
130
|
+
// Append elements at the end
|
|
131
|
+
val append = JsonDiffer.diff(
|
|
132
|
+
Json.Array(Json.Number(1), Json.Number(2)),
|
|
133
|
+
Json.Array(Json.Number(1), Json.Number(2), Json.Number(3))
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
// Delete and reorder — LCS finds the common elements
|
|
137
|
+
val reorder = JsonDiffer.diff(
|
|
138
|
+
Json.Array(Json.Number(1), Json.Number(2), Json.Number(3)),
|
|
139
|
+
Json.Array(Json.Number(3), Json.Number(2), Json.Number(1))
|
|
140
|
+
)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Both patches use efficient `ArrayEdit` operations:
|
|
144
|
+
|
|
145
|
+
```scala
|
|
146
|
+
append.apply(Json.Array(Json.Number(1), Json.Number(2)))
|
|
147
|
+
reorder.apply(Json.Array(Json.Number(1), Json.Number(2), Json.Number(3)))
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Objects
|
|
151
|
+
|
|
152
|
+
For objects, `JsonDiffer` compares field-by-field:
|
|
153
|
+
|
|
154
|
+
- Fields in the target but not in the source become `Add` operations.
|
|
155
|
+
- Fields in the source but not in the target become `Remove` operations.
|
|
156
|
+
- Fields in both are recursively diffed — if the values differ, a `Modify` with a sub-patch is emitted.
|
|
157
|
+
|
|
158
|
+
```scala
|
|
159
|
+
import zio.blocks.schema.json.{Json, JsonDiffer}
|
|
160
|
+
|
|
161
|
+
val original = Json.Object(
|
|
162
|
+
"name" -> Json.String("Alice"),
|
|
163
|
+
"age" -> Json.Number(25),
|
|
164
|
+
"city" -> Json.String("NYC")
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
val updated = Json.Object(
|
|
168
|
+
"name" -> Json.String("Alice"),
|
|
169
|
+
"age" -> Json.Number(26),
|
|
170
|
+
"email" -> Json.String("alice@example.com")
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
val patch = JsonDiffer.diff(original, updated)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
The patch contains `Add`, `Remove`, and `Modify` operations:
|
|
177
|
+
|
|
178
|
+
```scala
|
|
179
|
+
patch.apply(original)
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Nested Structures
|
|
183
|
+
|
|
184
|
+
`JsonDiffer` handles deeply nested objects and arrays recursively. Each field or element is diffed independently, producing compact patches at every level:
|
|
185
|
+
|
|
186
|
+
```scala
|
|
187
|
+
import zio.blocks.schema.json.{Json, JsonDiffer}
|
|
188
|
+
|
|
189
|
+
val original = Json.Object(
|
|
190
|
+
"user" -> Json.Object(
|
|
191
|
+
"name" -> Json.String("Alice"),
|
|
192
|
+
"scores" -> Json.Array(Json.Number(95), Json.Number(87))
|
|
193
|
+
)
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
val updated = Json.Object(
|
|
197
|
+
"user" -> Json.Object(
|
|
198
|
+
"name" -> Json.String("Alice"),
|
|
199
|
+
"scores" -> Json.Array(Json.Number(95), Json.Number(88), Json.Number(92))
|
|
200
|
+
)
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
val patch = JsonDiffer.diff(original, updated)
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
The patch navigates to the nested array and emits only the array changes:
|
|
207
|
+
|
|
208
|
+
```scala
|
|
209
|
+
patch.apply(original)
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Core Operations
|
|
213
|
+
|
|
214
|
+
`JsonDiffer` exposes a single public operation: `diff`, which computes the minimal `JsonPatch` that transforms `source` into `target`. Returns an empty patch if the values are already equal:
|
|
215
|
+
|
|
216
|
+
```scala
|
|
217
|
+
object JsonDiffer {
|
|
218
|
+
def diff(source: Json, target: Json): JsonPatch
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
The roundtrip property always holds — applying the patch to the source always yields the target:
|
|
223
|
+
|
|
224
|
+
```scala
|
|
225
|
+
import zio.blocks.schema.json.{Json, JsonDiffer}
|
|
226
|
+
|
|
227
|
+
val source = Json.Object("x" -> Json.Number(10), "y" -> Json.Number(20))
|
|
228
|
+
val target = Json.Object("x" -> Json.Number(10), "y" -> Json.Number(21))
|
|
229
|
+
val patch = JsonDiffer.diff(source, target)
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
```scala
|
|
233
|
+
patch.apply(source) == Right(target)
|
|
234
|
+
// res13: Boolean = true
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
`JsonDiffer.diff` is also available as `JsonPatch.diff` and as the `Json#diff` extension method:
|
|
238
|
+
|
|
239
|
+
```scala
|
|
240
|
+
import zio.blocks.schema.json.{Json, JsonPatch}
|
|
241
|
+
|
|
242
|
+
// Via JsonPatch companion
|
|
243
|
+
val p1 = JsonPatch.diff(Json.Number(1), Json.Number(2))
|
|
244
|
+
|
|
245
|
+
// Via Json extension method
|
|
246
|
+
val p2 = Json.Number(1).diff(Json.Number(2))
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
## Integration
|
|
250
|
+
|
|
251
|
+
`JsonDiffer` is the implementation behind the public `JsonPatch.diff` API. You typically interact with it through `JsonPatch.diff` or the `Json#diff` extension method rather than calling `JsonDiffer.diff` directly.
|
|
252
|
+
|
|
253
|
+
The relationship is simple: `JsonPatch.diff` delegates to `JsonDiffer.diff` and wraps the result in a `JsonPatch` for further composition and application.
|
|
254
|
+
|
|
255
|
+
Once you have a `JsonPatch` from `JsonDiffer`, use the full `JsonPatch` API to apply it with different modes, compose multiple patches, or convert to/from `DynamicPatch`:
|
|
256
|
+
|
|
257
|
+
```scala
|
|
258
|
+
import zio.blocks.schema.json.{Json, JsonDiffer, JsonPatch}
|
|
259
|
+
import zio.blocks.schema.patch.PatchMode
|
|
260
|
+
|
|
261
|
+
val source = Json.Object("count" -> Json.Number(0))
|
|
262
|
+
val target = Json.Object("count" -> Json.Number(1))
|
|
263
|
+
val patch = JsonDiffer.diff(source, target)
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Apply with different failure-handling modes:
|
|
267
|
+
|
|
268
|
+
```scala
|
|
269
|
+
patch.apply(source, PatchMode.Strict)
|
|
270
|
+
patch.apply(source, PatchMode.Lenient)
|
|
271
|
+
patch.apply(source, PatchMode.Clobber)
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
Compose patches with `++`:
|
|
275
|
+
|
|
276
|
+
```scala
|
|
277
|
+
val patch2 = JsonDiffer.diff(target, Json.Object("count" -> Json.Number(2)))
|
|
278
|
+
val combined = patch ++ patch2
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
## Implementation Notes
|
|
282
|
+
|
|
283
|
+
`JsonDiffer.diff` uses the LCS (Longest Common Subsequence) algorithm for both strings and arrays, ensuring that the number of edit operations is minimized. For strings, it compares character sequences; for arrays, it compares JSON elements by structural equality.
|
|
284
|
+
|
|
285
|
+
The LCS-based approach is particularly effective for arrays and strings with significant common subsequences — a common pattern in real-world data mutation scenarios (e.g., adding an item to a list, inserting a few characters into a string).
|
|
286
|
+
|
|
287
|
+
:::note
|
|
288
|
+
The LCS algorithm has O(n·m) time complexity, where n and m are the lengths of the two sequences. For very large strings or arrays, consider whether you need the minimal patch or can accept a faster approximation.
|
|
289
|
+
:::
|
|
290
|
+
|
|
291
|
+
## Running the Examples
|
|
292
|
+
|
|
293
|
+
`JsonDiffer` is the foundation of the `JsonPatch` API. Runnable examples demonstrating `JsonPatch.diff` (which internally uses `JsonDiffer`) are available in the `schema-examples` module.
|
|
294
|
+
|
|
295
|
+
**1. Clone the repository and navigate to the project:**
|
|
296
|
+
|
|
297
|
+
```bash
|
|
298
|
+
git clone https://github.com/zio/zio-blocks.git
|
|
299
|
+
cd zio-blocks
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
**2. Run JsonPatch examples with sbt:**
|
|
303
|
+
|
|
304
|
+
These examples demonstrate how `JsonDiffer.diff` computes minimal patches through the public `JsonPatch.diff` API:
|
|
305
|
+
|
|
306
|
+
```bash
|
|
307
|
+
sbt "schema-examples/runMain jsonpatch.JsonPatchDiffAndApplyExample"
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
```bash
|
|
311
|
+
sbt "schema-examples/runMain jsonpatch.JsonPatchOperationsExample"
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
```bash
|
|
315
|
+
sbt "schema-examples/runMain jsonpatch.JsonPatchCompositionExample"
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
```bash
|
|
319
|
+
sbt "schema-examples/runMain jsonpatch.CompleteJsonPatchExample"
|
|
320
|
+
```
|
package/reference/json-patch.md
CHANGED
|
@@ -640,7 +640,7 @@ You rarely need to construct `Nested` manually; it is primarily an internal opti
|
|
|
640
640
|
|
|
641
641
|
## Diffing Algorithm
|
|
642
642
|
|
|
643
|
-
`JsonPatch.diff` (and its alias `Json#diff`) delegate to `JsonDiffer.diff
|
|
643
|
+
`JsonPatch.diff` (and its alias `Json#diff`) delegate to [`JsonDiffer.diff`](./json-differ.md), which selects the most compact representation for each type of change:
|
|
644
644
|
|
|
645
645
|
| Value type | Change | Strategy |
|
|
646
646
|
|------------|--------|----------|
|
package/reference/media-type.md
CHANGED
|
@@ -75,13 +75,13 @@ textAny.matches(html) // true
|
|
|
75
75
|
Add the following to your `build.sbt`:
|
|
76
76
|
|
|
77
77
|
```scala
|
|
78
|
-
libraryDependencies += "dev.zio" %% "zio-blocks-mediatype" % "0.0.
|
|
78
|
+
libraryDependencies += "dev.zio" %% "zio-blocks-mediatype" % "0.0.30"
|
|
79
79
|
```
|
|
80
80
|
|
|
81
81
|
For cross-platform projects (Scala.js):
|
|
82
82
|
|
|
83
83
|
```scala
|
|
84
|
-
libraryDependencies += "dev.zio" %%% "zio-blocks-mediatype" % "0.0.
|
|
84
|
+
libraryDependencies += "dev.zio" %%% "zio-blocks-mediatype" % "0.0.30"
|
|
85
85
|
```
|
|
86
86
|
|
|
87
87
|
Supported Scala versions: 2.13.x and 3.x.
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: defer-handle
|
|
3
|
+
title: "DeferHandle"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
`DeferHandle` is a handle returned by `Scope.defer` that allows cancelling a registered finalizer before the scope closes:
|
|
7
|
+
|
|
8
|
+
```scala
|
|
9
|
+
abstract class DeferHandle {
|
|
10
|
+
def cancel(): Unit
|
|
11
|
+
}
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
When `Scope#defer(cleanup)` is called, the cleanup action is registered and a `DeferHandle` is returned. This handle can be used to remove that finalizer early, preventing it from running when the scope closes. This is useful when a resource is explicitly released before the scope ends, and running the finalizer again would be unnecessary or harmful.
|
|
15
|
+
|
|
16
|
+
## Construction
|
|
17
|
+
|
|
18
|
+
`DeferHandle` is not instantiated directly. Instead, it is created by calling `Scope#defer` with a cleanup action:
|
|
19
|
+
|
|
20
|
+
```scala
|
|
21
|
+
import zio.blocks.scope.{Scope, DeferHandle}
|
|
22
|
+
|
|
23
|
+
trait Scope {
|
|
24
|
+
def defer(cleanup: => Unit): DeferHandle
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The following example demonstrates creating a `DeferHandle`:
|
|
29
|
+
|
|
30
|
+
```scala
|
|
31
|
+
import zio.blocks.scope.Scope
|
|
32
|
+
|
|
33
|
+
Scope.global.scoped { scope =>
|
|
34
|
+
import scope._
|
|
35
|
+
|
|
36
|
+
val handle = defer {
|
|
37
|
+
println("This cleanup will run when scope closes, unless cancelled")
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// The handle can now be used to cancel the cleanup
|
|
41
|
+
handle.cancel()
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Core Operations
|
|
46
|
+
|
|
47
|
+
The `DeferHandle#cancel` method removes the registered finalizer so it will not run when the scope closes:
|
|
48
|
+
|
|
49
|
+
```scala
|
|
50
|
+
trait DeferHandle {
|
|
51
|
+
def cancel(): Unit
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
This method is:
|
|
56
|
+
|
|
57
|
+
- **Thread-safe**: Can be called from any thread without synchronization
|
|
58
|
+
- **Idempotent**: Calling it multiple times has the same effect as calling once
|
|
59
|
+
|
|
60
|
+
If the scope has already closed (and the finalizer has already run or been discarded), calling `DeferHandle#cancel` is a no-op. In the following example, we register a cleanup action, then cancel it before the scope closes:
|
|
61
|
+
|
|
62
|
+
```scala
|
|
63
|
+
import zio.blocks.scope.Scope
|
|
64
|
+
import java.io.ByteArrayOutputStream
|
|
65
|
+
|
|
66
|
+
Scope.global.scoped { scope =>
|
|
67
|
+
import scope._
|
|
68
|
+
|
|
69
|
+
val buffer = allocate(ByteArrayOutputStream())
|
|
70
|
+
val closeHandle = defer {
|
|
71
|
+
println("Auto-closing buffer")
|
|
72
|
+
$(buffer)(_.close())
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Manually close the buffer
|
|
76
|
+
$(buffer)(_.close())
|
|
77
|
+
|
|
78
|
+
// Cancel the automatic finalizer since we already closed it
|
|
79
|
+
closeHandle.cancel()
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Use Cases
|
|
84
|
+
|
|
85
|
+
`DeferHandle` is useful in several common scenarios:
|
|
86
|
+
|
|
87
|
+
### Preventing Duplicate Cleanup
|
|
88
|
+
|
|
89
|
+
When a resource is explicitly released before the scope ends, cancel the automatic finalizer to avoid duplicate cleanup:
|
|
90
|
+
|
|
91
|
+
```scala
|
|
92
|
+
import zio.blocks.scope.Scope
|
|
93
|
+
import java.io.ByteArrayOutputStream
|
|
94
|
+
|
|
95
|
+
val result = Scope.global.scoped { scope =>
|
|
96
|
+
import scope._
|
|
97
|
+
|
|
98
|
+
val buffer = allocate(ByteArrayOutputStream())
|
|
99
|
+
|
|
100
|
+
val finalizeHandle = defer {
|
|
101
|
+
println(s"Finalizer running, buffer closing")
|
|
102
|
+
$(buffer)(_.close())
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Explicit cleanup
|
|
106
|
+
$(buffer) { buf =>
|
|
107
|
+
buf.write("data".getBytes)
|
|
108
|
+
println(s"Manual use: buffer has ${buf.size()} bytes")
|
|
109
|
+
buf.close()
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Cancel the automatic finalizer
|
|
113
|
+
finalizeHandle.cancel()
|
|
114
|
+
|
|
115
|
+
"done"
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Conditional Cleanup
|
|
120
|
+
|
|
121
|
+
Cancel finalizers based on runtime conditions:
|
|
122
|
+
|
|
123
|
+
```scala
|
|
124
|
+
import zio.blocks.scope.Scope
|
|
125
|
+
|
|
126
|
+
def acquireResource(shouldCleanup: Boolean) = Scope.global.scoped { scope =>
|
|
127
|
+
import scope._
|
|
128
|
+
|
|
129
|
+
val resource = "important resource"
|
|
130
|
+
val handle = scope.defer {
|
|
131
|
+
println("Cleaning up resource")
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (!shouldCleanup) {
|
|
135
|
+
handle.cancel()
|
|
136
|
+
println("Cleanup disabled")
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
resource
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
acquireResource(shouldCleanup = false)
|
|
143
|
+
acquireResource(shouldCleanup = true)
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Transferring Ownership
|
|
147
|
+
|
|
148
|
+
When transferring a resource to external management, cancel its finalizer so the external system can control cleanup:
|
|
149
|
+
|
|
150
|
+
```scala
|
|
151
|
+
import zio.blocks.scope.Scope
|
|
152
|
+
import java.io.ByteArrayInputStream
|
|
153
|
+
|
|
154
|
+
val result = Scope.global.scoped { scope =>
|
|
155
|
+
import scope._
|
|
156
|
+
|
|
157
|
+
val stream = allocate(ByteArrayInputStream("data".getBytes))
|
|
158
|
+
val handle = defer {
|
|
159
|
+
println("Scope finalizer would close stream")
|
|
160
|
+
$(stream)(_.close())
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Transfer ownership to external manager
|
|
164
|
+
// (In real code, this might pass to a thread pool or async framework)
|
|
165
|
+
handle.cancel() // Let the manager handle cleanup
|
|
166
|
+
|
|
167
|
+
"transferred"
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Noop Handle
|
|
172
|
+
|
|
173
|
+
When `defer()` is called on an already-closed scope, a no-op handle is returned:
|
|
174
|
+
|
|
175
|
+
```scala
|
|
176
|
+
import zio.blocks.scope.Scope
|
|
177
|
+
|
|
178
|
+
Scope.global.scoped { scope =>
|
|
179
|
+
import scope._
|
|
180
|
+
|
|
181
|
+
val handle = scope.defer {
|
|
182
|
+
println("This will run when scope closes")
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Subsequent calls to cancel() remove the finalizer
|
|
186
|
+
handle.cancel()
|
|
187
|
+
|
|
188
|
+
println("Finalizer has been cancelled")
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Thread Safety
|
|
193
|
+
|
|
194
|
+
`DeferHandle` is thread-safe. Multiple threads can call `cancel()` on the same handle without external synchronization:
|
|
195
|
+
|
|
196
|
+
```scala
|
|
197
|
+
import zio.blocks.scope.Scope
|
|
198
|
+
import java.util.concurrent.CountDownLatch
|
|
199
|
+
import java.util.concurrent.Executors
|
|
200
|
+
|
|
201
|
+
val result = Scope.global.scoped { scope =>
|
|
202
|
+
import scope._
|
|
203
|
+
|
|
204
|
+
val handle = defer {
|
|
205
|
+
println("Finalizer")
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Use a thread pool to simulate concurrent access
|
|
209
|
+
val executor = Executors.newFixedThreadPool(5)
|
|
210
|
+
val latch = new CountDownLatch(5)
|
|
211
|
+
|
|
212
|
+
(1 to 5).foreach { i =>
|
|
213
|
+
executor.submit(new Runnable {
|
|
214
|
+
def run(): Unit = {
|
|
215
|
+
handle.cancel()
|
|
216
|
+
println(s"Thread $i cancelled")
|
|
217
|
+
latch.countDown()
|
|
218
|
+
}
|
|
219
|
+
})
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Wait for all threads to finish
|
|
223
|
+
latch.await()
|
|
224
|
+
executor.shutdown()
|
|
225
|
+
|
|
226
|
+
"completed"
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## See Also
|
|
231
|
+
|
|
232
|
+
- [`Scope#defer`](./scope.md#registering-finalizers) — the method that returns a `DeferHandle`
|
|
233
|
+
- [`Finalizer`](./finalizer.md) — the trait defining `Finalizer#defer`
|
|
234
|
+
- [`Finalization`](./finalization.md) — the result of running all finalizers
|
|
235
|
+
|
|
236
|
+
## Integration
|
|
237
|
+
|
|
238
|
+
`DeferHandle` is part of ZIO Blocks' resource management system. It works directly with:
|
|
239
|
+
|
|
240
|
+
- **[`Scope`](./scope.md)** — The primary way to create a `DeferHandle` is via `Scope#defer`. A scope manages multiple finalizers and runs them all when the scope closes. `DeferHandle` allows selective cancellation of individual finalizers before that happens.
|
|
241
|
+
|
|
242
|
+
- **[`Finalizer`](./finalizer.md)** — `Finalizer` defines the `Finalizer#defer` operation that returns a `DeferHandle`. It abstracts the concept of registering cleanup actions.
|
|
243
|
+
|
|
244
|
+
- **[`Finalization`](./finalization.md)** — When a scope closes, it runs all registered finalizers. A cancelled `DeferHandle` removes its associated finalizer from this process.
|
|
245
|
+
|
|
246
|
+
Together, these types form the foundation of compile-time resource safety in ZIO Blocks, allowing you to manage resource lifecycles with certainty that cleanup will occur exactly when needed.
|