effect-encore 0.1.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/README.md +132 -0
- package/dist/_virtual/_rolldown/runtime.js +13 -0
- package/dist/actor.d.ts +113 -0
- package/dist/actor.js +127 -0
- package/dist/client.d.ts +2 -0
- package/dist/client.js +0 -0
- package/dist/handlers.d.ts +2 -0
- package/dist/handlers.js +0 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/observability.d.ts +9 -0
- package/dist/observability.js +13 -0
- package/dist/peek.d.ts +19 -0
- package/dist/peek.js +61 -0
- package/dist/receipt.d.ts +50 -0
- package/dist/receipt.js +36 -0
- package/dist/testing.d.ts +2 -0
- package/dist/testing.js +0 -0
- package/dist/workflow.d.ts +45 -0
- package/dist/workflow.js +67 -0
- package/package.json +84 -0
- package/v3/dist/_virtual/_rolldown/runtime.js +13 -0
- package/v3/dist/actor.d.ts +113 -0
- package/v3/dist/actor.js +127 -0
- package/v3/dist/client.d.ts +2 -0
- package/v3/dist/client.js +0 -0
- package/v3/dist/handlers.d.ts +2 -0
- package/v3/dist/handlers.js +0 -0
- package/v3/dist/index.d.ts +6 -0
- package/v3/dist/index.js +6 -0
- package/v3/dist/observability.d.ts +9 -0
- package/v3/dist/observability.js +13 -0
- package/v3/dist/peek.d.ts +19 -0
- package/v3/dist/peek.js +72 -0
- package/v3/dist/receipt.d.ts +66 -0
- package/v3/dist/receipt.js +36 -0
- package/v3/dist/testing.d.ts +2 -0
- package/v3/dist/testing.js +0 -0
- package/v3/dist/workflow.d.ts +45 -0
- package/v3/dist/workflow.js +66 -0
package/README.md
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# effect-encore
|
|
2
|
+
|
|
3
|
+
Erlang gen_server semantics over `@effect/cluster`.
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
|
|
7
|
+
`@effect/cluster` is powerful but low-level. Defining a single entity requires custom `Schema.Class` implementations, `Rpc.make`, `RpcGroup`, `Entity.make`, handler wiring via `entity.toLayer`, and a hand-rolled client service. A typical entity runs 100+ lines before any business logic.
|
|
8
|
+
|
|
9
|
+
effect-encore compresses this into a declarative DSL:
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { Actor } from "effect-encore";
|
|
13
|
+
import { Schema } from "effect";
|
|
14
|
+
|
|
15
|
+
const Counter = Actor.make("Counter", {
|
|
16
|
+
Increment: {
|
|
17
|
+
payload: { amount: Schema.Number },
|
|
18
|
+
success: Schema.Number,
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Every operation supports `call` (block for reply), `cast` (fire-and-forget with receipt), and `peek`/`watch` (check or stream results). Delivery mode is the caller's choice, not the definition's — just like Erlang's `gen_server:call` vs `gen_server:cast`.
|
|
24
|
+
|
|
25
|
+
## Core API
|
|
26
|
+
|
|
27
|
+
### Define
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
// Multi-operation actor
|
|
31
|
+
const OrderValidation = Actor.make("OrderValidation", {
|
|
32
|
+
Validate: {
|
|
33
|
+
payload: { orderId: Schema.String },
|
|
34
|
+
success: Schema.String,
|
|
35
|
+
error: ValidationError,
|
|
36
|
+
persisted: true,
|
|
37
|
+
primaryKey: (p) => p.orderId,
|
|
38
|
+
},
|
|
39
|
+
Cancel: {
|
|
40
|
+
payload: { orderId: Schema.String },
|
|
41
|
+
persisted: true,
|
|
42
|
+
primaryKey: (p) => p.orderId,
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Single-operation actor (no operation namespace on ref)
|
|
47
|
+
const VectorUpdate = Actor.single("VectorUpdate", {
|
|
48
|
+
payload: { locationId: Schema.String },
|
|
49
|
+
persisted: true,
|
|
50
|
+
primaryKey: (p) => p.locationId,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Delayed delivery
|
|
54
|
+
const Scheduled = Actor.make("Scheduled", {
|
|
55
|
+
Process: {
|
|
56
|
+
payload: { id: Schema.String, deliverAt: Schema.DateTimeUtc },
|
|
57
|
+
primaryKey: (p) => p.id,
|
|
58
|
+
deliverAt: (p) => p.deliverAt,
|
|
59
|
+
persisted: true,
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Pre-built Schema.Class (escape hatch for custom symbol implementations)
|
|
64
|
+
const WithCustomPayload = Actor.make("Custom", {
|
|
65
|
+
Run: { payload: MySchemaClass, success: Schema.Void },
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Handle
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
// Plain handlers
|
|
73
|
+
const handlers = Handlers.handlers(OrderValidation, {
|
|
74
|
+
Validate: (req) => validateOrder(req.payload.orderId),
|
|
75
|
+
Cancel: (req) => cancelOrder(req.payload.orderId),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// From Effect context
|
|
79
|
+
const handlers = Handlers.handlers(
|
|
80
|
+
OrderValidation,
|
|
81
|
+
Effect.gen(function* () {
|
|
82
|
+
const db = yield* Database;
|
|
83
|
+
return {
|
|
84
|
+
Validate: (req) => db.validate(req.payload.orderId),
|
|
85
|
+
Cancel: (req) => db.cancel(req.payload.orderId),
|
|
86
|
+
};
|
|
87
|
+
}),
|
|
88
|
+
);
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Call & Cast
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
const makeRef = yield * Actor.client(OrderValidation);
|
|
95
|
+
const ref = makeRef("order-123");
|
|
96
|
+
|
|
97
|
+
// call — block for reply
|
|
98
|
+
const result = yield * ref.Validate.call({ orderId: "abc" });
|
|
99
|
+
|
|
100
|
+
// cast — fire-and-forget, get receipt
|
|
101
|
+
const receipt = yield * ref.Validate.cast({ orderId: "abc" });
|
|
102
|
+
|
|
103
|
+
// peek — one-shot status check via receipt
|
|
104
|
+
const status = yield * Peek.peek(OrderValidation, receipt);
|
|
105
|
+
|
|
106
|
+
// watch — polling stream of status changes
|
|
107
|
+
const stream = Peek.watch(OrderValidation, receipt);
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Test
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
const makeRef = yield * Testing.testClient(Counter, handlerLayer);
|
|
114
|
+
const ref = yield * makeRef("counter-1");
|
|
115
|
+
const result = yield * ref.Increment.call({ amount: 5 });
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## v3 Support
|
|
119
|
+
|
|
120
|
+
Import from `effect-encore/v3` for `@effect/cluster` v3 compatibility. Same API, different import paths under the hood.
|
|
121
|
+
|
|
122
|
+
## Install
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
bun add effect-encore
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Peer dependencies: `effect`, `@effect/cluster` (v3) or `effect` with `effect/unstable/cluster` (v4).
|
|
129
|
+
|
|
130
|
+
## License
|
|
131
|
+
|
|
132
|
+
MIT
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region \0rolldown/runtime.js
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __exportAll = (all, no_symbols) => {
|
|
4
|
+
let target = {};
|
|
5
|
+
for (var name in all) __defProp(target, name, {
|
|
6
|
+
get: all[name],
|
|
7
|
+
enumerable: true
|
|
8
|
+
});
|
|
9
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
10
|
+
return target;
|
|
11
|
+
};
|
|
12
|
+
//#endregion
|
|
13
|
+
export { __exportAll };
|
package/dist/actor.d.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { CastReceipt } from "./receipt.js";
|
|
2
|
+
import { Entity, Sharding, ShardingConfig } from "effect/unstable/cluster";
|
|
3
|
+
import { Rpc } from "effect/unstable/rpc";
|
|
4
|
+
import { Context, DateTime, Effect, Layer, Schema, Scope } from "effect";
|
|
5
|
+
|
|
6
|
+
//#region src/actor.d.ts
|
|
7
|
+
interface OperationDef {
|
|
8
|
+
readonly payload?: Schema.Top | Schema.Struct.Fields;
|
|
9
|
+
readonly success?: Schema.Top;
|
|
10
|
+
readonly error?: Schema.Top;
|
|
11
|
+
readonly persisted?: boolean;
|
|
12
|
+
readonly primaryKey?: (payload: never) => string;
|
|
13
|
+
readonly deliverAt?: (payload: never) => DateTime.DateTime;
|
|
14
|
+
}
|
|
15
|
+
type OperationDefs = Record<string, OperationDef>;
|
|
16
|
+
type ReservedKeys = "_tag" | "_meta" | "$is" | "Context";
|
|
17
|
+
type AssertNoReservedKeys<Defs extends OperationDefs> = Extract<keyof Defs, ReservedKeys> extends never ? Defs : never;
|
|
18
|
+
type PayloadOf<C extends OperationDef> = C extends {
|
|
19
|
+
readonly payload: infer P extends Schema.Top;
|
|
20
|
+
} ? P : C extends {
|
|
21
|
+
readonly payload: infer F extends Schema.Struct.Fields;
|
|
22
|
+
} ? Schema.Struct<F> : typeof Schema.Void;
|
|
23
|
+
type SuccessOf<C extends OperationDef> = C extends {
|
|
24
|
+
readonly success: infer S extends Schema.Top;
|
|
25
|
+
} ? S : typeof Schema.Void;
|
|
26
|
+
type ErrorOf<C extends OperationDef> = C extends {
|
|
27
|
+
readonly error: infer E extends Schema.Top;
|
|
28
|
+
} ? E : typeof Schema.Never;
|
|
29
|
+
type DefRpc<Tag extends string, C extends OperationDef> = Rpc.Rpc<Tag, PayloadOf<C>, SuccessOf<C>, ErrorOf<C>>;
|
|
30
|
+
type DefRpcs<Defs extends OperationDefs> = { readonly [Tag in keyof Defs & string]: DefRpc<Tag, Defs[Tag]> }[keyof Defs & string];
|
|
31
|
+
declare const OperationBrandId: unique symbol;
|
|
32
|
+
interface OperationBrand<Name extends string, Tag extends string, Output, Error> {
|
|
33
|
+
readonly [OperationBrandId]: {
|
|
34
|
+
readonly name: Name;
|
|
35
|
+
readonly tag: Tag;
|
|
36
|
+
readonly output: Output;
|
|
37
|
+
readonly error: Error;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
type OperationOutput<V> = V extends {
|
|
41
|
+
readonly [OperationBrandId]: {
|
|
42
|
+
readonly output: infer A;
|
|
43
|
+
};
|
|
44
|
+
} ? A : never;
|
|
45
|
+
type OperationError<V> = V extends {
|
|
46
|
+
readonly [OperationBrandId]: {
|
|
47
|
+
readonly error: infer E;
|
|
48
|
+
};
|
|
49
|
+
} ? E : never;
|
|
50
|
+
type OperationValue<Name extends string, Tag extends string, C extends OperationDef> = {
|
|
51
|
+
readonly _tag: Tag;
|
|
52
|
+
} & PayloadFieldsType<C> & OperationBrand<Name, Tag, Schema.Schema.Type<SuccessOf<C>>, Schema.Schema.Type<ErrorOf<C>>>;
|
|
53
|
+
type PayloadFieldsType<C extends OperationDef> = C extends {
|
|
54
|
+
readonly payload: infer F extends Schema.Struct.Fields;
|
|
55
|
+
} ? { readonly [K in keyof F]: Schema.Schema.Type<F[K] extends Schema.Top ? F[K] : never> } : C extends {
|
|
56
|
+
readonly payload: infer P extends Schema.Top;
|
|
57
|
+
} ? Schema.Schema.Type<P> : {};
|
|
58
|
+
type OperationConstructorPayload<C extends OperationDef> = C extends {
|
|
59
|
+
readonly payload: infer F extends Schema.Struct.Fields;
|
|
60
|
+
} ? {} extends { [K in keyof F]: Schema.Schema.Type<F[K] extends Schema.Top ? F[K] : never> } ? [] : [payload: { readonly [K in keyof F]: Schema.Schema.Type<F[K] extends Schema.Top ? F[K] : never> }] : C extends {
|
|
61
|
+
readonly payload: infer _P extends Schema.Top;
|
|
62
|
+
} ? [payload: unknown] : [];
|
|
63
|
+
type OperationConstructor<Name extends string, Tag extends string, C extends OperationDef> = (...args: OperationConstructorPayload<C>) => OperationValue<Name, Tag, C>;
|
|
64
|
+
type OperationUnion<Name extends string, Defs extends OperationDefs> = { [Tag in keyof Defs & string]: OperationValue<Name, Tag, Defs[Tag]> }[keyof Defs & string];
|
|
65
|
+
interface ActorRef<Name extends string, Defs extends OperationDefs> {
|
|
66
|
+
readonly call: <V extends OperationUnion<Name, Defs>>(op: V) => Effect.Effect<OperationOutput<V>, OperationError<V>>;
|
|
67
|
+
readonly cast: <V extends OperationUnion<Name, Defs>>(op: V) => Effect.Effect<CastReceipt>;
|
|
68
|
+
}
|
|
69
|
+
type HandlerRequest<Tag extends string, C extends OperationDef> = {
|
|
70
|
+
readonly operation: {
|
|
71
|
+
readonly _tag: Tag;
|
|
72
|
+
} & PayloadFieldsType<C>;
|
|
73
|
+
readonly request: unknown;
|
|
74
|
+
};
|
|
75
|
+
type ActorHandlers<Defs extends OperationDefs> = { readonly [Tag in keyof Defs & string]: (req: HandlerRequest<Tag, Defs[Tag]>) => Effect.Effect<Schema.Schema.Type<SuccessOf<Defs[Tag]>>, Schema.Schema.Type<ErrorOf<Defs[Tag]>>, any> };
|
|
76
|
+
interface HandlerOptions {
|
|
77
|
+
readonly spanAttributes?: Record<string, string>;
|
|
78
|
+
readonly maxIdleTime?: number;
|
|
79
|
+
readonly concurrency?: number | "unbounded";
|
|
80
|
+
readonly mailboxCapacity?: number | "unbounded";
|
|
81
|
+
}
|
|
82
|
+
interface ActorMeta<Name extends string, Defs extends OperationDefs, Rpcs extends Rpc.Any = DefRpcs<Defs>> {
|
|
83
|
+
readonly name: Name;
|
|
84
|
+
readonly definitions: Defs;
|
|
85
|
+
readonly entity: Entity.Entity<Name, Rpcs>;
|
|
86
|
+
}
|
|
87
|
+
declare const ActorClientServiceId: unique symbol;
|
|
88
|
+
interface ActorClientService<Name extends string, Defs extends OperationDefs> {
|
|
89
|
+
readonly [ActorClientServiceId]: {
|
|
90
|
+
readonly name: Name;
|
|
91
|
+
readonly defs: Defs;
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
type ActorConstructors<Name extends string, Defs extends OperationDefs> = { readonly [Tag in keyof Defs & string]: OperationConstructor<Name, Tag, Defs[Tag]> };
|
|
95
|
+
type ActorObject<Name extends string, Defs extends OperationDefs, Rpcs extends Rpc.Any = DefRpcs<Defs>> = ActorConstructors<Name, Defs> & {
|
|
96
|
+
readonly _tag: "ActorObject";
|
|
97
|
+
readonly _meta: ActorMeta<Name, Defs, Rpcs>;
|
|
98
|
+
readonly Context: Context.Service<ActorClientService<Name, Defs>, (entityId: string) => ActorRef<Name, Defs>>;
|
|
99
|
+
readonly $is: <Tag extends keyof Defs & string>(tag: Tag) => (value: unknown) => value is OperationValue<Name, Tag, Defs[Tag]>;
|
|
100
|
+
};
|
|
101
|
+
declare const Actor: {
|
|
102
|
+
readonly make: <const Name extends string, const Defs extends OperationDefs>(name: Name, definitions: AssertNoReservedKeys<Defs>) => ActorObject<Name, Defs>;
|
|
103
|
+
readonly toLayer: <Name extends string, Defs extends OperationDefs, Rpcs extends Rpc.Any = DefRpcs<Defs>, RX = never>(actor: ActorObject<Name, Defs, Rpcs>, build: ActorHandlers<Defs> | Effect.Effect<ActorHandlers<Defs>, never, RX>, options?: HandlerOptions) => ReturnType<Entity.Entity<Name, Rpcs>["toLayer"]>;
|
|
104
|
+
readonly Live: <Name extends string, Defs extends OperationDefs, Rpcs extends Rpc.Any = DefRpcs<Defs>>(actor: ActorObject<Name, Defs, Rpcs>) => Layer.Layer<ActorClientService<Name, Defs>, never, Scope.Scope | Rpc.MiddlewareClient<Rpcs>>;
|
|
105
|
+
readonly Test: <Name extends string, Defs extends OperationDefs, Rpcs extends Rpc.Any = DefRpcs<Defs>, LA = never, LE = never, LR = never>(actor: ActorObject<Name, Defs, Rpcs>, handlerLayer: Layer.Layer<LA, LE, LR>) => Effect.Effect<(entityId: string) => Effect.Effect<ActorRef<Name, Defs>>, LE, Scope.Scope | ShardingConfig.ShardingConfig | Exclude<LR, Sharding.Sharding> | Rpc.MiddlewareClient<Rpcs>>;
|
|
106
|
+
};
|
|
107
|
+
declare const fromRpcs: <const Name extends string, const Rpcs extends ReadonlyArray<Rpc.Any>>(name: Name, rpcs: Rpcs) => {
|
|
108
|
+
readonly _tag: "RawActorDefinition";
|
|
109
|
+
readonly name: Name;
|
|
110
|
+
readonly entity: Entity.Entity<Name, Rpcs[number]>;
|
|
111
|
+
};
|
|
112
|
+
//#endregion
|
|
113
|
+
export { Actor, ActorMeta, ActorObject, ActorRef, HandlerOptions, OperationBrand, OperationDef, OperationDefs, OperationError, OperationOutput, fromRpcs };
|
package/dist/actor.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { makeCastReceipt } from "./receipt.js";
|
|
2
|
+
import { ClusterSchema, Entity } from "effect/unstable/cluster";
|
|
3
|
+
import * as DeliverAt from "effect/unstable/cluster/DeliverAt";
|
|
4
|
+
import { Rpc } from "effect/unstable/rpc";
|
|
5
|
+
import { Context, Effect, Layer, PrimaryKey, Schema } from "effect";
|
|
6
|
+
//#region src/actor.ts
|
|
7
|
+
const RESERVED_KEYS = new Set([
|
|
8
|
+
"_tag",
|
|
9
|
+
"_meta",
|
|
10
|
+
"$is",
|
|
11
|
+
"Context"
|
|
12
|
+
]);
|
|
13
|
+
const compileRpc = (actorName, tag, def) => {
|
|
14
|
+
const options = {};
|
|
15
|
+
const payload = def["payload"];
|
|
16
|
+
const pkFn = def["primaryKey"];
|
|
17
|
+
const daFn = def["deliverAt"];
|
|
18
|
+
if (payload) if (Schema.isSchema(payload)) options["payload"] = payload;
|
|
19
|
+
else {
|
|
20
|
+
const fields = payload;
|
|
21
|
+
const Base = Schema.Class(`effect-encore/${actorName}/${tag}/Payload`)(fields);
|
|
22
|
+
class PayloadClass extends Base {}
|
|
23
|
+
const proto = PayloadClass.prototype;
|
|
24
|
+
if (pkFn) proto[PrimaryKey.symbol] = function() {
|
|
25
|
+
return pkFn(this);
|
|
26
|
+
};
|
|
27
|
+
if (daFn) proto[DeliverAt.symbol] = function() {
|
|
28
|
+
return daFn(this);
|
|
29
|
+
};
|
|
30
|
+
options["payload"] = PayloadClass;
|
|
31
|
+
}
|
|
32
|
+
if (def["success"]) options["success"] = def["success"];
|
|
33
|
+
if (def["error"]) options["error"] = def["error"];
|
|
34
|
+
let rpc = Rpc.make(tag, options);
|
|
35
|
+
if (def["persisted"]) rpc = rpc.annotate(ClusterSchema.Persisted, true);
|
|
36
|
+
return rpc;
|
|
37
|
+
};
|
|
38
|
+
const make = (name, definitions) => {
|
|
39
|
+
for (const tag of Object.keys(definitions)) if (RESERVED_KEYS.has(tag)) throw new Error(`effect-encore: operation "${tag}" collides with reserved property. Reserved: ${[...RESERVED_KEYS].join(", ")}`);
|
|
40
|
+
const rpcs = Object.entries(definitions).map(([tag, def]) => compileRpc(name, tag, def));
|
|
41
|
+
const entity = Entity.make(name, rpcs);
|
|
42
|
+
const constructors = {};
|
|
43
|
+
for (const tag of Object.keys(definitions)) constructors[tag] = (input) => ({
|
|
44
|
+
_tag: tag,
|
|
45
|
+
...input != null && typeof input === "object" ? input : {}
|
|
46
|
+
});
|
|
47
|
+
const contextTag = Context.Service(`effect-encore/${name}/Client`);
|
|
48
|
+
const $is = (tag) => (value) => value != null && typeof value === "object" && "_tag" in value && value["_tag"] === tag;
|
|
49
|
+
return {
|
|
50
|
+
_tag: "ActorObject",
|
|
51
|
+
_meta: {
|
|
52
|
+
name,
|
|
53
|
+
definitions,
|
|
54
|
+
entity
|
|
55
|
+
},
|
|
56
|
+
Context: contextTag,
|
|
57
|
+
$is,
|
|
58
|
+
...constructors
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
const toLayer = (actor, build, options) => {
|
|
62
|
+
const transformed = transformHandlers(build);
|
|
63
|
+
return actor._meta.entity.toLayer(transformed, {
|
|
64
|
+
spanAttributes: options?.spanAttributes,
|
|
65
|
+
maxIdleTime: options?.maxIdleTime,
|
|
66
|
+
concurrency: options?.concurrency,
|
|
67
|
+
mailboxCapacity: options?.mailboxCapacity
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
const Live = (actor) => Layer.effect(actor.Context, Effect.map(actor._meta.entity.client, (makeClient) => (entityId) => buildActorRef(actor._meta.name, entityId, actor._meta.definitions, makeClient(entityId))));
|
|
71
|
+
const Test = (actor, handlerLayer) => Effect.map(Entity.makeTestClient(actor._meta.entity, handlerLayer), (makeClient) => (entityId) => Effect.map(makeClient(entityId), (rpcClient) => buildActorRef(actor._meta.name, entityId, actor._meta.definitions, rpcClient)));
|
|
72
|
+
const transformHandlers = (build) => {
|
|
73
|
+
if (build != null && typeof build === "object" && !Effect.isEffect(build)) {
|
|
74
|
+
const handlers = build;
|
|
75
|
+
const transformed = {};
|
|
76
|
+
for (const tag of Object.keys(handlers)) {
|
|
77
|
+
const handler = handlers[tag];
|
|
78
|
+
if (!handler) continue;
|
|
79
|
+
transformed[tag] = (request) => {
|
|
80
|
+
return handler({
|
|
81
|
+
operation: {
|
|
82
|
+
_tag: tag,
|
|
83
|
+
...request["payload"] ?? {}
|
|
84
|
+
},
|
|
85
|
+
request
|
|
86
|
+
});
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
return transformed;
|
|
90
|
+
}
|
|
91
|
+
return Effect.map(build, transformHandlers);
|
|
92
|
+
};
|
|
93
|
+
const buildActorRef = (actorName, entityId, definitions, rpcClient) => {
|
|
94
|
+
const client = rpcClient;
|
|
95
|
+
return {
|
|
96
|
+
call: (op) => {
|
|
97
|
+
const tag = op["_tag"];
|
|
98
|
+
const fn = client[tag];
|
|
99
|
+
return definitions[tag]?.["payload"] !== void 0 ? fn?.(op) : fn?.();
|
|
100
|
+
},
|
|
101
|
+
cast: (op) => {
|
|
102
|
+
const tag = op["_tag"];
|
|
103
|
+
const fn = client[tag];
|
|
104
|
+
const def = definitions[tag];
|
|
105
|
+
const discardCall = def?.["payload"] !== void 0 ? fn?.(op, { discard: true }) : fn?.(void 0, { discard: true });
|
|
106
|
+
return Effect.map(discardCall ?? Effect.void, () => makeCastReceipt({
|
|
107
|
+
actorType: actorName,
|
|
108
|
+
entityId,
|
|
109
|
+
operation: tag,
|
|
110
|
+
primaryKey: def?.["primaryKey"] ? def["primaryKey"](op) : void 0
|
|
111
|
+
}));
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
};
|
|
115
|
+
const Actor = {
|
|
116
|
+
make,
|
|
117
|
+
toLayer,
|
|
118
|
+
Live,
|
|
119
|
+
Test
|
|
120
|
+
};
|
|
121
|
+
const fromRpcs = (name, rpcs) => ({
|
|
122
|
+
_tag: "RawActorDefinition",
|
|
123
|
+
name,
|
|
124
|
+
entity: Entity.make(name, rpcs)
|
|
125
|
+
});
|
|
126
|
+
//#endregion
|
|
127
|
+
export { Actor, fromRpcs };
|
package/dist/client.d.ts
ADDED
package/dist/client.js
ADDED
|
File without changes
|
package/dist/handlers.js
ADDED
|
File without changes
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { CastReceipt, Defect, Failure, Interrupted, PeekResult, Pending, Success, isFailure, isPending, isSuccess, isTerminal, makeCastReceipt } from "./receipt.js";
|
|
2
|
+
import { Actor, ActorMeta, ActorObject, ActorRef, HandlerOptions, OperationBrand, OperationDef, OperationDefs, OperationError, OperationOutput, fromRpcs } from "./actor.js";
|
|
3
|
+
import { NoPrimaryKeyError, peek, watch } from "./peek.js";
|
|
4
|
+
import { observability_d_exports } from "./observability.js";
|
|
5
|
+
import { workflow_d_exports } from "./workflow.js";
|
|
6
|
+
export { Actor, type ActorMeta, type ActorObject, type ActorRef, CastReceipt, Defect, Failure, type HandlerOptions, Interrupted, NoPrimaryKeyError, observability_d_exports as Observability, type OperationBrand, type OperationDef, type OperationDefs, type OperationError, type OperationOutput, type PeekResult, Pending, Success, workflow_d_exports as Workflow, fromRpcs, isFailure, isPending, isSuccess, isTerminal, makeCastReceipt, peek, watch };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { CastReceipt, Defect, Failure, Interrupted, Pending, Success, isFailure, isPending, isSuccess, isTerminal, makeCastReceipt } from "./receipt.js";
|
|
2
|
+
import { Actor, fromRpcs } from "./actor.js";
|
|
3
|
+
import { NoPrimaryKeyError, peek, watch } from "./peek.js";
|
|
4
|
+
import { observability_exports } from "./observability.js";
|
|
5
|
+
import { workflow_exports } from "./workflow.js";
|
|
6
|
+
export { Actor, CastReceipt, Defect, Failure, Interrupted, NoPrimaryKeyError, observability_exports as Observability, Pending, Success, workflow_exports as Workflow, fromRpcs, isFailure, isPending, isSuccess, isTerminal, makeCastReceipt, peek, watch };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { CurrentAddress } from "effect/unstable/cluster/Entity";
|
|
2
|
+
|
|
3
|
+
//#region src/observability.d.ts
|
|
4
|
+
declare namespace observability_d_exports {
|
|
5
|
+
export { CurrentAddress, defaultSpanAttributes };
|
|
6
|
+
}
|
|
7
|
+
declare const defaultSpanAttributes: (actorName: string) => Record<string, string>;
|
|
8
|
+
//#endregion
|
|
9
|
+
export { CurrentAddress, defaultSpanAttributes, observability_d_exports };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { __exportAll } from "./_virtual/_rolldown/runtime.js";
|
|
2
|
+
import { CurrentAddress } from "effect/unstable/cluster/Entity";
|
|
3
|
+
//#region src/observability.ts
|
|
4
|
+
var observability_exports = /* @__PURE__ */ __exportAll({
|
|
5
|
+
CurrentAddress: () => CurrentAddress,
|
|
6
|
+
defaultSpanAttributes: () => defaultSpanAttributes
|
|
7
|
+
});
|
|
8
|
+
const defaultSpanAttributes = (actorName) => ({
|
|
9
|
+
"actor.name": actorName,
|
|
10
|
+
"actor.library": "effect-encore"
|
|
11
|
+
});
|
|
12
|
+
//#endregion
|
|
13
|
+
export { CurrentAddress, defaultSpanAttributes, observability_exports };
|
package/dist/peek.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { CastReceipt, PeekResult } from "./receipt.js";
|
|
2
|
+
import { ActorObject, OperationDefs } from "./actor.js";
|
|
3
|
+
import { MessageStorage, Sharding } from "effect/unstable/cluster";
|
|
4
|
+
import { Duration, Effect, Stream } from "effect";
|
|
5
|
+
import { MalformedMessage, PersistenceError } from "effect/unstable/cluster/ClusterError";
|
|
6
|
+
|
|
7
|
+
//#region src/peek.d.ts
|
|
8
|
+
declare class NoPrimaryKeyError {
|
|
9
|
+
readonly receipt: CastReceipt;
|
|
10
|
+
readonly _tag = "NoPrimaryKeyError";
|
|
11
|
+
readonly message: string;
|
|
12
|
+
constructor(receipt: CastReceipt);
|
|
13
|
+
}
|
|
14
|
+
declare const peek: <Name extends string, Defs extends OperationDefs>(actor: ActorObject<Name, Defs>, receipt: CastReceipt) => Effect.Effect<PeekResult, PersistenceError | MalformedMessage | NoPrimaryKeyError, MessageStorage.MessageStorage | Sharding.Sharding>;
|
|
15
|
+
declare const watch: <Name extends string, Defs extends OperationDefs>(actor: ActorObject<Name, Defs>, receipt: CastReceipt, options?: {
|
|
16
|
+
readonly interval?: Duration.Input;
|
|
17
|
+
}) => Stream.Stream<PeekResult, PersistenceError | MalformedMessage | NoPrimaryKeyError, MessageStorage.MessageStorage | Sharding.Sharding>;
|
|
18
|
+
//#endregion
|
|
19
|
+
export { NoPrimaryKeyError, peek, watch };
|
package/dist/peek.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Defect, Failure, Interrupted, Pending, Success, isTerminal } from "./receipt.js";
|
|
2
|
+
import { EntityAddress, EntityId, EntityType, MessageStorage, Sharding } from "effect/unstable/cluster";
|
|
3
|
+
import { Duration, Effect, Option, Schedule, Stream } from "effect";
|
|
4
|
+
//#region src/peek.ts
|
|
5
|
+
var NoPrimaryKeyError = class {
|
|
6
|
+
constructor(receipt) {
|
|
7
|
+
this.receipt = receipt;
|
|
8
|
+
this._tag = "NoPrimaryKeyError";
|
|
9
|
+
this.message = `Cannot peek receipt for ${receipt.actorType}.${receipt.operation}: no primaryKey defined on operation`;
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
const peek = (actor, receipt) => {
|
|
13
|
+
const op = actor._meta.definitions[receipt.operation];
|
|
14
|
+
if (!op || !op["primaryKey"] || !receipt.primaryKey) return Effect.fail(new NoPrimaryKeyError(receipt));
|
|
15
|
+
const primaryKey = receipt.primaryKey;
|
|
16
|
+
return Effect.gen(function* () {
|
|
17
|
+
const sharding = yield* Sharding.Sharding;
|
|
18
|
+
const storage = yield* MessageStorage.MessageStorage;
|
|
19
|
+
const entityId = EntityId.make(receipt.entityId);
|
|
20
|
+
const group = actor._meta.entity.getShardGroup(entityId);
|
|
21
|
+
const shardId = sharding.getShardId(entityId, group);
|
|
22
|
+
const address = EntityAddress.make({
|
|
23
|
+
entityType: EntityType.make(actor._meta.name),
|
|
24
|
+
entityId,
|
|
25
|
+
shardId
|
|
26
|
+
});
|
|
27
|
+
const maybeRequestId = yield* storage.requestIdForPrimaryKey({
|
|
28
|
+
address,
|
|
29
|
+
tag: receipt.operation,
|
|
30
|
+
id: primaryKey
|
|
31
|
+
});
|
|
32
|
+
if (Option.isNone(maybeRequestId)) return Pending;
|
|
33
|
+
const replies = yield* storage.repliesForUnfiltered([maybeRequestId.value]);
|
|
34
|
+
const last = replies[replies.length - 1];
|
|
35
|
+
if (!last || last._tag !== "WithExit") return Pending;
|
|
36
|
+
return mapExitToPeekResult(last.exit);
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
const watch = (actor, receipt, options) => {
|
|
40
|
+
const interval = options?.interval ?? Duration.millis(200);
|
|
41
|
+
return Stream.fromEffectSchedule(peek(actor, receipt), Schedule.spaced(interval)).pipe(Stream.changesWith(peekResultEquals), Stream.takeUntil(isTerminal));
|
|
42
|
+
};
|
|
43
|
+
const peekResultEquals = (a, b) => {
|
|
44
|
+
if (a._tag !== b._tag) return false;
|
|
45
|
+
if (a._tag === "Success" && b._tag === "Success") return a.value === b.value;
|
|
46
|
+
if (a._tag === "Failure" && b._tag === "Failure") return a.error === b.error;
|
|
47
|
+
if (a._tag === "Defect" && b._tag === "Defect") return a.cause === b.cause;
|
|
48
|
+
return true;
|
|
49
|
+
};
|
|
50
|
+
const mapExitToPeekResult = (exit) => {
|
|
51
|
+
if (exit._tag === "Success") return Success(exit.value);
|
|
52
|
+
const cause = exit.cause[0];
|
|
53
|
+
if (!cause) return Pending;
|
|
54
|
+
switch (cause._tag) {
|
|
55
|
+
case "Fail": return Failure(cause.error);
|
|
56
|
+
case "Die": return Defect(cause.defect);
|
|
57
|
+
case "Interrupt": return Interrupted;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
//#endregion
|
|
61
|
+
export { NoPrimaryKeyError, peek, watch };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
|
|
3
|
+
//#region src/receipt.d.ts
|
|
4
|
+
declare const CastReceipt_base: Schema.Class<CastReceipt, Schema.Struct<{
|
|
5
|
+
readonly _tag: Schema.Literal<"CastReceipt">;
|
|
6
|
+
readonly actorType: Schema.String;
|
|
7
|
+
readonly entityId: Schema.String;
|
|
8
|
+
readonly operation: Schema.String;
|
|
9
|
+
readonly primaryKey: Schema.optional<Schema.String>;
|
|
10
|
+
}>, {}>;
|
|
11
|
+
declare class CastReceipt extends CastReceipt_base {}
|
|
12
|
+
declare const makeCastReceipt: (options: {
|
|
13
|
+
readonly actorType: string;
|
|
14
|
+
readonly entityId: string;
|
|
15
|
+
readonly operation: string;
|
|
16
|
+
readonly primaryKey?: string | undefined;
|
|
17
|
+
}) => CastReceipt;
|
|
18
|
+
type PeekResult<A = unknown, E = unknown> = {
|
|
19
|
+
readonly _tag: "Pending";
|
|
20
|
+
} | {
|
|
21
|
+
readonly _tag: "Success";
|
|
22
|
+
readonly value: A;
|
|
23
|
+
} | {
|
|
24
|
+
readonly _tag: "Failure";
|
|
25
|
+
readonly error: E;
|
|
26
|
+
} | {
|
|
27
|
+
readonly _tag: "Interrupted";
|
|
28
|
+
} | {
|
|
29
|
+
readonly _tag: "Defect";
|
|
30
|
+
readonly cause: unknown;
|
|
31
|
+
};
|
|
32
|
+
declare const Pending: PeekResult;
|
|
33
|
+
declare const Success: <A>(value: A) => PeekResult<A, never>;
|
|
34
|
+
declare const Failure: <E>(error: E) => PeekResult<never, E>;
|
|
35
|
+
declare const Interrupted: PeekResult;
|
|
36
|
+
declare const Defect: (cause: unknown) => PeekResult;
|
|
37
|
+
declare const isPending: <A, E>(result: PeekResult<A, E>) => result is {
|
|
38
|
+
_tag: "Pending";
|
|
39
|
+
};
|
|
40
|
+
declare const isSuccess: <A, E>(result: PeekResult<A, E>) => result is {
|
|
41
|
+
_tag: "Success";
|
|
42
|
+
value: A;
|
|
43
|
+
};
|
|
44
|
+
declare const isFailure: <A, E>(result: PeekResult<A, E>) => result is {
|
|
45
|
+
_tag: "Failure";
|
|
46
|
+
error: E;
|
|
47
|
+
};
|
|
48
|
+
declare const isTerminal: <A, E>(result: PeekResult<A, E>) => boolean;
|
|
49
|
+
//#endregion
|
|
50
|
+
export { CastReceipt, Defect, Failure, Interrupted, PeekResult, Pending, Success, isFailure, isPending, isSuccess, isTerminal, makeCastReceipt };
|
package/dist/receipt.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
//#region src/receipt.ts
|
|
3
|
+
var CastReceipt = class extends Schema.Class("effect-encore/CastReceipt")({
|
|
4
|
+
_tag: Schema.Literal("CastReceipt"),
|
|
5
|
+
actorType: Schema.String,
|
|
6
|
+
entityId: Schema.String,
|
|
7
|
+
operation: Schema.String,
|
|
8
|
+
primaryKey: Schema.optional(Schema.String)
|
|
9
|
+
}) {};
|
|
10
|
+
const makeCastReceipt = (options) => new CastReceipt({
|
|
11
|
+
_tag: "CastReceipt",
|
|
12
|
+
actorType: options.actorType,
|
|
13
|
+
entityId: options.entityId,
|
|
14
|
+
operation: options.operation,
|
|
15
|
+
primaryKey: options.primaryKey
|
|
16
|
+
});
|
|
17
|
+
const Pending = { _tag: "Pending" };
|
|
18
|
+
const Success = (value) => ({
|
|
19
|
+
_tag: "Success",
|
|
20
|
+
value
|
|
21
|
+
});
|
|
22
|
+
const Failure = (error) => ({
|
|
23
|
+
_tag: "Failure",
|
|
24
|
+
error
|
|
25
|
+
});
|
|
26
|
+
const Interrupted = { _tag: "Interrupted" };
|
|
27
|
+
const Defect = (cause) => ({
|
|
28
|
+
_tag: "Defect",
|
|
29
|
+
cause
|
|
30
|
+
});
|
|
31
|
+
const isPending = (result) => result._tag === "Pending";
|
|
32
|
+
const isSuccess = (result) => result._tag === "Success";
|
|
33
|
+
const isFailure = (result) => result._tag === "Failure";
|
|
34
|
+
const isTerminal = (result) => result._tag !== "Pending";
|
|
35
|
+
//#endregion
|
|
36
|
+
export { CastReceipt, Defect, Failure, Interrupted, Pending, Success, isFailure, isPending, isSuccess, isTerminal, makeCastReceipt };
|
package/dist/testing.js
ADDED
|
File without changes
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Effect, Layer } from "effect";
|
|
2
|
+
import { Activity, DurableDeferred, Workflow } from "effect/unstable/workflow";
|
|
3
|
+
|
|
4
|
+
//#region src/workflow.d.ts
|
|
5
|
+
declare namespace workflow_d_exports {
|
|
6
|
+
export { Activity, DurableDeferred, WorkflowDefinition, WorkflowReceipt, WorkflowRef, makeWorkflowReceipt, workflow, workflowClient, workflowHandlers, workflowPoll };
|
|
7
|
+
}
|
|
8
|
+
interface WorkflowDefinition<Name extends string = string> {
|
|
9
|
+
readonly _tag: "WorkflowDefinition";
|
|
10
|
+
readonly name: Name;
|
|
11
|
+
readonly workflow: Workflow.Any;
|
|
12
|
+
}
|
|
13
|
+
interface WorkflowReceipt<Type extends string = string> {
|
|
14
|
+
readonly _tag: "WorkflowReceipt";
|
|
15
|
+
readonly workflowName: Type;
|
|
16
|
+
readonly executionId: string;
|
|
17
|
+
}
|
|
18
|
+
declare const makeWorkflowReceipt: <Type extends string>(workflowName: Type, executionId: string) => WorkflowReceipt<Type>;
|
|
19
|
+
declare const workflow: <const Name extends string>(name: Name, options: {
|
|
20
|
+
readonly payload: Record<string, unknown>;
|
|
21
|
+
readonly idempotencyKey: (payload: Record<string, unknown>) => string;
|
|
22
|
+
readonly success?: unknown;
|
|
23
|
+
readonly error?: unknown;
|
|
24
|
+
}) => WorkflowDefinition<Name>;
|
|
25
|
+
type WorkflowRef<Name extends string = string> = {
|
|
26
|
+
readonly call: (payload: unknown) => Effect.Effect<unknown, unknown>;
|
|
27
|
+
readonly cast: (payload: unknown) => Effect.Effect<WorkflowReceipt<Name>, unknown>;
|
|
28
|
+
readonly interrupt: () => Effect.Effect<void>;
|
|
29
|
+
readonly resume: () => Effect.Effect<void>;
|
|
30
|
+
};
|
|
31
|
+
declare const workflowClient: <Name extends string>(def: WorkflowDefinition<Name>) => ((executionId: string) => WorkflowRef<Name>);
|
|
32
|
+
declare const workflowPoll: <Name extends string>(def: WorkflowDefinition<Name>, executionId: string) => Effect.Effect<{
|
|
33
|
+
_tag: "Pending";
|
|
34
|
+
} | {
|
|
35
|
+
_tag: "Success";
|
|
36
|
+
value: unknown;
|
|
37
|
+
} | {
|
|
38
|
+
_tag: "Failure";
|
|
39
|
+
error: unknown;
|
|
40
|
+
} | {
|
|
41
|
+
_tag: "Suspended";
|
|
42
|
+
}, never, never>;
|
|
43
|
+
declare const workflowHandlers: <Name extends string>(def: WorkflowDefinition<Name>, handler: Function) => Layer.Layer<never, never, never>;
|
|
44
|
+
//#endregion
|
|
45
|
+
export { Activity, DurableDeferred, WorkflowDefinition, WorkflowReceipt, WorkflowRef, makeWorkflowReceipt, workflow, workflowClient, workflowHandlers, workflowPoll, workflow_d_exports };
|