| /** | |
| * Defines addressable entity types for Effect Cluster. | |
| * | |
| * An entity gives a stable name and RPC protocol to a group of values that are | |
| * addressed by id. The cluster uses that information to choose a shard and | |
| * route each request to the runner responsible for that id. This module | |
| * includes constructors for entity definitions, helpers for creating sharded | |
| * clients, layer builders for registering handlers, and services that expose | |
| * the current entity address while a request is being handled. | |
| * | |
| * @since 4.0.0 | |
| */ | |
| import * as Arr from "../../Array.js"; | |
| import * as Context from "../../Context.js"; | |
| import * as Data from "../../Data.js"; | |
| import * as Effect from "../../Effect.js"; | |
| import * as Equal from "../../Equal.js"; | |
| import * as Exit from "../../Exit.js"; | |
| import { identity } from "../../Function.js"; | |
| import * as Hash from "../../Hash.js"; | |
| import * as Layer from "../../Layer.js"; | |
| import * as Option from "../../Option.js"; | |
| import * as Predicate from "../../Predicate.js"; | |
| import * as Queue from "../../Queue.js"; | |
| import { Scope } from "../../Scope.js"; | |
| import * as Stream from "../../Stream.js"; | |
| import * as Headers from "../http/Headers.js"; | |
| import * as Rpc from "../rpc/Rpc.js"; | |
| import * as RpcClient from "../rpc/RpcClient.js"; | |
| import * as RpcGroup from "../rpc/RpcGroup.js"; | |
| import * as RpcSchema from "../rpc/RpcSchema.js"; | |
| import * as RpcServer from "../rpc/RpcServer.js"; | |
| import { Persisted, ShardGroup, Uninterruptible } from "./ClusterSchema.js"; | |
| import { EntityAddress } from "./EntityAddress.js"; | |
| import { EntityType } from "./EntityType.js"; | |
| import * as Envelope from "./Envelope.js"; | |
| import { hashString } from "./internal/hash.js"; | |
| import { ResourceMap } from "./internal/resourceMap.js"; | |
| import * as Message from "./Message.js"; | |
| import { RunnerAddress } from "./RunnerAddress.js"; | |
| import * as ShardId from "./ShardId.js"; | |
| import { ShardingConfig } from "./ShardingConfig.js"; | |
| import * as Snowflake from "./Snowflake.js"; | |
| const TypeId = "~effect/cluster/Entity"; | |
| /** | |
| * Returns `true` when the supplied value is a cluster `Entity`. | |
| * | |
| * **Details** | |
| * | |
| * The check is based on the internal entity type identifier. | |
| * | |
| * @category refinements | |
| * @since 4.0.0 | |
| */ | |
| export const isEntity = u => Predicate.hasProperty(u, TypeId); | |
| const Proto = { | |
| [TypeId]: TypeId, | |
| [Hash.symbol]() { | |
| return Hash.structure({ | |
| type: this.type | |
| }); | |
| }, | |
| [Equal.symbol](that) { | |
| return isEntity(that) && this.type === that.type; | |
| }, | |
| annotate(key, value) { | |
| return fromRpcGroup(this.type, this.protocol.annotate(key, value)); | |
| }, | |
| annotateRpcs(key, value) { | |
| return fromRpcGroup(this.type, this.protocol.annotateRpcs(key, value)); | |
| }, | |
| annotateMerge(annotations) { | |
| return fromRpcGroup(this.type, this.protocol.annotateMerge(annotations)); | |
| }, | |
| annotateRpcsMerge(annotations) { | |
| return fromRpcGroup(this.type, this.protocol.annotateRpcsMerge(annotations)); | |
| }, | |
| getShardId(entityId) { | |
| return Effect.map(shardingTag, sharding => sharding.getShardId(entityId, this.getShardGroup(entityId))); | |
| }, | |
| get client() { | |
| return shardingTag.pipe(Effect.flatMap(sharding => sharding.makeClient(this))); | |
| }, | |
| toLayer(build, options) { | |
| return shardingTag.pipe(Effect.flatMap(sharding => sharding.registerEntity(this, Effect.isEffect(build) ? build : Effect.succeed(build), options)), Layer.effectDiscard); | |
| }, | |
| of: identity, | |
| toLayerQueue(build, options) { | |
| const buildHandlers = Effect.gen({ | |
| self: this | |
| }, function* () { | |
| const behaviour = Effect.isEffect(build) ? yield* build : build; | |
| const queue = yield* Queue.make(); | |
| // create the rpc handlers for the entity | |
| const handler = envelope => Effect.callback(resume => { | |
| Queue.offerUnsafe(queue, envelope); | |
| resumes.set(envelope, resume); | |
| }); | |
| const streamHandler = envelope => Effect.callback(resume => { | |
| Queue.offerUnsafe(queue, envelope); | |
| resumes.set(envelope, resume); | |
| }).pipe(Effect.map(streamOrQueue => Stream.isStream(streamOrQueue) ? streamOrQueue : Stream.fromQueue(streamOrQueue)), Stream.unwrap); | |
| const handlers = {}; | |
| for (const rpc_ of this.protocol.requests.values()) { | |
| const rpc = rpc_; | |
| handlers[rpc._tag] = RpcSchema.isStreamSchema(rpc.successSchema) ? streamHandler : handler; | |
| } | |
| // make the Replier for the behaviour | |
| const resumes = new Map(); | |
| const complete = (request, exit) => Effect.sync(() => { | |
| const resume = resumes.get(request); | |
| if (resume) { | |
| resumes.delete(request); | |
| resume(exit); | |
| } | |
| }); | |
| const replier = { | |
| succeed: (request, value) => complete(request, Exit.succeed(value)), | |
| fail: (request, error) => complete(request, Exit.fail(error)), | |
| failCause: (request, cause) => complete(request, Exit.failCause(cause)), | |
| complete | |
| }; | |
| // fork the behaviour into the layer scope | |
| yield* behaviour(queue, replier).pipe(Effect.catchCause(cause => { | |
| const exit = Exit.failCause(cause); | |
| for (const resume of resumes.values()) { | |
| resume(exit); | |
| } | |
| return Effect.void; | |
| }), Effect.interruptible, Effect.forkScoped); | |
| return handlers; | |
| }); | |
| return this.toLayer(buildHandlers, { | |
| ...options, | |
| concurrency: "unbounded" | |
| }); | |
| } | |
| }; | |
| /** | |
| * Creates a new `Entity` of the specified `type` which will accept messages | |
| * that adhere to the provided `RpcGroup`. | |
| * | |
| * @category constructors | |
| * @since 4.0.0 | |
| */ | |
| export const fromRpcGroup = ( | |
| /** | |
| * The entity type name. | |
| */ | |
| type, | |
| /** | |
| * The schema definition for messages that the entity is capable of | |
| * processing. | |
| */ | |
| protocol) => { | |
| const self = Object.create(Proto); | |
| self.type = EntityType.make(type); | |
| self.protocol = protocol; | |
| self.getShardGroup = Context.get(protocol.annotations, ShardGroup); | |
| return self; | |
| }; | |
| /** | |
| * Creates a new `Entity` of the specified `type` which will accept messages | |
| * that adhere to the provided schemas. | |
| * | |
| * **When to use** | |
| * | |
| * Use to define a cluster entity from individual `Rpc` definitions, giving the | |
| * cluster runtime a typed protocol for handlers and per-entity clients. | |
| * | |
| * **Details** | |
| * | |
| * The `type` argument is stored as the entity `EntityType`, and the RPC array | |
| * is grouped into the entity's `protocol`. | |
| * | |
| * **Gotchas** | |
| * | |
| * RPC tags should be unique within the array. If multiple definitions use the | |
| * same tag, the resulting protocol keeps the later definition for that tag. | |
| * | |
| * @see {@link fromRpcGroup} for creating an entity from an existing `RpcGroup` | |
| * | |
| * @category constructors | |
| * @since 4.0.0 | |
| */ | |
| export const make = ( | |
| /** | |
| * The entity type name. | |
| */ | |
| type, | |
| /** | |
| * The schema definition for messages that the entity is capable of | |
| * processing. | |
| */ | |
| protocol) => fromRpcGroup(type, RpcGroup.make(...protocol)); | |
| /** | |
| * Service tag for the entity address currently being processed. | |
| * | |
| * **When to use** | |
| * | |
| * Use to read the current entity identity and shard address from entity | |
| * handlers and keep-alive logic. | |
| * | |
| * @category context | |
| * @since 4.0.0 | |
| */ | |
| export class CurrentAddress extends /*#__PURE__*/Context.Service()("effect/cluster/Entity/EntityAddress") {} | |
| /** | |
| * Service tag for the runner address currently registering entity handlers. | |
| * | |
| * **When to use** | |
| * | |
| * Use to read the runner address associated with the current entity handler | |
| * registration. | |
| * | |
| * @category context | |
| * @since 4.0.0 | |
| */ | |
| export class CurrentRunnerAddress extends /*#__PURE__*/Context.Service()("effect/cluster/Entity/RunnerAddress") {} | |
| /** | |
| * Represents an entity request envelope delivered to entity handlers. | |
| * | |
| * **Details** | |
| * | |
| * It includes the underlying request envelope plus the last stream reply chunk | |
| * that was sent, allowing handlers to resume chunk sequencing after a restart. | |
| * | |
| * @category request | |
| * @since 4.0.0 | |
| */ | |
| export class Request extends Data.Class { | |
| /** | |
| * Most recent success chunk value sent by the entity, when one exists. | |
| * | |
| * @since 4.0.0 | |
| */ | |
| get lastSentChunkValue() { | |
| return Option.map(this.lastSentChunk, chunk => Arr.lastNonEmpty(chunk.values)); | |
| } | |
| /** | |
| * Sequence number to use for the entity's next outgoing success chunk. | |
| * | |
| * @since 4.0.0 | |
| */ | |
| get nextSequence() { | |
| if (Option.isNone(this.lastSentChunk)) { | |
| return 0; | |
| } | |
| return this.lastSentChunk.value.sequence + 1; | |
| } | |
| } | |
| const shardingTag = /*#__PURE__*/Context.Service("effect/cluster/Sharding"); | |
| /** | |
| * Builds an in-memory test client for an entity layer. | |
| * | |
| * **Details** | |
| * | |
| * The returned function creates a no-serialization RPC client for each entity ID, | |
| * using a test sharding service instead of the cluster transport. | |
| * | |
| * @category testing | |
| * @since 4.0.0 | |
| */ | |
| export const makeTestClient = /*#__PURE__*/Effect.fnUntraced(function* (entity, layer) { | |
| const config = yield* ShardingConfig; | |
| const makeShardId = entityId => ShardId.make(entity.getShardGroup(entityId), Math.abs(hashString(entityId) % config.shardsPerGroup) + 1); | |
| const snowflakeGen = yield* Snowflake.makeGenerator; | |
| const runnerAddress = new RunnerAddress({ | |
| host: "localhost", | |
| port: 3000 | |
| }); | |
| const entityMap = new Map(); | |
| const sharding = shardingTag.of({ | |
| ...{}, | |
| registerEntity: (entity, handlers, options) => Effect.contextWith(context => { | |
| entityMap.set(entity.type, { | |
| context: context, | |
| concurrency: options?.concurrency ?? 1, | |
| build: entity.protocol.toHandlers(handlers).pipe(Effect.provideContext(Context.mutate(context, context => context.pipe(Context.add(CurrentRunnerAddress, runnerAddress), Context.omit(Scope))))) | |
| }); | |
| return Effect.void; | |
| }) | |
| }); | |
| yield* Layer.build(Layer.provide(layer, Layer.succeed(shardingTag)(sharding))); | |
| const entityEntry = entityMap.get(entity.type); | |
| if (!entityEntry) { | |
| return yield* Effect.die(`Entity.makeTestClient: ${entity.type} was not registered by layer`); | |
| } | |
| const map = yield* ResourceMap.make(Effect.fnUntraced(function* (entityId) { | |
| const address = new EntityAddress({ | |
| entityType: entity.type, | |
| entityId: entityId, | |
| shardId: makeShardId(entityId) | |
| }); | |
| const handlers = yield* entityEntry.build.pipe(Effect.provideService(CurrentAddress, address)); | |
| // oxlint-disable-next-line prefer-const | |
| let client; | |
| const server = yield* RpcServer.makeNoSerialization(entity.protocol, { | |
| concurrency: entityEntry.concurrency, | |
| onFromServer(response) { | |
| return client.write(response); | |
| } | |
| }).pipe(Effect.provide(handlers)); | |
| client = yield* RpcClient.makeNoSerialization(entity.protocol, { | |
| supportsAck: true, | |
| generateRequestId: () => snowflakeGen.nextUnsafe(), | |
| onFromClient({ | |
| message | |
| }) { | |
| if (message._tag === "Request") { | |
| return server.write(0, { | |
| ...message, | |
| payload: new Request({ | |
| ...message, | |
| [Envelope.TypeId]: Envelope.TypeId, | |
| address, | |
| requestId: Snowflake.Snowflake(message.id), | |
| lastSentChunk: Option.none() | |
| }) | |
| }); | |
| } | |
| return server.write(0, message); | |
| } | |
| }); | |
| return client.client; | |
| })); | |
| return entityId => map.get(entityId); | |
| }); | |
| /** | |
| * Enables or disables keep-alive for the current entity. | |
| * | |
| * **Details** | |
| * | |
| * When enabled it sends the internal keep-alive RPC for the current address; when | |
| * disabled it releases the keep-alive latch if one is present. | |
| * | |
| * @category Keep alive | |
| * @since 4.0.0 | |
| */ | |
| export const keepAlive = /*#__PURE__*/Effect.fnUntraced(function* (enabled) { | |
| const olatch = yield* Effect.serviceOption(KeepAliveLatch); | |
| if (olatch._tag === "None") return; | |
| if (!enabled) { | |
| yield* olatch.value.open; | |
| return; | |
| } | |
| const sharding = yield* shardingTag; | |
| const address = yield* CurrentAddress; | |
| const requestId = yield* sharding.getSnowflake; | |
| const span = yield* Effect.orDie(Effect.currentSpan); | |
| olatch.value.closeUnsafe(); | |
| yield* Effect.orDie(sharding.sendOutgoing(new Message.OutgoingRequest({ | |
| annotations: KeepAliveRpc.annotations, | |
| rpc: KeepAliveRpc, | |
| context: Context.empty(), | |
| envelope: Envelope.makeRequest({ | |
| requestId, | |
| address, | |
| tag: KeepAliveRpc._tag, | |
| payload: void 0, | |
| headers: Headers.empty, | |
| traceId: span.traceId, | |
| spanId: span.spanId, | |
| sampled: span.sampled | |
| }), | |
| lastReceivedReply: Option.none(), | |
| respond: () => Effect.void | |
| }), true)); | |
| }, (effect, enabled) => Effect.withSpan(effect, "Entity/keepAlive", { | |
| attributes: { | |
| enabled | |
| }, | |
| captureStackTrace: false | |
| })); | |
| /** | |
| * RPC used internally to keep an entity active while a resource is held. | |
| * | |
| * **Details** | |
| * | |
| * The RPC is marked as persisted and uninterruptible so the keep-alive signal | |
| * survives normal entity restarts. | |
| * | |
| * @category Keep alive | |
| * @since 4.0.0 | |
| */ | |
| export const KeepAliveRpc = /*#__PURE__*/Rpc.make("Cluster/Entity/keepAlive").annotate(Persisted, true).annotate(Uninterruptible, true); | |
| /** | |
| * Service tag for the latch that coordinates entity keep-alive state. | |
| * | |
| * **Details** | |
| * | |
| * `keepAlive` closes the latch when keep-alive is active and opens it again when | |
| * the resource no longer needs to keep the entity alive. | |
| * | |
| * @category Keep alive | |
| * @since 4.0.0 | |
| */ | |
| export class KeepAliveLatch extends /*#__PURE__*/Context.Service()("effect/cluster/Entity/KeepAliveLatch") {} | |
| //# sourceMappingURL=Entity.js.map |
Xet Storage Details
- Size:
- 13.5 kB
- Xet hash:
- 67eb19d42d47318e940d732bbf31b7be7264d6d21d210f7f5452e129b41d5f21
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.