| 1 | import { assert, describe, it } from "@effect/vitest" |
| 2 | import { Cause, Data, Deferred, Effect, Exit, Fiber, MutableHashMap, Option, RcMap, Ref, Scope } from "effect" |
| 3 | import { TestClock } from "effect/testing" |
| 4 | |
| 5 | describe("RcMap", () => { |
| 6 | describe("lookup defects", () => { |
| 7 | for (const throws of [false, true]) { |
| 8 | it.effect.each([0, 1000])( |
| 9 | `releases capacity after a ${throws ? "throwing" : "dying"} lookup with TTL %s`, |
| 10 | (idleTimeToLive) => |
| 11 | Effect.gen(function*() { |
| 12 | const map = yield* RcMap.make({ |
| 13 | lookup: (key: string) => { |
| 14 | if (key !== "bad") return Effect.succeed(key) |
| 15 | if (throws) throw "boom" |
| 16 | return Effect.die("boom") |
| 17 | }, |
| 18 | capacity: 1, |
| 19 | idleTimeToLive |
| 20 | }) |
| 21 | |
| 22 | assert.deepStrictEqual(yield* RcMap.get(map, "bad").pipe(Effect.scoped, Effect.exit), Exit.die("boom")) |
| 23 | if (idleTimeToLive > 0) { |
| 24 | assert.isTrue(yield* RcMap.has(map, "bad")) |
| 25 | yield* TestClock.adjust(idleTimeToLive) |
| 26 | } |
| 27 | assert.deepStrictEqual(yield* RcMap.get(map, "good").pipe(Effect.exit), Exit.succeed("good")) |
| 28 | assert.isFalse(yield* RcMap.has(map, "bad")) |
| 29 | }) |
| 30 | ) |
| 31 | |
| 32 | it.effect(`shares a ${throws ? "throwing" : "dying"} lookup's defect with later borrowers`, () => |
| 33 | Effect.gen(function*() { |
| 34 | const map = yield* RcMap.make({ |
| 35 | lookup: (_key: string): Effect.Effect<string> => { |
| 36 | if (throws) throw "boom" |
| 37 | return Effect.die("boom") |
| 38 | } |
| 39 | }) |
| 40 | |
| 41 | assert.deepStrictEqual(yield* RcMap.get(map, "bad").pipe(Effect.exit), Exit.die("boom")) |
| 42 | const borrower = yield* RcMap.getOption(map, "bad").pipe( |
| 43 | Effect.exit, |
| 44 | Effect.timeoutOption(1000), |
| 45 | Effect.forkChild({ startImmediately: true }) |
| 46 | ) |
| 47 | yield* TestClock.adjust(1000) |
| 48 | assert.deepStrictEqual(yield* Fiber.join(borrower), Option.some(Exit.die("boom"))) |
| 49 | })) |
| 50 | } |
| 51 | }) |
| 52 | |
| 53 | describe("invalidate", () => { |
| 54 | it.effect.each([0, 1000, Infinity])( |
| 55 | "releases only the invalidated entry with TTL %s", |
| 56 | (idleTimeToLive) => |
| 57 | Effect.gen(function*() { |
| 58 | const acquired: Array<number> = [] |
| 59 | const released: Array<number> = [] |
| 60 | const mapScope = yield* Scope.make() |
| 61 | const oldScope = yield* Scope.make() |
| 62 | const newScope = yield* Scope.make() |
| 63 | const map = yield* RcMap.make({ |
| 64 | lookup: (_key: string) => |
| 65 | Effect.acquireRelease( |
| 66 | Effect.sync(() => { |
| 67 | const value = acquired.length + 1 |
| 68 | acquired.push(value) |
| 69 | return value |
| 70 | }), |
| 71 | (value) => Effect.sync(() => released.push(value)) |
| 72 | ), |
| 73 | idleTimeToLive |
| 74 | }).pipe(Scope.provide(mapScope)) |
| 75 | |
| 76 | assert.strictEqual(yield* RcMap.get(map, "key").pipe(Scope.provide(oldScope)), 1) |
| 77 | yield* RcMap.invalidate(map, "key") |
| 78 | assert.isFalse(yield* RcMap.has(map, "key")) |
| 79 | assert.deepStrictEqual(released, []) |
| 80 | assert.strictEqual(yield* RcMap.get(map, "key").pipe(Scope.provide(newScope)), 2) |
| 81 | |
| 82 | yield* Scope.close(oldScope, Exit.void) |
| 83 | const afterOldRelease = { released: [...released], has: yield* RcMap.has(map, "key") } |
| 84 | const replacement = yield* Effect.scoped(RcMap.getOption(map, "key")) |
| 85 | yield* Scope.close(newScope, Exit.void) |
| 86 | const afterNewRelease = [...released] |
| 87 | if (idleTimeToLive === 1000) { |
| 88 | yield* TestClock.adjust(999) |
| 89 | assert.isTrue(yield* RcMap.has(map, "key")) |
| 90 | yield* TestClock.adjust(1) |
| 91 | assert.isFalse(yield* RcMap.has(map, "key")) |
| 92 | } |
| 93 | yield* Scope.close(mapScope, Exit.void) |
| 94 | |
| 95 | assert.deepStrictEqual(acquired, [1, 2]) |
| 96 | assert.deepStrictEqual(afterOldRelease, { released: [1], has: true }) |
| 97 | assert.deepStrictEqual(replacement, Option.some(2)) |
| 98 | assert.deepStrictEqual(afterNewRelease, idleTimeToLive === 0 ? [1, 2] : [1]) |
| 99 | assert.deepStrictEqual(released, [1, 2]) |
| 100 | }) |
| 101 | ) |
| 102 | |
| 103 | it.effect("an invalidated entry's idle timer cannot remove its replacement", () => |
| 104 | Effect.gen(function*() { |
| 105 | const acquired = yield* Ref.make(0) |
| 106 | const released: Array<number> = [] |
| 107 | const mapScope = yield* Scope.make() |
| 108 | const map = yield* RcMap.make({ |
| 109 | lookup: (_key: string) => |
| 110 | Effect.acquireRelease( |
| 111 | Ref.updateAndGet(acquired, (n) => n + 1), |
| 112 | (value) => Effect.sync(() => released.push(value)) |
| 113 | ), |
| 114 | idleTimeToLive: 1000 |
| 115 | }).pipe(Scope.provide(mapScope)) |
| 116 | |
| 117 | assert.strictEqual(yield* Effect.scoped(RcMap.get(map, "key")), 1) |
| 118 | yield* TestClock.adjust(500) |
| 119 | const oldEntry = map.state._tag === "Open" |
| 120 | ? Option.getOrThrow(MutableHashMap.get(map.state.map, "key")) |
| 121 | : undefined |
| 122 | assert.isDefined(oldEntry) |
| 123 | assert.isDefined(oldEntry.fiber) |
| 124 | const idleFiber = oldEntry.fiber |
| 125 | const oldScope = yield* Scope.make() |
| 126 | const newScope = yield* Scope.make() |
| 127 | assert.strictEqual(yield* RcMap.get(map, "key").pipe(Scope.provide(oldScope)), 1) |
| 128 | yield* RcMap.invalidate(map, "key") |
| 129 | assert.deepStrictEqual(released, []) |
| 130 | assert.strictEqual(yield* RcMap.get(map, "key").pipe(Scope.provide(newScope)), 2) |
| 131 | yield* Scope.close(oldScope, Exit.void) |
| 132 | const idleExit = idleFiber.pollUnsafe() |
| 133 | assert.isDefined(idleExit) |
| 134 | assert.isTrue(Exit.hasInterrupts(idleExit)) |
| 135 | yield* TestClock.adjust(1000) |
| 136 | const replacement = yield* Effect.scoped(RcMap.getOption(map, "key")) |
| 137 | const afterTimer = [...released] |
| 138 | yield* Scope.close(mapScope, Exit.void) |
| 139 | assert.deepStrictEqual(released, [1, 2]) |
| 140 | yield* Scope.close(newScope, Exit.void) |
| 141 | |
| 142 | assert.deepStrictEqual(replacement, Option.some(2)) |
| 143 | assert.deepStrictEqual(afterTimer, [1]) |
| 144 | assert.deepStrictEqual(released, [1, 2]) |
| 145 | assert.strictEqual(yield* Ref.get(acquired), 2) |
| 146 | })) |
| 147 | }) |
| 148 | |
| 149 | describe("getOption", () => { |
| 150 | it.effect("returns None without lookup or capacity acquisition when missing", () => |
| 151 | Effect.gen(function*() { |
| 152 | const lookups = yield* Ref.make(0) |
| 153 | const map = yield* RcMap.make({ |
| 154 | lookup: (key: string) => Ref.update(lookups, (n) => n + 1).pipe(Effect.as(key)), |
| 155 | capacity: 0 |
| 156 | }) |
| 157 | |
| 158 | assert.deepStrictEqual(yield* RcMap.getOption(map, "missing"), Option.none()) |
| 159 | assert.deepStrictEqual(yield* RcMap.getOption("missing")(map), Option.none()) |
| 160 | assert.strictEqual(yield* Ref.get(lookups), 0) |
| 161 | })) |
| 162 | |
| 163 | it.effect("retains a ready entry until the caller scope closes", () => |
| 164 | Effect.gen(function*() { |
| 165 | const released = yield* Ref.make(0) |
| 166 | const map = yield* RcMap.make({ |
| 167 | lookup: (key: string) => |
| 168 | Effect.acquireRelease( |
| 169 | Effect.succeed(key), |
| 170 | () => Ref.update(released, (n) => n + 1) |
| 171 | ) |
| 172 | }) |
| 173 | const ownerScope = yield* Scope.make() |
| 174 | const borrowerScope = yield* Scope.make() |
| 175 | |
| 176 | yield* RcMap.get(map, "ready").pipe(Scope.provide(ownerScope)) |
| 177 | assert.deepStrictEqual( |
| 178 | yield* RcMap.getOption("ready")(map).pipe(Scope.provide(borrowerScope)), |
| 179 | Option.some("ready") |
| 180 | ) |
| 181 | |
| 182 | yield* Scope.close(ownerScope, Exit.void) |
| 183 | assert.strictEqual(yield* Ref.get(released), 0) |
| 184 | yield* Scope.close(borrowerScope, Exit.void) |
| 185 | assert.strictEqual(yield* Ref.get(released), 1) |
| 186 | })) |
| 187 | |
| 188 | it.effect("shares an in-flight entry", () => |
| 189 | Effect.gen(function*() { |
| 190 | const started = yield* Deferred.make<void>() |
| 191 | const complete = yield* Deferred.make<void>() |
| 192 | const released = yield* Ref.make(0) |
| 193 | const map = yield* RcMap.make({ |
| 194 | lookup: (key: string) => |
| 195 | Effect.acquireRelease( |
| 196 | Deferred.succeed(started, void 0).pipe( |
| 197 | Effect.andThen(Deferred.await(complete)), |
| 198 | Effect.as(key) |
| 199 | ), |
| 200 | () => Ref.update(released, (n) => n + 1) |
| 201 | ) |
| 202 | }) |
| 203 | const ownerScope = yield* Scope.make() |
| 204 | const borrowerScope = yield* Scope.make() |
| 205 | const owner = yield* RcMap.get(map, "pending").pipe( |
| 206 | Scope.provide(ownerScope), |
| 207 | Effect.forkChild({ startImmediately: true }) |
| 208 | ) |
| 209 | yield* Deferred.await(started) |
| 210 | const borrower = yield* RcMap.getOption(map, "pending").pipe( |
| 211 | Scope.provide(borrowerScope), |
| 212 | Effect.forkChild({ startImmediately: true }) |
| 213 | ) |
| 214 | |
| 215 | assert.strictEqual(map.state._tag === "Open" ? Array.from(map.state.map)[0][1].refCount : 0, 2) |
| 216 | yield* Deferred.succeed(complete, void 0) |
| 217 | assert.strictEqual(yield* Fiber.join(owner), "pending") |
| 218 | assert.deepStrictEqual(yield* Fiber.join(borrower), Option.some("pending")) |
| 219 | |
| 220 | yield* Scope.close(ownerScope, Exit.void) |
| 221 | assert.strictEqual(yield* Ref.get(released), 0) |
| 222 | yield* Scope.close(borrowerScope, Exit.void) |
| 223 | assert.strictEqual(yield* Ref.get(released), 1) |
| 224 | })) |
| 225 | |
| 226 | it.effect("propagates a cached failure", () => |
| 227 | Effect.gen(function*() { |
| 228 | const lookups = yield* Ref.make(0) |
| 229 | const map = yield* RcMap.make({ |
| 230 | lookup: (_key: string) => Ref.update(lookups, (n) => n + 1).pipe(Effect.andThen(Effect.fail("boom"))) |
| 231 | }) |
| 232 | const ownerScope = yield* Scope.make() |
| 233 | const borrowerScope = yield* Scope.make() |
| 234 | |
| 235 | assert.deepStrictEqual( |
| 236 | yield* RcMap.get(map, "failed").pipe(Scope.provide(ownerScope), Effect.exit), |
| 237 | Exit.fail("boom") |
| 238 | ) |
| 239 | assert.deepStrictEqual( |
| 240 | yield* RcMap.getOption(map, "failed").pipe(Scope.provide(borrowerScope), Effect.exit), |
| 241 | Exit.fail("boom") |
| 242 | ) |
| 243 | assert.strictEqual(yield* Ref.get(lookups), 1) |
| 244 | |
| 245 | yield* Scope.close(ownerScope, Exit.void) |
| 246 | yield* Scope.close(borrowerScope, Exit.void) |
| 247 | })) |
| 248 | |
| 249 | it.effect("retains an idle entry and restarts its TTL on release", () => |
| 250 | Effect.gen(function*() { |
| 251 | const released = yield* Ref.make(0) |
| 252 | const map = yield* RcMap.make({ |
| 253 | lookup: (key: string) => |
| 254 | Effect.acquireRelease( |
| 255 | Effect.succeed(key), |
| 256 | () => Ref.update(released, (n) => n + 1) |
| 257 | ), |
| 258 | idleTimeToLive: 1000 |
| 259 | }) |
| 260 | const ownerScope = yield* Scope.make() |
| 261 | yield* RcMap.get(map, "ttl").pipe(Scope.provide(ownerScope)) |
| 262 | yield* Scope.close(ownerScope, Exit.void) |
| 263 | |
| 264 | yield* TestClock.adjust(500) |
| 265 | const borrowerScope = yield* Scope.make() |
| 266 | assert.deepStrictEqual( |
| 267 | yield* RcMap.getOption(map, "ttl").pipe(Scope.provide(borrowerScope)), |
| 268 | Option.some("ttl") |
| 269 | ) |
| 270 | yield* TestClock.adjust(500) |
| 271 | assert.strictEqual(yield* Ref.get(released), 0) |
| 272 | |
| 273 | yield* Scope.close(borrowerScope, Exit.void) |
| 274 | yield* TestClock.adjust(999) |
| 275 | assert.strictEqual(yield* Ref.get(released), 0) |
| 276 | yield* TestClock.adjust(1) |
| 277 | assert.strictEqual(yield* Ref.get(released), 1) |
| 278 | assert.deepStrictEqual(yield* RcMap.getOption(map, "ttl"), Option.none()) |
| 279 | })) |
| 280 | |
| 281 | it.effect("linearizes retention before invalidation", () => |
| 282 | Effect.gen(function*() { |
| 283 | const released = yield* Ref.make(0) |
| 284 | const map = yield* RcMap.make({ |
| 285 | lookup: (key: string) => |
| 286 | Effect.acquireRelease( |
| 287 | Effect.succeed(key), |
| 288 | () => Ref.update(released, (n) => n + 1) |
| 289 | ) |
| 290 | }) |
| 291 | const ownerScope = yield* Scope.make() |
| 292 | const borrowerScope = yield* Scope.make() |
| 293 | yield* RcMap.get(map, "key").pipe(Scope.provide(ownerScope)) |
| 294 | assert.deepStrictEqual( |
| 295 | yield* RcMap.getOption(map, "key").pipe(Scope.provide(borrowerScope)), |
| 296 | Option.some("key") |
| 297 | ) |
| 298 | |
| 299 | yield* RcMap.invalidate(map, "key") |
| 300 | assert.isFalse(yield* RcMap.has(map, "key")) |
| 301 | yield* Scope.close(ownerScope, Exit.void) |
| 302 | assert.strictEqual(yield* Ref.get(released), 0) |
| 303 | yield* Scope.close(borrowerScope, Exit.void) |
| 304 | assert.strictEqual(yield* Ref.get(released), 1) |
| 305 | })) |
| 306 | |
| 307 | it.effect("returns None when the map is closed", () => |
| 308 | Effect.gen(function*() { |
| 309 | const mapScope = yield* Scope.make() |
| 310 | const map = yield* RcMap.make({ |
| 311 | lookup: (key: string) => Effect.succeed(key) |
| 312 | }).pipe(Scope.provide(mapScope)) |
| 313 | yield* Scope.close(mapScope, Exit.void) |
| 314 | |
| 315 | assert.deepStrictEqual(yield* RcMap.getOption(map, "key"), Option.none()) |
| 316 | })) |
| 317 | }) |
| 318 | |
| 319 | it.effect("deallocation", () => |
| 320 | Effect.gen(function*() { |
| 321 | const acquired: Array<string> = [] |
| 322 | const released: Array<string> = [] |
| 323 | const mapScope = yield* Scope.make() |
| 324 | const map = yield* RcMap.make({ |
| 325 | lookup: (key: string) => |
| 326 | Effect.acquireRelease( |
| 327 | Effect.sync(() => { |
| 328 | acquired.push(key) |
| 329 | return key |
| 330 | }), |
| 331 | () => Effect.sync(() => released.push(key)) |
| 332 | ) |
| 333 | }).pipe( |
| 334 | Scope.provide(mapScope) |
| 335 | ) |
| 336 | |
| 337 | assert.deepStrictEqual(acquired, []) |
| 338 | assert.strictEqual(yield* Effect.scoped(RcMap.get(map, "foo")), "foo") |
| 339 | assert.deepStrictEqual(acquired, ["foo"]) |
| 340 | assert.deepStrictEqual(released, ["foo"]) |
| 341 | |
| 342 | const scopeA = yield* Scope.make() |
| 343 | const scopeB = yield* Scope.make() |
| 344 | yield* RcMap.get(map, "bar").pipe(Scope.provide(scopeA)) |
| 345 | yield* Effect.scoped(RcMap.get(map, "bar")) |
| 346 | yield* RcMap.get(map, "baz").pipe(Scope.provide(scopeB)) |
| 347 | yield* Effect.scoped(RcMap.get(map, "baz")) |
| 348 | assert.deepStrictEqual(acquired, ["foo", "bar", "baz"]) |
| 349 | assert.deepStrictEqual(released, ["foo"]) |
| 350 | yield* Scope.close(scopeB, Exit.void) |
| 351 | assert.deepStrictEqual(acquired, ["foo", "bar", "baz"]) |
| 352 | assert.deepStrictEqual(released, ["foo", "baz"]) |
| 353 | yield* Scope.close(scopeA, Exit.void) |
| 354 | assert.deepStrictEqual(acquired, ["foo", "bar", "baz"]) |
| 355 | assert.deepStrictEqual(released, ["foo", "baz", "bar"]) |
| 356 | |
| 357 | const scopeC = yield* Scope.make() |
| 358 | yield* RcMap.get(map, "qux").pipe(Scope.provide(scopeC)) |
| 359 | assert.deepStrictEqual(acquired, ["foo", "bar", "baz", "qux"]) |
| 360 | assert.deepStrictEqual(released, ["foo", "baz", "bar"]) |
| 361 | |
| 362 | yield* Scope.close(mapScope, Exit.void) |
| 363 | assert.deepStrictEqual(acquired, ["foo", "bar", "baz", "qux"]) |
| 364 | assert.deepStrictEqual(released, ["foo", "baz", "bar", "qux"]) |
| 365 | |
| 366 | const exit = yield* RcMap.get(map, "boom").pipe(Effect.scoped, Effect.exit) |
| 367 | assert.isTrue(Exit.hasInterrupts(exit)) |
| 368 | })) |
| 369 | |
| 370 | it.effect("idleTimeToLive", () => |
| 371 | Effect.gen(function*() { |
| 372 | const acquired: Array<string> = [] |
| 373 | const released: Array<string> = [] |
| 374 | const map = yield* RcMap.make({ |
| 375 | lookup: (key: string) => |
| 376 | Effect.acquireRelease( |
| 377 | Effect.sync(() => { |
| 378 | acquired.push(key) |
| 379 | return key |
| 380 | }), |
| 381 | () => Effect.sync(() => released.push(key)) |
| 382 | ), |
| 383 | idleTimeToLive: 1000 |
| 384 | }) |
| 385 | |
| 386 | assert.deepStrictEqual(acquired, []) |
| 387 | assert.strictEqual(yield* Effect.scoped(RcMap.get(map, "foo")), "foo") |
| 388 | assert.deepStrictEqual(acquired, ["foo"]) |
| 389 | assert.deepStrictEqual(released, []) |
| 390 | |
| 391 | yield* TestClock.adjust(1000) |
| 392 | assert.deepStrictEqual(released, ["foo"]) |
| 393 | |
| 394 | assert.strictEqual(yield* Effect.scoped(RcMap.get(map, "bar")), "bar") |
| 395 | assert.deepStrictEqual(acquired, ["foo", "bar"]) |
| 396 | assert.deepStrictEqual(released, ["foo"]) |
| 397 | |
| 398 | yield* TestClock.adjust(500) |
| 399 | assert.strictEqual(yield* Effect.scoped(RcMap.get(map, "bar")), "bar") |
| 400 | assert.deepStrictEqual(acquired, ["foo", "bar"]) |
| 401 | assert.deepStrictEqual(released, ["foo"]) |
| 402 | |
| 403 | yield* TestClock.adjust(1000) |
| 404 | assert.deepStrictEqual(released, ["foo", "bar"]) |
| 405 | |
| 406 | yield* Effect.scoped(RcMap.get(map, "baz")) |
| 407 | assert.deepStrictEqual(acquired, ["foo", "bar", "baz"]) |
| 408 | yield* RcMap.invalidate(map, "baz") |
| 409 | assert.deepStrictEqual(acquired, ["foo", "bar", "baz"]) |
| 410 | assert.deepStrictEqual(released, ["foo", "bar", "baz"]) |
| 411 | })) |
| 412 | |
| 413 | it.effect(".touch", () => |
| 414 | Effect.gen(function*() { |
| 415 | const acquired: Array<string> = [] |
| 416 | const released: Array<string> = [] |
| 417 | const map = yield* RcMap.make({ |
| 418 | lookup: (key: string) => |
| 419 | Effect.acquireRelease( |
| 420 | Effect.sync(() => { |
| 421 | acquired.push(key) |
| 422 | return key |
| 423 | }), |
| 424 | () => Effect.sync(() => released.push(key)) |
| 425 | ), |
| 426 | idleTimeToLive: 1000 |
| 427 | }) |
| 428 | |
| 429 | assert.deepStrictEqual(acquired, []) |
| 430 | assert.strictEqual(yield* Effect.scoped(RcMap.get(map, "foo")), "foo") |
| 431 | assert.deepStrictEqual(acquired, ["foo"]) |
| 432 | assert.deepStrictEqual(released, []) |
| 433 | |
| 434 | yield* TestClock.adjust(500) |
| 435 | assert.deepStrictEqual(released, []) |
| 436 | |
| 437 | yield* RcMap.touch(map, "foo") |
| 438 | yield* TestClock.adjust(500) |
| 439 | assert.deepStrictEqual(released, []) |
| 440 | yield* TestClock.adjust(500) |
| 441 | assert.deepStrictEqual(released, ["foo"]) |
| 442 | })) |
| 443 | |
| 444 | it.effect("capacity", () => |
| 445 | Effect.gen(function*() { |
| 446 | const map = yield* RcMap.make({ |
| 447 | lookup: (key: string) => Effect.succeed(key), |
| 448 | capacity: 2, |
| 449 | idleTimeToLive: 1000 |
| 450 | }) |
| 451 | |
| 452 | assert.strictEqual(yield* Effect.scoped(RcMap.get(map, "foo")), "foo") |
| 453 | assert.strictEqual(yield* Effect.scoped(RcMap.get(map, "foo")), "foo") |
| 454 | assert.strictEqual(yield* Effect.scoped(RcMap.get(map, "bar")), "bar") |
| 455 | |
| 456 | const exit = yield* RcMap.get(map, "baz").pipe(Effect.scoped, Effect.exit) |
| 457 | assert.deepStrictEqual( |
| 458 | exit, |
| 459 | Exit.fail(new Cause.ExceededCapacityError(`RcMap attempted to exceed capacity of 2`)) |
| 460 | ) |
| 461 | |
| 462 | yield* TestClock.adjust(1000) |
| 463 | assert.strictEqual(yield* Effect.scoped(RcMap.get(map, "baz")), "baz") |
| 464 | })) |
| 465 | |
| 466 | it.effect("complex key", () => |
| 467 | Effect.gen(function*() { |
| 468 | class Key extends Data.Class<{ readonly id: number }> {} |
| 469 | const map = yield* RcMap.make({ |
| 470 | lookup: (key: Key) => Effect.succeed(key.id), |
| 471 | capacity: 1 |
| 472 | }) |
| 473 | |
| 474 | assert.strictEqual(yield* RcMap.get(map, new Key({ id: 1 })), 1) |
| 475 | // no failure means a hit |
| 476 | assert.strictEqual(yield* RcMap.get(map, new Key({ id: 1 })), 1) |
| 477 | })) |
| 478 | |
| 479 | it.effect("keys lookup", () => |
| 480 | Effect.gen(function*() { |
| 481 | const map = yield* RcMap.make({ |
| 482 | lookup: (key: string) => Effect.succeed(key) |
| 483 | }) |
| 484 | |
| 485 | yield* RcMap.get(map, "foo") |
| 486 | yield* RcMap.get(map, "bar") |
| 487 | yield* RcMap.get(map, "baz") |
| 488 | |
| 489 | assert.deepStrictEqual(Array.from(yield* RcMap.keys(map)), ["foo", "bar", "baz"]) |
| 490 | })) |
| 491 | |
| 492 | it.effect("dynamic idleTimeToLive", () => |
| 493 | Effect.gen(function*() { |
| 494 | const acquired: Array<string> = [] |
| 495 | const released: Array<string> = [] |
| 496 | const map = yield* RcMap.make({ |
| 497 | lookup: (key: string) => |
| 498 | Effect.acquireRelease( |
| 499 | Effect.sync(() => { |
| 500 | acquired.push(key) |
| 501 | return key |
| 502 | }), |
| 503 | () => Effect.sync(() => released.push(key)) |
| 504 | ), |
| 505 | idleTimeToLive: (key: string) => key.startsWith("short:") ? 500 : 2000 |
| 506 | }) |
| 507 | |
| 508 | assert.deepStrictEqual(acquired, []) |
| 509 | |
| 510 | yield* Effect.scoped(RcMap.get(map, "short:a")) |
| 511 | yield* Effect.scoped(RcMap.get(map, "long:b")) |
| 512 | assert.deepStrictEqual(acquired, ["short:a", "long:b"]) |
| 513 | assert.deepStrictEqual(released, []) |
| 514 | |
| 515 | yield* TestClock.adjust(500) |
| 516 | assert.deepStrictEqual(released, ["short:a"]) |
| 517 | |
| 518 | yield* TestClock.adjust(1500) |
| 519 | assert.deepStrictEqual(released, ["short:a", "long:b"]) |
| 520 | })) |
| 521 | |
| 522 | it.effect("dynamic idleTimeToLive with touch", () => |
| 523 | Effect.gen(function*() { |
| 524 | const acquired: Array<string> = [] |
| 525 | const released: Array<string> = [] |
| 526 | const map = yield* RcMap.make({ |
| 527 | lookup: (key: string) => |
| 528 | Effect.acquireRelease( |
| 529 | Effect.sync(() => { |
| 530 | acquired.push(key) |
| 531 | return key |
| 532 | }), |
| 533 | () => Effect.sync(() => released.push(key)) |
| 534 | ), |
| 535 | idleTimeToLive: (key: string) => key.startsWith("short:") ? 500 : 2000 |
| 536 | }) |
| 537 | |
| 538 | yield* Effect.scoped(RcMap.get(map, "short:a")) |
| 539 | assert.deepStrictEqual(acquired, ["short:a"]) |
| 540 | assert.deepStrictEqual(released, []) |
| 541 | |
| 542 | yield* TestClock.adjust(250) |
| 543 | yield* RcMap.touch(map, "short:a") |
| 544 | yield* TestClock.adjust(250) |
| 545 | assert.deepStrictEqual(released, []) |
| 546 | |
| 547 | yield* TestClock.adjust(250) |
| 548 | assert.deepStrictEqual(released, ["short:a"]) |
| 549 | })) |
| 550 | |
| 551 | it.effect("closing the map while an idle entry is being released runs every finalizer", () => |
| 552 | Effect.gen(function*() { |
| 553 | const gate = yield* Deferred.make<void>() |
| 554 | let released = 0 |
| 555 | const scope = yield* Scope.make() |
| 556 | const map = yield* RcMap.make({ |
| 557 | lookup: (_: string) => |
| 558 | Effect.gen(function*() { |
| 559 | yield* Effect.addFinalizer(() => Effect.sync(() => released++)) |
| 560 | yield* Effect.addFinalizer(() => Effect.andThen(Deferred.await(gate), Effect.sync(() => released++))) |
| 561 | }), |
| 562 | idleTimeToLive: "1 second" |
| 563 | }).pipe(Scope.provide(scope)) |
| 564 | yield* Effect.scoped(RcMap.get(map, "a")) |
| 565 | yield* TestClock.adjust("1 second") |
| 566 | assert.isFalse(yield* RcMap.has(map, "a")) |
| 567 | const close = yield* Effect.forkChild(Scope.close(scope, Exit.void), { startImmediately: true }) |
| 568 | yield* Deferred.succeed(gate, void 0) |
| 569 | yield* Fiber.join(close) |
| 570 | assert.strictEqual(released, 2) |
| 571 | })) |
| 572 | |
| 573 | it.effect("drops an interrupted lookup and closes its scope", () => |
| 574 | Effect.gen(function*() { |
| 575 | let lookups = 0 |
| 576 | const started = yield* Deferred.make<void>() |
| 577 | const interrupt = yield* Deferred.make<void>() |
| 578 | const finalized = yield* Deferred.make<void>() |
| 579 | const map = yield* RcMap.make({ |
| 580 | lookup: (_key: string) => |
| 581 | Effect.gen(function*() { |
| 582 | if (++lookups > 1) return lookups |
| 583 | yield* Effect.addFinalizer(() => Deferred.succeed(finalized, void 0)) |
| 584 | yield* Deferred.succeed(started, void 0) |
| 585 | yield* Deferred.await(interrupt) |
| 586 | return yield* Effect.interrupt |
| 587 | }), |
| 588 | idleTimeToLive: "1 minute" |
| 589 | }) |
| 590 | |
| 591 | // The only borrower leaves, so nothing else releases the entry. |
| 592 | const borrower = yield* Effect.forkChild(Effect.scoped(RcMap.get(map, "key")), { startImmediately: true }) |
| 593 | yield* Deferred.await(started) |
| 594 | yield* Fiber.interrupt(borrower) |
| 595 | yield* Deferred.succeed(interrupt, void 0) |
| 596 | |
| 597 | assert.strictEqual(yield* Effect.scoped(RcMap.get(map, "key")), 2) |
| 598 | yield* Deferred.await(finalized) |
| 599 | })) |
| 600 | }) |