reactor-effect-client 0.7.0 → 0.8.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/NOTICE +7 -2
- package/README.md +103 -141
- package/dist/Coordinator.d.ts +382 -0
- package/dist/Coordinator.d.ts.map +1 -0
- package/dist/Coordinator.js +732 -0
- package/dist/Coordinator.js.map +1 -0
- package/dist/H3.d.ts +159 -0
- package/dist/H3.d.ts.map +1 -0
- package/dist/H3.js +21 -0
- package/dist/H3.js.map +1 -0
- package/dist/H3Source.d.ts +93 -0
- package/dist/H3Source.d.ts.map +1 -0
- package/dist/H3Source.js +401 -0
- package/dist/H3Source.js.map +1 -0
- package/dist/LocalSource.d.ts +68 -0
- package/dist/LocalSource.d.ts.map +1 -0
- package/dist/LocalSource.js +354 -0
- package/dist/LocalSource.js.map +1 -0
- package/dist/Media.d.ts +106 -0
- package/dist/Media.d.ts.map +1 -0
- package/dist/Media.js +16 -0
- package/dist/Media.js.map +1 -0
- package/dist/Peer.d.ts +134 -0
- package/dist/Peer.d.ts.map +1 -0
- package/dist/Peer.js +57 -0
- package/dist/Peer.js.map +1 -0
- package/dist/Playout.d.ts +724 -0
- package/dist/Playout.d.ts.map +1 -0
- package/dist/Playout.js +28 -0
- package/dist/Playout.js.map +1 -0
- package/dist/Reactor.d.ts +135 -0
- package/dist/Reactor.d.ts.map +1 -0
- package/dist/Reactor.js +158 -0
- package/dist/Reactor.js.map +1 -0
- package/dist/ReactorError.d.ts +331 -0
- package/dist/ReactorError.d.ts.map +1 -0
- package/dist/ReactorError.js +316 -0
- package/dist/ReactorError.js.map +1 -0
- package/dist/ReactorTest.d.ts +383 -0
- package/dist/ReactorTest.d.ts.map +1 -0
- package/dist/ReactorTest.js +317 -0
- package/dist/ReactorTest.js.map +1 -0
- package/dist/Session.d.ts +314 -0
- package/dist/Session.d.ts.map +1 -0
- package/dist/Session.js +51 -0
- package/dist/Session.js.map +1 -0
- package/dist/index.d.ts +12 -22
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -11
- package/dist/index.js.map +1 -1
- package/dist/internal/correlator.d.ts +46 -0
- package/dist/internal/correlator.d.ts.map +1 -0
- package/dist/internal/correlator.js +150 -0
- package/dist/internal/correlator.js.map +1 -0
- package/dist/internal/deadline.d.ts +24 -0
- package/dist/internal/deadline.d.ts.map +1 -0
- package/dist/internal/deadline.js +44 -0
- package/dist/internal/deadline.js.map +1 -0
- package/dist/{h3/_internal/contracts.d.ts → internal/h3/commands.d.ts} +39 -33
- package/dist/internal/h3/commands.d.ts.map +1 -0
- package/dist/internal/h3/commands.js +128 -0
- package/dist/internal/h3/commands.js.map +1 -0
- package/dist/{h3 → internal/h3}/messages.d.ts +16 -11
- package/dist/internal/h3/messages.d.ts.map +1 -0
- package/dist/{h3 → internal/h3}/messages.js +19 -31
- package/dist/internal/h3/messages.js.map +1 -0
- package/dist/internal/h3/operations.d.ts +145 -0
- package/dist/internal/h3/operations.d.ts.map +1 -0
- package/dist/internal/h3/operations.js +284 -0
- package/dist/internal/h3/operations.js.map +1 -0
- package/dist/{h3 → internal/h3}/profile.d.ts +63 -65
- package/dist/internal/h3/profile.d.ts.map +1 -0
- package/dist/{h3 → internal/h3}/profile.js +39 -39
- package/dist/internal/h3/profile.js.map +1 -0
- package/dist/internal/h3/provider.d.ts +14 -0
- package/dist/internal/h3/provider.d.ts.map +1 -0
- package/dist/internal/h3/provider.js +672 -0
- package/dist/internal/h3/provider.js.map +1 -0
- package/dist/internal/h3/references.d.ts +68 -0
- package/dist/internal/h3/references.d.ts.map +1 -0
- package/dist/internal/h3/references.js +339 -0
- package/dist/internal/h3/references.js.map +1 -0
- package/dist/internal/h3/request.d.ts +56 -0
- package/dist/internal/h3/request.d.ts.map +1 -0
- package/dist/internal/h3/request.js +70 -0
- package/dist/internal/h3/request.js.map +1 -0
- package/dist/internal/h3/state.d.ts +120 -0
- package/dist/internal/h3/state.d.ts.map +1 -0
- package/dist/internal/h3/state.js +270 -0
- package/dist/internal/h3/state.js.map +1 -0
- package/dist/{Submission.d.ts → internal/h3/submission.d.ts} +1 -3
- package/dist/internal/h3/submission.d.ts.map +1 -0
- package/dist/{Submission.js → internal/h3/submission.js} +1 -9
- package/dist/internal/h3/submission.js.map +1 -0
- package/dist/internal/hub.d.ts +32 -0
- package/dist/internal/hub.d.ts.map +1 -0
- package/dist/internal/hub.js +92 -0
- package/dist/internal/hub.js.map +1 -0
- package/dist/internal/playout/errors.d.ts +51 -0
- package/dist/internal/playout/errors.d.ts.map +1 -0
- package/dist/internal/playout/errors.js +40 -0
- package/dist/internal/playout/errors.js.map +1 -0
- package/dist/internal/playout/policy.d.ts +559 -0
- package/dist/internal/playout/policy.d.ts.map +1 -0
- package/dist/internal/playout/policy.js +2338 -0
- package/dist/internal/playout/policy.js.map +1 -0
- package/dist/internal/playout/runtime.d.ts +28 -0
- package/dist/internal/playout/runtime.d.ts.map +1 -0
- package/dist/internal/playout/runtime.js +766 -0
- package/dist/internal/playout/runtime.js.map +1 -0
- package/dist/internal/playout/tag.d.ts +14 -0
- package/dist/internal/playout/tag.d.ts.map +1 -0
- package/dist/internal/playout/tag.js +43 -0
- package/dist/internal/playout/tag.js.map +1 -0
- package/dist/internal/proto/reactor_wire/v1/common_pb.d.ts +55 -0
- package/dist/internal/proto/reactor_wire/v1/common_pb.d.ts.map +1 -0
- package/dist/internal/proto/reactor_wire/v1/common_pb.js +42 -0
- package/dist/internal/proto/reactor_wire/v1/common_pb.js.map +1 -0
- package/dist/internal/proto/reactor_wire/v1/control_pb.d.ts +160 -0
- package/dist/internal/proto/reactor_wire/v1/control_pb.d.ts.map +1 -0
- package/dist/internal/proto/reactor_wire/v1/control_pb.js +21 -0
- package/dist/internal/proto/reactor_wire/v1/control_pb.js.map +1 -0
- package/dist/internal/proto/reactor_wire/v1/data_pb.d.ts +89 -0
- package/dist/internal/proto/reactor_wire/v1/data_pb.d.ts.map +1 -0
- package/dist/internal/proto/reactor_wire/v1/data_pb.js +20 -0
- package/dist/internal/proto/reactor_wire/v1/data_pb.js.map +1 -0
- package/dist/internal/proto/reactor_wire/v1/model_pb.d.ts +85 -0
- package/dist/internal/proto/reactor_wire/v1/model_pb.d.ts.map +1 -0
- package/dist/internal/proto/reactor_wire/v1/model_pb.js +24 -0
- package/dist/internal/proto/reactor_wire/v1/model_pb.js.map +1 -0
- package/dist/internal/proto/reactor_wire/v1/platform_pb.d.ts +190 -0
- package/dist/internal/proto/reactor_wire/v1/platform_pb.d.ts.map +1 -0
- package/dist/internal/proto/reactor_wire/v1/platform_pb.js +54 -0
- package/dist/internal/proto/reactor_wire/v1/platform_pb.js.map +1 -0
- package/dist/internal/proto/reactor_wire/v1/track_pb.d.ts +88 -0
- package/dist/internal/proto/reactor_wire/v1/track_pb.d.ts.map +1 -0
- package/dist/internal/proto/reactor_wire/v1/track_pb.js +33 -0
- package/dist/internal/proto/reactor_wire/v1/track_pb.js.map +1 -0
- package/dist/internal/queue.d.ts +4 -0
- package/dist/internal/queue.d.ts.map +1 -0
- package/dist/{_internal → internal}/queue.js +0 -9
- package/dist/internal/queue.js.map +1 -0
- package/dist/internal/reactorTest/coordinator.d.ts +5 -0
- package/dist/internal/reactorTest/coordinator.d.ts.map +1 -0
- package/dist/internal/reactorTest/coordinator.js +178 -0
- package/dist/internal/reactorTest/coordinator.js.map +1 -0
- package/dist/internal/reactorTest/faults.d.ts +172 -0
- package/dist/internal/reactorTest/faults.d.ts.map +1 -0
- package/dist/internal/reactorTest/faults.js +23 -0
- package/dist/internal/reactorTest/faults.js.map +1 -0
- package/dist/internal/reactorTest/h3.d.ts +431 -0
- package/dist/internal/reactorTest/h3.d.ts.map +1 -0
- package/dist/internal/reactorTest/h3.js +625 -0
- package/dist/internal/reactorTest/h3.js.map +1 -0
- package/dist/internal/reactorTest/media.d.ts +22 -0
- package/dist/internal/reactorTest/media.d.ts.map +1 -0
- package/dist/internal/reactorTest/media.js +34 -0
- package/dist/internal/reactorTest/media.js.map +1 -0
- package/dist/internal/reactorTest/peer.d.ts +318 -0
- package/dist/internal/reactorTest/peer.d.ts.map +1 -0
- package/dist/internal/reactorTest/peer.js +206 -0
- package/dist/internal/reactorTest/peer.js.map +1 -0
- package/dist/internal/reactorTest/playout.d.ts +34 -0
- package/dist/internal/reactorTest/playout.d.ts.map +1 -0
- package/dist/internal/reactorTest/playout.js +340 -0
- package/dist/internal/reactorTest/playout.js.map +1 -0
- package/dist/internal/reactorTest/references.d.ts +25 -0
- package/dist/internal/reactorTest/references.d.ts.map +1 -0
- package/dist/{testing/Png.js → internal/reactorTest/references.js} +50 -12
- package/dist/internal/reactorTest/references.js.map +1 -0
- package/dist/internal/reactorTest/sessions.d.ts +364 -0
- package/dist/internal/reactorTest/sessions.d.ts.map +1 -0
- package/dist/internal/reactorTest/sessions.js +592 -0
- package/dist/internal/reactorTest/sessions.js.map +1 -0
- package/dist/internal/reactorTest/timing.d.ts +13 -0
- package/dist/internal/reactorTest/timing.d.ts.map +1 -0
- package/dist/internal/reactorTest/timing.js +20 -0
- package/dist/internal/reactorTest/timing.js.map +1 -0
- package/dist/internal/recording.d.ts +52 -0
- package/dist/internal/recording.d.ts.map +1 -0
- package/dist/internal/recording.js +141 -0
- package/dist/internal/recording.js.map +1 -0
- package/dist/internal/session/connect.d.ts +34 -0
- package/dist/internal/session/connect.d.ts.map +1 -0
- package/dist/internal/session/connect.js +502 -0
- package/dist/internal/session/connect.js.map +1 -0
- package/dist/internal/session/generation.d.ts +35 -0
- package/dist/internal/session/generation.d.ts.map +1 -0
- package/dist/internal/session/generation.js +185 -0
- package/dist/internal/session/generation.js.map +1 -0
- package/dist/internal/session/ice.d.ts +23 -0
- package/dist/internal/session/ice.d.ts.map +1 -0
- package/dist/internal/session/ice.js +57 -0
- package/dist/internal/session/ice.js.map +1 -0
- package/dist/internal/session/inbound.d.ts +50 -0
- package/dist/internal/session/inbound.d.ts.map +1 -0
- package/dist/internal/session/inbound.js +202 -0
- package/dist/internal/session/inbound.js.map +1 -0
- package/dist/internal/session/model.d.ts +163 -0
- package/dist/internal/session/model.d.ts.map +1 -0
- package/dist/internal/session/model.js +78 -0
- package/dist/internal/session/model.js.map +1 -0
- package/dist/internal/session/requests.d.ts +71 -0
- package/dist/internal/session/requests.d.ts.map +1 -0
- package/dist/internal/session/requests.js +240 -0
- package/dist/internal/session/requests.js.map +1 -0
- package/dist/internal/session/tracks.d.ts +21 -0
- package/dist/internal/session/tracks.d.ts.map +1 -0
- package/dist/internal/session/tracks.js +166 -0
- package/dist/internal/session/tracks.js.map +1 -0
- package/dist/internal/session/upload.d.ts +21 -0
- package/dist/internal/session/upload.d.ts.map +1 -0
- package/dist/internal/session/upload.js +68 -0
- package/dist/internal/session/upload.js.map +1 -0
- package/dist/internal/session.d.ts +66 -0
- package/dist/internal/session.d.ts.map +1 -0
- package/dist/internal/session.js +280 -0
- package/dist/internal/session.js.map +1 -0
- package/dist/{stats.d.ts → internal/stats.d.ts} +30 -16
- package/dist/internal/stats.d.ts.map +1 -0
- package/dist/internal/stats.js +201 -0
- package/dist/internal/stats.js.map +1 -0
- package/dist/internal/token.d.ts +22 -0
- package/dist/internal/token.d.ts.map +1 -0
- package/dist/internal/token.js +65 -0
- package/dist/internal/token.js.map +1 -0
- package/dist/internal/wire.d.ts +37 -0
- package/dist/internal/wire.d.ts.map +1 -0
- package/dist/internal/wire.js +49 -0
- package/dist/internal/wire.js.map +1 -0
- package/notices/bufbuild-protobuf-LICENSE +201 -0
- package/package.json +17 -38
- package/dist/FetchHttp.d.ts +0 -9
- package/dist/FetchHttp.d.ts.map +0 -1
- package/dist/FetchHttp.js +0 -63
- package/dist/FetchHttp.js.map +0 -1
- package/dist/PeerFactory.d.ts +0 -26
- package/dist/PeerFactory.d.ts.map +0 -1
- package/dist/PeerFactory.js +0 -4
- package/dist/PeerFactory.js.map +0 -1
- package/dist/PeerTypes.d.ts +0 -64
- package/dist/PeerTypes.d.ts.map +0 -1
- package/dist/PeerTypes.js +0 -2
- package/dist/PeerTypes.js.map +0 -1
- package/dist/Sequence.d.ts +0 -85
- package/dist/Sequence.d.ts.map +0 -1
- package/dist/Sequence.js +0 -210
- package/dist/Sequence.js.map +0 -1
- package/dist/SessionTypes.d.ts +0 -312
- package/dist/SessionTypes.d.ts.map +0 -1
- package/dist/SessionTypes.js +0 -19
- package/dist/SessionTypes.js.map +0 -1
- package/dist/Submission.d.ts.map +0 -1
- package/dist/Submission.js.map +0 -1
- package/dist/_internal/queue.d.ts +0 -6
- package/dist/_internal/queue.d.ts.map +0 -1
- package/dist/_internal/queue.js.map +0 -1
- package/dist/bytes.d.ts +0 -6
- package/dist/bytes.d.ts.map +0 -1
- package/dist/bytes.js +0 -32
- package/dist/bytes.js.map +0 -1
- package/dist/contract.d.ts +0 -149
- package/dist/contract.d.ts.map +0 -1
- package/dist/contract.js +0 -119
- package/dist/contract.js.map +0 -1
- package/dist/coordinator/_internal/client.d.ts +0 -215
- package/dist/coordinator/_internal/client.d.ts.map +0 -1
- package/dist/coordinator/_internal/client.js +0 -563
- package/dist/coordinator/_internal/client.js.map +0 -1
- package/dist/coordinator/_internal/recording.d.ts +0 -31
- package/dist/coordinator/_internal/recording.d.ts.map +0 -1
- package/dist/coordinator/_internal/recording.js +0 -123
- package/dist/coordinator/_internal/recording.js.map +0 -1
- package/dist/coordinator/_internal/response.d.ts +0 -15
- package/dist/coordinator/_internal/response.d.ts.map +0 -1
- package/dist/coordinator/_internal/response.js +0 -55
- package/dist/coordinator/_internal/response.js.map +0 -1
- package/dist/coordinator/_internal/schemas.d.ts +0 -73
- package/dist/coordinator/_internal/schemas.d.ts.map +0 -1
- package/dist/coordinator/_internal/schemas.js +0 -200
- package/dist/coordinator/_internal/schemas.js.map +0 -1
- package/dist/coordinator/index.d.ts +0 -40
- package/dist/coordinator/index.d.ts.map +0 -1
- package/dist/coordinator/index.js +0 -21
- package/dist/coordinator/index.js.map +0 -1
- package/dist/correlation.d.ts +0 -47
- package/dist/correlation.d.ts.map +0 -1
- package/dist/correlation.js +0 -131
- package/dist/correlation.js.map +0 -1
- package/dist/duration.d.ts +0 -27
- package/dist/duration.d.ts.map +0 -1
- package/dist/duration.js +0 -47
- package/dist/duration.js.map +0 -1
- package/dist/errors.d.ts +0 -1303
- package/dist/errors.d.ts.map +0 -1
- package/dist/errors.js +0 -745
- package/dist/errors.js.map +0 -1
- package/dist/h3/_internal/client.d.ts +0 -9
- package/dist/h3/_internal/client.d.ts.map +0 -1
- package/dist/h3/_internal/client.js +0 -652
- package/dist/h3/_internal/client.js.map +0 -1
- package/dist/h3/_internal/contracts.d.ts.map +0 -1
- package/dist/h3/_internal/contracts.js +0 -123
- package/dist/h3/_internal/contracts.js.map +0 -1
- package/dist/h3/_internal/deployment.d.ts +0 -8
- package/dist/h3/_internal/deployment.d.ts.map +0 -1
- package/dist/h3/_internal/deployment.js +0 -149
- package/dist/h3/_internal/deployment.js.map +0 -1
- package/dist/h3/_internal/evidence.d.ts +0 -17
- package/dist/h3/_internal/evidence.d.ts.map +0 -1
- package/dist/h3/_internal/evidence.js +0 -48
- package/dist/h3/_internal/evidence.js.map +0 -1
- package/dist/h3/_internal/operations.d.ts +0 -58
- package/dist/h3/_internal/operations.d.ts.map +0 -1
- package/dist/h3/_internal/operations.js +0 -268
- package/dist/h3/_internal/operations.js.map +0 -1
- package/dist/h3/_internal/references.d.ts +0 -24
- package/dist/h3/_internal/references.d.ts.map +0 -1
- package/dist/h3/_internal/references.js +0 -309
- package/dist/h3/_internal/references.js.map +0 -1
- package/dist/h3/_internal/request.d.ts +0 -21
- package/dist/h3/_internal/request.d.ts.map +0 -1
- package/dist/h3/_internal/request.js +0 -97
- package/dist/h3/_internal/request.js.map +0 -1
- package/dist/h3/_internal/retained.d.ts +0 -22
- package/dist/h3/_internal/retained.d.ts.map +0 -1
- package/dist/h3/_internal/retained.js +0 -89
- package/dist/h3/_internal/retained.js.map +0 -1
- package/dist/h3/_internal/state.d.ts +0 -44
- package/dist/h3/_internal/state.d.ts.map +0 -1
- package/dist/h3/_internal/state.js +0 -283
- package/dist/h3/_internal/state.js.map +0 -1
- package/dist/h3/index.d.ts +0 -10
- package/dist/h3/index.d.ts.map +0 -1
- package/dist/h3/index.js +0 -7
- package/dist/h3/index.js.map +0 -1
- package/dist/h3/messages.d.ts.map +0 -1
- package/dist/h3/messages.js.map +0 -1
- package/dist/h3/profile.d.ts.map +0 -1
- package/dist/h3/profile.js.map +0 -1
- package/dist/h3/types.d.ts +0 -291
- package/dist/h3/types.d.ts.map +0 -1
- package/dist/h3/types.js +0 -2
- package/dist/h3/types.js.map +0 -1
- package/dist/host.d.ts +0 -26
- package/dist/host.d.ts.map +0 -1
- package/dist/host.js +0 -20
- package/dist/host.js.map +0 -1
- package/dist/json.d.ts +0 -25
- package/dist/json.d.ts.map +0 -1
- package/dist/json.js +0 -143
- package/dist/json.js.map +0 -1
- package/dist/media-stream.d.ts +0 -10
- package/dist/media-stream.d.ts.map +0 -1
- package/dist/media-stream.js +0 -18
- package/dist/media-stream.js.map +0 -1
- package/dist/observation.d.ts +0 -38
- package/dist/observation.d.ts.map +0 -1
- package/dist/observation.js +0 -113
- package/dist/observation.js.map +0 -1
- package/dist/orchestration/elapsed.d.ts +0 -9
- package/dist/orchestration/elapsed.d.ts.map +0 -1
- package/dist/orchestration/elapsed.js +0 -8
- package/dist/orchestration/elapsed.js.map +0 -1
- package/dist/orchestration/h3-source.d.ts +0 -58
- package/dist/orchestration/h3-source.d.ts.map +0 -1
- package/dist/orchestration/h3-source.js +0 -362
- package/dist/orchestration/h3-source.js.map +0 -1
- package/dist/orchestration/index.d.ts +0 -17
- package/dist/orchestration/index.d.ts.map +0 -1
- package/dist/orchestration/index.js +0 -12
- package/dist/orchestration/index.js.map +0 -1
- package/dist/orchestration/media-buffer.d.ts +0 -32
- package/dist/orchestration/media-buffer.d.ts.map +0 -1
- package/dist/orchestration/media-buffer.js +0 -94
- package/dist/orchestration/media-buffer.js.map +0 -1
- package/dist/orchestration/open-h3.d.ts +0 -101
- package/dist/orchestration/open-h3.d.ts.map +0 -1
- package/dist/orchestration/open-h3.js +0 -127
- package/dist/orchestration/open-h3.js.map +0 -1
- package/dist/orchestration/queries.d.ts +0 -10
- package/dist/orchestration/queries.d.ts.map +0 -1
- package/dist/orchestration/queries.js +0 -43
- package/dist/orchestration/queries.js.map +0 -1
- package/dist/orchestration/references.d.ts +0 -18
- package/dist/orchestration/references.d.ts.map +0 -1
- package/dist/orchestration/references.js +0 -79
- package/dist/orchestration/references.js.map +0 -1
- package/dist/orchestration/renewal-state.d.ts +0 -84
- package/dist/orchestration/renewal-state.d.ts.map +0 -1
- package/dist/orchestration/renewal-state.js +0 -95
- package/dist/orchestration/renewal-state.js.map +0 -1
- package/dist/orchestration/renewal.d.ts +0 -103
- package/dist/orchestration/renewal.d.ts.map +0 -1
- package/dist/orchestration/renewal.js +0 -1088
- package/dist/orchestration/renewal.js.map +0 -1
- package/dist/orchestration/request.d.ts +0 -65
- package/dist/orchestration/request.d.ts.map +0 -1
- package/dist/orchestration/request.js +0 -134
- package/dist/orchestration/request.js.map +0 -1
- package/dist/orchestration/retention.d.ts +0 -100
- package/dist/orchestration/retention.d.ts.map +0 -1
- package/dist/orchestration/retention.js +0 -134
- package/dist/orchestration/retention.js.map +0 -1
- package/dist/orchestration/routing.d.ts +0 -21
- package/dist/orchestration/routing.d.ts.map +0 -1
- package/dist/orchestration/routing.js +0 -93
- package/dist/orchestration/routing.js.map +0 -1
- package/dist/orchestration/scheduler-key.d.ts +0 -14
- package/dist/orchestration/scheduler-key.d.ts.map +0 -1
- package/dist/orchestration/scheduler-key.js +0 -62
- package/dist/orchestration/scheduler-key.js.map +0 -1
- package/dist/orchestration/scheduler-policy.d.ts +0 -222
- package/dist/orchestration/scheduler-policy.d.ts.map +0 -1
- package/dist/orchestration/scheduler-policy.js +0 -582
- package/dist/orchestration/scheduler-policy.js.map +0 -1
- package/dist/orchestration/scheduler.d.ts +0 -383
- package/dist/orchestration/scheduler.d.ts.map +0 -1
- package/dist/orchestration/scheduler.js +0 -2597
- package/dist/orchestration/scheduler.js.map +0 -1
- package/dist/orchestration/source-slot.d.ts +0 -124
- package/dist/orchestration/source-slot.d.ts.map +0 -1
- package/dist/orchestration/source-slot.js +0 -348
- package/dist/orchestration/source-slot.js.map +0 -1
- package/dist/orchestration/types.d.ts +0 -1462
- package/dist/orchestration/types.d.ts.map +0 -1
- package/dist/orchestration/types.js +0 -135
- package/dist/orchestration/types.js.map +0 -1
- package/dist/protobuf.d.ts +0 -63
- package/dist/protobuf.d.ts.map +0 -1
- package/dist/protobuf.js +0 -252
- package/dist/protobuf.js.map +0 -1
- package/dist/session/_internal/acquire.d.ts +0 -21
- package/dist/session/_internal/acquire.d.ts.map +0 -1
- package/dist/session/_internal/acquire.js +0 -168
- package/dist/session/_internal/acquire.js.map +0 -1
- package/dist/session/_internal/cleanup.d.ts +0 -19
- package/dist/session/_internal/cleanup.d.ts.map +0 -1
- package/dist/session/_internal/cleanup.js +0 -102
- package/dist/session/_internal/cleanup.js.map +0 -1
- package/dist/session/_internal/lifecycle.d.ts +0 -67
- package/dist/session/_internal/lifecycle.d.ts.map +0 -1
- package/dist/session/_internal/lifecycle.js +0 -142
- package/dist/session/_internal/lifecycle.js.map +0 -1
- package/dist/session/_internal/remote.d.ts +0 -28
- package/dist/session/_internal/remote.d.ts.map +0 -1
- package/dist/session/_internal/remote.js +0 -71
- package/dist/session/_internal/remote.js.map +0 -1
- package/dist/session/_internal/uploads.d.ts +0 -7
- package/dist/session/_internal/uploads.d.ts.map +0 -1
- package/dist/session/_internal/uploads.js +0 -31
- package/dist/session/_internal/uploads.js.map +0 -1
- package/dist/session/commands.d.ts +0 -2
- package/dist/session/commands.d.ts.map +0 -1
- package/dist/session/commands.js +0 -2
- package/dist/session/commands.js.map +0 -1
- package/dist/session/files.d.ts +0 -21
- package/dist/session/files.d.ts.map +0 -1
- package/dist/session/files.js +0 -49
- package/dist/session/files.js.map +0 -1
- package/dist/session/index.d.ts +0 -84
- package/dist/session/index.d.ts.map +0 -1
- package/dist/session/index.js +0 -16
- package/dist/session/index.js.map +0 -1
- package/dist/session/media.d.ts +0 -114
- package/dist/session/media.d.ts.map +0 -1
- package/dist/session/media.js +0 -2
- package/dist/session/media.js.map +0 -1
- package/dist/session/recorder.d.ts +0 -24
- package/dist/session/recorder.d.ts.map +0 -1
- package/dist/session/recorder.js +0 -19
- package/dist/session/recorder.js.map +0 -1
- package/dist/session.d.ts +0 -105
- package/dist/session.d.ts.map +0 -1
- package/dist/session.js +0 -976
- package/dist/session.js.map +0 -1
- package/dist/simulation/_internal/source.d.ts +0 -12
- package/dist/simulation/_internal/source.d.ts.map +0 -1
- package/dist/simulation/_internal/source.js +0 -513
- package/dist/simulation/_internal/source.js.map +0 -1
- package/dist/simulation/index.d.ts +0 -13
- package/dist/simulation/index.d.ts.map +0 -1
- package/dist/simulation/index.js +0 -12
- package/dist/simulation/index.js.map +0 -1
- package/dist/simulation/types.d.ts +0 -39
- package/dist/simulation/types.d.ts.map +0 -1
- package/dist/simulation/types.js +0 -2
- package/dist/simulation/types.js.map +0 -1
- package/dist/stats.d.ts.map +0 -1
- package/dist/stats.js +0 -205
- package/dist/stats.js.map +0 -1
- package/dist/testing/Faults.d.ts +0 -4
- package/dist/testing/Faults.d.ts.map +0 -1
- package/dist/testing/Faults.js +0 -7
- package/dist/testing/Faults.js.map +0 -1
- package/dist/testing/Png.d.ts +0 -3
- package/dist/testing/Png.d.ts.map +0 -1
- package/dist/testing/Png.js.map +0 -1
- package/dist/testing/Wav.d.ts +0 -14
- package/dist/testing/Wav.d.ts.map +0 -1
- package/dist/testing/Wav.js +0 -37
- package/dist/testing/Wav.js.map +0 -1
- package/dist/testing/index.d.ts +0 -5
- package/dist/testing/index.d.ts.map +0 -1
- package/dist/testing/index.js +0 -4
- package/dist/testing/index.js.map +0 -1
- package/dist/wire.d.ts +0 -7
- package/dist/wire.d.ts.map +0 -1
- package/dist/wire.generated.d.ts +0 -305
- package/dist/wire.generated.d.ts.map +0 -1
- package/dist/wire.generated.js +0 -1257
- package/dist/wire.generated.js.map +0 -1
- package/dist/wire.js +0 -5
- package/dist/wire.js.map +0 -1
|
@@ -1,2597 +0,0 @@
|
|
|
1
|
-
import * as Clock from "effect/Clock";
|
|
2
|
-
import * as Context from "effect/Context";
|
|
3
|
-
import * as Deferred from "effect/Deferred";
|
|
4
|
-
import * as Duration from "effect/Duration";
|
|
5
|
-
import * as Effect from "effect/Effect";
|
|
6
|
-
import * as Exit from "effect/Exit";
|
|
7
|
-
import * as Layer from "effect/Layer";
|
|
8
|
-
import * as Option from "effect/Option";
|
|
9
|
-
import * as PubSub from "effect/PubSub";
|
|
10
|
-
import * as Queue from "effect/Queue";
|
|
11
|
-
import * as Result from "effect/Result";
|
|
12
|
-
import * as Schema from "effect/Schema";
|
|
13
|
-
import * as Scope from "effect/Scope";
|
|
14
|
-
import * as Stream from "effect/Stream";
|
|
15
|
-
import * as SubscriptionRef from "effect/SubscriptionRef";
|
|
16
|
-
import { take as takeQueue } from "../_internal/queue.js";
|
|
17
|
-
import { duration } from "../duration.js";
|
|
18
|
-
import { parsedInput, ReactorError } from "../errors.js";
|
|
19
|
-
import { monotonicMillis } from "./elapsed.js";
|
|
20
|
-
import { requestSeconds as h3RequestSeconds } from "../h3/profile.js";
|
|
21
|
-
import { captureRequest, PolicyFailure } from "./request.js";
|
|
22
|
-
import { activeIds } from "./routing.js";
|
|
23
|
-
import { fillerIndexFromKey, fillerKey, isReservedSchedulerKey, keyedRequest, keyFromProviderMetadata, } from "./scheduler-key.js";
|
|
24
|
-
import { dueCues, estimatesFrom, plan, projectedStartMs, runwaySeconds, } from "./scheduler-policy.js";
|
|
25
|
-
import { Engine } from "./types.js";
|
|
26
|
-
/** Stable caller identity for a scheduled item. */
|
|
27
|
-
export const ItemKey = Schema.NonEmptyString.pipe(Schema.brand("ItemKey"));
|
|
28
|
-
const isFirstDecisive = (status) => status._tag === "Started" ||
|
|
29
|
-
status._tag === "Ended" ||
|
|
30
|
-
status._tag === "Dropped" ||
|
|
31
|
-
status._tag === "Failed" ||
|
|
32
|
-
status._tag === "Unobserved" ||
|
|
33
|
-
(status._tag === "Unknown" && status.terminal === true);
|
|
34
|
-
export class KeyMismatch extends Schema.TaggedError()("KeyMismatch", {
|
|
35
|
-
key: ItemKey,
|
|
36
|
-
}) {
|
|
37
|
-
static of(key) {
|
|
38
|
-
return new KeyMismatch({ key });
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
export class LaneBusy extends Schema.TaggedError()("LaneBusy", {
|
|
42
|
-
key: ItemKey,
|
|
43
|
-
lane: Schema.String,
|
|
44
|
-
}) {
|
|
45
|
-
static of(key, lane) {
|
|
46
|
-
return new LaneBusy({ key, lane });
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
export class WouldMissDeadline extends Schema.TaggedError()("WouldMissDeadline", { key: ItemKey }) {
|
|
50
|
-
static of(key) {
|
|
51
|
-
return new WouldMissDeadline({ key });
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
const maxEarlyClipIds = 4096;
|
|
55
|
-
const invalid = (message) => PolicyFailure.refuse("InvalidRequest", message, "submit");
|
|
56
|
-
const ownedData = (input, fields, name) => {
|
|
57
|
-
if (input === null || typeof input !== "object" || Array.isArray(input))
|
|
58
|
-
return Result.fail(invalid(`${name} must be an object`));
|
|
59
|
-
const descriptors = Object.getOwnPropertyDescriptors(input);
|
|
60
|
-
if (Object.getOwnPropertySymbols(input).length > 0 ||
|
|
61
|
-
Object.keys(descriptors).some((field) => !fields.includes(field) || !("value" in descriptors[field])))
|
|
62
|
-
return Result.fail(invalid(`${name} has unsupported fields or accessors`));
|
|
63
|
-
return Result.succeed(Object.fromEntries(Object.entries(descriptors).map(([field, descriptor]) => [field, descriptor.value])));
|
|
64
|
-
};
|
|
65
|
-
/** A Duration tuple or object read from own data properties; no accessor runs. */
|
|
66
|
-
const durationData = (input) => {
|
|
67
|
-
if (Array.isArray(input)) {
|
|
68
|
-
const descriptors = Object.getOwnPropertyDescriptors(input);
|
|
69
|
-
if (Object.getOwnPropertySymbols(input).length > 0 ||
|
|
70
|
-
Object.keys(descriptors).some((field) => !["0", "1", "length"].includes(field) || !("value" in descriptors[field])) ||
|
|
71
|
-
descriptors.length?.value !== 2 ||
|
|
72
|
-
descriptors["0"] === undefined ||
|
|
73
|
-
descriptors["1"] === undefined)
|
|
74
|
-
return Result.fail(invalid("Duration tuple has unsupported fields or accessors"));
|
|
75
|
-
const seconds = descriptors["0"].value;
|
|
76
|
-
const nanos = descriptors["1"].value;
|
|
77
|
-
return Result.succeed([seconds, nanos]);
|
|
78
|
-
}
|
|
79
|
-
const fields = ownedData(input, ["weeks", "days", "hours", "minutes", "seconds", "milliseconds", "microseconds", "nanoseconds"], "Duration");
|
|
80
|
-
return Result.isFailure(fields) ? Result.fail(fields.failure) : Result.succeed(fields.success);
|
|
81
|
-
};
|
|
82
|
-
const captureDuration = (input) => Effect.gen(function* () {
|
|
83
|
-
if (input === null || typeof input !== "object" || Duration.isDuration(input))
|
|
84
|
-
return input;
|
|
85
|
-
return yield* Effect.fromResult(durationData(input));
|
|
86
|
-
});
|
|
87
|
-
/**
|
|
88
|
-
* A Duration option read as caller data: a data property, own or inherited,
|
|
89
|
-
* captured without running any accessor. Rejections throw for `parsedInput`.
|
|
90
|
-
*/
|
|
91
|
-
const optionDuration = (options, field, name) => {
|
|
92
|
-
let descriptor;
|
|
93
|
-
for (let owner = options; descriptor === undefined && owner !== null; owner = Object.getPrototypeOf(owner))
|
|
94
|
-
descriptor = Object.getOwnPropertyDescriptor(owner, field);
|
|
95
|
-
if (descriptor === undefined)
|
|
96
|
-
return undefined;
|
|
97
|
-
const reject = () => ReactorError.fromCode("InvalidInput", `${name} must be a Duration of data properties`);
|
|
98
|
-
if (!("value" in descriptor))
|
|
99
|
-
throw reject();
|
|
100
|
-
const value = descriptor.value;
|
|
101
|
-
if (value === undefined)
|
|
102
|
-
return undefined;
|
|
103
|
-
if (value === null)
|
|
104
|
-
throw reject();
|
|
105
|
-
if (typeof value !== "object" || Duration.isDuration(value))
|
|
106
|
-
return value;
|
|
107
|
-
const captured = durationData(value);
|
|
108
|
-
if (Result.isFailure(captured))
|
|
109
|
-
throw reject();
|
|
110
|
-
return captured.success;
|
|
111
|
-
};
|
|
112
|
-
const requestDuration = (input, name, allowZero = false) => Effect.gen(function* () {
|
|
113
|
-
const captured = yield* captureDuration(input);
|
|
114
|
-
return yield* parsedInput(() => Duration.toMillis(duration(captured, name, { allowZero, allowNegative: name === "startBy" })), "submit").pipe(Effect.mapError((error) => invalid(error.message)));
|
|
115
|
-
});
|
|
116
|
-
/** A continuity choice, read as caller data, checked against the request it would continue. */
|
|
117
|
-
const captureContinuity = (input, request) => Effect.gen(function* () {
|
|
118
|
-
if (input === undefined)
|
|
119
|
-
return undefined;
|
|
120
|
-
if (input !== "previous")
|
|
121
|
-
return yield* invalid('Continuity is "previous" or absent');
|
|
122
|
-
if ((request.audio?.length ?? 0) > 2)
|
|
123
|
-
return yield* invalid("A clip that continues takes at most two audio references");
|
|
124
|
-
return "previous";
|
|
125
|
-
});
|
|
126
|
-
/** Enough cues for a line's overlays and actions, and a bound on one clip's timers. */
|
|
127
|
-
const maxCues = 32;
|
|
128
|
-
/** A clip's cues, read as caller data. */
|
|
129
|
-
const captureCues = (input) => Effect.gen(function* () {
|
|
130
|
-
if (input === undefined)
|
|
131
|
-
return [];
|
|
132
|
-
if (!Array.isArray(input))
|
|
133
|
-
return yield* invalid("Cues must be an array");
|
|
134
|
-
// Read as a plain object: a mapped array type would hide the descriptors.
|
|
135
|
-
const descriptors = Object.getOwnPropertyDescriptors(input);
|
|
136
|
-
const length = descriptors.length?.value;
|
|
137
|
-
if (typeof length !== "number" || length > maxCues)
|
|
138
|
-
return yield* invalid(`A clip takes at most ${maxCues} cues`);
|
|
139
|
-
const cues = [];
|
|
140
|
-
for (let index = 0; index < length; index++) {
|
|
141
|
-
const descriptor = descriptors[String(index)];
|
|
142
|
-
if (descriptor === undefined || !("value" in descriptor))
|
|
143
|
-
return yield* invalid("Cues must be data");
|
|
144
|
-
const cue = yield* Effect.fromResult(ownedData(descriptor.value, ["name", "at"], "Cue"));
|
|
145
|
-
const at = yield* Effect.fromResult(ownedData(cue.at, ["from", "offset"], "Cue time"));
|
|
146
|
-
if (typeof cue.name !== "string" || cue.name.length === 0)
|
|
147
|
-
return yield* invalid("A cue needs a nonempty name");
|
|
148
|
-
if (at.from !== "start" && at.from !== "end")
|
|
149
|
-
return yield* invalid("A cue is timed from the start or the end");
|
|
150
|
-
cues.push({
|
|
151
|
-
name: cue.name,
|
|
152
|
-
from: at.from,
|
|
153
|
-
offsetMs: yield* requestDuration(at.offset, "cue offset", true),
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
return cues;
|
|
157
|
-
});
|
|
158
|
-
/** Capture scheduling fields before reading the request's placement fields. */
|
|
159
|
-
/** A caller's clip request, without the placement and source fields the scheduler owns. */
|
|
160
|
-
const captureClip = (input) => Effect.gen(function* () {
|
|
161
|
-
const request = yield* captureRequest(input);
|
|
162
|
-
if (request.position !== undefined ||
|
|
163
|
-
request.before !== undefined ||
|
|
164
|
-
request.sameSessionAs !== undefined ||
|
|
165
|
-
request.continueFrom !== undefined ||
|
|
166
|
-
request.sequence !== undefined)
|
|
167
|
-
return yield* invalid("Scheduler owns placement and source selection");
|
|
168
|
-
return request;
|
|
169
|
-
});
|
|
170
|
-
/** A window's offsets from admission, read as caller data. */
|
|
171
|
-
const captureWindow = (input) => Effect.gen(function* () {
|
|
172
|
-
const window = input === undefined
|
|
173
|
-
? undefined
|
|
174
|
-
: yield* Effect.fromResult(ownedData(input, ["notBefore", "startBy", "firm"], "Scheduled window"));
|
|
175
|
-
const notBeforeOffsetMs = window?.notBefore === undefined
|
|
176
|
-
? undefined
|
|
177
|
-
: yield* requestDuration(window.notBefore, "notBefore", true);
|
|
178
|
-
const startByOffsetMs = window?.startBy === undefined
|
|
179
|
-
? undefined
|
|
180
|
-
: yield* requestDuration(window.startBy, "startBy", true);
|
|
181
|
-
if (window !== undefined &&
|
|
182
|
-
(typeof window.firm !== "boolean" ||
|
|
183
|
-
(notBeforeOffsetMs !== undefined &&
|
|
184
|
-
startByOffsetMs !== undefined &&
|
|
185
|
-
startByOffsetMs >= 0 &&
|
|
186
|
-
notBeforeOffsetMs > startByOffsetMs)))
|
|
187
|
-
return yield* invalid("Scheduled window is inconsistent");
|
|
188
|
-
return { notBeforeOffsetMs, startByOffsetMs, firm: window?.firm === true };
|
|
189
|
-
});
|
|
190
|
-
const captureItem = (input, lanes) => Effect.gen(function* () {
|
|
191
|
-
if (input === null || typeof input !== "object" || Array.isArray(input))
|
|
192
|
-
return yield* invalid("Scheduled item must be an object");
|
|
193
|
-
const descriptors = Object.getOwnPropertyDescriptors(input);
|
|
194
|
-
if (Object.getOwnPropertySymbols(input).length > 0 ||
|
|
195
|
-
Object.keys(descriptors).some((field) => !["key", "lane", "request", "window", "start", "cues", "continuity"].includes(field) ||
|
|
196
|
-
!("value" in descriptors[field])))
|
|
197
|
-
return yield* invalid("Scheduled item has unsupported fields or accessors");
|
|
198
|
-
const key = yield* Schema.decodeUnknownEffect(ItemKey)(descriptors.key?.value).pipe(Effect.mapError(() => invalid("Scheduled item key must be nonempty")));
|
|
199
|
-
if (isReservedSchedulerKey(key))
|
|
200
|
-
return yield* invalid("Scheduled item key is reserved");
|
|
201
|
-
const lane = descriptors.lane?.value;
|
|
202
|
-
if (typeof lane !== "string" || !lanes.has(lane))
|
|
203
|
-
return yield* invalid("Scheduled item lane is not configured");
|
|
204
|
-
const request = yield* captureClip(descriptors.request?.value);
|
|
205
|
-
const { notBeforeOffsetMs, startByOffsetMs, firm } = yield* captureWindow(descriptors.window?.value);
|
|
206
|
-
const cues = yield* captureCues(descriptors.cues?.value);
|
|
207
|
-
const continuity = yield* captureContinuity(descriptors.continuity?.value, request);
|
|
208
|
-
const rawStart = descriptors.start?.value;
|
|
209
|
-
const start = rawStart === undefined
|
|
210
|
-
? undefined
|
|
211
|
-
: yield* Effect.fromResult(ownedData(rawStart, ["_tag", "time", "late"], "Scheduled start mode"));
|
|
212
|
-
let atWallMs;
|
|
213
|
-
let late = "nextBoundary";
|
|
214
|
-
const mode = start?._tag === "Asap" ? "asap" : start?._tag === "Manual" ? "manual" : undefined;
|
|
215
|
-
if ((start?._tag === "Follow" || mode !== undefined) &&
|
|
216
|
-
(start?.time !== undefined || start?.late !== undefined))
|
|
217
|
-
return yield* invalid(`${String(start?._tag)} cannot contain an anchor or lateness policy`);
|
|
218
|
-
if (start !== undefined && start._tag !== "Follow" && mode === undefined) {
|
|
219
|
-
if (start._tag !== "At" || !Number.isFinite(start.time))
|
|
220
|
-
return yield* invalid("Scheduled start mode is invalid");
|
|
221
|
-
atWallMs = start.time;
|
|
222
|
-
const policy = yield* Effect.fromResult(ownedData(start.late, ["_tag", "by"], "Scheduled lateness policy"));
|
|
223
|
-
if (policy._tag === "drop" && policy.by === undefined)
|
|
224
|
-
late = "drop";
|
|
225
|
-
else if (policy._tag === "skipIfLaterThan")
|
|
226
|
-
late = {
|
|
227
|
-
skipIfLaterThanMs: yield* requestDuration(policy.by, "skipIfLaterThan", true),
|
|
228
|
-
};
|
|
229
|
-
else if (policy._tag !== "nextBoundary" || policy.by !== undefined)
|
|
230
|
-
return yield* invalid("Scheduled lateness policy is invalid");
|
|
231
|
-
}
|
|
232
|
-
return {
|
|
233
|
-
key,
|
|
234
|
-
lane,
|
|
235
|
-
request,
|
|
236
|
-
notBeforeOffsetMs,
|
|
237
|
-
startByOffsetMs,
|
|
238
|
-
firm,
|
|
239
|
-
atWallMs,
|
|
240
|
-
late,
|
|
241
|
-
...(mode === undefined ? {} : { mode }),
|
|
242
|
-
...(cues.length === 0 ? {} : { cues }),
|
|
243
|
-
...(continuity === undefined ? {} : { continuity }),
|
|
244
|
-
fingerprint: JSON.stringify({
|
|
245
|
-
lane,
|
|
246
|
-
request,
|
|
247
|
-
notBeforeOffsetMs,
|
|
248
|
-
startByOffsetMs,
|
|
249
|
-
firm,
|
|
250
|
-
atWallMs,
|
|
251
|
-
late,
|
|
252
|
-
mode,
|
|
253
|
-
...(cues.length === 0 ? {} : { cues }),
|
|
254
|
-
...(continuity === undefined ? {} : { continuity }),
|
|
255
|
-
}),
|
|
256
|
-
};
|
|
257
|
-
});
|
|
258
|
-
/** Enough parts for a spoken line of beats, and a bound on one submission's work. */
|
|
259
|
-
const maxGroupParts = 64;
|
|
260
|
-
const captureGroup = (input, lanes) => Effect.gen(function* () {
|
|
261
|
-
const group = yield* Effect.fromResult(ownedData(input, ["key", "lane", "parts", "window"], "Scheduled group"));
|
|
262
|
-
const key = yield* Schema.decodeUnknownEffect(ItemKey)(group.key).pipe(Effect.mapError(() => invalid("Scheduled group key must be nonempty")));
|
|
263
|
-
if (isReservedSchedulerKey(key))
|
|
264
|
-
return yield* invalid("Scheduled group key is reserved");
|
|
265
|
-
if (!Array.isArray(group.parts))
|
|
266
|
-
return yield* invalid("Scheduled group parts must be an array");
|
|
267
|
-
// Read as a plain object: a mapped array type would hide the descriptors.
|
|
268
|
-
const descriptors = Object.getOwnPropertyDescriptors(group.parts);
|
|
269
|
-
const length = descriptors.length?.value;
|
|
270
|
-
if (typeof length !== "number" || length < 1 || length > maxGroupParts)
|
|
271
|
-
return yield* invalid(`Scheduled group needs 1 to ${maxGroupParts} parts`);
|
|
272
|
-
const parts = [];
|
|
273
|
-
for (let index = 0; index < length; index++) {
|
|
274
|
-
const descriptor = descriptors[String(index)];
|
|
275
|
-
if (descriptor === undefined || !("value" in descriptor))
|
|
276
|
-
return yield* invalid("Scheduled group parts must be data");
|
|
277
|
-
const part = yield* Effect.fromResult(ownedData(descriptor.value, ["key", "request", "cues", "continuity"], "Scheduled group part"));
|
|
278
|
-
parts.push(yield* captureItem({
|
|
279
|
-
key: part.key,
|
|
280
|
-
lane: group.lane,
|
|
281
|
-
request: part.request,
|
|
282
|
-
...(part.cues === undefined ? {} : { cues: part.cues }),
|
|
283
|
-
...(part.continuity === undefined ? {} : { continuity: part.continuity }),
|
|
284
|
-
...(index === 0 && group.window !== undefined ? { window: group.window } : {}),
|
|
285
|
-
}, lanes));
|
|
286
|
-
}
|
|
287
|
-
const [first, ...rest] = parts;
|
|
288
|
-
if (first === undefined || new Set([key, ...parts.map((part) => part.key)]).size !== length + 1)
|
|
289
|
-
return yield* invalid("Scheduled group and part keys must be distinct");
|
|
290
|
-
return {
|
|
291
|
-
key,
|
|
292
|
-
parts: [first, ...rest],
|
|
293
|
-
fingerprint: JSON.stringify(parts.map((part) => [part.key, part.fingerprint])),
|
|
294
|
-
};
|
|
295
|
-
});
|
|
296
|
-
const captureReplacement = (input) => Effect.gen(function* () {
|
|
297
|
-
const next = yield* Effect.fromResult(ownedData(input, ["key", "request", "cues", "continuity"], "Replacement"));
|
|
298
|
-
const key = yield* Schema.decodeUnknownEffect(ItemKey)(next.key).pipe(Effect.mapError(() => invalid("Replacement key must be nonempty")));
|
|
299
|
-
if (isReservedSchedulerKey(key))
|
|
300
|
-
return yield* invalid("Replacement key is reserved");
|
|
301
|
-
const request = yield* captureClip(next.request);
|
|
302
|
-
return {
|
|
303
|
-
key,
|
|
304
|
-
request,
|
|
305
|
-
cues: yield* captureCues(next.cues),
|
|
306
|
-
continuity: yield* captureContinuity(next.continuity, request),
|
|
307
|
-
};
|
|
308
|
-
});
|
|
309
|
-
const captureInsert = (input) => Effect.gen(function* () {
|
|
310
|
-
const spec = yield* Effect.fromResult(ownedData(input, ["key", "request", "before", "after", "window", "cues", "continuity"], "Insert"));
|
|
311
|
-
const key = yield* Schema.decodeUnknownEffect(ItemKey)(spec.key).pipe(Effect.mapError(() => invalid("Insert key must be nonempty")));
|
|
312
|
-
if (isReservedSchedulerKey(key))
|
|
313
|
-
return yield* invalid("Insert key is reserved");
|
|
314
|
-
if ((spec.before === undefined) === (spec.after === undefined))
|
|
315
|
-
return yield* invalid("Insert needs exactly one of before or after");
|
|
316
|
-
const side = spec.before === undefined ? "after" : "before";
|
|
317
|
-
const anchor = yield* Schema.decodeUnknownEffect(ItemKey)(spec.before ?? spec.after).pipe(Effect.mapError(() => invalid("Insert anchor must be a nonempty key")));
|
|
318
|
-
const request = yield* captureClip(spec.request);
|
|
319
|
-
const window = yield* captureWindow(spec.window);
|
|
320
|
-
const cues = yield* captureCues(spec.cues);
|
|
321
|
-
const continuity = yield* captureContinuity(spec.continuity, request);
|
|
322
|
-
return {
|
|
323
|
-
key,
|
|
324
|
-
request,
|
|
325
|
-
side,
|
|
326
|
-
anchor,
|
|
327
|
-
...window,
|
|
328
|
-
cues,
|
|
329
|
-
continuity,
|
|
330
|
-
fingerprint: JSON.stringify({
|
|
331
|
-
[side]: anchor,
|
|
332
|
-
request,
|
|
333
|
-
...window,
|
|
334
|
-
...(cues.length === 0 ? {} : { cues }),
|
|
335
|
-
...(continuity === undefined ? {} : { continuity }),
|
|
336
|
-
}),
|
|
337
|
-
};
|
|
338
|
-
});
|
|
339
|
-
/** A bound on one batch's work, generous for a spoken plan of several lines. */
|
|
340
|
-
const maxEdits = 256;
|
|
341
|
-
const editKey = (input, name) => Schema.decodeUnknownEffect(ItemKey)(input).pipe(Effect.mapError(() => invalid(`${name} must be a nonempty key`)));
|
|
342
|
-
const captureEdits = (input, lanes) => Effect.gen(function* () {
|
|
343
|
-
if (!Array.isArray(input))
|
|
344
|
-
return yield* invalid("Edits must be an array");
|
|
345
|
-
// Read as a plain object: a mapped array type would hide the descriptors.
|
|
346
|
-
const descriptors = Object.getOwnPropertyDescriptors(input);
|
|
347
|
-
const length = descriptors.length?.value;
|
|
348
|
-
if (typeof length !== "number" || length < 1 || length > maxEdits)
|
|
349
|
-
return yield* invalid(`An edit batch needs 1 to ${maxEdits} edits`);
|
|
350
|
-
const edits = [];
|
|
351
|
-
for (let index = 0; index < length; index++) {
|
|
352
|
-
const descriptor = descriptors[String(index)];
|
|
353
|
-
if (descriptor === undefined || !("value" in descriptor))
|
|
354
|
-
return yield* invalid("Edits must be data");
|
|
355
|
-
const raw = descriptor.value;
|
|
356
|
-
const tag = raw !== null && typeof raw === "object"
|
|
357
|
-
? Object.getOwnPropertyDescriptor(raw, "_tag")?.value
|
|
358
|
-
: undefined;
|
|
359
|
-
switch (tag) {
|
|
360
|
-
case "Submit": {
|
|
361
|
-
const edit = yield* Effect.fromResult(ownedData(raw, ["_tag", "item"], "Edit"));
|
|
362
|
-
edits.push({ _tag: "Submit", item: yield* captureItem(edit.item, lanes) });
|
|
363
|
-
break;
|
|
364
|
-
}
|
|
365
|
-
case "SubmitGroup": {
|
|
366
|
-
const edit = yield* Effect.fromResult(ownedData(raw, ["_tag", "group"], "Edit"));
|
|
367
|
-
edits.push({
|
|
368
|
-
_tag: "SubmitGroup",
|
|
369
|
-
group: yield* captureGroup(edit.group, lanes),
|
|
370
|
-
});
|
|
371
|
-
break;
|
|
372
|
-
}
|
|
373
|
-
case "Insert": {
|
|
374
|
-
const edit = yield* Effect.fromResult(ownedData(raw, ["_tag", "insert"], "Edit"));
|
|
375
|
-
edits.push({ _tag: "Insert", insert: yield* captureInsert(edit.insert) });
|
|
376
|
-
break;
|
|
377
|
-
}
|
|
378
|
-
case "Replace": {
|
|
379
|
-
const edit = yield* Effect.fromResult(ownedData(raw, ["_tag", "key", "next"], "Edit"));
|
|
380
|
-
edits.push({
|
|
381
|
-
_tag: "Replace",
|
|
382
|
-
key: yield* editKey(edit.key, "A replaced key"),
|
|
383
|
-
next: yield* captureReplacement(edit.next),
|
|
384
|
-
});
|
|
385
|
-
break;
|
|
386
|
-
}
|
|
387
|
-
case "Withdraw": {
|
|
388
|
-
const edit = yield* Effect.fromResult(ownedData(raw, ["_tag", "key"], "Edit"));
|
|
389
|
-
edits.push({ _tag: "Withdraw", key: yield* editKey(edit.key, "A withdrawn key") });
|
|
390
|
-
break;
|
|
391
|
-
}
|
|
392
|
-
default:
|
|
393
|
-
return yield* invalid("An edit's _tag is not one the scheduler knows");
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
return edits;
|
|
397
|
-
});
|
|
398
|
-
export const lineup = (filler) => ({
|
|
399
|
-
lanes: [{ name: "line" }],
|
|
400
|
-
filler,
|
|
401
|
-
});
|
|
402
|
-
export class Scheduler extends Context.Service()("reactor-effect-client/Orchestration/Scheduler") {
|
|
403
|
-
static lineup = lineup;
|
|
404
|
-
}
|
|
405
|
-
export const makeScheduler = (options) => Effect.gen(function* () {
|
|
406
|
-
const engine = yield* Engine;
|
|
407
|
-
const clock = yield* Clock.Clock;
|
|
408
|
-
const lanes = yield* parsedInput(() => {
|
|
409
|
-
if (options.lanes.length === 0 ||
|
|
410
|
-
options.lanes.some((lane) => typeof lane.name !== "string" || lane.name.length === 0) ||
|
|
411
|
-
new Set(options.lanes.map((lane) => lane.name)).size !== options.lanes.length)
|
|
412
|
-
throw ReactorError.fromCode("InvalidInput", "Scheduler lanes must be unique and nonempty");
|
|
413
|
-
if (options.lanes.some((lane) => (lane.conflict !== undefined &&
|
|
414
|
-
!["queue", "replace", "skip"].includes(lane.conflict)) ||
|
|
415
|
-
(lane.cut !== undefined && typeof lane.cut !== "boolean")))
|
|
416
|
-
throw ReactorError.fromCode("InvalidInput", "A lane's conflict or cut is invalid");
|
|
417
|
-
return options.lanes.map((lane) => lane.name);
|
|
418
|
-
}, "makeScheduler");
|
|
419
|
-
const conflicts = new Map(options.lanes.map((lane) => [lane.name, lane.conflict ?? "queue"]));
|
|
420
|
-
const cutLanes = new Set(options.lanes.filter((lane) => lane.cut === true).map((lane) => lane.name));
|
|
421
|
-
const { floorSeconds, targetSeconds, maxBuildsInFlight, maxHistory, unknownRecoveryMs, fillLengths, } = yield* parsedInput(() => {
|
|
422
|
-
const floorSeconds = Duration.toMillis(duration(options.filler.runway.floor, "runway floor", { allowZero: true })) / 1000;
|
|
423
|
-
const targetSeconds = Duration.toMillis(duration(options.filler.runway.target, "runway target")) / 1000;
|
|
424
|
-
const maxBuildsInFlight = options.maxBuildsInFlight ?? 1;
|
|
425
|
-
const maxHistory = options.maxHistory ?? 4096;
|
|
426
|
-
const unknownRecoveryMs = Duration.toMillis(duration(optionDuration(options, "unknownRecoveryTimeout", "Unknown recovery timeout") ??
|
|
427
|
-
"60 seconds", "unknown recovery timeout", { maximum: "10 minutes" }));
|
|
428
|
-
if (floorSeconds > targetSeconds ||
|
|
429
|
-
!Number.isSafeInteger(maxBuildsInFlight) ||
|
|
430
|
-
maxBuildsInFlight < 1 ||
|
|
431
|
-
maxBuildsInFlight > 1024 ||
|
|
432
|
-
!Number.isSafeInteger(maxHistory) ||
|
|
433
|
-
maxHistory < 0 ||
|
|
434
|
-
maxHistory > 65_536)
|
|
435
|
-
throw ReactorError.fromCode("InvalidInput", "Scheduler runway or build cap is invalid");
|
|
436
|
-
const lengths = options.filler.lengths;
|
|
437
|
-
const fillLengths = {
|
|
438
|
-
min: lengths === undefined
|
|
439
|
-
? h3RequestSeconds.min
|
|
440
|
-
: Duration.toMillis(duration(lengths.min, "filler minimum length")) / 1000,
|
|
441
|
-
max: lengths === undefined
|
|
442
|
-
? h3RequestSeconds.max
|
|
443
|
-
: Duration.toMillis(duration(lengths.max, "filler maximum length")) / 1000,
|
|
444
|
-
};
|
|
445
|
-
if (fillLengths.min > fillLengths.max)
|
|
446
|
-
throw ReactorError.fromCode("InvalidInput", "Filler lengths are inconsistent");
|
|
447
|
-
return {
|
|
448
|
-
floorSeconds,
|
|
449
|
-
targetSeconds,
|
|
450
|
-
maxBuildsInFlight,
|
|
451
|
-
maxHistory,
|
|
452
|
-
unknownRecoveryMs,
|
|
453
|
-
fillLengths,
|
|
454
|
-
};
|
|
455
|
-
}, "makeScheduler");
|
|
456
|
-
const captureFiller = (index, runway, durationSeconds, targetSeconds) => Effect.gen(function* () {
|
|
457
|
-
const request = yield* captureRequest(options.filler.clip({
|
|
458
|
-
index,
|
|
459
|
-
runwaySeconds: runway,
|
|
460
|
-
durationSeconds,
|
|
461
|
-
...(targetSeconds === undefined ? {} : { targetSeconds }),
|
|
462
|
-
}));
|
|
463
|
-
if (request.position !== undefined ||
|
|
464
|
-
request.before !== undefined ||
|
|
465
|
-
request.sameSessionAs !== undefined ||
|
|
466
|
-
request.continueFrom !== undefined ||
|
|
467
|
-
request.sequence !== undefined)
|
|
468
|
-
return yield* PolicyFailure.refuse("InvalidRequest", "Scheduler filler cannot choose placement or source");
|
|
469
|
-
return request;
|
|
470
|
-
});
|
|
471
|
-
let fillerIndex = 0;
|
|
472
|
-
let fillerRetryAtMs = 0;
|
|
473
|
-
const unknownFillers = new Map();
|
|
474
|
-
const maxUnknownFillers = 4096;
|
|
475
|
-
let upcomingFiller = yield* captureFiller(0, 0, fillLengths.min);
|
|
476
|
-
const actorScope = yield* Effect.scope;
|
|
477
|
-
const inbox = yield* Queue.unbounded();
|
|
478
|
-
const commandQueue = yield* Queue.unbounded();
|
|
479
|
-
const events = yield* PubSub.unbounded();
|
|
480
|
-
const cueEvents = yield* PubSub.unbounded();
|
|
481
|
-
yield* Effect.addFinalizer(() => Effect.gen(function* () {
|
|
482
|
-
yield* Queue.shutdown(commandQueue);
|
|
483
|
-
yield* Queue.shutdown(inbox);
|
|
484
|
-
yield* PubSub.shutdown(events);
|
|
485
|
-
yield* PubSub.shutdown(cueEvents);
|
|
486
|
-
}));
|
|
487
|
-
const stateRef = yield* SubscriptionRef.make({
|
|
488
|
-
accepting: true,
|
|
489
|
-
runwaySeconds: 0,
|
|
490
|
-
playing: null,
|
|
491
|
-
lanes: lanes.map((name) => ({ name, keys: [] })),
|
|
492
|
-
sessions: [],
|
|
493
|
-
starved: 0,
|
|
494
|
-
estimates: { build: undefined, length: 1 },
|
|
495
|
-
});
|
|
496
|
-
const initialized = yield* Deferred.make();
|
|
497
|
-
const stopped = yield* Deferred.make();
|
|
498
|
-
const settled = yield* Deferred.make();
|
|
499
|
-
const closedCall = Deferred.await(stopped).pipe(Effect.flatMap((cause) => PolicyFailure.refuse("SessionClosed", cause.message)));
|
|
500
|
-
const items = new Map();
|
|
501
|
-
const history = new Map();
|
|
502
|
-
/** Kept while any part is active or in history, for idempotency and withdrawal. */
|
|
503
|
-
const groups = new Map();
|
|
504
|
-
const owned = new Map();
|
|
505
|
-
// An event can overtake its enqueue reply. Keep only the evidence needed to
|
|
506
|
-
// decide that clip's fate until the one in-flight command returns its ID.
|
|
507
|
-
const earlyClipEvents = new Map();
|
|
508
|
-
const removedIds = new Set();
|
|
509
|
-
const filler = new Map();
|
|
510
|
-
const playingStartedMs = new Map();
|
|
511
|
-
let admission = 0;
|
|
512
|
-
let accepting = true;
|
|
513
|
-
let starved = 0;
|
|
514
|
-
let refillActive = false;
|
|
515
|
-
let ended;
|
|
516
|
-
let drainFailure;
|
|
517
|
-
const drainReplies = [];
|
|
518
|
-
const pendingWithdrawals = new Map();
|
|
519
|
-
/** Edit batches that have not taken effect yet. */
|
|
520
|
-
const batches = new Map();
|
|
521
|
-
let batchCount = 0;
|
|
522
|
-
const pendingBuilds = new Set();
|
|
523
|
-
// A Build whose enqueue has started and whose result the worker has not
|
|
524
|
-
// yet seen. The provider may still accept it after the scheduler ends.
|
|
525
|
-
let dispatching;
|
|
526
|
-
const pendingAtDeferrals = new Set();
|
|
527
|
-
const pendingItemRemovals = new Set();
|
|
528
|
-
const pendingFillerRemovals = new Set();
|
|
529
|
-
const removalRetryAtMs = new Map();
|
|
530
|
-
const removalUnknownAtSerial = new Map();
|
|
531
|
-
let commandCount = 0;
|
|
532
|
-
let snapshotAcquiredSerial = 0;
|
|
533
|
-
let lastProcessedSnapshotSerial = 0;
|
|
534
|
-
let draining = false;
|
|
535
|
-
let finishAccepted = false;
|
|
536
|
-
let autoplayPaused = false;
|
|
537
|
-
let drained = false;
|
|
538
|
-
let observationReady = false;
|
|
539
|
-
let blockedMove;
|
|
540
|
-
let blockedCut;
|
|
541
|
-
// Recent measured builds: build seconds per requested second, and actual over requested length.
|
|
542
|
-
const buildSamples = [];
|
|
543
|
-
const lengthSamples = [];
|
|
544
|
-
const maxSamples = 32;
|
|
545
|
-
let lastFillerSeconds;
|
|
546
|
-
/** Filler builds sent and not yet Ready, by filler index. */
|
|
547
|
-
const fillerDispatch = new Map();
|
|
548
|
-
const measure = (atMs, requested, actual) => {
|
|
549
|
-
if (!(requested > 0))
|
|
550
|
-
return;
|
|
551
|
-
buildSamples.push((monotonicMillis(clock) - atMs) / 1000 / requested);
|
|
552
|
-
lengthSamples.push(actual / requested);
|
|
553
|
-
if (buildSamples.length > maxSamples)
|
|
554
|
-
buildSamples.shift();
|
|
555
|
-
if (lengthSamples.length > maxSamples)
|
|
556
|
-
lengthSamples.shift();
|
|
557
|
-
};
|
|
558
|
-
/** An item's build is measured once, when its clip is first seen Ready. */
|
|
559
|
-
const measureItem = (item, actual) => {
|
|
560
|
-
if (item.dispatchedAtMs === undefined)
|
|
561
|
-
return;
|
|
562
|
-
measure(item.dispatchedAtMs, item.request.durationSeconds, actual);
|
|
563
|
-
delete item.dispatchedAtMs;
|
|
564
|
-
};
|
|
565
|
-
/** Fires the cues of a playing item that are due by `untilMs`, in the order they fall. */
|
|
566
|
-
const fireCues = (entry, untilMs) => {
|
|
567
|
-
if (entry.status._tag !== "Started" || entry.startedAtMonoMs === undefined)
|
|
568
|
-
return;
|
|
569
|
-
for (const due of dueCues(entry.cues, entry.firedCues, entry.startedAtMonoMs, entry.status.durationSeconds, untilMs)) {
|
|
570
|
-
entry.firedCues.add(due);
|
|
571
|
-
PubSub.publishUnsafe(cueEvents, {
|
|
572
|
-
key: entry.key,
|
|
573
|
-
name: entry.cues[due].name,
|
|
574
|
-
at: clock.currentTimeMillisUnsafe(),
|
|
575
|
-
});
|
|
576
|
-
}
|
|
577
|
-
};
|
|
578
|
-
const emit = (entry, status, terminal = false) => Effect.gen(function* () {
|
|
579
|
-
if (ended !== undefined && !terminal)
|
|
580
|
-
return;
|
|
581
|
-
if (JSON.stringify(entry.status) === JSON.stringify(status))
|
|
582
|
-
return;
|
|
583
|
-
// A clip that ends fires only the cues that fell within the airtime it had.
|
|
584
|
-
if (status._tag === "Ended" && entry.startedAtMonoMs !== undefined)
|
|
585
|
-
fireCues(entry, entry.startedAtMonoMs +
|
|
586
|
-
1000 *
|
|
587
|
-
(status.termination === "finished" && entry.status._tag === "Started"
|
|
588
|
-
? Math.max(status.airedSeconds, entry.status.durationSeconds)
|
|
589
|
-
: status.airedSeconds));
|
|
590
|
-
entry.status = status;
|
|
591
|
-
entry.phase =
|
|
592
|
-
status._tag === "Accepted" ||
|
|
593
|
-
status._tag === "Building" ||
|
|
594
|
-
status._tag === "Ready" ||
|
|
595
|
-
status._tag === "Started"
|
|
596
|
-
? status._tag
|
|
597
|
-
: status._tag === "Unknown" && status.terminal !== true
|
|
598
|
-
? "Unknown"
|
|
599
|
-
: "Terminal";
|
|
600
|
-
const replaced = entry.replaces === undefined ? undefined : items.get(entry.replaces);
|
|
601
|
-
if (entry.group !== undefined &&
|
|
602
|
-
entry.inserted !== true &&
|
|
603
|
-
(status._tag === "Failed" || status._tag === "Dropped") &&
|
|
604
|
-
!(status._tag === "Dropped" && status.reason === "replaced") &&
|
|
605
|
-
(replaced === undefined || replaced.phase === "Terminal")) {
|
|
606
|
-
const group = groups.get(entry.group.key);
|
|
607
|
-
if (group !== undefined)
|
|
608
|
-
group.brokenAt = Math.min(group.brokenAt ?? entry.group.index, entry.group.index);
|
|
609
|
-
}
|
|
610
|
-
// Publish in the step that records it (the PubSub is unbounded): a claim
|
|
611
|
-
// from another fiber cannot land between the two, so asRun never carries
|
|
612
|
-
// a settlement ahead of a recorded status, nor omits one.
|
|
613
|
-
PubSub.publishUnsafe(events, {
|
|
614
|
-
key: entry.key,
|
|
615
|
-
at: clock.currentTimeMillisUnsafe(),
|
|
616
|
-
status,
|
|
617
|
-
});
|
|
618
|
-
if (isFirstDecisive(status))
|
|
619
|
-
yield* Deferred.succeed(entry.firstDecisiveWaiter, status);
|
|
620
|
-
if (status._tag === "Started")
|
|
621
|
-
yield* Deferred.succeed(entry.startedWaiter, status);
|
|
622
|
-
if (status._tag === "Ended" ||
|
|
623
|
-
status._tag === "Dropped" ||
|
|
624
|
-
status._tag === "Failed" ||
|
|
625
|
-
status._tag === "Unobserved" ||
|
|
626
|
-
(status._tag === "Unknown" && status.terminal === true)) {
|
|
627
|
-
yield* Deferred.succeed(entry.startedWaiter, status);
|
|
628
|
-
yield* Deferred.succeed(entry.outcomeWaiter, status);
|
|
629
|
-
}
|
|
630
|
-
});
|
|
631
|
-
// The claim is synchronous; interruption masking alone would still allow
|
|
632
|
-
// two closers to publish conflicting results. Join only local bookkeeping,
|
|
633
|
-
// never the worker scope (a worker can itself own terminal settlement).
|
|
634
|
-
const terminate = (terminal) => Effect.suspend(() => {
|
|
635
|
-
if (ended !== undefined)
|
|
636
|
-
return Deferred.await(settled);
|
|
637
|
-
ended = terminal;
|
|
638
|
-
accepting = false;
|
|
639
|
-
// Wake the watchdog so an episode left armed by another owner cannot
|
|
640
|
-
// keep re-arming its timer.
|
|
641
|
-
Deferred.doneUnsafe(recoveryChanged, Exit.void);
|
|
642
|
-
return Effect.gen(function* () {
|
|
643
|
-
for (const item of items.values()) {
|
|
644
|
-
if (Exit.isFailure(terminal)) {
|
|
645
|
-
yield* Deferred.failCause(item.startedWaiter, terminal.cause);
|
|
646
|
-
yield* Deferred.failCause(item.outcomeWaiter, terminal.cause);
|
|
647
|
-
yield* Deferred.failCause(item.firstDecisiveWaiter, terminal.cause);
|
|
648
|
-
continue;
|
|
649
|
-
}
|
|
650
|
-
// emit records and publishes a status before completing its waits,
|
|
651
|
-
// and a claim can land in between: a completion can resume an
|
|
652
|
-
// observer that closes the owner, and another fiber can claim while
|
|
653
|
-
// the actor is preempted. Finish those waits from the record.
|
|
654
|
-
const recorded = item.status;
|
|
655
|
-
if (item.phase === "Started" || item.phase === "Terminal") {
|
|
656
|
-
if (isFirstDecisive(recorded))
|
|
657
|
-
yield* Deferred.succeed(item.firstDecisiveWaiter, recorded);
|
|
658
|
-
yield* Deferred.succeed(item.startedWaiter, recorded);
|
|
659
|
-
}
|
|
660
|
-
if (item.phase === "Terminal")
|
|
661
|
-
yield* Deferred.succeed(item.outcomeWaiter, recorded);
|
|
662
|
-
else
|
|
663
|
-
yield* emit(item, item.phase === "Unknown" ||
|
|
664
|
-
(item.clipId === undefined &&
|
|
665
|
-
(item.unknownAtMs !== undefined || item.key === dispatching))
|
|
666
|
-
? { _tag: "Unknown", terminal: true }
|
|
667
|
-
: { _tag: "Failed", reason: { _tag: "Scheduler", cause: terminal.value } }, true);
|
|
668
|
-
}
|
|
669
|
-
const closed = Exit.isSuccess(terminal)
|
|
670
|
-
? Exit.fail(PolicyFailure.refuse("SessionClosed", terminal.value.message))
|
|
671
|
-
: Exit.failCause(terminal.cause);
|
|
672
|
-
for (const [key, pending] of pendingWithdrawals) {
|
|
673
|
-
// A drop recorded before the claim is the withdrawal's actual result.
|
|
674
|
-
const reply = Exit.isSuccess(terminal) && items.get(key)?.status._tag === "Dropped"
|
|
675
|
-
? Exit.succeed("withdrawn")
|
|
676
|
-
: closed;
|
|
677
|
-
for (const waiter of pending.replies)
|
|
678
|
-
yield* Deferred.done(waiter, reply);
|
|
679
|
-
}
|
|
680
|
-
pendingWithdrawals.clear();
|
|
681
|
-
for (const batch of batches.values()) {
|
|
682
|
-
for (const target of batch.targets.values())
|
|
683
|
-
if (target.reply !== undefined)
|
|
684
|
-
yield* Deferred.done(target.reply, closed);
|
|
685
|
-
yield* Deferred.done(batch.committed, closed);
|
|
686
|
-
}
|
|
687
|
-
batches.clear();
|
|
688
|
-
for (const reply of drainReplies.splice(0))
|
|
689
|
-
yield* Deferred.done(reply, closed);
|
|
690
|
-
yield* SubscriptionRef.update(stateRef, (state) => ({ ...state, accepting: false }));
|
|
691
|
-
yield* Deferred.done(initialized, Exit.isFailure(terminal)
|
|
692
|
-
? Exit.failCause(terminal.cause)
|
|
693
|
-
: Exit.fail(ReactorError.fromCode("Closed", terminal.value.message)));
|
|
694
|
-
// Supervisors may close the owner immediately when this wakes them.
|
|
695
|
-
yield* Deferred.done(stopped, terminal);
|
|
696
|
-
}).pipe(Effect.onExit((exit) => Effect.gen(function* () {
|
|
697
|
-
// A bookkeeping defect must release joiners with that Cause, never
|
|
698
|
-
// leave an unfinished latch or report a partial settlement as success.
|
|
699
|
-
yield* Deferred.done(settled, exit);
|
|
700
|
-
if (Exit.isFailure(exit)) {
|
|
701
|
-
yield* Deferred.failCause(initialized, exit.cause);
|
|
702
|
-
yield* Deferred.failCause(stopped, exit.cause);
|
|
703
|
-
}
|
|
704
|
-
})));
|
|
705
|
-
}).pipe(Effect.uninterruptible);
|
|
706
|
-
const closeActor = (cause) => terminate(Exit.succeed(cause));
|
|
707
|
-
let recoveryState;
|
|
708
|
-
let capacityRecovery;
|
|
709
|
-
let recoveryDeadline;
|
|
710
|
-
let recoveryChanged = Deferred.makeUnsafe();
|
|
711
|
-
// Retain the original age while Closing hides the old source and acquisition
|
|
712
|
-
// is still pending. Only accepted evidence or a Ready replacement clears it.
|
|
713
|
-
const updateRecovery = (state = recoveryState, fresh) => {
|
|
714
|
-
if (ended !== undefined)
|
|
715
|
-
return;
|
|
716
|
-
recoveryState = state;
|
|
717
|
-
const preferred = state === undefined ? undefined : Option.getOrUndefined(state.preferredSessionId);
|
|
718
|
-
const ready = preferred !== undefined &&
|
|
719
|
-
state?.sessions.some((source) => source.sessionId === preferred && source.availability === "Ready") === true;
|
|
720
|
-
let earliest;
|
|
721
|
-
let drainDeadline;
|
|
722
|
-
const consider = (atMs, sessionId, cause, newlyObserved = false) => {
|
|
723
|
-
const deadlineMs = atMs + unknownRecoveryMs;
|
|
724
|
-
if (((!ready && newlyObserved) || sessionId === undefined || sessionId === preferred) &&
|
|
725
|
-
(earliest === undefined || deadlineMs < earliest.deadlineMs))
|
|
726
|
-
earliest = { deadlineMs, sessionId, cause };
|
|
727
|
-
if (draining && (drainDeadline === undefined || deadlineMs < drainDeadline.deadlineMs))
|
|
728
|
-
drainDeadline = { deadlineMs, sessionId, cause };
|
|
729
|
-
};
|
|
730
|
-
for (const entry of unknownFillers.values())
|
|
731
|
-
consider(entry.atMs, entry.sessionId, entry.cause);
|
|
732
|
-
for (const item of items.values())
|
|
733
|
-
if (item.phase !== "Terminal" &&
|
|
734
|
-
item.clipId === undefined &&
|
|
735
|
-
item.unknownAtMs !== undefined &&
|
|
736
|
-
item.unknownCause !== undefined)
|
|
737
|
-
consider(item.unknownAtMs, item.unknownSessionId, item.unknownCause);
|
|
738
|
-
if (fresh !== undefined)
|
|
739
|
-
consider(fresh.atMs, fresh.sessionId, fresh.cause, true);
|
|
740
|
-
if (ready &&
|
|
741
|
-
(earliest === undefined ||
|
|
742
|
-
(capacityRecovery?.sessionId !== undefined && capacityRecovery.sessionId !== preferred)))
|
|
743
|
-
capacityRecovery = undefined;
|
|
744
|
-
if (earliest !== undefined &&
|
|
745
|
-
(capacityRecovery === undefined || earliest.deadlineMs < capacityRecovery.deadlineMs))
|
|
746
|
-
capacityRecovery = earliest;
|
|
747
|
-
const next = drainDeadline !== undefined &&
|
|
748
|
-
(capacityRecovery === undefined || drainDeadline.deadlineMs < capacityRecovery.deadlineMs)
|
|
749
|
-
? drainDeadline
|
|
750
|
-
: capacityRecovery;
|
|
751
|
-
if (next?.deadlineMs !== recoveryDeadline?.deadlineMs ||
|
|
752
|
-
next?.cause !== recoveryDeadline?.cause) {
|
|
753
|
-
recoveryDeadline = next;
|
|
754
|
-
Deferred.doneUnsafe(recoveryChanged, Exit.void);
|
|
755
|
-
}
|
|
756
|
-
};
|
|
757
|
-
const provedUnknown = (atMs) => {
|
|
758
|
-
if (atMs !== undefined && capacityRecovery?.deadlineMs === atMs + unknownRecoveryMs)
|
|
759
|
-
capacityRecovery = undefined;
|
|
760
|
-
};
|
|
761
|
-
const watchdog = Effect.gen(function* () {
|
|
762
|
-
for (;;) {
|
|
763
|
-
// A committed result and its queued actor evidence can arrive in one
|
|
764
|
-
// turn. Coalesce those revisions before allocating a replacement timer;
|
|
765
|
-
// the original monotonic deadline remains unchanged.
|
|
766
|
-
yield* Effect.yieldNow;
|
|
767
|
-
if (ended !== undefined)
|
|
768
|
-
return;
|
|
769
|
-
recoveryChanged = Deferred.makeUnsafe();
|
|
770
|
-
const episode = recoveryDeadline;
|
|
771
|
-
if (episode === undefined) {
|
|
772
|
-
yield* Deferred.await(recoveryChanged);
|
|
773
|
-
continue;
|
|
774
|
-
}
|
|
775
|
-
yield* Deferred.await(recoveryChanged).pipe(Effect.timeoutOrElse({
|
|
776
|
-
duration: Math.max(0, episode.deadlineMs - monotonicMillis(clock)),
|
|
777
|
-
orElse: () => Effect.void,
|
|
778
|
-
}));
|
|
779
|
-
// A canceled timer may already have resumed. Recheck the retained
|
|
780
|
-
// identity without awaiting the actor, Engine state, or its permits.
|
|
781
|
-
if (ended === undefined &&
|
|
782
|
-
recoveryDeadline === episode &&
|
|
783
|
-
monotonicMillis(clock) >= episode.deadlineMs) {
|
|
784
|
-
const causes = new Set([episode.cause]);
|
|
785
|
-
for (const item of items.values())
|
|
786
|
-
if (item.unknownCause !== undefined)
|
|
787
|
-
causes.add(item.unknownCause);
|
|
788
|
-
for (const entry of unknownFillers.values())
|
|
789
|
-
causes.add(entry.cause);
|
|
790
|
-
yield* closeActor(ReactorError.fromCode("Timeout", "Scheduler unknown recovery deadline elapsed", {
|
|
791
|
-
operation: "scheduler.unknownRecovery",
|
|
792
|
-
detail: [...causes],
|
|
793
|
-
}));
|
|
794
|
-
return;
|
|
795
|
-
}
|
|
796
|
-
}
|
|
797
|
-
});
|
|
798
|
-
const prune = (state) => {
|
|
799
|
-
const active = new Set(activeIds(state));
|
|
800
|
-
if (state.availability === "Ready") {
|
|
801
|
-
for (const id of removedIds)
|
|
802
|
-
if (!active.has(id))
|
|
803
|
-
removedIds.delete(id);
|
|
804
|
-
for (const id of removalRetryAtMs.keys())
|
|
805
|
-
if (!active.has(id))
|
|
806
|
-
removalRetryAtMs.delete(id);
|
|
807
|
-
for (const id of removalUnknownAtSerial.keys())
|
|
808
|
-
if (!active.has(id))
|
|
809
|
-
removalUnknownAtSerial.delete(id);
|
|
810
|
-
}
|
|
811
|
-
for (const [key, item] of items) {
|
|
812
|
-
if (item.phase !== "Terminal" ||
|
|
813
|
-
pendingBuilds.has(key) ||
|
|
814
|
-
pendingWithdrawals.has(key) ||
|
|
815
|
-
pendingAtDeferrals.has(key) ||
|
|
816
|
-
pendingItemRemovals.has(key) ||
|
|
817
|
-
[...batches.values()].some((batch) => batch.targets.has(key)) ||
|
|
818
|
-
// A replacement dropped before it was built still links its own replacement to
|
|
819
|
-
// the item in the place.
|
|
820
|
-
[...items.values()].some((other) => other.replaces === key && other.phase !== "Terminal") ||
|
|
821
|
-
(item.clipId !== undefined && active.has(item.clipId)))
|
|
822
|
-
continue;
|
|
823
|
-
if (item.clipId !== undefined) {
|
|
824
|
-
owned.delete(item.clipId);
|
|
825
|
-
playingStartedMs.delete(item.clipId);
|
|
826
|
-
}
|
|
827
|
-
history.set(key, { fingerprint: item.fingerprint, handle: item.handle });
|
|
828
|
-
items.delete(key);
|
|
829
|
-
}
|
|
830
|
-
while (history.size > maxHistory)
|
|
831
|
-
history.delete(history.keys().next().value);
|
|
832
|
-
for (const [key, group] of groups)
|
|
833
|
-
if (!group.parts.some((part) => items.has(part) || history.has(part)))
|
|
834
|
-
groups.delete(key);
|
|
835
|
-
};
|
|
836
|
-
// Capture membership and public rows in one pass. A fresh token reads every
|
|
837
|
-
// source on every observation, including in-place changes to Engine arrays;
|
|
838
|
-
// frozen empty rows can be shared by every published state without treating
|
|
839
|
-
// array identity as proof.
|
|
840
|
-
const noReady = Object.freeze([]);
|
|
841
|
-
const sourceRows = new Map();
|
|
842
|
-
const projectSources = (state) => {
|
|
843
|
-
const seen = Symbol();
|
|
844
|
-
const sessions = state.sessions.map(({ sessionId }) => {
|
|
845
|
-
let cached = sourceRows.get(sessionId);
|
|
846
|
-
if (cached === undefined) {
|
|
847
|
-
cached = { empty: Object.freeze({ sessionId, ready: noReady }), seen };
|
|
848
|
-
sourceRows.set(sessionId, cached);
|
|
849
|
-
}
|
|
850
|
-
else
|
|
851
|
-
cached.seen = seen;
|
|
852
|
-
if (state.ready.length === 0)
|
|
853
|
-
return cached.empty;
|
|
854
|
-
return {
|
|
855
|
-
sessionId,
|
|
856
|
-
ready: state.ready
|
|
857
|
-
.filter((clip) => clip.sessionId === sessionId)
|
|
858
|
-
.map((clip) => {
|
|
859
|
-
const owner = owned.get(clip.clipId);
|
|
860
|
-
return owner === undefined ? "other" : owner._tag === "Filler" ? "filler" : owner.key;
|
|
861
|
-
}),
|
|
862
|
-
};
|
|
863
|
-
});
|
|
864
|
-
// Bound cached rows by snapshot size without scanning unchanged overlap.
|
|
865
|
-
// Membership still requires this observation's token, even before pruning.
|
|
866
|
-
if (sourceRows.size > state.sessions.length)
|
|
867
|
-
for (const [id, cached] of sourceRows)
|
|
868
|
-
if (cached.seen !== seen)
|
|
869
|
-
sourceRows.delete(id);
|
|
870
|
-
// Only the serialized actor uses this membership view, before its next projection.
|
|
871
|
-
return { sessions, has: (id) => sourceRows.get(id)?.seen === seen };
|
|
872
|
-
};
|
|
873
|
-
const publishState = (state, sessions = projectSources(state).sessions) => {
|
|
874
|
-
const playing = Option.getOrUndefined(state.playing);
|
|
875
|
-
const playingOwner = playing === undefined ? undefined : owned.get(playing.clipId);
|
|
876
|
-
const playingKey = playing === undefined
|
|
877
|
-
? null
|
|
878
|
-
: playingOwner === undefined
|
|
879
|
-
? "other"
|
|
880
|
-
: playingOwner._tag === "Filler"
|
|
881
|
-
? "filler"
|
|
882
|
-
: playingOwner.key;
|
|
883
|
-
return SubscriptionRef.set(stateRef, {
|
|
884
|
-
accepting,
|
|
885
|
-
runwaySeconds: runwaySeconds(state, monotonicMillis(clock), playingStartedMs, owned, [
|
|
886
|
-
...items.values(),
|
|
887
|
-
]),
|
|
888
|
-
playing: playingKey,
|
|
889
|
-
lanes: lanes.map((name) => ({
|
|
890
|
-
name,
|
|
891
|
-
keys: [...items.values()]
|
|
892
|
-
.filter((item) => item.lane === name && item.phase !== "Terminal")
|
|
893
|
-
.map((item) => item.key),
|
|
894
|
-
})),
|
|
895
|
-
sessions,
|
|
896
|
-
starved,
|
|
897
|
-
estimates: estimatesFrom(buildSamples, lengthSamples),
|
|
898
|
-
});
|
|
899
|
-
};
|
|
900
|
-
/** When the filler build in flight was sent, and the next filler clip's requested length. */
|
|
901
|
-
const fillerView = () => ({
|
|
902
|
-
fillerDispatchedAtMs: fillerDispatch.size === 0
|
|
903
|
-
? undefined
|
|
904
|
-
: Math.min(...[...fillerDispatch.values()].map((sent) => sent.atMs)),
|
|
905
|
-
fillerSeconds: upcomingFiller?.durationSeconds ?? lastFillerSeconds,
|
|
906
|
-
});
|
|
907
|
-
const activeRecords = (state) => [
|
|
908
|
-
...state.queued,
|
|
909
|
-
...state.ready,
|
|
910
|
-
...Option.match(state.building, {
|
|
911
|
-
onNone: () => [],
|
|
912
|
-
onSome: (build) => [build.record],
|
|
913
|
-
}),
|
|
914
|
-
...Option.match(state.playing, {
|
|
915
|
-
onNone: () => [],
|
|
916
|
-
onSome: (playing) => Option.match(playing.record, {
|
|
917
|
-
onNone: () => [],
|
|
918
|
-
onSome: (record) => [record],
|
|
919
|
-
}),
|
|
920
|
-
}),
|
|
921
|
-
];
|
|
922
|
-
const knownRecord = (state, clipId) => activeRecords(state).find((record) => record.clipId === clipId);
|
|
923
|
-
const refreshAnchors = () => {
|
|
924
|
-
const nowMs = monotonicMillis(clock);
|
|
925
|
-
const wallMs = clock.currentTimeMillisUnsafe();
|
|
926
|
-
for (const item of items.values())
|
|
927
|
-
if (item.atWallMs !== undefined)
|
|
928
|
-
item.atMs = nowMs + item.atWallMs - wallMs;
|
|
929
|
-
};
|
|
930
|
-
const observed = (state, recoverMissing = false) => Effect.gen(function* () {
|
|
931
|
-
const active = new Set(activeIds(state));
|
|
932
|
-
if (recoverMissing) {
|
|
933
|
-
for (const [clipId, owner] of owned) {
|
|
934
|
-
if (owner._tag !== "Filler" || active.has(clipId))
|
|
935
|
-
continue;
|
|
936
|
-
const tracked = filler.get(owner.index);
|
|
937
|
-
if (tracked?.clipId === clipId &&
|
|
938
|
-
tracked?.acknowledgedAtSnapshotSerial !== undefined &&
|
|
939
|
-
lastProcessedSnapshotSerial <= tracked.acknowledgedAtSnapshotSerial)
|
|
940
|
-
continue;
|
|
941
|
-
const sessionId = owner.sessionId;
|
|
942
|
-
const source = state.sessions.find((entry) => entry.sessionId === sessionId);
|
|
943
|
-
if (sessionId === undefined
|
|
944
|
-
? state.availability !== "Ready"
|
|
945
|
-
: source !== undefined && source.availability !== "Ready")
|
|
946
|
-
continue;
|
|
947
|
-
owned.delete(clipId);
|
|
948
|
-
if (tracked?.clipId === clipId)
|
|
949
|
-
filler.delete(owner.index);
|
|
950
|
-
playingStartedMs.delete(clipId);
|
|
951
|
-
pendingFillerRemovals.delete(clipId);
|
|
952
|
-
}
|
|
953
|
-
}
|
|
954
|
-
const keyed = new Map();
|
|
955
|
-
for (const record of activeRecords(state)) {
|
|
956
|
-
const key = keyFromProviderMetadata(record.provider.metadata);
|
|
957
|
-
if (key !== undefined && !removedIds.has(record.clipId))
|
|
958
|
-
keyed.set(key, record);
|
|
959
|
-
const index = fillerIndexFromKey(key);
|
|
960
|
-
if (index === undefined || removedIds.has(record.clipId))
|
|
961
|
-
continue;
|
|
962
|
-
const existing = owned.get(record.clipId);
|
|
963
|
-
if (existing?._tag === "Filler") {
|
|
964
|
-
if (existing.sessionId !== record.sessionId)
|
|
965
|
-
owned.set(record.clipId, { ...existing, sessionId: record.sessionId });
|
|
966
|
-
const tracked = filler.get(index);
|
|
967
|
-
if (tracked?.clipId === record.clipId)
|
|
968
|
-
tracked.sessionId = record.sessionId;
|
|
969
|
-
continue;
|
|
970
|
-
}
|
|
971
|
-
if (existing !== undefined)
|
|
972
|
-
continue;
|
|
973
|
-
owned.set(record.clipId, { _tag: "Filler", index, sessionId: record.sessionId });
|
|
974
|
-
filler.set(index, { index, clipId: record.clipId, sessionId: record.sessionId });
|
|
975
|
-
const priorIndex = fillerIndex;
|
|
976
|
-
fillerIndex = Math.max(fillerIndex, index + 1);
|
|
977
|
-
provedUnknown(unknownFillers.get(index)?.atMs);
|
|
978
|
-
unknownFillers.delete(index);
|
|
979
|
-
if (fillerIndex !== priorIndex)
|
|
980
|
-
upcomingFiller = undefined;
|
|
981
|
-
}
|
|
982
|
-
for (const item of items.values()) {
|
|
983
|
-
if (item.phase === "Terminal")
|
|
984
|
-
continue;
|
|
985
|
-
if (item.clipId === undefined) {
|
|
986
|
-
const resumed = keyed.get(item.key);
|
|
987
|
-
if (resumed !== undefined) {
|
|
988
|
-
item.clipId = resumed.clipId;
|
|
989
|
-
item.sessionId = resumed.sessionId;
|
|
990
|
-
provedUnknown(item.unknownAtMs);
|
|
991
|
-
item.unknownAtMs = undefined;
|
|
992
|
-
item.unknownCause = undefined;
|
|
993
|
-
item.unknownSessionId = undefined;
|
|
994
|
-
owned.set(resumed.clipId, { _tag: "Item", key: item.key });
|
|
995
|
-
}
|
|
996
|
-
}
|
|
997
|
-
const clipId = item.clipId;
|
|
998
|
-
if (clipId === undefined)
|
|
999
|
-
continue;
|
|
1000
|
-
const playing = Option.getOrUndefined(state.playing);
|
|
1001
|
-
if (playing?.clipId === clipId) {
|
|
1002
|
-
const record = Option.getOrUndefined(playing.record);
|
|
1003
|
-
const at = Option.getOrUndefined(playing.startedAt);
|
|
1004
|
-
const startedAtMonotonicMillis = playing.startedAtMonotonicMillis;
|
|
1005
|
-
if (record !== undefined &&
|
|
1006
|
-
at !== undefined &&
|
|
1007
|
-
startedAtMonotonicMillis !== undefined &&
|
|
1008
|
-
item.phase !== "Started") {
|
|
1009
|
-
item.sessionId = record.sessionId;
|
|
1010
|
-
item.startedAtEpochMs = at;
|
|
1011
|
-
item.startedAtMonoMs = startedAtMonotonicMillis;
|
|
1012
|
-
playingStartedMs.set(clipId, startedAtMonotonicMillis);
|
|
1013
|
-
yield* emit(item, {
|
|
1014
|
-
_tag: "Started",
|
|
1015
|
-
at,
|
|
1016
|
-
sessionId: record.sessionId,
|
|
1017
|
-
durationSeconds: record.durationSeconds,
|
|
1018
|
-
...((item.atMs ?? item.startByMs) === undefined ||
|
|
1019
|
-
startedAtMonotonicMillis <= (item.atMs ?? item.startByMs)
|
|
1020
|
-
? {}
|
|
1021
|
-
: { lateByMillis: startedAtMonotonicMillis - (item.atMs ?? item.startByMs) }),
|
|
1022
|
-
});
|
|
1023
|
-
}
|
|
1024
|
-
else if (recoverMissing &&
|
|
1025
|
-
(record === undefined || at === undefined) &&
|
|
1026
|
-
item.phase !== "Started")
|
|
1027
|
-
yield* emit(item, { _tag: "Unobserved" });
|
|
1028
|
-
}
|
|
1029
|
-
else if (state.ready.some((record) => record.clipId === clipId)) {
|
|
1030
|
-
const record = knownRecord(state, clipId);
|
|
1031
|
-
measureItem(item, record.durationSeconds);
|
|
1032
|
-
item.sessionId = record.sessionId;
|
|
1033
|
-
if (item.phase !== "Started")
|
|
1034
|
-
yield* emit(item, { _tag: "Ready", sessionId: record.sessionId });
|
|
1035
|
-
}
|
|
1036
|
-
else if (state.queued.some((record) => record.clipId === clipId) ||
|
|
1037
|
-
state.generationOrder.includes(clipId) ||
|
|
1038
|
-
Option.getOrUndefined(state.building)?.record.clipId === clipId) {
|
|
1039
|
-
item.sessionId = knownRecord(state, clipId)?.sessionId ?? item.sessionId;
|
|
1040
|
-
if (item.phase === "Accepted" || item.phase === "Unknown")
|
|
1041
|
-
yield* emit(item, { _tag: "Building" });
|
|
1042
|
-
}
|
|
1043
|
-
else if (state.failed.includes(clipId))
|
|
1044
|
-
yield* emit(item, {
|
|
1045
|
-
_tag: "Failed",
|
|
1046
|
-
reason: { _tag: "Clip", message: "The clip failed unobserved" },
|
|
1047
|
-
});
|
|
1048
|
-
else if (recoverMissing &&
|
|
1049
|
-
item.sessionId !== undefined &&
|
|
1050
|
-
(state.sessions.find((source) => source.sessionId === item.sessionId)?.availability ??
|
|
1051
|
-
"Ready") === "Ready" &&
|
|
1052
|
-
!active.has(clipId) &&
|
|
1053
|
-
(item.acknowledgedAtSnapshotSerial === undefined ||
|
|
1054
|
-
lastProcessedSnapshotSerial > item.acknowledgedAtSnapshotSerial))
|
|
1055
|
-
yield* emit(item, { _tag: "Unobserved" });
|
|
1056
|
-
}
|
|
1057
|
-
updateRecovery(state);
|
|
1058
|
-
});
|
|
1059
|
-
const onEvent = (event) => Effect.gen(function* () {
|
|
1060
|
-
if (event._tag === "Starved")
|
|
1061
|
-
starved++;
|
|
1062
|
-
if (event._tag === "SessionFailed") {
|
|
1063
|
-
yield* closeActor(event.failure);
|
|
1064
|
-
return;
|
|
1065
|
-
}
|
|
1066
|
-
if (!("clipId" in event))
|
|
1067
|
-
return;
|
|
1068
|
-
if (commandCount > 0 &&
|
|
1069
|
-
(event._tag === "Started" || event._tag === "Ended" || event._tag === "Failed")) {
|
|
1070
|
-
const prior = earlyClipEvents.get(event.clipId);
|
|
1071
|
-
if (prior === undefined) {
|
|
1072
|
-
if (earlyClipEvents.size >= maxEarlyClipIds) {
|
|
1073
|
-
// A lost pre-ack start cannot be recast as a definite failure.
|
|
1074
|
-
for (const key of pendingBuilds) {
|
|
1075
|
-
const item = items.get(key);
|
|
1076
|
-
if (item !== undefined && item.phase !== "Terminal" && item.phase !== "Started") {
|
|
1077
|
-
yield* emit(item, { _tag: "Unknown", terminal: true });
|
|
1078
|
-
item.phase = "Terminal";
|
|
1079
|
-
}
|
|
1080
|
-
}
|
|
1081
|
-
yield* closeActor(ReactorError.fromCode("Overflow", "Unattributed clip evidence exceeded its bound"));
|
|
1082
|
-
return;
|
|
1083
|
-
}
|
|
1084
|
-
earlyClipEvents.set(event.clipId, [event]);
|
|
1085
|
-
}
|
|
1086
|
-
else if (!prior.some((seen) => event._tag === "Started"
|
|
1087
|
-
? seen._tag === "Started"
|
|
1088
|
-
: seen._tag === "Ended" || seen._tag === "Failed"))
|
|
1089
|
-
prior.push(event);
|
|
1090
|
-
}
|
|
1091
|
-
const owner = owned.get(event.clipId);
|
|
1092
|
-
if (owner === undefined)
|
|
1093
|
-
return;
|
|
1094
|
-
if (owner._tag === "Filler") {
|
|
1095
|
-
const sent = fillerDispatch.get(owner.index);
|
|
1096
|
-
if (event._tag === "Ready" && sent !== undefined)
|
|
1097
|
-
measure(sent.atMs, sent.seconds, event.durationSeconds);
|
|
1098
|
-
if (event._tag === "Ready" || event._tag === "Ended" || event._tag === "Failed")
|
|
1099
|
-
fillerDispatch.delete(owner.index);
|
|
1100
|
-
if (event._tag === "Started")
|
|
1101
|
-
playingStartedMs.set(event.clipId, event.atMonotonicMillis ?? monotonicMillis(clock));
|
|
1102
|
-
if (event._tag === "Ended" || event._tag === "Failed") {
|
|
1103
|
-
owned.delete(event.clipId);
|
|
1104
|
-
if (filler.get(owner.index)?.clipId === event.clipId)
|
|
1105
|
-
filler.delete(owner.index);
|
|
1106
|
-
playingStartedMs.delete(event.clipId);
|
|
1107
|
-
}
|
|
1108
|
-
return;
|
|
1109
|
-
}
|
|
1110
|
-
const item = items.get(owner.key);
|
|
1111
|
-
if (item === undefined || item.phase === "Terminal")
|
|
1112
|
-
return;
|
|
1113
|
-
switch (event._tag) {
|
|
1114
|
-
case "Queued":
|
|
1115
|
-
case "Building":
|
|
1116
|
-
if (item.phase === "Accepted" || item.phase === "Unknown")
|
|
1117
|
-
yield* emit(item, { _tag: "Building" });
|
|
1118
|
-
break;
|
|
1119
|
-
case "Ready": {
|
|
1120
|
-
measureItem(item, event.durationSeconds);
|
|
1121
|
-
const record = knownRecord(yield* engine.state, event.clipId);
|
|
1122
|
-
if (record !== undefined) {
|
|
1123
|
-
item.sessionId = record.sessionId;
|
|
1124
|
-
if (item.phase !== "Started")
|
|
1125
|
-
yield* emit(item, { _tag: "Ready", sessionId: record.sessionId });
|
|
1126
|
-
}
|
|
1127
|
-
break;
|
|
1128
|
-
}
|
|
1129
|
-
case "Started": {
|
|
1130
|
-
if (item.phase === "Started")
|
|
1131
|
-
break;
|
|
1132
|
-
const state = yield* engine.state;
|
|
1133
|
-
const record = knownRecord(state, event.clipId);
|
|
1134
|
-
const sessionId = record?.sessionId ?? item.sessionId;
|
|
1135
|
-
if (sessionId === undefined)
|
|
1136
|
-
yield* emit(item, { _tag: "Unobserved" });
|
|
1137
|
-
else {
|
|
1138
|
-
item.sessionId = sessionId;
|
|
1139
|
-
const playing = Option.getOrUndefined(state.playing);
|
|
1140
|
-
item.startedAtMonoMs =
|
|
1141
|
-
event.atMonotonicMillis ??
|
|
1142
|
-
(playing?.clipId === event.clipId
|
|
1143
|
-
? (playing.startedAtMonotonicMillis ?? monotonicMillis(clock))
|
|
1144
|
-
: monotonicMillis(clock));
|
|
1145
|
-
item.startedAtEpochMs = event.at;
|
|
1146
|
-
playingStartedMs.set(event.clipId, item.startedAtMonoMs);
|
|
1147
|
-
yield* emit(item, {
|
|
1148
|
-
_tag: "Started",
|
|
1149
|
-
at: event.at,
|
|
1150
|
-
sessionId,
|
|
1151
|
-
durationSeconds: event.durationSeconds,
|
|
1152
|
-
...((item.atMs ?? item.startByMs) === undefined ||
|
|
1153
|
-
item.startedAtMonoMs <= (item.atMs ?? item.startByMs)
|
|
1154
|
-
? {}
|
|
1155
|
-
: { lateByMillis: item.startedAtMonoMs - (item.atMs ?? item.startByMs) }),
|
|
1156
|
-
});
|
|
1157
|
-
}
|
|
1158
|
-
break;
|
|
1159
|
-
}
|
|
1160
|
-
case "Ended":
|
|
1161
|
-
yield* emit(item, item.startedAtMonoMs === undefined
|
|
1162
|
-
? { _tag: "Unobserved" }
|
|
1163
|
-
: {
|
|
1164
|
-
_tag: "Ended",
|
|
1165
|
-
at: event.at ?? clock.currentTimeMillisUnsafe(),
|
|
1166
|
-
termination: event.termination,
|
|
1167
|
-
airedSeconds: Math.max(0, ((event.atMonotonicMillis ?? monotonicMillis(clock)) - item.startedAtMonoMs) /
|
|
1168
|
-
1000),
|
|
1169
|
-
});
|
|
1170
|
-
playingStartedMs.delete(event.clipId);
|
|
1171
|
-
break;
|
|
1172
|
-
case "Failed": {
|
|
1173
|
-
// A clip lost with a replaced session before it aired is rebuilt from the plan.
|
|
1174
|
-
if (event.lost === true &&
|
|
1175
|
-
(item.phase === "Accepted" || item.phase === "Building" || item.phase === "Ready")) {
|
|
1176
|
-
const sessionId = event.sessionId ?? item.sessionId;
|
|
1177
|
-
// Its session may still list the clip until it closes; it is not adopted again.
|
|
1178
|
-
removedIds.add(event.clipId);
|
|
1179
|
-
owned.delete(event.clipId);
|
|
1180
|
-
playingStartedMs.delete(event.clipId);
|
|
1181
|
-
delete item.clipId;
|
|
1182
|
-
delete item.dispatchedAtMs;
|
|
1183
|
-
delete item.acknowledgedAtSnapshotSerial;
|
|
1184
|
-
item.sessionId = undefined;
|
|
1185
|
-
const pending = pendingWithdrawals.get(item.key);
|
|
1186
|
-
if (pending !== undefined) {
|
|
1187
|
-
yield* emit(item, { _tag: "Dropped", reason: pending.reason });
|
|
1188
|
-
yield* finishWithdrawal(item.key, Result.succeed("withdrawn"));
|
|
1189
|
-
}
|
|
1190
|
-
else
|
|
1191
|
-
yield* emit(item, sessionId === undefined
|
|
1192
|
-
? { _tag: "Accepted" }
|
|
1193
|
-
: { _tag: "Accepted", carried: { sessionId } });
|
|
1194
|
-
break;
|
|
1195
|
-
}
|
|
1196
|
-
yield* emit(item, { _tag: "Failed", reason: { _tag: "Clip", message: event.reason } });
|
|
1197
|
-
break;
|
|
1198
|
-
}
|
|
1199
|
-
default:
|
|
1200
|
-
break;
|
|
1201
|
-
}
|
|
1202
|
-
});
|
|
1203
|
-
const replayEarlyClipEvents = (clipId) => Effect.gen(function* () {
|
|
1204
|
-
const saved = earlyClipEvents.get(clipId);
|
|
1205
|
-
earlyClipEvents.delete(clipId);
|
|
1206
|
-
if (saved === undefined)
|
|
1207
|
-
return;
|
|
1208
|
-
for (const event of saved)
|
|
1209
|
-
yield* onEvent(event);
|
|
1210
|
-
});
|
|
1211
|
-
const executeCommand = (command) => {
|
|
1212
|
-
switch (command._tag) {
|
|
1213
|
-
case "Build":
|
|
1214
|
-
case "BuildFiller":
|
|
1215
|
-
return Effect.gen(function* () {
|
|
1216
|
-
if (command.sessionId === undefined ||
|
|
1217
|
-
Option.getOrUndefined((yield* engine.state).preferredSessionId) !== command.sessionId)
|
|
1218
|
-
return yield* PolicyFailure.refuse("RouteChanged", "The preferred source changed before dispatch");
|
|
1219
|
-
// Termination can be claimed while the route is read; its settlement
|
|
1220
|
-
// must not be followed by a new dispatch.
|
|
1221
|
-
if (ended !== undefined)
|
|
1222
|
-
return yield* PolicyFailure.refuse("SessionClosed", "The scheduler closed");
|
|
1223
|
-
if (command._tag === "Build")
|
|
1224
|
-
dispatching = command.key;
|
|
1225
|
-
return yield* engine.enqueueOnSource === undefined
|
|
1226
|
-
? engine.enqueue(command.request)
|
|
1227
|
-
: engine.enqueueOnSource(command.request, command.sessionId);
|
|
1228
|
-
});
|
|
1229
|
-
case "RemoveItem":
|
|
1230
|
-
case "RemoveFiller":
|
|
1231
|
-
case "RemoveDuplicate":
|
|
1232
|
-
case "DeferAt":
|
|
1233
|
-
return engine.remove(command.clipId);
|
|
1234
|
-
case "Order":
|
|
1235
|
-
return engine.move(command.clipId, command.position, "playout");
|
|
1236
|
-
case "PauseAutoplay":
|
|
1237
|
-
return engine.setAutoplay(false);
|
|
1238
|
-
case "Cut":
|
|
1239
|
-
return engine.cut(command.clipId);
|
|
1240
|
-
}
|
|
1241
|
-
};
|
|
1242
|
-
const commandWorker = Effect.gen(function* () {
|
|
1243
|
-
for (;;) {
|
|
1244
|
-
const command = yield* takeQueue(commandQueue);
|
|
1245
|
-
if (ended !== undefined)
|
|
1246
|
-
continue;
|
|
1247
|
-
const result = yield* Effect.result(executeCommand(command));
|
|
1248
|
-
dispatching = undefined;
|
|
1249
|
-
// Capture uncertainty before actor delivery: drain may be awaiting a
|
|
1250
|
-
// permit in stopRenewal while this committed command finishes.
|
|
1251
|
-
if (ended === undefined &&
|
|
1252
|
-
Result.isFailure(result) &&
|
|
1253
|
-
result.failure.context.outcome === "unknown") {
|
|
1254
|
-
let fresh;
|
|
1255
|
-
const sessionId = engine.enqueueOnSource === undefined ||
|
|
1256
|
-
(command._tag !== "Build" && command._tag !== "BuildFiller")
|
|
1257
|
-
? undefined
|
|
1258
|
-
: command.sessionId;
|
|
1259
|
-
if (command._tag === "Build") {
|
|
1260
|
-
const item = items.get(command.key);
|
|
1261
|
-
if (item !== undefined && item.phase !== "Terminal" && item.clipId === undefined) {
|
|
1262
|
-
item.unknownAtMs ??= monotonicMillis(clock);
|
|
1263
|
-
item.unknownCause = result.failure;
|
|
1264
|
-
item.unknownSessionId = sessionId;
|
|
1265
|
-
fresh = { sessionId, atMs: item.unknownAtMs, cause: result.failure };
|
|
1266
|
-
}
|
|
1267
|
-
}
|
|
1268
|
-
else if (command._tag === "BuildFiller" &&
|
|
1269
|
-
!filler.has(command.index) &&
|
|
1270
|
-
!unknownFillers.has(command.index)) {
|
|
1271
|
-
fresh = { sessionId, atMs: monotonicMillis(clock), cause: result.failure };
|
|
1272
|
-
unknownFillers.set(command.index, { ...fresh, index: command.index });
|
|
1273
|
-
}
|
|
1274
|
-
updateRecovery(recoveryState, fresh);
|
|
1275
|
-
}
|
|
1276
|
-
yield* Queue.offer(inbox, { _tag: "CommandDone", command, result });
|
|
1277
|
-
}
|
|
1278
|
-
});
|
|
1279
|
-
const sendCommand = (command) => Effect.gen(function* () {
|
|
1280
|
-
if (ended !== undefined)
|
|
1281
|
-
return;
|
|
1282
|
-
if (command._tag === "Build") {
|
|
1283
|
-
pendingBuilds.add(command.key);
|
|
1284
|
-
const item = items.get(command.key);
|
|
1285
|
-
if (item !== undefined)
|
|
1286
|
-
item.dispatchedAtMs = monotonicMillis(clock);
|
|
1287
|
-
}
|
|
1288
|
-
if (command._tag === "BuildFiller") {
|
|
1289
|
-
lastFillerSeconds = command.request.durationSeconds;
|
|
1290
|
-
fillerDispatch.set(command.index, {
|
|
1291
|
-
atMs: monotonicMillis(clock),
|
|
1292
|
-
seconds: command.request.durationSeconds,
|
|
1293
|
-
});
|
|
1294
|
-
// In-flight filler is bounded like the samples it feeds.
|
|
1295
|
-
for (const index of fillerDispatch.keys())
|
|
1296
|
-
if (fillerDispatch.size > maxSamples)
|
|
1297
|
-
fillerDispatch.delete(index);
|
|
1298
|
-
}
|
|
1299
|
-
if (command._tag === "DeferAt")
|
|
1300
|
-
pendingAtDeferrals.add(command.key);
|
|
1301
|
-
commandCount++;
|
|
1302
|
-
yield* Queue.offer(commandQueue, command);
|
|
1303
|
-
});
|
|
1304
|
-
const canRetryRemoval = (clipId) => monotonicMillis(clock) >= (removalRetryAtMs.get(clipId) ?? 0) &&
|
|
1305
|
-
(removalUnknownAtSerial.get(clipId) === undefined ||
|
|
1306
|
-
lastProcessedSnapshotSerial > removalUnknownAtSerial.get(clipId));
|
|
1307
|
-
const finishWithdrawal = (key, result) => Effect.gen(function* () {
|
|
1308
|
-
const pending = pendingWithdrawals.get(key);
|
|
1309
|
-
pendingWithdrawals.delete(key);
|
|
1310
|
-
if (pending === undefined)
|
|
1311
|
-
return;
|
|
1312
|
-
for (const reply of pending.replies)
|
|
1313
|
-
if (Result.isSuccess(result))
|
|
1314
|
-
yield* Deferred.succeed(reply, result.success);
|
|
1315
|
-
else
|
|
1316
|
-
yield* Deferred.fail(reply, result.failure);
|
|
1317
|
-
});
|
|
1318
|
-
const sendItemRemoval = (item, reason) => Effect.gen(function* () {
|
|
1319
|
-
if (item.clipId === undefined ||
|
|
1320
|
-
pendingItemRemovals.has(item.key) ||
|
|
1321
|
-
!canRetryRemoval(item.clipId))
|
|
1322
|
-
return;
|
|
1323
|
-
pendingItemRemovals.add(item.key);
|
|
1324
|
-
yield* sendCommand({ _tag: "RemoveItem", key: item.key, clipId: item.clipId, reason });
|
|
1325
|
-
});
|
|
1326
|
-
/** A withdrawal's answer for an item that has started or settled another way. */
|
|
1327
|
-
const settledOutcome = (item) => item.phase === "Started" || item.status._tag === "Ended" || item.status._tag === "Unobserved"
|
|
1328
|
-
? "already-started"
|
|
1329
|
-
: item.status._tag === "Dropped"
|
|
1330
|
-
? "withdrawn"
|
|
1331
|
-
: "not-found";
|
|
1332
|
-
const requestWithdrawal = (item, reason, reply) => Effect.gen(function* () {
|
|
1333
|
-
if (item.phase === "Started" || item.phase === "Terminal") {
|
|
1334
|
-
if (reply !== undefined)
|
|
1335
|
-
yield* Deferred.succeed(reply, settledOutcome(item));
|
|
1336
|
-
return;
|
|
1337
|
-
}
|
|
1338
|
-
const pending = pendingWithdrawals.get(item.key);
|
|
1339
|
-
if (pending !== undefined) {
|
|
1340
|
-
if (reply !== undefined)
|
|
1341
|
-
pending.replies.push(reply);
|
|
1342
|
-
return;
|
|
1343
|
-
}
|
|
1344
|
-
pendingWithdrawals.set(item.key, { reason, replies: reply === undefined ? [] : [reply] });
|
|
1345
|
-
if (pendingAtDeferrals.has(item.key))
|
|
1346
|
-
return;
|
|
1347
|
-
if (item.clipId === undefined) {
|
|
1348
|
-
if (item.phase === "Unknown" || pendingBuilds.has(item.key))
|
|
1349
|
-
return;
|
|
1350
|
-
yield* emit(item, { _tag: "Dropped", reason });
|
|
1351
|
-
yield* finishWithdrawal(item.key, Result.succeed("withdrawn"));
|
|
1352
|
-
return;
|
|
1353
|
-
}
|
|
1354
|
-
yield* sendItemRemoval(item, reason);
|
|
1355
|
-
});
|
|
1356
|
-
const applyPolicy = (action, state) => Effect.gen(function* () {
|
|
1357
|
-
switch (action._tag) {
|
|
1358
|
-
case "DeferAt":
|
|
1359
|
-
if (canRetryRemoval(action.clipId))
|
|
1360
|
-
yield* sendCommand({ _tag: "DeferAt", key: action.key, clipId: action.clipId });
|
|
1361
|
-
break;
|
|
1362
|
-
case "Cut":
|
|
1363
|
-
yield* sendCommand({ _tag: "Cut", clipId: action.clipId });
|
|
1364
|
-
break;
|
|
1365
|
-
case "Order": {
|
|
1366
|
-
const signature = state.ready.map((record) => record.clipId).join(",") +
|
|
1367
|
-
":" +
|
|
1368
|
-
action.clipId +
|
|
1369
|
-
":" +
|
|
1370
|
-
action.position;
|
|
1371
|
-
if (blockedMove !== signature)
|
|
1372
|
-
yield* sendCommand({
|
|
1373
|
-
_tag: "Order",
|
|
1374
|
-
clipId: action.clipId,
|
|
1375
|
-
position: action.position,
|
|
1376
|
-
signature,
|
|
1377
|
-
});
|
|
1378
|
-
break;
|
|
1379
|
-
}
|
|
1380
|
-
case "Build": {
|
|
1381
|
-
const item = items.get(action.key);
|
|
1382
|
-
if (item?.phase !== "Accepted")
|
|
1383
|
-
break;
|
|
1384
|
-
const prepared = yield* Effect.result(keyedRequest(item.request, item.key, action.continueFrom));
|
|
1385
|
-
if (Result.isFailure(prepared)) {
|
|
1386
|
-
yield* emit(item, {
|
|
1387
|
-
_tag: "Failed",
|
|
1388
|
-
reason: { _tag: "Command", cause: prepared.failure },
|
|
1389
|
-
});
|
|
1390
|
-
break;
|
|
1391
|
-
}
|
|
1392
|
-
yield* sendCommand({
|
|
1393
|
-
_tag: "Build",
|
|
1394
|
-
key: item.key,
|
|
1395
|
-
request: prepared.success,
|
|
1396
|
-
sessionId: Option.getOrUndefined(state.preferredSessionId),
|
|
1397
|
-
});
|
|
1398
|
-
break;
|
|
1399
|
-
}
|
|
1400
|
-
case "BuildFiller": {
|
|
1401
|
-
if (unknownFillers.size >= maxUnknownFillers) {
|
|
1402
|
-
yield* closeActor(ReactorError.fromCode("Overflow", "Scheduler uncertain filler ledger is full", {
|
|
1403
|
-
operation: "scheduler.unknownRecovery",
|
|
1404
|
-
}));
|
|
1405
|
-
break;
|
|
1406
|
-
}
|
|
1407
|
-
const captured = upcomingFiller === undefined
|
|
1408
|
-
? yield* Effect.result(captureFiller(fillerIndex, runwaySeconds(state, monotonicMillis(clock), playingStartedMs, owned, [
|
|
1409
|
-
...items.values(),
|
|
1410
|
-
]), action.durationSeconds, action.targetSeconds))
|
|
1411
|
-
: Result.succeed(upcomingFiller);
|
|
1412
|
-
if (Result.isFailure(captured)) {
|
|
1413
|
-
yield* closeActor(captured.failure);
|
|
1414
|
-
break;
|
|
1415
|
-
}
|
|
1416
|
-
upcomingFiller = captured.success;
|
|
1417
|
-
const prepared = yield* Effect.result(keyedRequest(captured.success, fillerKey(fillerIndex)));
|
|
1418
|
-
if (Result.isFailure(prepared)) {
|
|
1419
|
-
yield* closeActor(prepared.failure);
|
|
1420
|
-
break;
|
|
1421
|
-
}
|
|
1422
|
-
yield* sendCommand({
|
|
1423
|
-
_tag: "BuildFiller",
|
|
1424
|
-
index: fillerIndex,
|
|
1425
|
-
request: prepared.success,
|
|
1426
|
-
sessionId: Option.getOrUndefined(state.preferredSessionId),
|
|
1427
|
-
});
|
|
1428
|
-
break;
|
|
1429
|
-
}
|
|
1430
|
-
}
|
|
1431
|
-
});
|
|
1432
|
-
const resolveRetiredUnknown = (item) => Effect.gen(function* () {
|
|
1433
|
-
if (item.phase !== "Unknown" || item.clipId !== undefined)
|
|
1434
|
-
return;
|
|
1435
|
-
item.unknownSessionId = undefined;
|
|
1436
|
-
// A retired source cannot play this key again, but it may have played
|
|
1437
|
-
// during a missed observation. Preserve Unknown instead of replaying it.
|
|
1438
|
-
yield* emit(item, { _tag: "Unknown", terminal: true });
|
|
1439
|
-
item.phase = "Terminal";
|
|
1440
|
-
const pending = pendingWithdrawals.get(item.key);
|
|
1441
|
-
if (pending !== undefined)
|
|
1442
|
-
yield* finishWithdrawal(item.key, Result.fail(item.unknownCause ??
|
|
1443
|
-
PolicyFailure.refuse("SessionRecovering", "The original source retired without an enqueue result")));
|
|
1444
|
-
});
|
|
1445
|
-
const handleCommandDone = (command, result) => Effect.gen(function* () {
|
|
1446
|
-
commandCount--;
|
|
1447
|
-
if (ended !== undefined) {
|
|
1448
|
-
if (command._tag === "Build")
|
|
1449
|
-
pendingBuilds.delete(command.key);
|
|
1450
|
-
if (command._tag === "DeferAt")
|
|
1451
|
-
pendingAtDeferrals.delete(command.key);
|
|
1452
|
-
if (command._tag === "RemoveItem")
|
|
1453
|
-
pendingItemRemovals.delete(command.key);
|
|
1454
|
-
if (command._tag === "RemoveFiller")
|
|
1455
|
-
pendingFillerRemovals.delete(command.clipId);
|
|
1456
|
-
return;
|
|
1457
|
-
}
|
|
1458
|
-
switch (command._tag) {
|
|
1459
|
-
case "Build": {
|
|
1460
|
-
const item = items.get(command.key);
|
|
1461
|
-
pendingBuilds.delete(command.key);
|
|
1462
|
-
if (item === undefined)
|
|
1463
|
-
break;
|
|
1464
|
-
if (Result.isSuccess(result)) {
|
|
1465
|
-
const clipId = result.success;
|
|
1466
|
-
if (item.clipId !== undefined && item.clipId !== clipId) {
|
|
1467
|
-
yield* sendCommand({ _tag: "RemoveDuplicate", clipId });
|
|
1468
|
-
break;
|
|
1469
|
-
}
|
|
1470
|
-
item.clipId = clipId;
|
|
1471
|
-
item.acknowledgedAtSnapshotSerial = snapshotAcquiredSerial;
|
|
1472
|
-
owned.set(clipId, { _tag: "Item", key: item.key });
|
|
1473
|
-
item.sessionId =
|
|
1474
|
-
knownRecord(yield* engine.state, clipId)?.sessionId ??
|
|
1475
|
-
(engine.enqueueOnSource === undefined ? undefined : command.sessionId);
|
|
1476
|
-
provedUnknown(item.unknownAtMs);
|
|
1477
|
-
item.unknownAtMs = undefined;
|
|
1478
|
-
item.unknownSessionId = undefined;
|
|
1479
|
-
item.unknownCause = undefined;
|
|
1480
|
-
yield* replayEarlyClipEvents(clipId);
|
|
1481
|
-
// Evidence that overtook the reply may already have carried the clip away
|
|
1482
|
-
// with a lost session; the item then waits to be rebuilt, not Building.
|
|
1483
|
-
const carried = item.clipId === undefined;
|
|
1484
|
-
const pending = pendingWithdrawals.get(item.key);
|
|
1485
|
-
if (carried) {
|
|
1486
|
-
// The carry settled any withdrawal that was waiting.
|
|
1487
|
-
}
|
|
1488
|
-
else if (pending !== undefined) {
|
|
1489
|
-
if (item.phase === "Started" || item.phase === "Terminal")
|
|
1490
|
-
yield* finishWithdrawal(item.key, Result.succeed(settledOutcome(item)));
|
|
1491
|
-
else
|
|
1492
|
-
yield* sendItemRemoval(item, pending.reason);
|
|
1493
|
-
}
|
|
1494
|
-
else if (item.phase === "Accepted" || item.phase === "Unknown")
|
|
1495
|
-
yield* emit(item, { _tag: "Building" });
|
|
1496
|
-
yield* observed(yield* engine.state);
|
|
1497
|
-
}
|
|
1498
|
-
else if (result.failure.context.outcome === "unknown") {
|
|
1499
|
-
// Its time to Ready spans the uncertainty, so it is never measured.
|
|
1500
|
-
delete item.dispatchedAtMs;
|
|
1501
|
-
if (item.clipId === undefined) {
|
|
1502
|
-
// A generic engine may re-route between the state read and dispatch.
|
|
1503
|
-
// Only an owner-fenced enqueue can attribute its unknown result.
|
|
1504
|
-
item.unknownSessionId =
|
|
1505
|
-
engine.enqueueOnSource === undefined ? undefined : command.sessionId;
|
|
1506
|
-
item.unknownCause = result.failure;
|
|
1507
|
-
yield* emit(item, { _tag: "Unknown" });
|
|
1508
|
-
}
|
|
1509
|
-
}
|
|
1510
|
-
else {
|
|
1511
|
-
const pending = pendingWithdrawals.get(item.key);
|
|
1512
|
-
if (pending !== undefined) {
|
|
1513
|
-
yield* emit(item, { _tag: "Dropped", reason: pending.reason });
|
|
1514
|
-
yield* finishWithdrawal(item.key, Result.succeed("withdrawn"));
|
|
1515
|
-
}
|
|
1516
|
-
else if (result.failure.context.outcome !== "not-submitted" ||
|
|
1517
|
-
!["QueueFull", "SessionRecovering", "RouteChanged"].includes(result.failure.reason._tag))
|
|
1518
|
-
yield* emit(item, {
|
|
1519
|
-
_tag: "Failed",
|
|
1520
|
-
reason: { _tag: "Command", cause: result.failure },
|
|
1521
|
-
});
|
|
1522
|
-
else
|
|
1523
|
-
item.retryAtMs = monotonicMillis(clock) + 1_000;
|
|
1524
|
-
delete item.dispatchedAtMs;
|
|
1525
|
-
}
|
|
1526
|
-
break;
|
|
1527
|
-
}
|
|
1528
|
-
case "BuildFiller": {
|
|
1529
|
-
if (Result.isSuccess(result)) {
|
|
1530
|
-
const clipId = result.success;
|
|
1531
|
-
const actualSessionId = knownRecord(yield* engine.state, clipId)?.sessionId ??
|
|
1532
|
-
(engine.enqueueOnSource === undefined ? undefined : command.sessionId);
|
|
1533
|
-
owned.set(clipId, {
|
|
1534
|
-
_tag: "Filler",
|
|
1535
|
-
index: command.index,
|
|
1536
|
-
sessionId: actualSessionId,
|
|
1537
|
-
});
|
|
1538
|
-
filler.set(command.index, {
|
|
1539
|
-
index: command.index,
|
|
1540
|
-
clipId,
|
|
1541
|
-
sessionId: actualSessionId,
|
|
1542
|
-
acknowledgedAtSnapshotSerial: snapshotAcquiredSerial,
|
|
1543
|
-
});
|
|
1544
|
-
fillerIndex = Math.max(fillerIndex, command.index + 1);
|
|
1545
|
-
upcomingFiller = undefined;
|
|
1546
|
-
yield* replayEarlyClipEvents(clipId);
|
|
1547
|
-
}
|
|
1548
|
-
else if (result.failure.context.outcome === "unknown") {
|
|
1549
|
-
// The worker already retained this identity and its original age.
|
|
1550
|
-
}
|
|
1551
|
-
else {
|
|
1552
|
-
fillerDispatch.delete(command.index);
|
|
1553
|
-
fillerRetryAtMs = monotonicMillis(clock) + 1_000;
|
|
1554
|
-
if (result.failure.context.outcome === "replied") {
|
|
1555
|
-
fillerIndex = Math.max(fillerIndex, command.index + 1);
|
|
1556
|
-
upcomingFiller = undefined;
|
|
1557
|
-
}
|
|
1558
|
-
}
|
|
1559
|
-
yield* observed(yield* engine.state);
|
|
1560
|
-
break;
|
|
1561
|
-
}
|
|
1562
|
-
case "RemoveItem": {
|
|
1563
|
-
pendingItemRemovals.delete(command.key);
|
|
1564
|
-
const item = items.get(command.key);
|
|
1565
|
-
if (Result.isSuccess(result)) {
|
|
1566
|
-
removalRetryAtMs.delete(command.clipId);
|
|
1567
|
-
removalUnknownAtSerial.delete(command.clipId);
|
|
1568
|
-
removedIds.add(command.clipId);
|
|
1569
|
-
owned.delete(command.clipId);
|
|
1570
|
-
playingStartedMs.delete(command.clipId);
|
|
1571
|
-
if (item?.phase === "Started")
|
|
1572
|
-
yield* finishWithdrawal(command.key, Result.succeed("already-started"));
|
|
1573
|
-
else {
|
|
1574
|
-
if (item !== undefined && item.phase !== "Terminal")
|
|
1575
|
-
yield* emit(item, { _tag: "Dropped", reason: command.reason });
|
|
1576
|
-
yield* finishWithdrawal(command.key, Result.succeed("withdrawn"));
|
|
1577
|
-
}
|
|
1578
|
-
}
|
|
1579
|
-
else {
|
|
1580
|
-
removalRetryAtMs.set(command.clipId, monotonicMillis(clock) + 1_000);
|
|
1581
|
-
if (result.failure.context.outcome === "unknown")
|
|
1582
|
-
removalUnknownAtSerial.set(command.clipId, snapshotAcquiredSerial);
|
|
1583
|
-
if (item?.phase === "Started")
|
|
1584
|
-
yield* finishWithdrawal(command.key, Result.succeed("already-started"));
|
|
1585
|
-
// A refusal is not an outcome: the withdrawal stays pending and is retried.
|
|
1586
|
-
// An unknown removal may already have taken effect, so it ends the wait.
|
|
1587
|
-
else if (result.failure.context.outcome === "unknown")
|
|
1588
|
-
yield* finishWithdrawal(command.key, Result.fail(result.failure));
|
|
1589
|
-
}
|
|
1590
|
-
break;
|
|
1591
|
-
}
|
|
1592
|
-
case "RemoveFiller": {
|
|
1593
|
-
pendingFillerRemovals.delete(command.clipId);
|
|
1594
|
-
if (Result.isSuccess(result)) {
|
|
1595
|
-
removalRetryAtMs.delete(command.clipId);
|
|
1596
|
-
removalUnknownAtSerial.delete(command.clipId);
|
|
1597
|
-
removedIds.add(command.clipId);
|
|
1598
|
-
const owner = owned.get(command.clipId);
|
|
1599
|
-
if (owner?._tag === "Filler" && filler.get(owner.index)?.clipId === command.clipId)
|
|
1600
|
-
filler.delete(owner.index);
|
|
1601
|
-
owned.delete(command.clipId);
|
|
1602
|
-
playingStartedMs.delete(command.clipId);
|
|
1603
|
-
}
|
|
1604
|
-
else {
|
|
1605
|
-
removalRetryAtMs.set(command.clipId, monotonicMillis(clock) + 1_000);
|
|
1606
|
-
if (result.failure.context.outcome === "unknown")
|
|
1607
|
-
removalUnknownAtSerial.set(command.clipId, snapshotAcquiredSerial);
|
|
1608
|
-
}
|
|
1609
|
-
break;
|
|
1610
|
-
}
|
|
1611
|
-
case "RemoveDuplicate":
|
|
1612
|
-
if (Result.isFailure(result))
|
|
1613
|
-
yield* closeActor(result.failure);
|
|
1614
|
-
break;
|
|
1615
|
-
case "DeferAt": {
|
|
1616
|
-
pendingAtDeferrals.delete(command.key);
|
|
1617
|
-
const item = items.get(command.key);
|
|
1618
|
-
if (Result.isFailure(result)) {
|
|
1619
|
-
removalRetryAtMs.set(command.clipId, monotonicMillis(clock) + 1_000);
|
|
1620
|
-
if (result.failure.context.outcome === "unknown")
|
|
1621
|
-
removalUnknownAtSerial.set(command.clipId, snapshotAcquiredSerial);
|
|
1622
|
-
break;
|
|
1623
|
-
}
|
|
1624
|
-
removalRetryAtMs.delete(command.clipId);
|
|
1625
|
-
removalUnknownAtSerial.delete(command.clipId);
|
|
1626
|
-
owned.delete(command.clipId);
|
|
1627
|
-
removedIds.add(command.clipId);
|
|
1628
|
-
playingStartedMs.delete(command.clipId);
|
|
1629
|
-
if (item === undefined || item.phase === "Started" || item.phase === "Terminal")
|
|
1630
|
-
break;
|
|
1631
|
-
delete item.clipId;
|
|
1632
|
-
item.sessionId = undefined;
|
|
1633
|
-
const pending = pendingWithdrawals.get(command.key);
|
|
1634
|
-
if (pending !== undefined) {
|
|
1635
|
-
yield* emit(item, { _tag: "Dropped", reason: pending.reason });
|
|
1636
|
-
yield* finishWithdrawal(command.key, Result.succeed("withdrawn"));
|
|
1637
|
-
}
|
|
1638
|
-
else
|
|
1639
|
-
yield* emit(item, { _tag: "Accepted" });
|
|
1640
|
-
break;
|
|
1641
|
-
}
|
|
1642
|
-
case "Order":
|
|
1643
|
-
blockedMove = Result.isFailure(result) ? command.signature : undefined;
|
|
1644
|
-
break;
|
|
1645
|
-
case "Cut":
|
|
1646
|
-
if (Result.isFailure(result))
|
|
1647
|
-
blockedCut = command.clipId;
|
|
1648
|
-
break;
|
|
1649
|
-
case "PauseAutoplay":
|
|
1650
|
-
if (Result.isFailure(result)) {
|
|
1651
|
-
drainFailure = result.failure;
|
|
1652
|
-
const replies = drainReplies.splice(0);
|
|
1653
|
-
for (const reply of replies)
|
|
1654
|
-
yield* Deferred.fail(reply, result.failure);
|
|
1655
|
-
}
|
|
1656
|
-
break;
|
|
1657
|
-
}
|
|
1658
|
-
});
|
|
1659
|
-
const pendingBatches = () => [...batches.values()].map((batch) => ({
|
|
1660
|
-
id: batch.id,
|
|
1661
|
-
waitFor: batch.waitFor,
|
|
1662
|
-
targets: [...batch.targets].map(([key, target]) => ({ key, reason: target.reason })),
|
|
1663
|
-
}));
|
|
1664
|
-
/** The reply a batch keeps for its withdrawal of `key`, taken once. */
|
|
1665
|
-
const takeBatchReply = (key) => {
|
|
1666
|
-
for (const batch of batches.values()) {
|
|
1667
|
-
const target = batch.targets.get(key);
|
|
1668
|
-
if (target === undefined)
|
|
1669
|
-
continue;
|
|
1670
|
-
batch.targets.delete(key);
|
|
1671
|
-
return target.reply;
|
|
1672
|
-
}
|
|
1673
|
-
return undefined;
|
|
1674
|
-
};
|
|
1675
|
-
/** A batch takes effect: what it withdraws goes at once, and its replacements take over. */
|
|
1676
|
-
const commitBatch = (id) => Effect.gen(function* () {
|
|
1677
|
-
const batch = batches.get(id);
|
|
1678
|
-
if (batch === undefined)
|
|
1679
|
-
return;
|
|
1680
|
-
batches.delete(id);
|
|
1681
|
-
for (const [key, target] of batch.targets) {
|
|
1682
|
-
const item = items.get(key);
|
|
1683
|
-
if (item !== undefined)
|
|
1684
|
-
yield* requestWithdrawal(item, target.reason, target.reply);
|
|
1685
|
-
else if (target.reply !== undefined)
|
|
1686
|
-
yield* Deferred.succeed(target.reply, "not-found");
|
|
1687
|
-
}
|
|
1688
|
-
yield* Deferred.succeed(batch.committed, undefined);
|
|
1689
|
-
});
|
|
1690
|
-
const reconcile = Effect.gen(function* () {
|
|
1691
|
-
const state = yield* engine.state;
|
|
1692
|
-
if (ended !== undefined)
|
|
1693
|
-
return;
|
|
1694
|
-
prune(state);
|
|
1695
|
-
refreshAnchors();
|
|
1696
|
-
const sources = projectSources(state);
|
|
1697
|
-
if (!observationReady) {
|
|
1698
|
-
yield* publishState(state, sources.sessions);
|
|
1699
|
-
return;
|
|
1700
|
-
}
|
|
1701
|
-
const nowMs = monotonicMillis(clock);
|
|
1702
|
-
for (const item of items.values())
|
|
1703
|
-
if (item.phase === "Started")
|
|
1704
|
-
fireCues(item, nowMs);
|
|
1705
|
-
for (const uncertain of unknownFillers.values()) {
|
|
1706
|
-
const sourceRetired = uncertain.sessionId !== undefined && !sources.has(uncertain.sessionId);
|
|
1707
|
-
if (sourceRetired)
|
|
1708
|
-
unknownFillers.delete(uncertain.index);
|
|
1709
|
-
if (sourceRetired ||
|
|
1710
|
-
(uncertain.sessionId !== undefined &&
|
|
1711
|
-
Option.getOrUndefined(state.preferredSessionId) !== uncertain.sessionId)) {
|
|
1712
|
-
if (fillerIndex <= uncertain.index) {
|
|
1713
|
-
fillerIndex = uncertain.index + 1;
|
|
1714
|
-
upcomingFiller = undefined;
|
|
1715
|
-
}
|
|
1716
|
-
}
|
|
1717
|
-
}
|
|
1718
|
-
for (const [key, pending] of pendingWithdrawals) {
|
|
1719
|
-
const item = items.get(key);
|
|
1720
|
-
if (item === undefined)
|
|
1721
|
-
continue;
|
|
1722
|
-
if (item.phase === "Started")
|
|
1723
|
-
yield* finishWithdrawal(key, Result.succeed("already-started"));
|
|
1724
|
-
// A withdrawal still being retried when its item settles another way. Terminal
|
|
1725
|
-
// Unknown keeps its uncertain result from the retirement or terminal settlement.
|
|
1726
|
-
else if (item.phase === "Terminal" && item.status._tag !== "Unknown")
|
|
1727
|
-
yield* finishWithdrawal(key, Result.succeed(settledOutcome(item)));
|
|
1728
|
-
else if (item.clipId !== undefined)
|
|
1729
|
-
yield* sendItemRemoval(item, pending.reason);
|
|
1730
|
-
}
|
|
1731
|
-
for (const item of items.values())
|
|
1732
|
-
if (item.phase === "Unknown" &&
|
|
1733
|
-
item.clipId === undefined &&
|
|
1734
|
-
item.unknownSessionId !== undefined &&
|
|
1735
|
-
!sources.has(item.unknownSessionId))
|
|
1736
|
-
yield* resolveRetiredUnknown(item);
|
|
1737
|
-
// After retirement, so a deadline no longer outlives the uncertainty that
|
|
1738
|
-
// set it until the next message.
|
|
1739
|
-
updateRecovery(state);
|
|
1740
|
-
const preferred = Option.getOrUndefined(state.preferredSessionId);
|
|
1741
|
-
let unknownFillerCount = 0;
|
|
1742
|
-
for (const entry of unknownFillers.values())
|
|
1743
|
-
if (entry.sessionId === undefined || entry.sessionId === preferred)
|
|
1744
|
-
unknownFillerCount++;
|
|
1745
|
-
const brokenGroups = new Map();
|
|
1746
|
-
for (const [key, group] of groups)
|
|
1747
|
-
if (group.brokenAt !== undefined)
|
|
1748
|
-
brokenGroups.set(key, group.brokenAt);
|
|
1749
|
-
const decide = () => plan({
|
|
1750
|
-
engine: state,
|
|
1751
|
-
items: [...items.values()],
|
|
1752
|
-
owned,
|
|
1753
|
-
lanes,
|
|
1754
|
-
nowMs: monotonicMillis(clock),
|
|
1755
|
-
playingStartedMs,
|
|
1756
|
-
floorSeconds,
|
|
1757
|
-
targetSeconds,
|
|
1758
|
-
refillActive,
|
|
1759
|
-
maxBuildsInFlight,
|
|
1760
|
-
accepting,
|
|
1761
|
-
drain: draining ? (finishAccepted ? "accepted" : "playing") : undefined,
|
|
1762
|
-
fillerRetryAtMs,
|
|
1763
|
-
unknownFillerCount,
|
|
1764
|
-
blockedMove,
|
|
1765
|
-
withdrawing: new Set(pendingWithdrawals.keys()),
|
|
1766
|
-
dispatched: pendingBuilds,
|
|
1767
|
-
brokenGroups,
|
|
1768
|
-
batches: pendingBatches(),
|
|
1769
|
-
estimates: estimatesFrom(buildSamples, lengthSamples),
|
|
1770
|
-
...fillerView(),
|
|
1771
|
-
cutLanes,
|
|
1772
|
-
fillLengths,
|
|
1773
|
-
blockedCut,
|
|
1774
|
-
});
|
|
1775
|
-
let decision = decide();
|
|
1776
|
-
// A commit changes what is withdrawn and what takes over, so the plan is read again.
|
|
1777
|
-
if (decision.commit.length > 0) {
|
|
1778
|
-
for (const id of decision.commit)
|
|
1779
|
-
yield* commitBatch(id);
|
|
1780
|
-
decision = decide();
|
|
1781
|
-
}
|
|
1782
|
-
refillActive = decision.refillActive;
|
|
1783
|
-
for (const { key, reason } of decision.withdraw) {
|
|
1784
|
-
const item = items.get(key);
|
|
1785
|
-
if (item !== undefined)
|
|
1786
|
-
yield* requestWithdrawal(item, reason, takeBatchReply(key));
|
|
1787
|
-
}
|
|
1788
|
-
for (const clipId of decision.withdrawFiller)
|
|
1789
|
-
if (!pendingFillerRemovals.has(clipId) && canRetryRemoval(clipId)) {
|
|
1790
|
-
pendingFillerRemovals.add(clipId);
|
|
1791
|
-
yield* sendCommand({ _tag: "RemoveFiller", clipId });
|
|
1792
|
-
}
|
|
1793
|
-
yield* publishState(state, sources.sessions);
|
|
1794
|
-
if ((!draining || finishAccepted) && commandCount === 0 && decision.action !== undefined)
|
|
1795
|
-
yield* applyPolicy(decision.action, state);
|
|
1796
|
-
if (draining &&
|
|
1797
|
-
finishAccepted &&
|
|
1798
|
-
!autoplayPaused &&
|
|
1799
|
-
commandCount === 0 &&
|
|
1800
|
-
[...items.values()].every((item) => item.phase === "Terminal")) {
|
|
1801
|
-
autoplayPaused = true;
|
|
1802
|
-
yield* sendCommand({ _tag: "PauseAutoplay" });
|
|
1803
|
-
}
|
|
1804
|
-
if (drainFailure === undefined &&
|
|
1805
|
-
drainReplies.length > 0 &&
|
|
1806
|
-
commandCount === 0 &&
|
|
1807
|
-
[...items.values()].every((item) => item.phase === "Terminal") &&
|
|
1808
|
-
pendingWithdrawals.size === 0 &&
|
|
1809
|
-
unknownFillers.size === 0) {
|
|
1810
|
-
const after = yield* engine.state;
|
|
1811
|
-
if (Option.isNone(after.playing) && !activeIds(after).some((clipId) => owned.has(clipId))) {
|
|
1812
|
-
drained = true;
|
|
1813
|
-
// A truthful completed drain no longer needs a replacement to resume
|
|
1814
|
-
// service, even when retirement left a capacity episode behind.
|
|
1815
|
-
capacityRecovery = undefined;
|
|
1816
|
-
updateRecovery(after);
|
|
1817
|
-
const replies = drainReplies.splice(0);
|
|
1818
|
-
for (const reply of replies)
|
|
1819
|
-
yield* Deferred.succeed(reply, undefined);
|
|
1820
|
-
}
|
|
1821
|
-
}
|
|
1822
|
-
});
|
|
1823
|
-
/** Registers a captured item, without yielding; its caller publishes the Accepted evidence. */
|
|
1824
|
-
const accept = (captured, nowMs, startByMs, group, place) => {
|
|
1825
|
-
const startedWaiter = Deferred.makeUnsafe();
|
|
1826
|
-
const outcomeWaiter = Deferred.makeUnsafe();
|
|
1827
|
-
const firstDecisiveWaiter = Deferred.makeUnsafe();
|
|
1828
|
-
const item = {
|
|
1829
|
-
key: captured.key,
|
|
1830
|
-
lane: captured.lane,
|
|
1831
|
-
request: captured.request,
|
|
1832
|
-
fingerprint: captured.fingerprint,
|
|
1833
|
-
admission: place === undefined ? ++admission : place.admission,
|
|
1834
|
-
seconds: captured.request.durationSeconds,
|
|
1835
|
-
phase: "Accepted",
|
|
1836
|
-
status: { _tag: "Accepted" },
|
|
1837
|
-
...(place === undefined
|
|
1838
|
-
? captured.notBeforeOffsetMs === undefined
|
|
1839
|
-
? {}
|
|
1840
|
-
: { notBeforeMs: nowMs + captured.notBeforeOffsetMs }
|
|
1841
|
-
: place.notBeforeMs === undefined
|
|
1842
|
-
? {}
|
|
1843
|
-
: { notBeforeMs: place.notBeforeMs }),
|
|
1844
|
-
...(place?.replaces === undefined
|
|
1845
|
-
? {}
|
|
1846
|
-
: { replaces: place.replaces, generation: place.generation ?? 1 }),
|
|
1847
|
-
...(place?.inserted === true ? { inserted: true } : {}),
|
|
1848
|
-
...(startByMs === undefined ? {} : { startByMs }),
|
|
1849
|
-
firm: captured.firm,
|
|
1850
|
-
...(captured.atWallMs === undefined
|
|
1851
|
-
? {}
|
|
1852
|
-
: {
|
|
1853
|
-
atWallMs: captured.atWallMs,
|
|
1854
|
-
atMs: nowMs + captured.atWallMs - clock.currentTimeMillisUnsafe(),
|
|
1855
|
-
}),
|
|
1856
|
-
late: captured.late,
|
|
1857
|
-
cues: captured.cues ?? [],
|
|
1858
|
-
firedCues: new Set(),
|
|
1859
|
-
...(captured.continuity === undefined ? {} : { continuity: captured.continuity }),
|
|
1860
|
-
...(captured.mode === "asap" ? { asap: true } : {}),
|
|
1861
|
-
...(captured.mode === "manual" ? { held: true } : {}),
|
|
1862
|
-
...(group === undefined ? {} : { group }),
|
|
1863
|
-
sessionId: undefined,
|
|
1864
|
-
unknownSessionId: undefined,
|
|
1865
|
-
unknownCause: undefined,
|
|
1866
|
-
unknownAtMs: undefined,
|
|
1867
|
-
handle: {
|
|
1868
|
-
key: captured.key,
|
|
1869
|
-
started: Deferred.await(startedWaiter),
|
|
1870
|
-
outcome: Deferred.await(outcomeWaiter),
|
|
1871
|
-
firstDecisive: Deferred.await(firstDecisiveWaiter),
|
|
1872
|
-
},
|
|
1873
|
-
startedWaiter,
|
|
1874
|
-
outcomeWaiter,
|
|
1875
|
-
firstDecisiveWaiter,
|
|
1876
|
-
};
|
|
1877
|
-
items.set(item.key, item);
|
|
1878
|
-
return item;
|
|
1879
|
-
};
|
|
1880
|
-
/**
|
|
1881
|
-
* One reply for withdrawals across a group's parts. A group key reports
|
|
1882
|
-
* `withdrawn` if any part was; a part key reports that part's own outcome,
|
|
1883
|
-
* while the parts after it are withdrawn too.
|
|
1884
|
-
*/
|
|
1885
|
-
const settleWithdrawals = (replies, reply, wholeGroup) => Effect.gen(function* () {
|
|
1886
|
-
const outcomes = [];
|
|
1887
|
-
for (const pending of replies) {
|
|
1888
|
-
const result = yield* Effect.result(Deferred.await(pending));
|
|
1889
|
-
if (Result.isFailure(result)) {
|
|
1890
|
-
yield* Deferred.fail(reply, result.failure);
|
|
1891
|
-
return;
|
|
1892
|
-
}
|
|
1893
|
-
outcomes.push(result.success);
|
|
1894
|
-
}
|
|
1895
|
-
yield* Deferred.succeed(reply, wholeGroup && outcomes.includes("withdrawn") ? "withdrawn" : (outcomes[0] ?? "not-found"));
|
|
1896
|
-
});
|
|
1897
|
-
/**
|
|
1898
|
-
* The place an insert takes beside its anchor, or why it cannot: the anchor's lane, an
|
|
1899
|
-
* admission between the anchor and its neighbour in that lane, and a group place when
|
|
1900
|
-
* the insert falls between two parts of a group.
|
|
1901
|
-
*/
|
|
1902
|
-
const placeInsert = (insert) => {
|
|
1903
|
-
const live = (entry) => entry !== undefined && entry.phase !== "Terminal" && entry.replacedBy === undefined;
|
|
1904
|
-
const named = groups.get(insert.anchor);
|
|
1905
|
-
let anchor;
|
|
1906
|
-
if (named === undefined)
|
|
1907
|
-
anchor = items.get(insert.anchor);
|
|
1908
|
-
else {
|
|
1909
|
-
const parts = named.parts
|
|
1910
|
-
.map((key) => items.get(key))
|
|
1911
|
-
.filter(live)
|
|
1912
|
-
.sort((a, b) => a.group.index - b.group.index);
|
|
1913
|
-
if (insert.side === "before" &&
|
|
1914
|
-
(parts.some((part) => part.phase === "Started") || parts[0]?.group?.index !== 0))
|
|
1915
|
-
return "The anchor group already started";
|
|
1916
|
-
anchor = insert.side === "before" ? parts[0] : parts.at(-1);
|
|
1917
|
-
}
|
|
1918
|
-
if (!live(anchor))
|
|
1919
|
-
return "Nothing queued under the anchor key";
|
|
1920
|
-
if (anchor.phase === "Unknown")
|
|
1921
|
-
return "The anchor's admission is uncertain";
|
|
1922
|
-
if (anchor.held === true)
|
|
1923
|
-
return "The anchor is held until it is released";
|
|
1924
|
-
if (insert.side === "before" && anchor.phase === "Started")
|
|
1925
|
-
return "The anchor already started";
|
|
1926
|
-
const neighbours = [...items.values()]
|
|
1927
|
-
.filter((entry) => entry.lane === anchor.lane && entry.phase !== "Terminal")
|
|
1928
|
-
.map((entry) => entry.admission);
|
|
1929
|
-
const bound = insert.side === "before"
|
|
1930
|
-
? Math.max(...neighbours.filter((value) => value < anchor.admission))
|
|
1931
|
-
: Math.min(...neighbours.filter((value) => value > anchor.admission));
|
|
1932
|
-
const admission = Number.isFinite(bound)
|
|
1933
|
-
? (anchor.admission + bound) / 2
|
|
1934
|
-
: anchor.admission + (insert.side === "before" ? -0.5 : 0.5);
|
|
1935
|
-
if (admission === anchor.admission || admission === bound)
|
|
1936
|
-
return "No room is left to insert at that place";
|
|
1937
|
-
let group;
|
|
1938
|
-
if (anchor.group !== undefined) {
|
|
1939
|
-
const indexes = (groups.get(anchor.group.key)?.parts ?? [])
|
|
1940
|
-
.map((key) => items.get(key)?.group?.index)
|
|
1941
|
-
.filter((value) => value !== undefined);
|
|
1942
|
-
const index = anchor.group.index;
|
|
1943
|
-
const neighbour = insert.side === "before"
|
|
1944
|
-
? Math.max(...indexes.filter((value) => value < index))
|
|
1945
|
-
: Math.min(...indexes.filter((value) => value > index));
|
|
1946
|
-
// Before the first part or after the last one, the insert stays outside the group.
|
|
1947
|
-
// Once parts have aired and settled, the place before the first remaining part is
|
|
1948
|
-
// still inside the group.
|
|
1949
|
-
const between = insert.side === "before"
|
|
1950
|
-
? index > 0
|
|
1951
|
-
? (Number.isFinite(neighbour) ? neighbour : index - 1) + index
|
|
1952
|
-
: undefined
|
|
1953
|
-
: Number.isFinite(neighbour)
|
|
1954
|
-
? index + neighbour
|
|
1955
|
-
: undefined;
|
|
1956
|
-
if (between !== undefined) {
|
|
1957
|
-
if (between / 2 === index || between / 2 === neighbour)
|
|
1958
|
-
return "No room is left to insert at that place";
|
|
1959
|
-
group = { key: anchor.group.key, index: between / 2 };
|
|
1960
|
-
}
|
|
1961
|
-
}
|
|
1962
|
-
const timing = {
|
|
1963
|
-
...(anchor.atWallMs === undefined ? {} : { atWallMs: anchor.atWallMs }),
|
|
1964
|
-
late: anchor.late,
|
|
1965
|
-
asap: anchor.asap === true,
|
|
1966
|
-
};
|
|
1967
|
-
return group === undefined
|
|
1968
|
-
? { lane: anchor.lane, admission, ...timing }
|
|
1969
|
-
: { lane: anchor.lane, admission, group, ...timing };
|
|
1970
|
-
};
|
|
1971
|
-
/** The items a withdrawal of `key` names: a group's parts, a part and those after it, or one item. */
|
|
1972
|
-
const withdrawalTargets = (key) => {
|
|
1973
|
-
const item = items.get(key);
|
|
1974
|
-
const named = groups.get(key);
|
|
1975
|
-
const group = named ?? (item?.group === undefined ? undefined : groups.get(item.group.key));
|
|
1976
|
-
if (group === undefined || (named === undefined && item?.inserted === true))
|
|
1977
|
-
return item === undefined ? [] : [item];
|
|
1978
|
-
const from = named === undefined ? item.group.index : 0;
|
|
1979
|
-
const targets = group.parts
|
|
1980
|
-
.map((part) => items.get(part))
|
|
1981
|
-
.filter((part) => part?.group !== undefined && part.group.index >= from);
|
|
1982
|
-
// A part key's reply is that part's own outcome, so it goes first.
|
|
1983
|
-
if (named === undefined)
|
|
1984
|
-
targets.sort((a, b) => Number(b === item) - Number(a === item));
|
|
1985
|
-
return targets;
|
|
1986
|
-
};
|
|
1987
|
-
/**
|
|
1988
|
-
* Checks and admits a call's edits in order, without yielding. A refused edit undoes the
|
|
1989
|
-
* edits before it, so nothing of a refused batch is admitted or published.
|
|
1990
|
-
*/
|
|
1991
|
-
const stageEdits = (edits, batched, state, nowMs) => {
|
|
1992
|
-
const undo = [];
|
|
1993
|
-
const added = [];
|
|
1994
|
-
const waitFor = [];
|
|
1995
|
-
const replaced = [];
|
|
1996
|
-
const results = [];
|
|
1997
|
-
const refuse = (error) => {
|
|
1998
|
-
for (const step of undo.reverse())
|
|
1999
|
-
step();
|
|
2000
|
-
return Result.fail(error);
|
|
2001
|
-
};
|
|
2002
|
-
// Clips a batch withdraws or replaces do not count ahead of the clips it adds.
|
|
2003
|
-
const excluding = new Set();
|
|
2004
|
-
const named = [];
|
|
2005
|
-
for (const edit of edits)
|
|
2006
|
-
switch (edit._tag) {
|
|
2007
|
-
case "Submit":
|
|
2008
|
-
named.push(edit.item.key);
|
|
2009
|
-
break;
|
|
2010
|
-
case "SubmitGroup":
|
|
2011
|
-
named.push(edit.group.key, ...edit.group.parts.map((part) => part.key));
|
|
2012
|
-
break;
|
|
2013
|
-
case "Insert":
|
|
2014
|
-
named.push(edit.insert.key);
|
|
2015
|
-
break;
|
|
2016
|
-
case "Replace":
|
|
2017
|
-
named.push(edit.key, edit.next.key);
|
|
2018
|
-
excluding.add(edit.key);
|
|
2019
|
-
break;
|
|
2020
|
-
case "Withdraw": {
|
|
2021
|
-
const targets = withdrawalTargets(edit.key);
|
|
2022
|
-
named.push(edit.key, ...targets.map((target) => target.key).filter((key) => key !== edit.key));
|
|
2023
|
-
for (const target of targets)
|
|
2024
|
-
excluding.add(target.key);
|
|
2025
|
-
break;
|
|
2026
|
-
}
|
|
2027
|
-
}
|
|
2028
|
-
if (new Set(named).size !== named.length)
|
|
2029
|
-
return refuse(invalid("An edit batch names each key once"));
|
|
2030
|
-
for (const edit of edits)
|
|
2031
|
-
if (edit._tag === "Insert" && excluding.has(edit.insert.anchor))
|
|
2032
|
-
return refuse(invalid("An insert's anchor is withdrawn or replaced in the same batch"));
|
|
2033
|
-
const view = (place) => projectedStartMs({
|
|
2034
|
-
engine: state,
|
|
2035
|
-
items: [...items.values()],
|
|
2036
|
-
owned,
|
|
2037
|
-
lanes,
|
|
2038
|
-
playingStartedMs,
|
|
2039
|
-
nowMs,
|
|
2040
|
-
batches: pendingBatches(),
|
|
2041
|
-
estimates: estimatesFrom(buildSamples, lengthSamples),
|
|
2042
|
-
...fillerView(),
|
|
2043
|
-
withdrawing: new Set([...pendingWithdrawals.keys(), ...excluding]),
|
|
2044
|
-
}, place);
|
|
2045
|
-
/**
|
|
2046
|
-
* A new submission's lane policy: a skip lane refuses it while the lane has an item
|
|
2047
|
-
* waiting or playing; a replace lane supersedes the items still waiting there.
|
|
2048
|
-
*/
|
|
2049
|
-
const conflict = (key, lane) => {
|
|
2050
|
-
const others = [...items.values()].filter((entry) => entry.lane === lane &&
|
|
2051
|
-
entry.phase !== "Terminal" &&
|
|
2052
|
-
!excluding.has(entry.key) &&
|
|
2053
|
-
!added.includes(entry));
|
|
2054
|
-
switch (conflicts.get(lane)) {
|
|
2055
|
-
case "skip":
|
|
2056
|
-
return others.length > 0 ? LaneBusy.of(key, lane) : undefined;
|
|
2057
|
-
case "replace":
|
|
2058
|
-
for (const entry of others)
|
|
2059
|
-
if (entry.phase !== "Started" && !pendingWithdrawals.has(entry.key)) {
|
|
2060
|
-
replaced.push(entry.key);
|
|
2061
|
-
excluding.add(entry.key);
|
|
2062
|
-
}
|
|
2063
|
-
return undefined;
|
|
2064
|
-
default:
|
|
2065
|
-
return undefined;
|
|
2066
|
-
}
|
|
2067
|
-
};
|
|
2068
|
-
const admitted = (entry) => {
|
|
2069
|
-
added.push(entry);
|
|
2070
|
-
waitFor.push(entry.key);
|
|
2071
|
-
undo.push(() => items.delete(entry.key));
|
|
2072
|
-
};
|
|
2073
|
-
/** An existing key: the same spec returns its handle, and the batch waits for it too. */
|
|
2074
|
-
const repeated = (key, fingerprint) => {
|
|
2075
|
-
const existing = items.get(key) ?? history.get(key);
|
|
2076
|
-
if (existing === undefined && !groups.has(key))
|
|
2077
|
-
return Result.succeed(undefined);
|
|
2078
|
-
if (existing?.fingerprint !== fingerprint)
|
|
2079
|
-
return Result.fail(KeyMismatch.of(key));
|
|
2080
|
-
waitFor.push(key);
|
|
2081
|
-
return Result.succeed(existing.handle);
|
|
2082
|
-
};
|
|
2083
|
-
for (const edit of edits) {
|
|
2084
|
-
switch (edit._tag) {
|
|
2085
|
-
case "Submit": {
|
|
2086
|
-
const { item } = edit;
|
|
2087
|
-
const known = repeated(item.key, item.fingerprint);
|
|
2088
|
-
if (Result.isFailure(known))
|
|
2089
|
-
return refuse(known.failure);
|
|
2090
|
-
if (known.success !== undefined) {
|
|
2091
|
-
results.push({ _tag: "Added", handle: known.success });
|
|
2092
|
-
break;
|
|
2093
|
-
}
|
|
2094
|
-
const busy = conflict(item.key, item.lane);
|
|
2095
|
-
if (busy !== undefined)
|
|
2096
|
-
return refuse(busy);
|
|
2097
|
-
const startByMs = item.startByOffsetMs === undefined ? undefined : nowMs + item.startByOffsetMs;
|
|
2098
|
-
if (startByMs !== undefined &&
|
|
2099
|
-
view({
|
|
2100
|
-
lane: item.lane,
|
|
2101
|
-
admission: Infinity,
|
|
2102
|
-
seconds: item.request.durationSeconds,
|
|
2103
|
-
startByMs,
|
|
2104
|
-
...(item.mode === "asap" ? { asap: true } : {}),
|
|
2105
|
-
...(item.mode === "manual" ? { held: true } : {}),
|
|
2106
|
-
}) > startByMs)
|
|
2107
|
-
return refuse(WouldMissDeadline.of(item.key));
|
|
2108
|
-
const entry = accept(item, nowMs, startByMs, undefined);
|
|
2109
|
-
admitted(entry);
|
|
2110
|
-
results.push({ _tag: "Added", handle: entry.handle });
|
|
2111
|
-
break;
|
|
2112
|
-
}
|
|
2113
|
-
case "SubmitGroup": {
|
|
2114
|
-
const { group } = edit;
|
|
2115
|
-
const known = groups.get(group.key);
|
|
2116
|
-
if (known !== undefined) {
|
|
2117
|
-
if (known.fingerprint !== group.fingerprint)
|
|
2118
|
-
return refuse(KeyMismatch.of(group.key));
|
|
2119
|
-
waitFor.push(...known.parts);
|
|
2120
|
-
results.push({ _tag: "AddedGroup", handle: known.handle });
|
|
2121
|
-
break;
|
|
2122
|
-
}
|
|
2123
|
-
const taken = [group.key, ...group.parts.map((part) => part.key)].find((key) => items.has(key) || history.has(key) || groups.has(key));
|
|
2124
|
-
if (taken !== undefined)
|
|
2125
|
-
return refuse(KeyMismatch.of(taken));
|
|
2126
|
-
const [first, ...rest] = group.parts;
|
|
2127
|
-
const busy = conflict(group.key, first.lane);
|
|
2128
|
-
if (busy !== undefined)
|
|
2129
|
-
return refuse(busy);
|
|
2130
|
-
const startByMs = first.startByOffsetMs === undefined ? undefined : nowMs + first.startByOffsetMs;
|
|
2131
|
-
if (startByMs !== undefined &&
|
|
2132
|
-
view({
|
|
2133
|
-
lane: first.lane,
|
|
2134
|
-
admission: Infinity,
|
|
2135
|
-
seconds: first.request.durationSeconds,
|
|
2136
|
-
startByMs,
|
|
2137
|
-
}) > startByMs)
|
|
2138
|
-
return refuse(WouldMissDeadline.of(group.key));
|
|
2139
|
-
const parts = [
|
|
2140
|
-
accept(first, nowMs, startByMs, { key: group.key, index: 0 }),
|
|
2141
|
-
...rest.map((part, index) => accept(part, nowMs, undefined, { key: group.key, index: index + 1 })),
|
|
2142
|
-
];
|
|
2143
|
-
for (const part of parts)
|
|
2144
|
-
admitted(part);
|
|
2145
|
-
const handle = {
|
|
2146
|
-
key: group.key,
|
|
2147
|
-
parts: [parts[0].handle, ...parts.slice(1).map((part) => part.handle)],
|
|
2148
|
-
};
|
|
2149
|
-
groups.set(group.key, {
|
|
2150
|
-
fingerprint: group.fingerprint,
|
|
2151
|
-
handle,
|
|
2152
|
-
parts: group.parts.map((part) => part.key),
|
|
2153
|
-
brokenAt: undefined,
|
|
2154
|
-
});
|
|
2155
|
-
undo.push(() => groups.delete(group.key));
|
|
2156
|
-
results.push({ _tag: "AddedGroup", handle });
|
|
2157
|
-
break;
|
|
2158
|
-
}
|
|
2159
|
-
case "Insert": {
|
|
2160
|
-
const { insert } = edit;
|
|
2161
|
-
const known = repeated(insert.key, insert.fingerprint);
|
|
2162
|
-
if (Result.isFailure(known))
|
|
2163
|
-
return refuse(known.failure);
|
|
2164
|
-
if (known.success !== undefined) {
|
|
2165
|
-
results.push({ _tag: "Added", handle: known.success });
|
|
2166
|
-
break;
|
|
2167
|
-
}
|
|
2168
|
-
const placed = placeInsert(insert);
|
|
2169
|
-
if (typeof placed === "string")
|
|
2170
|
-
return refuse(invalid(placed));
|
|
2171
|
-
const startByMs = insert.startByOffsetMs === undefined ? undefined : nowMs + insert.startByOffsetMs;
|
|
2172
|
-
if (startByMs !== undefined &&
|
|
2173
|
-
view({ ...placed, seconds: insert.request.durationSeconds, startByMs }) > startByMs)
|
|
2174
|
-
return refuse(WouldMissDeadline.of(insert.key));
|
|
2175
|
-
const entry = accept({
|
|
2176
|
-
key: insert.key,
|
|
2177
|
-
lane: placed.lane,
|
|
2178
|
-
request: insert.request,
|
|
2179
|
-
fingerprint: insert.fingerprint,
|
|
2180
|
-
notBeforeOffsetMs: insert.notBeforeOffsetMs,
|
|
2181
|
-
startByOffsetMs: insert.startByOffsetMs,
|
|
2182
|
-
firm: insert.firm,
|
|
2183
|
-
atWallMs: placed.atWallMs,
|
|
2184
|
-
late: placed.late,
|
|
2185
|
-
...(placed.asap ? { mode: "asap" } : {}),
|
|
2186
|
-
cues: insert.cues,
|
|
2187
|
-
...(insert.continuity === undefined ? {} : { continuity: insert.continuity }),
|
|
2188
|
-
}, nowMs, startByMs, placed.group, {
|
|
2189
|
-
admission: placed.admission,
|
|
2190
|
-
inserted: true,
|
|
2191
|
-
notBeforeMs: insert.notBeforeOffsetMs === undefined
|
|
2192
|
-
? undefined
|
|
2193
|
-
: nowMs + insert.notBeforeOffsetMs,
|
|
2194
|
-
});
|
|
2195
|
-
admitted(entry);
|
|
2196
|
-
if (placed.group !== undefined) {
|
|
2197
|
-
const parts = groups.get(placed.group.key)?.parts;
|
|
2198
|
-
parts?.push(entry.key);
|
|
2199
|
-
undo.push(() => parts?.pop());
|
|
2200
|
-
}
|
|
2201
|
-
results.push({ _tag: "Added", handle: entry.handle });
|
|
2202
|
-
break;
|
|
2203
|
-
}
|
|
2204
|
-
case "Replace": {
|
|
2205
|
-
const { next } = edit;
|
|
2206
|
-
const fingerprint = JSON.stringify({
|
|
2207
|
-
replaces: edit.key,
|
|
2208
|
-
request: next.request,
|
|
2209
|
-
...(next.cues.length === 0 ? {} : { cues: next.cues }),
|
|
2210
|
-
...(next.continuity === undefined ? {} : { continuity: next.continuity }),
|
|
2211
|
-
});
|
|
2212
|
-
const known = repeated(next.key, fingerprint);
|
|
2213
|
-
if (Result.isFailure(known))
|
|
2214
|
-
return refuse(known.failure);
|
|
2215
|
-
if (known.success !== undefined) {
|
|
2216
|
-
results.push({ _tag: "Added", handle: known.success });
|
|
2217
|
-
break;
|
|
2218
|
-
}
|
|
2219
|
-
const old = items.get(edit.key);
|
|
2220
|
-
if (old === undefined ||
|
|
2221
|
-
old.replacedBy !== undefined ||
|
|
2222
|
-
(old.phase !== "Accepted" && old.phase !== "Building" && old.phase !== "Ready"))
|
|
2223
|
-
return refuse(invalid("Nothing queued to replace under that key"));
|
|
2224
|
-
const entry = accept({
|
|
2225
|
-
key: next.key,
|
|
2226
|
-
lane: old.lane,
|
|
2227
|
-
request: next.request,
|
|
2228
|
-
fingerprint,
|
|
2229
|
-
notBeforeOffsetMs: undefined,
|
|
2230
|
-
startByOffsetMs: undefined,
|
|
2231
|
-
firm: old.firm,
|
|
2232
|
-
atWallMs: old.atWallMs,
|
|
2233
|
-
late: old.late,
|
|
2234
|
-
...(old.asap === true ? { mode: "asap" } : {}),
|
|
2235
|
-
...(old.held === true ? { mode: "manual" } : {}),
|
|
2236
|
-
cues: next.cues,
|
|
2237
|
-
...(next.continuity === undefined ? {} : { continuity: next.continuity }),
|
|
2238
|
-
}, nowMs, old.startByMs, old.group, {
|
|
2239
|
-
admission: old.admission,
|
|
2240
|
-
replaces: old.key,
|
|
2241
|
-
generation: (old.generation ?? 0) + 1,
|
|
2242
|
-
inserted: old.inserted === true,
|
|
2243
|
-
notBeforeMs: old.notBeforeMs,
|
|
2244
|
-
});
|
|
2245
|
-
admitted(entry);
|
|
2246
|
-
old.replacedBy = entry.key;
|
|
2247
|
-
undo.push(() => delete old.replacedBy);
|
|
2248
|
-
// A pending batch that waits for the replaced item waits for its replacement.
|
|
2249
|
-
for (const batch of batches.values())
|
|
2250
|
-
if (batch.waitFor.includes(old.key)) {
|
|
2251
|
-
batch.waitFor.push(entry.key);
|
|
2252
|
-
undo.push(() => batch.waitFor.pop());
|
|
2253
|
-
}
|
|
2254
|
-
if (old.group !== undefined) {
|
|
2255
|
-
const parts = groups.get(old.group.key)?.parts;
|
|
2256
|
-
parts?.push(entry.key);
|
|
2257
|
-
undo.push(() => parts?.pop());
|
|
2258
|
-
}
|
|
2259
|
-
results.push({ _tag: "Added", handle: entry.handle });
|
|
2260
|
-
break;
|
|
2261
|
-
}
|
|
2262
|
-
case "Withdraw":
|
|
2263
|
-
results.push({ _tag: "Withdrawal", key: edit.key });
|
|
2264
|
-
break;
|
|
2265
|
-
}
|
|
2266
|
-
}
|
|
2267
|
-
return Result.succeed({ results, added, waitFor, replaced });
|
|
2268
|
-
};
|
|
2269
|
-
/**
|
|
2270
|
-
* Makes a staged call's handle. A batch keeps what it withdraws as cover until it commits;
|
|
2271
|
-
* a single call adds one item or group and takes effect at once.
|
|
2272
|
-
*/
|
|
2273
|
-
const openBatch = (staged, batched) => {
|
|
2274
|
-
// A single call adds one item or group and never withdraws; only a replace lane gives
|
|
2275
|
-
// it something to supersede.
|
|
2276
|
-
if (!batched && staged.replaced.length === 0)
|
|
2277
|
-
return {
|
|
2278
|
-
handle: {
|
|
2279
|
-
results: staged.results.filter((result) => result._tag !== "Withdrawal"),
|
|
2280
|
-
committed: Effect.void,
|
|
2281
|
-
},
|
|
2282
|
-
settle: [],
|
|
2283
|
-
};
|
|
2284
|
-
const settle = [];
|
|
2285
|
-
const batch = {
|
|
2286
|
-
id: ++batchCount,
|
|
2287
|
-
waitFor: [...staged.waitFor],
|
|
2288
|
-
targets: new Map(staged.replaced.map((key) => [key, { reason: "replaced", reply: undefined }])),
|
|
2289
|
-
committed: Deferred.makeUnsafe(),
|
|
2290
|
-
};
|
|
2291
|
-
const results = staged.results.map((result) => {
|
|
2292
|
-
if (result._tag !== "Withdrawal")
|
|
2293
|
-
return result;
|
|
2294
|
-
const replies = [];
|
|
2295
|
-
for (const target of withdrawalTargets(result.key)) {
|
|
2296
|
-
const reply = Deferred.makeUnsafe();
|
|
2297
|
-
replies.push(reply);
|
|
2298
|
-
batch.targets.set(target.key, { reason: "withdrawn", reply });
|
|
2299
|
-
}
|
|
2300
|
-
const outcome = Deferred.makeUnsafe();
|
|
2301
|
-
settle.push(settleWithdrawals(replies, outcome, groups.has(result.key)));
|
|
2302
|
-
return {
|
|
2303
|
-
_tag: "Withdrawal",
|
|
2304
|
-
outcome: Deferred.await(outcome).pipe(Effect.raceFirst(closedCall)),
|
|
2305
|
-
};
|
|
2306
|
-
});
|
|
2307
|
-
// A pending batch whose new clip this one withdraws now also waits for this one's, so
|
|
2308
|
-
// its own cover is not withdrawn before anything replaces it.
|
|
2309
|
-
for (const other of batches.values())
|
|
2310
|
-
if (other.waitFor.some((key) => batch.targets.has(key)))
|
|
2311
|
-
other.waitFor.push(...batch.waitFor);
|
|
2312
|
-
batches.set(batch.id, batch);
|
|
2313
|
-
return {
|
|
2314
|
-
handle: {
|
|
2315
|
-
results: batched ? results : results.filter((result) => result._tag !== "Withdrawal"),
|
|
2316
|
-
committed: batched ? Deferred.await(batch.committed) : Effect.void,
|
|
2317
|
-
},
|
|
2318
|
-
settle,
|
|
2319
|
-
};
|
|
2320
|
-
};
|
|
2321
|
-
const actor = Effect.gen(function* () {
|
|
2322
|
-
for (;;) {
|
|
2323
|
-
const message = yield* takeQueue(inbox);
|
|
2324
|
-
// Queued public calls observe the same terminal result through closedCall.
|
|
2325
|
-
if (ended !== undefined && message._tag !== "CommandDone")
|
|
2326
|
-
continue;
|
|
2327
|
-
switch (message._tag) {
|
|
2328
|
-
case "Edit": {
|
|
2329
|
-
if (!accepting) {
|
|
2330
|
-
yield* Deferred.fail(message.reply, invalid("Scheduler is draining or closed"));
|
|
2331
|
-
break;
|
|
2332
|
-
}
|
|
2333
|
-
const nowMs = monotonicMillis(clock);
|
|
2334
|
-
const state = yield* engine.state;
|
|
2335
|
-
if (ended !== undefined)
|
|
2336
|
-
continue;
|
|
2337
|
-
// No yield from the terminal check above through admission and its
|
|
2338
|
-
// publication below: a claim either precedes the items or settles them
|
|
2339
|
-
// after their Accepted evidence.
|
|
2340
|
-
const staged = stageEdits(message.edits, message.batched, state, nowMs);
|
|
2341
|
-
if (Result.isFailure(staged)) {
|
|
2342
|
-
yield* Deferred.fail(message.reply, staged.failure);
|
|
2343
|
-
break;
|
|
2344
|
-
}
|
|
2345
|
-
for (const entry of staged.success.added)
|
|
2346
|
-
PubSub.publishUnsafe(events, {
|
|
2347
|
-
key: entry.key,
|
|
2348
|
-
at: clock.currentTimeMillisUnsafe(),
|
|
2349
|
-
status: entry.status,
|
|
2350
|
-
});
|
|
2351
|
-
const opened = openBatch(staged.success, message.batched);
|
|
2352
|
-
for (const effect of opened.settle)
|
|
2353
|
-
yield* Effect.forkIn(effect, actorScope);
|
|
2354
|
-
yield* observed(state);
|
|
2355
|
-
// An edit returns its committed view; other messages publish once
|
|
2356
|
-
// through reconciliation after all their evidence is adopted.
|
|
2357
|
-
yield* publishState(state);
|
|
2358
|
-
yield* Deferred.succeed(message.reply, opened.handle);
|
|
2359
|
-
break;
|
|
2360
|
-
}
|
|
2361
|
-
case "Withdraw": {
|
|
2362
|
-
const targets = withdrawalTargets(message.key);
|
|
2363
|
-
if (targets.length <= 1 && !groups.has(message.key)) {
|
|
2364
|
-
if (targets[0] === undefined)
|
|
2365
|
-
yield* Deferred.succeed(message.reply, "not-found");
|
|
2366
|
-
else
|
|
2367
|
-
yield* requestWithdrawal(targets[0], "withdrawn", message.reply);
|
|
2368
|
-
break;
|
|
2369
|
-
}
|
|
2370
|
-
const replies = [];
|
|
2371
|
-
for (const part of targets) {
|
|
2372
|
-
const reply = Deferred.makeUnsafe();
|
|
2373
|
-
replies.push(reply);
|
|
2374
|
-
yield* requestWithdrawal(part, "withdrawn", reply);
|
|
2375
|
-
}
|
|
2376
|
-
yield* Effect.forkIn(settleWithdrawals(replies, message.reply, groups.has(message.key)), actorScope);
|
|
2377
|
-
break;
|
|
2378
|
-
}
|
|
2379
|
-
case "Release": {
|
|
2380
|
-
const item = items.get(message.key);
|
|
2381
|
-
if (item?.held !== true || item.phase === "Started" || item.phase === "Terminal") {
|
|
2382
|
-
yield* Deferred.fail(message.reply, invalid("Nothing is held under that key"));
|
|
2383
|
-
break;
|
|
2384
|
-
}
|
|
2385
|
-
item.held = false;
|
|
2386
|
-
item.asap = true;
|
|
2387
|
-
yield* Deferred.succeed(message.reply, undefined);
|
|
2388
|
-
break;
|
|
2389
|
-
}
|
|
2390
|
-
case "Drain": {
|
|
2391
|
-
if (drainFailure !== undefined) {
|
|
2392
|
-
yield* Deferred.fail(message.reply, drainFailure);
|
|
2393
|
-
break;
|
|
2394
|
-
}
|
|
2395
|
-
accepting = false;
|
|
2396
|
-
if (drained) {
|
|
2397
|
-
yield* Deferred.succeed(message.reply, undefined);
|
|
2398
|
-
break;
|
|
2399
|
-
}
|
|
2400
|
-
drainReplies.push(message.reply);
|
|
2401
|
-
if (!draining) {
|
|
2402
|
-
draining = true;
|
|
2403
|
-
finishAccepted = message.finish === "accepted";
|
|
2404
|
-
updateRecovery();
|
|
2405
|
-
// Renewal admission must stop even while an earlier enqueue is
|
|
2406
|
-
// awaiting its reply in the command worker.
|
|
2407
|
-
const stoppedRenewal = yield* Effect.result(engine.stopRenewal);
|
|
2408
|
-
if (ended !== undefined)
|
|
2409
|
-
break;
|
|
2410
|
-
if (Result.isFailure(stoppedRenewal)) {
|
|
2411
|
-
drainFailure = stoppedRenewal.failure;
|
|
2412
|
-
for (const reply of drainReplies.splice(0))
|
|
2413
|
-
yield* Deferred.fail(reply, stoppedRenewal.failure);
|
|
2414
|
-
break;
|
|
2415
|
-
}
|
|
2416
|
-
if (!finishAccepted) {
|
|
2417
|
-
autoplayPaused = true;
|
|
2418
|
-
yield* sendCommand({ _tag: "PauseAutoplay" });
|
|
2419
|
-
}
|
|
2420
|
-
}
|
|
2421
|
-
break;
|
|
2422
|
-
}
|
|
2423
|
-
case "Snapshot": {
|
|
2424
|
-
lastProcessedSnapshotSerial = message.acquiredSerial;
|
|
2425
|
-
observationReady = true;
|
|
2426
|
-
yield* observed(message.state, true);
|
|
2427
|
-
blockedMove = undefined;
|
|
2428
|
-
break;
|
|
2429
|
-
}
|
|
2430
|
-
case "Event":
|
|
2431
|
-
yield* onEvent(message.event);
|
|
2432
|
-
yield* observed(yield* engine.state);
|
|
2433
|
-
blockedMove = undefined;
|
|
2434
|
-
break;
|
|
2435
|
-
case "Tick":
|
|
2436
|
-
break;
|
|
2437
|
-
case "CommandDone":
|
|
2438
|
-
yield* handleCommandDone(message.command, message.result);
|
|
2439
|
-
if (commandCount === 0)
|
|
2440
|
-
earlyClipEvents.clear();
|
|
2441
|
-
break;
|
|
2442
|
-
case "Closed":
|
|
2443
|
-
yield* closeActor(message.cause);
|
|
2444
|
-
yield* Deferred.fail(initialized, ReactorError.fromCode("Closed", message.cause.message));
|
|
2445
|
-
break;
|
|
2446
|
-
}
|
|
2447
|
-
if (ended === undefined)
|
|
2448
|
-
yield* reconcile;
|
|
2449
|
-
if (message._tag === "Snapshot")
|
|
2450
|
-
yield* Deferred.succeed(initialized, undefined);
|
|
2451
|
-
}
|
|
2452
|
-
});
|
|
2453
|
-
const observe = Effect.gen(function* () {
|
|
2454
|
-
for (;;) {
|
|
2455
|
-
const result = yield* Effect.result(Effect.scoped(Effect.gen(function* () {
|
|
2456
|
-
// Claim the serial before acquiring the initial state. A snapshot
|
|
2457
|
-
// begun before an enqueue result must not disprove its clip.
|
|
2458
|
-
const acquiredSerial = ++snapshotAcquiredSerial;
|
|
2459
|
-
const observation = yield* engine.observe({ capacity: 4096 });
|
|
2460
|
-
yield* Queue.offer(inbox, {
|
|
2461
|
-
_tag: "Snapshot",
|
|
2462
|
-
state: observation.initial,
|
|
2463
|
-
acquiredSerial,
|
|
2464
|
-
});
|
|
2465
|
-
yield* Stream.runForEach(observation.events, (event) => Queue.offer(inbox, { _tag: "Event", event }));
|
|
2466
|
-
})));
|
|
2467
|
-
if (Result.isSuccess(result)) {
|
|
2468
|
-
yield* Queue.offer(inbox, {
|
|
2469
|
-
_tag: "Closed",
|
|
2470
|
-
cause: ReactorError.fromCode("Closed", "The orchestration closed"),
|
|
2471
|
-
});
|
|
2472
|
-
return;
|
|
2473
|
-
}
|
|
2474
|
-
if (result.failure.reason._tag !== "Overflow") {
|
|
2475
|
-
yield* Queue.offer(inbox, { _tag: "Closed", cause: result.failure });
|
|
2476
|
-
return;
|
|
2477
|
-
}
|
|
2478
|
-
}
|
|
2479
|
-
});
|
|
2480
|
-
// Orderly scope teardown has already claimed and settled Closed before
|
|
2481
|
-
// worker interruption; unexpected interruption retains its original Cause.
|
|
2482
|
-
const supervise = (effect) => effect.pipe(Effect.onExit((exit) => Exit.isFailure(exit) && ended === undefined
|
|
2483
|
-
? terminate(Exit.failCause(exit.cause))
|
|
2484
|
-
: Effect.void));
|
|
2485
|
-
yield* Effect.forkScoped(supervise(watchdog));
|
|
2486
|
-
yield* Effect.forkScoped(supervise(commandWorker));
|
|
2487
|
-
yield* Effect.forkScoped(supervise(actor));
|
|
2488
|
-
yield* Effect.forkScoped(supervise(observe));
|
|
2489
|
-
yield* Effect.forkScoped(Effect.gen(function* () {
|
|
2490
|
-
for (;;) {
|
|
2491
|
-
yield* Effect.sleep("100 millis");
|
|
2492
|
-
yield* Queue.offer(inbox, { _tag: "Tick" });
|
|
2493
|
-
}
|
|
2494
|
-
}));
|
|
2495
|
-
yield* Queue.offer(inbox, { _tag: "Tick" });
|
|
2496
|
-
yield* Effect.addFinalizer(() => closeActor(ReactorError.fromCode("Closed", "The scheduler closed")));
|
|
2497
|
-
yield* Deferred.await(initialized);
|
|
2498
|
-
const laneSet = new Set(lanes);
|
|
2499
|
-
const send = (edits, batched) => Effect.gen(function* () {
|
|
2500
|
-
const reply = yield* Deferred.make();
|
|
2501
|
-
if (!(yield* Queue.offer(inbox, { _tag: "Edit", edits, batched, reply })))
|
|
2502
|
-
return yield* closedCall;
|
|
2503
|
-
return yield* Deferred.await(reply).pipe(Effect.raceFirst(closedCall));
|
|
2504
|
-
});
|
|
2505
|
-
/** The one result of a single call. */
|
|
2506
|
-
const only = (handle) => handle.results[0];
|
|
2507
|
-
const submit = (input) => Effect.gen(function* () {
|
|
2508
|
-
if (yield* Deferred.isDone(stopped))
|
|
2509
|
-
return yield* closedCall;
|
|
2510
|
-
const item = yield* captureItem(input, laneSet);
|
|
2511
|
-
yield* Effect.annotateCurrentSpan("reactor.scheduler.item.key", item.key);
|
|
2512
|
-
const result = only(yield* send([{ _tag: "Submit", item }], false));
|
|
2513
|
-
if (result?._tag !== "Added")
|
|
2514
|
-
return yield* Effect.die("A submission returned no item");
|
|
2515
|
-
return result.handle;
|
|
2516
|
-
}).pipe(Effect.withSpan("Scheduler.submit", {}, { captureStackTrace: false }));
|
|
2517
|
-
const submitGroup = (input) => Effect.gen(function* () {
|
|
2518
|
-
if (yield* Deferred.isDone(stopped))
|
|
2519
|
-
return yield* closedCall;
|
|
2520
|
-
const group = yield* captureGroup(input, laneSet);
|
|
2521
|
-
yield* Effect.annotateCurrentSpan("reactor.scheduler.group.key", group.key);
|
|
2522
|
-
const result = only(yield* send([{ _tag: "SubmitGroup", group }], false));
|
|
2523
|
-
if (result?._tag !== "AddedGroup")
|
|
2524
|
-
return yield* Effect.die("A group returned no handle");
|
|
2525
|
-
return result.handle;
|
|
2526
|
-
}).pipe(Effect.withSpan("Scheduler.submitGroup", {}, { captureStackTrace: false }));
|
|
2527
|
-
const replace = (key, input) => Effect.gen(function* () {
|
|
2528
|
-
if (yield* Deferred.isDone(stopped))
|
|
2529
|
-
return yield* closedCall;
|
|
2530
|
-
const next = yield* captureReplacement(input);
|
|
2531
|
-
const result = only(yield* send([{ _tag: "Replace", key, next }], false).pipe(
|
|
2532
|
-
// A replacement keeps the replaced item's place and window, so neither a deadline
|
|
2533
|
-
// nor its lane's policy is checked for it.
|
|
2534
|
-
Effect.catchTag(["WouldMissDeadline", "LaneBusy"], (error) => Effect.die(error))));
|
|
2535
|
-
if (result?._tag !== "Added")
|
|
2536
|
-
return yield* Effect.die("A replacement returned no item");
|
|
2537
|
-
return result.handle;
|
|
2538
|
-
}).pipe(Effect.withSpan("Scheduler.replace", { attributes: { key } }, { captureStackTrace: false }));
|
|
2539
|
-
const insert = (input) => Effect.gen(function* () {
|
|
2540
|
-
if (yield* Deferred.isDone(stopped))
|
|
2541
|
-
return yield* closedCall;
|
|
2542
|
-
const captured = yield* captureInsert(input);
|
|
2543
|
-
yield* Effect.annotateCurrentSpan("reactor.scheduler.item.key", captured.key);
|
|
2544
|
-
const result = only(yield* send([{ _tag: "Insert", insert: captured }], false).pipe(
|
|
2545
|
-
// An insert places itself, so its lane's policy does not apply.
|
|
2546
|
-
Effect.catchTag("LaneBusy", (error) => Effect.die(error))));
|
|
2547
|
-
if (result?._tag !== "Added")
|
|
2548
|
-
return yield* Effect.die("An insert returned no item");
|
|
2549
|
-
return result.handle;
|
|
2550
|
-
}).pipe(Effect.withSpan("Scheduler.insert", {}, { captureStackTrace: false }));
|
|
2551
|
-
const edit = (input) => Effect.gen(function* () {
|
|
2552
|
-
if (yield* Deferred.isDone(stopped))
|
|
2553
|
-
return yield* closedCall;
|
|
2554
|
-
const edits = yield* captureEdits(input, laneSet);
|
|
2555
|
-
yield* Effect.annotateCurrentSpan("reactor.scheduler.edits", edits.length);
|
|
2556
|
-
return yield* send(edits, true);
|
|
2557
|
-
}).pipe(Effect.withSpan("Scheduler.edit", {}, { captureStackTrace: false }));
|
|
2558
|
-
const release = (key) => Effect.gen(function* () {
|
|
2559
|
-
const reply = yield* Deferred.make();
|
|
2560
|
-
if (!(yield* Queue.offer(inbox, { _tag: "Release", key, reply })))
|
|
2561
|
-
return yield* closedCall;
|
|
2562
|
-
return yield* Deferred.await(reply).pipe(Effect.raceFirst(closedCall));
|
|
2563
|
-
}).pipe(Effect.withSpan("Scheduler.release", { attributes: { key } }, { captureStackTrace: false }));
|
|
2564
|
-
const withdraw = (key) => Effect.gen(function* () {
|
|
2565
|
-
const reply = yield* Deferred.make();
|
|
2566
|
-
if (!(yield* Queue.offer(inbox, { _tag: "Withdraw", key, reply })))
|
|
2567
|
-
return yield* closedCall;
|
|
2568
|
-
return yield* Deferred.await(reply).pipe(Effect.raceFirst(closedCall));
|
|
2569
|
-
}).pipe(Effect.withSpan("Scheduler.withdraw", { attributes: { key } }, {
|
|
2570
|
-
captureStackTrace: false,
|
|
2571
|
-
}));
|
|
2572
|
-
const drain = (options = {}) => Effect.gen(function* () {
|
|
2573
|
-
const reply = yield* Deferred.make();
|
|
2574
|
-
const finish = options.finish ?? "playing";
|
|
2575
|
-
if (finish !== "playing" && finish !== "accepted")
|
|
2576
|
-
return yield* invalid("Drain finish must be playing or accepted");
|
|
2577
|
-
if (!(yield* Queue.offer(inbox, { _tag: "Drain", finish, reply })))
|
|
2578
|
-
return yield* closedCall;
|
|
2579
|
-
yield* Deferred.await(reply).pipe(Effect.raceFirst(closedCall));
|
|
2580
|
-
}).pipe(Effect.withSpan("Scheduler.drain", {}, { captureStackTrace: false }));
|
|
2581
|
-
return {
|
|
2582
|
-
submit,
|
|
2583
|
-
submitGroup,
|
|
2584
|
-
replace,
|
|
2585
|
-
insert,
|
|
2586
|
-
edit,
|
|
2587
|
-
release,
|
|
2588
|
-
withdraw,
|
|
2589
|
-
drain,
|
|
2590
|
-
failure: Deferred.await(stopped),
|
|
2591
|
-
state: SubscriptionRef.get(stateRef),
|
|
2592
|
-
asRun: Stream.fromPubSub(events),
|
|
2593
|
-
cues: Stream.fromPubSub(cueEvents),
|
|
2594
|
-
};
|
|
2595
|
-
}).pipe(Effect.provideServiceEffect(Scope.Scope, Effect.flatMap(Scope.Scope, (parent) => Scope.fork(parent, "sequential"))));
|
|
2596
|
-
export const layerScheduler = (options) => Layer.effect(Scheduler, makeScheduler(options));
|
|
2597
|
-
//# sourceMappingURL=scheduler.js.map
|