@akterjs/sdk 0.0.0-stage → 0.1.1
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/LICENSE +201 -0
- package/README.md +105 -2
- package/dist/actor/json-document.schema.d.mts +14 -0
- package/dist/actor/json-document.schema.mjs +177 -0
- package/dist/actor/plain-actor.contract.d.mts +3 -0
- package/dist/actor/plain-actor.contract.mjs +63 -0
- package/dist/actor/plain-compile.contract.d.mts +100 -0
- package/dist/actor/plain-compile.contract.mjs +267 -0
- package/dist/actor/plain-definition.errors.d.mts +12 -0
- package/dist/actor/plain-definition.errors.mjs +10 -0
- package/dist/actor/plain-error.schema.d.mts +25 -0
- package/dist/actor/plain-error.schema.mjs +90 -0
- package/dist/actor/plain-handlers.contract.d.mts +87 -0
- package/dist/actor/plain-handlers.contract.mjs +94 -0
- package/dist/actor/plain-implement.contract.d.mts +27 -0
- package/dist/actor/plain-implement.contract.mjs +184 -0
- package/dist/actor/plain-types.contract.d.mts +316 -0
- package/dist/actor/plain-types.contract.mjs +1 -0
- package/dist/actor/plain-values.contract.d.mts +11 -0
- package/dist/actor/plain-values.contract.mjs +1 -0
- package/dist/actor/resource/plain-computers.contract.d.mts +89 -0
- package/dist/actor/resource/plain-computers.contract.mjs +34 -0
- package/dist/actor/resource/plain-file-access.contract.d.mts +38 -0
- package/dist/actor/resource/plain-file-access.contract.mjs +23 -0
- package/dist/actor/resource/plain-files.contract.d.mts +53 -0
- package/dist/actor/resource/plain-files.contract.mjs +47 -0
- package/dist/actor/standard-schema.schema.d.mts +76 -0
- package/dist/actor/standard-schema.schema.mjs +113 -0
- package/dist/local-server.adapter.d.mts +23 -0
- package/dist/local-server.adapter.mjs +43 -0
- package/dist/plain-client.adapter.d.mts +60 -0
- package/dist/plain-client.adapter.mjs +12 -0
- package/dist/plain-engine.layer.d.mts +9 -0
- package/dist/plain-engine.layer.mjs +10 -0
- package/dist/plain-sdk.contract.d.mts +7 -0
- package/dist/plain-sdk.contract.mjs +1 -0
- package/package.json +67 -3
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { DeclaredError, ErrorCodec } from "./plain-error.schema.mjs";
|
|
2
|
+
import { ActorContract, ActorOptions, CommandRef, JobRef, StateFields } from "./plain-types.contract.mjs";
|
|
3
|
+
import { JsonCodec, KeyCodec, StateCodec } from "./standard-schema.schema.mjs";
|
|
4
|
+
import { Option, Schema } from "effect";
|
|
5
|
+
import { Actor, ActorHistory, Akter, Command, DefinitionOf, FileAccess, Implementation, Query } from "@akterjs/effect/contract";
|
|
6
|
+
/** A payload or reply: a bridged validator, or nothing for a member declared without one. */
|
|
7
|
+
type ValueCodec = JsonCodec | typeof Schema.Void;
|
|
8
|
+
interface ErasedJobDeclaration {
|
|
9
|
+
readonly error: ErrorCodec;
|
|
10
|
+
readonly patches: readonly string[];
|
|
11
|
+
readonly payload: {
|
|
12
|
+
readonly input: ValueCodec;
|
|
13
|
+
};
|
|
14
|
+
readonly success: ValueCodec;
|
|
15
|
+
}
|
|
16
|
+
type ErasedCommand = Command<ValueCodec, ValueCodec, ErrorCodec, never>;
|
|
17
|
+
type ErasedQuery = Query<ValueCodec, ValueCodec, ErrorCodec, never>;
|
|
18
|
+
declare const erasedJob: (declaration: ErasedJobDeclaration) => ReturnType<typeof Akter.job<ErasedJobDeclaration>>;
|
|
19
|
+
type ErasedJob = ReturnType<typeof erasedJob>;
|
|
20
|
+
declare const PrincipalFields: Schema.StructWithRest<Schema.Struct<{
|
|
21
|
+
readonly id: Schema.brand<Schema.String, "PrincipalId">;
|
|
22
|
+
}>, readonly [Schema.$Record<Schema.String, Schema.Codec<Schema.Json, Schema.Json, never, never>>]>;
|
|
23
|
+
type PrincipalCodec = Schema.Codec<typeof PrincipalFields.Type, Schema.Json>;
|
|
24
|
+
interface ErasedMembers {
|
|
25
|
+
readonly callers: Readonly<Record<string, PrincipalCodec>>;
|
|
26
|
+
readonly commands: Readonly<Record<`command ${string}`, ErasedCommand>>;
|
|
27
|
+
readonly files: FileAccess;
|
|
28
|
+
readonly jobs: Readonly<Record<`job ${string}`, ErasedJob>>;
|
|
29
|
+
readonly key: KeyCodec;
|
|
30
|
+
readonly queries: Readonly<Record<`query ${string}`, ErasedQuery>>;
|
|
31
|
+
readonly state: StateCodec;
|
|
32
|
+
}
|
|
33
|
+
interface WithInitial extends ErasedMembers {
|
|
34
|
+
readonly initial: () => StateFields;
|
|
35
|
+
}
|
|
36
|
+
interface WithCreate extends ErasedMembers {
|
|
37
|
+
readonly create: JsonCodec;
|
|
38
|
+
}
|
|
39
|
+
type InitialDefinition = DefinitionOf<WithInitial>;
|
|
40
|
+
type InitialActor = Actor<string, DefinitionOf<WithInitial>> & ActorHistory<DefinitionOf<WithInitial>>;
|
|
41
|
+
type CreateActor = Actor<string, DefinitionOf<WithCreate>> & ActorHistory<DefinitionOf<WithCreate>>;
|
|
42
|
+
/** The Effect handlers an implementation compiles to. Both creation modes take the same command, query and result handlers. */
|
|
43
|
+
type ErasedImplementation = Implementation<DefinitionOf<WithInitial>>;
|
|
44
|
+
type CreateHandler = NonNullable<Implementation<DefinitionOf<WithCreate>>["create"]>;
|
|
45
|
+
type Compiled = {
|
|
46
|
+
readonly actor: CreateActor;
|
|
47
|
+
readonly mode: "create";
|
|
48
|
+
} | {
|
|
49
|
+
readonly actor: InitialActor;
|
|
50
|
+
readonly mode: "initial";
|
|
51
|
+
};
|
|
52
|
+
/** One actor compiled to the Effect SDK, with what the handler bridge needs besides the Effect actor. */
|
|
53
|
+
interface Compilation {
|
|
54
|
+
readonly compiled: Compiled;
|
|
55
|
+
readonly contract: ActorContract;
|
|
56
|
+
readonly errors: Readonly<Record<string, readonly DeclaredError[]>>;
|
|
57
|
+
readonly name: string;
|
|
58
|
+
}
|
|
59
|
+
/** What a client or app holds of an actor `Akter.make` defined. */
|
|
60
|
+
interface DefinedActor {
|
|
61
|
+
readonly contract: ActorContract;
|
|
62
|
+
readonly name: string;
|
|
63
|
+
}
|
|
64
|
+
interface Naming {
|
|
65
|
+
readonly actor: string;
|
|
66
|
+
readonly member: string;
|
|
67
|
+
}
|
|
68
|
+
interface Target extends Naming {
|
|
69
|
+
readonly compilation: () => Compilation;
|
|
70
|
+
}
|
|
71
|
+
interface Membered {
|
|
72
|
+
readonly name: string;
|
|
73
|
+
readonly options: ActorOptions;
|
|
74
|
+
}
|
|
75
|
+
/** Compiles a plain actor declaration to `Akter.make`, refusing a declaration it cannot store as JSON. */
|
|
76
|
+
declare const compile: (membered: Membered) => Compilation;
|
|
77
|
+
interface Refs {
|
|
78
|
+
readonly commands: Readonly<Record<string, CommandRef>>;
|
|
79
|
+
readonly jobs: Readonly<Record<string, JobRef>>;
|
|
80
|
+
}
|
|
81
|
+
declare const refsOf: (compilation: () => Compilation, members: Membered) => Refs;
|
|
82
|
+
declare const register: (actor: DefinedActor, compilation: Compilation) => void;
|
|
83
|
+
type AddressedCommand = CreateActor["commands"][`command ${string}`] | InitialActor["commands"][`command ${string}`];
|
|
84
|
+
type StartableJob = InitialActor["jobs"][`job ${string}`];
|
|
85
|
+
declare const addressedOf: (compilation: Compilation, member: string) => Option.Option<AddressedCommand>;
|
|
86
|
+
declare const startableOf: (compilation: Compilation, member: string) => Option.Option<StartableJob>;
|
|
87
|
+
declare const compilationOf: (actor: DefinedActor) => Option.Option<Compilation>;
|
|
88
|
+
declare const commandTargetOf: (ref: CommandRef) => Option.Option<Target>;
|
|
89
|
+
declare const jobTargetOf: (ref: JobRef) => Option.Option<Target>;
|
|
90
|
+
export declare const PlainCompiler: {
|
|
91
|
+
readonly addressedOf: typeof addressedOf;
|
|
92
|
+
readonly commandTargetOf: typeof commandTargetOf;
|
|
93
|
+
readonly compilationOf: typeof compilationOf;
|
|
94
|
+
readonly compile: typeof compile;
|
|
95
|
+
readonly jobTargetOf: typeof jobTargetOf;
|
|
96
|
+
readonly refsOf: typeof refsOf;
|
|
97
|
+
readonly register: typeof register;
|
|
98
|
+
readonly startableOf: typeof startableOf;
|
|
99
|
+
};
|
|
100
|
+
export type { AddressedCommand, Compilation, Compiled, CreateActor, CreateHandler, DefinedActor, ErasedImplementation, InitialActor, InitialDefinition, StartableJob, ValueCodec, WithCreate, WithInitial };
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { PlainErrors } from "./plain-error.schema.mjs";
|
|
2
|
+
import { PlainFileAccesses } from "./resource/plain-file-access.contract.mjs";
|
|
3
|
+
import { bridge, bridgeKey, bridgeState, refuse } from "./standard-schema.schema.mjs";
|
|
4
|
+
import { Array, Option, Record, Schema, SchemaTransformation } from "effect";
|
|
5
|
+
import { Akter } from "@akterjs/effect/contract";
|
|
6
|
+
// src/actor/plain-compile.contract.ts
|
|
7
|
+
const erasedJob = (declaration) => Akter.job(declaration);
|
|
8
|
+
const PrincipalFields = Schema.StructWithRest(Schema.Struct({ id: Akter.PrincipalId }), [Schema.Record(Schema.String, Schema.Json)]);
|
|
9
|
+
const nothing = {
|
|
10
|
+
codec: Schema.Void,
|
|
11
|
+
documents: Option.none()
|
|
12
|
+
};
|
|
13
|
+
const bridgedOrNothing = ({ declared, site }) => Option.match(Option.fromUndefinedOr(declared), {
|
|
14
|
+
onNone: () => nothing,
|
|
15
|
+
onSome: (validator) => {
|
|
16
|
+
const bridged = bridge({
|
|
17
|
+
site,
|
|
18
|
+
validator
|
|
19
|
+
});
|
|
20
|
+
return {
|
|
21
|
+
codec: bridged.codec,
|
|
22
|
+
documents: Option.some(bridged.documents)
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
const errorsOf = ({ actor, member }, classes) => Array.map(classes, (Class) => Option.getOrThrowWith(PlainErrors.declaredErrorOf(Class), () => refuse({
|
|
27
|
+
reasons: ["names an error class that Akter.error did not declare"],
|
|
28
|
+
site: {
|
|
29
|
+
actor,
|
|
30
|
+
member,
|
|
31
|
+
part: "errors"
|
|
32
|
+
}
|
|
33
|
+
})));
|
|
34
|
+
const errorContracts = (errors) => Record.fromEntries(Array.map(errors, (declared) => [declared.tag, declared.documents]));
|
|
35
|
+
const memberContract = (payload, success, errors) => ({
|
|
36
|
+
errors: errorContracts(errors),
|
|
37
|
+
...Option.match(payload.documents, {
|
|
38
|
+
onNone: () => ({}),
|
|
39
|
+
onSome: (documents) => ({ payload: documents })
|
|
40
|
+
}),
|
|
41
|
+
...Option.match(success.documents, {
|
|
42
|
+
onNone: () => ({}),
|
|
43
|
+
onSome: (documents) => ({ success: documents })
|
|
44
|
+
})
|
|
45
|
+
});
|
|
46
|
+
const memberOf = (naming, options) => {
|
|
47
|
+
const payload = bridgedOrNothing({
|
|
48
|
+
declared: options.payload,
|
|
49
|
+
site: {
|
|
50
|
+
...naming,
|
|
51
|
+
part: "payload"
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
const success = bridgedOrNothing({
|
|
55
|
+
declared: options.success,
|
|
56
|
+
site: {
|
|
57
|
+
...naming,
|
|
58
|
+
part: "success"
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
const errors = errorsOf(naming, options.errors ?? []);
|
|
62
|
+
const callers = Option.match(Option.fromUndefinedOr(options.callers), {
|
|
63
|
+
onNone: () => ({}),
|
|
64
|
+
onSome: (kinds) => ({ callers: kinds })
|
|
65
|
+
});
|
|
66
|
+
return {
|
|
67
|
+
contract: {
|
|
68
|
+
...memberContract(payload, success, errors),
|
|
69
|
+
...callers
|
|
70
|
+
},
|
|
71
|
+
declaration: {
|
|
72
|
+
...callers,
|
|
73
|
+
error: PlainErrors.errorsCodec(errors),
|
|
74
|
+
payload: payload.codec,
|
|
75
|
+
success: success.codec
|
|
76
|
+
},
|
|
77
|
+
errors
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
const erasedCommand = (declaration) => Akter.command(declaration);
|
|
81
|
+
const erasedQuery = (declaration) => Akter.query(declaration);
|
|
82
|
+
const principalCodec = (validator, site) => bridge({
|
|
83
|
+
site,
|
|
84
|
+
validator
|
|
85
|
+
}).codec.pipe(Schema.decodeTo(PrincipalFields, SchemaTransformation.passthrough({ strict: false })));
|
|
86
|
+
const userPrincipal = Akter.Principal.pipe(Schema.decodeTo(PrincipalFields, SchemaTransformation.passthrough({ strict: false })));
|
|
87
|
+
const commandTargets = /* @__PURE__ */ new WeakMap();
|
|
88
|
+
const jobTargets = /* @__PURE__ */ new WeakMap();
|
|
89
|
+
const callerSite = (actor, caller) => ({
|
|
90
|
+
actor,
|
|
91
|
+
member: caller,
|
|
92
|
+
part: "principal"
|
|
93
|
+
});
|
|
94
|
+
const contractOf = ({ name, options }, members) => ({
|
|
95
|
+
callers: Record.map(options.callers ?? {}, (validator, caller) => bridge({
|
|
96
|
+
site: callerSite(name, caller),
|
|
97
|
+
validator
|
|
98
|
+
}).documents),
|
|
99
|
+
commands: Record.map(members.commands, (member) => member.contract),
|
|
100
|
+
...Option.match(members.create, {
|
|
101
|
+
onNone: () => ({}),
|
|
102
|
+
onSome: (create) => ({ create: create.documents })
|
|
103
|
+
}),
|
|
104
|
+
jobs: Record.map(members.jobs, (member) => member.contract),
|
|
105
|
+
key: members.key.documents,
|
|
106
|
+
name,
|
|
107
|
+
queries: Record.map(members.queries, (member) => member.contract),
|
|
108
|
+
state: members.state.documents
|
|
109
|
+
});
|
|
110
|
+
const jobOf = (naming, options) => memberOf(naming, options);
|
|
111
|
+
const membersOf = ({ name, options }) => ({
|
|
112
|
+
commands: Record.map(options.commands, (command, member) => memberOf({
|
|
113
|
+
actor: name,
|
|
114
|
+
member
|
|
115
|
+
}, command.options)),
|
|
116
|
+
create: Option.map(Option.fromUndefinedOr(options.create), (validator) => bridge({
|
|
117
|
+
site: {
|
|
118
|
+
actor: name,
|
|
119
|
+
member: "create",
|
|
120
|
+
part: "input"
|
|
121
|
+
},
|
|
122
|
+
validator
|
|
123
|
+
})),
|
|
124
|
+
jobs: Record.map(options.jobs ?? {}, (job, member) => jobOf({
|
|
125
|
+
actor: name,
|
|
126
|
+
member
|
|
127
|
+
}, job.options)),
|
|
128
|
+
key: bridgeKey({
|
|
129
|
+
site: {
|
|
130
|
+
actor: name,
|
|
131
|
+
member: "key",
|
|
132
|
+
part: "key"
|
|
133
|
+
},
|
|
134
|
+
validator: options.key
|
|
135
|
+
}),
|
|
136
|
+
queries: Record.map(options.queries ?? {}, (query, member) => memberOf({
|
|
137
|
+
actor: name,
|
|
138
|
+
member
|
|
139
|
+
}, query.options)),
|
|
140
|
+
state: bridgeState({
|
|
141
|
+
site: {
|
|
142
|
+
actor: name,
|
|
143
|
+
member: "state",
|
|
144
|
+
part: "state"
|
|
145
|
+
},
|
|
146
|
+
validator: options.state
|
|
147
|
+
})
|
|
148
|
+
});
|
|
149
|
+
const erasedMembers = ({ name, options }, members) => ({
|
|
150
|
+
callers: Option.match(Option.fromUndefinedOr(options.callers), {
|
|
151
|
+
onNone: () => ({ user: userPrincipal }),
|
|
152
|
+
onSome: (callers) => Record.map(callers, (validator, caller) => principalCodec(validator, callerSite(name, caller)))
|
|
153
|
+
}),
|
|
154
|
+
commands: Record.map(members.commands, (declared) => erasedCommand(declared.declaration)),
|
|
155
|
+
files: PlainFileAccesses.compiledFileAccess(options.files),
|
|
156
|
+
jobs: Record.map(members.jobs, (declared, member) => erasedJob({
|
|
157
|
+
error: declared.declaration.error,
|
|
158
|
+
patches: Option.match(Record.get(options.jobs ?? {}, member), {
|
|
159
|
+
onNone: () => [],
|
|
160
|
+
onSome: (job) => job.options.patches ?? []
|
|
161
|
+
}),
|
|
162
|
+
payload: { input: declared.declaration.payload },
|
|
163
|
+
success: declared.declaration.success
|
|
164
|
+
})),
|
|
165
|
+
key: members.key.codec,
|
|
166
|
+
queries: Record.map(members.queries, (declared) => erasedQuery(declared.declaration)),
|
|
167
|
+
state: members.state.codec
|
|
168
|
+
});
|
|
169
|
+
const makeInitial = (name, options) => Akter.make(name, options);
|
|
170
|
+
const makeCreate = (name, options) => Akter.make(name, options);
|
|
171
|
+
const compiledOf = (membered, members) => {
|
|
172
|
+
const erased = erasedMembers(membered, members);
|
|
173
|
+
const creation = {
|
|
174
|
+
actor: membered.name,
|
|
175
|
+
member: "create",
|
|
176
|
+
part: "options"
|
|
177
|
+
};
|
|
178
|
+
const initial = Option.fromUndefinedOr(membered.options.initial);
|
|
179
|
+
if (Option.isSome(initial) && Option.isSome(members.create)) throw refuse({
|
|
180
|
+
reasons: ["declare initial or create, not both"],
|
|
181
|
+
site: creation
|
|
182
|
+
});
|
|
183
|
+
if (Option.isSome(initial)) return {
|
|
184
|
+
actor: makeInitial(membered.name, {
|
|
185
|
+
...erased,
|
|
186
|
+
initial: initial.value
|
|
187
|
+
}),
|
|
188
|
+
mode: "initial"
|
|
189
|
+
};
|
|
190
|
+
if (Option.isSome(members.create)) return {
|
|
191
|
+
actor: makeCreate(membered.name, {
|
|
192
|
+
...erased,
|
|
193
|
+
create: members.create.value.codec
|
|
194
|
+
}),
|
|
195
|
+
mode: "create"
|
|
196
|
+
};
|
|
197
|
+
throw refuse({
|
|
198
|
+
reasons: ["declare initial or create"],
|
|
199
|
+
site: creation
|
|
200
|
+
});
|
|
201
|
+
};
|
|
202
|
+
/** Compiles a plain actor declaration to `Akter.make`, refusing a declaration it cannot store as JSON. */
|
|
203
|
+
const compile = (membered) => {
|
|
204
|
+
const members = membersOf(membered);
|
|
205
|
+
return {
|
|
206
|
+
compiled: compiledOf(membered, members),
|
|
207
|
+
contract: contractOf(membered, members),
|
|
208
|
+
errors: Record.map({
|
|
209
|
+
...members.commands,
|
|
210
|
+
...members.queries,
|
|
211
|
+
...members.jobs
|
|
212
|
+
}, (member) => member.errors),
|
|
213
|
+
name: membered.name
|
|
214
|
+
};
|
|
215
|
+
};
|
|
216
|
+
const refsOf = (compilation, members) => ({
|
|
217
|
+
commands: Record.map(members.options.commands, (_command, member) => {
|
|
218
|
+
const ref = {
|
|
219
|
+
actor: members.name,
|
|
220
|
+
member
|
|
221
|
+
};
|
|
222
|
+
commandTargets.set(ref, {
|
|
223
|
+
actor: members.name,
|
|
224
|
+
compilation,
|
|
225
|
+
member
|
|
226
|
+
});
|
|
227
|
+
return ref;
|
|
228
|
+
}),
|
|
229
|
+
jobs: Record.map(members.options.jobs ?? {}, (_job, member) => {
|
|
230
|
+
const ref = {
|
|
231
|
+
actor: members.name,
|
|
232
|
+
member
|
|
233
|
+
};
|
|
234
|
+
jobTargets.set(ref, {
|
|
235
|
+
actor: members.name,
|
|
236
|
+
compilation,
|
|
237
|
+
member
|
|
238
|
+
});
|
|
239
|
+
return ref;
|
|
240
|
+
})
|
|
241
|
+
});
|
|
242
|
+
const compilations = /* @__PURE__ */ new WeakMap();
|
|
243
|
+
const register = (actor, compilation) => {
|
|
244
|
+
compilations.set(actor, compilation);
|
|
245
|
+
};
|
|
246
|
+
const addressedOf = (compilation, member) => {
|
|
247
|
+
const commands = compilation.compiled.actor.commands;
|
|
248
|
+
return Record.get(commands, member);
|
|
249
|
+
};
|
|
250
|
+
const startableOf = (compilation, member) => {
|
|
251
|
+
const jobs = compilation.compiled.actor.jobs;
|
|
252
|
+
return Record.get(jobs, member);
|
|
253
|
+
};
|
|
254
|
+
const compilationOf = (actor) => Option.fromUndefinedOr(compilations.get(actor));
|
|
255
|
+
const commandTargetOf = (ref) => Option.fromUndefinedOr(commandTargets.get(ref));
|
|
256
|
+
const jobTargetOf = (ref) => Option.fromUndefinedOr(jobTargets.get(ref));
|
|
257
|
+
const PlainCompiler = {
|
|
258
|
+
addressedOf,
|
|
259
|
+
commandTargetOf,
|
|
260
|
+
compilationOf,
|
|
261
|
+
compile,
|
|
262
|
+
jobTargetOf,
|
|
263
|
+
refsOf,
|
|
264
|
+
register,
|
|
265
|
+
startableOf
|
|
266
|
+
};
|
|
267
|
+
export { PlainCompiler };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
declare const PlainDefinitionError_base: Schema.Class<PlainDefinitionError, Schema.TaggedStruct<"PlainDefinitionError", {
|
|
3
|
+
readonly actor: Schema.String;
|
|
4
|
+
readonly member: Schema.String;
|
|
5
|
+
readonly message: Schema.String;
|
|
6
|
+
readonly part: Schema.String;
|
|
7
|
+
readonly reasons: Schema.$Array<Schema.String>;
|
|
8
|
+
}>, import("effect/Cause").YieldableError & {
|
|
9
|
+
readonly brand: unique symbol;
|
|
10
|
+
}>;
|
|
11
|
+
export declare class PlainDefinitionError extends PlainDefinitionError_base {}
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
// src/actor/plain-definition.errors.ts
|
|
3
|
+
var PlainDefinitionError = class extends Schema.TaggedError()("PlainDefinitionError", {
|
|
4
|
+
actor: Schema.String,
|
|
5
|
+
member: Schema.String,
|
|
6
|
+
message: Schema.String,
|
|
7
|
+
part: Schema.String,
|
|
8
|
+
reasons: Schema.Array(Schema.String)
|
|
9
|
+
}) {};
|
|
10
|
+
export { PlainDefinitionError };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { JsonNode } from "./json-document.schema.mjs";
|
|
2
|
+
import { Documents } from "./standard-schema.schema.mjs";
|
|
3
|
+
import { AnyErrorClass, ErrorInput, ErrorOutput, PlainErrorClass, Validator } from "./plain-types.contract.mjs";
|
|
4
|
+
import { Option, Schema } from "effect";
|
|
5
|
+
/** An error a plain handler throws: its class's instance, tagged on the wire. */
|
|
6
|
+
type PlainFailure = Error & {
|
|
7
|
+
readonly _tag: string;
|
|
8
|
+
};
|
|
9
|
+
type ErrorCodec = Schema.Codec<PlainFailure, Schema.Json>;
|
|
10
|
+
type ErrorConstructor = new (fields: JsonNode) => PlainFailure;
|
|
11
|
+
interface DeclaredError {
|
|
12
|
+
readonly Class: ErrorConstructor;
|
|
13
|
+
readonly codec: ErrorCodec;
|
|
14
|
+
readonly documents: Documents;
|
|
15
|
+
readonly tag: string;
|
|
16
|
+
}
|
|
17
|
+
declare function error<const Tag extends string, Fields extends Validator | undefined = undefined>(tag: Tag, fields?: Fields): PlainErrorClass<Tag, ErrorInput<Fields>, ErrorOutput<Fields>>;
|
|
18
|
+
declare const declaredErrorOf: (Class: AnyErrorClass) => Option.Option<DeclaredError>;
|
|
19
|
+
declare const errorsCodec: (declared: readonly DeclaredError[]) => ErrorCodec;
|
|
20
|
+
export declare const PlainErrors: {
|
|
21
|
+
readonly declaredErrorOf: typeof declaredErrorOf;
|
|
22
|
+
readonly error: typeof error;
|
|
23
|
+
readonly errorsCodec: typeof errorsCodec;
|
|
24
|
+
};
|
|
25
|
+
export type { DeclaredError, ErrorCodec, PlainFailure };
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { documentsOf, noFields, refuse, validating } from "./standard-schema.schema.mjs";
|
|
2
|
+
import { Array, Data, Effect, Option, Record, Schema, SchemaIssue, SchemaTransformation, pipe } from "effect";
|
|
3
|
+
// src/actor/plain-error.schema.ts
|
|
4
|
+
const declaredErrors = /* @__PURE__ */ new WeakMap();
|
|
5
|
+
const isStrings = Schema.is(Schema.Array(Schema.String));
|
|
6
|
+
const isJsonObject = Schema.is(Schema.JsonObject);
|
|
7
|
+
const objectOf = (node) => Option.liftPredicate(node, isJsonObject);
|
|
8
|
+
const taggedDocument = (tag, document) => {
|
|
9
|
+
const properties = pipe(Record.get(document, "properties"), Option.flatMap(objectOf), Option.getOrElse(() => ({})));
|
|
10
|
+
const required = pipe(Record.get(document, "required"), Option.filter(isStrings), Option.getOrElse(() => []));
|
|
11
|
+
return {
|
|
12
|
+
...document,
|
|
13
|
+
properties: {
|
|
14
|
+
...properties,
|
|
15
|
+
_tag: {
|
|
16
|
+
const: tag,
|
|
17
|
+
type: "string"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
required: Array.prepend(required, "_tag")
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
const issue = (message, input) => new SchemaIssue.InvalidValue({ message }, Option.some(input));
|
|
24
|
+
const fieldsOf = (tag, json) => Effect.mapError(Effect.fromOption(objectOf(json)), () => issue(`${tag}'s fields must be a JSON object`, json));
|
|
25
|
+
const taggedFields = (tag) => (json) => Effect.flatMap(fieldsOf(tag, json), (object) => {
|
|
26
|
+
if (Option.contains(Record.get(object, "_tag"), tag)) return Effect.succeed(Record.remove(object, "_tag"));
|
|
27
|
+
return Effect.fail(issue(`expected _tag ${tag}`, json));
|
|
28
|
+
});
|
|
29
|
+
const definedClass = (tag) => class extends Data.TaggedError(tag) {
|
|
30
|
+
constructor(fields) {
|
|
31
|
+
super(fields);
|
|
32
|
+
this.name = tag;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
const declare = ({ fields, tag }) => {
|
|
36
|
+
const Class = definedClass(tag);
|
|
37
|
+
const site = {
|
|
38
|
+
actor: tag,
|
|
39
|
+
member: "error",
|
|
40
|
+
part: "fields"
|
|
41
|
+
};
|
|
42
|
+
const documents = documentsOf({
|
|
43
|
+
site,
|
|
44
|
+
validator: fields
|
|
45
|
+
});
|
|
46
|
+
const check = validating(fields);
|
|
47
|
+
if (!Option.contains(Record.get(documents.output, "type"), "object")) throw refuse({
|
|
48
|
+
reasons: ["an error's fields must be an object"],
|
|
49
|
+
site
|
|
50
|
+
});
|
|
51
|
+
const output = taggedDocument(tag, documents.output);
|
|
52
|
+
return {
|
|
53
|
+
Class,
|
|
54
|
+
codec: Schema.Json.check(Schema.makeFilter(() => true, { toJsonSchema: () => output })).pipe(Schema.decodeTo(Schema.instanceOf(Class), SchemaTransformation.transformEffect({
|
|
55
|
+
decode: (json) => taggedFields(tag)(json).pipe(Effect.flatMap(check), Effect.flatMap((checked) => fieldsOf(tag, checked)), Effect.map((value) => new Class(value))),
|
|
56
|
+
encode: (instance) => Effect.map(Effect.flatMap(check(instance), (checked) => fieldsOf(tag, checked)), (value) => ({
|
|
57
|
+
...value,
|
|
58
|
+
_tag: tag
|
|
59
|
+
}))
|
|
60
|
+
}))),
|
|
61
|
+
documents: {
|
|
62
|
+
input: taggedDocument(tag, documents.input),
|
|
63
|
+
output
|
|
64
|
+
},
|
|
65
|
+
tag
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
function error(tag, fields) {
|
|
69
|
+
const declared = declare({
|
|
70
|
+
fields: fields ?? noFields,
|
|
71
|
+
tag
|
|
72
|
+
});
|
|
73
|
+
const Class = Object.assign(declared.Class, { _tag: tag });
|
|
74
|
+
declaredErrors.set(Class, declared);
|
|
75
|
+
return Class;
|
|
76
|
+
}
|
|
77
|
+
const declaredErrorOf = (Class) => Option.fromUndefinedOr(declaredErrors.get(Class));
|
|
78
|
+
const errorsCodec = (declared) => Array.match(declared, {
|
|
79
|
+
onEmpty: () => Schema.Never,
|
|
80
|
+
onNonEmpty: (all) => Array.match(Array.tailNonEmpty(all), {
|
|
81
|
+
onEmpty: () => Array.headNonEmpty(all).codec,
|
|
82
|
+
onNonEmpty: () => Schema.Union(Array.map(all, (each) => each.codec))
|
|
83
|
+
})
|
|
84
|
+
});
|
|
85
|
+
const PlainErrors = {
|
|
86
|
+
declaredErrorOf,
|
|
87
|
+
error,
|
|
88
|
+
errorsCodec
|
|
89
|
+
};
|
|
90
|
+
export { PlainErrors };
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { FileRequest } from "./resource/plain-file-access.contract.mjs";
|
|
2
|
+
import { DefinedActor, ErasedImplementation, ValueCodec } from "./plain-compile.contract.mjs";
|
|
3
|
+
import { Computers } from "./resource/plain-computers.contract.mjs";
|
|
4
|
+
import { Delay, Sql } from "./plain-values.contract.mjs";
|
|
5
|
+
import { Files, ReadingFiles } from "./resource/plain-files.contract.mjs";
|
|
6
|
+
import { DeclaredError, PlainFailure } from "./plain-error.schema.mjs";
|
|
7
|
+
import { CommandRef, FinishedJob, JobRef, JobResult, JobStep, Migrations as Migrations$1, StateFields, Validator } from "./plain-types.contract.mjs";
|
|
8
|
+
import { Effect, Exit, Schema } from "effect";
|
|
9
|
+
import { Akter, Migrations } from "@akterjs/effect/contract";
|
|
10
|
+
import { DurableDeferred } from "effect/workflow";
|
|
11
|
+
type CommandScope = Akter["Service"];
|
|
12
|
+
type CommandServices = Effect.Services<ReturnType<ErasedImplementation["commands"][`command ${string}`]>>;
|
|
13
|
+
type Run = <Success, Failure>(effect: Effect.Effect<Success, Failure, CommandServices>) => Success;
|
|
14
|
+
type ReadRun = <Success, Failure>(effect: Effect.Effect<Success, Failure, Akter>) => Success;
|
|
15
|
+
/** A payload or reply as the engine hands it over: a JSON value, or nothing for a member declared without one. */
|
|
16
|
+
type Value = ValueCodec["Type"];
|
|
17
|
+
/** The context every plain command, query, create and result handler is called with, before the facade types it per actor. */
|
|
18
|
+
interface ErasedContext {
|
|
19
|
+
readonly cancel?: (timer: string) => void;
|
|
20
|
+
/** A command's and a result handler's `Files`; a query's `ReadingFiles`. */
|
|
21
|
+
readonly files?: Files | ReadingFiles;
|
|
22
|
+
readonly id?: () => string;
|
|
23
|
+
readonly key: CommandScope["key"];
|
|
24
|
+
readonly now?: () => number;
|
|
25
|
+
readonly principal: CommandScope["principal"];
|
|
26
|
+
readonly schedule?: (delay: Delay, command: CommandRef, ...payload: readonly Schema.Json[]) => string;
|
|
27
|
+
readonly send?: (command: CommandRef, key: string, ...payload: readonly Schema.Json[]) => void;
|
|
28
|
+
readonly signal?: (job: string, name: string, value: Schema.Json) => void;
|
|
29
|
+
readonly sql: Sql;
|
|
30
|
+
readonly start?: (job: JobRef, ...payload: readonly Schema.Json[]) => string;
|
|
31
|
+
readonly state?: StateFields;
|
|
32
|
+
readonly update?: (patch: StateFields) => void;
|
|
33
|
+
}
|
|
34
|
+
type Handler = (ctx: ErasedContext, payload: Value) => Value;
|
|
35
|
+
type CreateHandler = (ctx: ErasedContext, input: Schema.Json) => StateFields;
|
|
36
|
+
interface ErasedSteps {
|
|
37
|
+
readonly computer: (actor: DefinedActor, key: string) => Computers;
|
|
38
|
+
readonly patched: (name: string) => boolean;
|
|
39
|
+
readonly run: (name: string, execute: (step: JobStep) => Promise<Schema.Json>) => Promise<Schema.Json>;
|
|
40
|
+
readonly sleep: (name: string, delay: Delay) => Promise<void>;
|
|
41
|
+
readonly waitFor: (name: string) => Promise<Schema.Json>;
|
|
42
|
+
}
|
|
43
|
+
type JobHandler = (payload: Value, steps: ErasedSteps) => Promise<Value>;
|
|
44
|
+
type ResultHandler = (ctx: ErasedContext, result: JobResult<Value, PlainFailure>, job: FinishedJob) => void;
|
|
45
|
+
interface StateUpgrade {
|
|
46
|
+
readonly from: Validator<StateFields, StateFields>;
|
|
47
|
+
readonly upgrade: (stored: StateFields) => StateFields;
|
|
48
|
+
}
|
|
49
|
+
/** A plain implementation with its handlers' types erased; the actor's `implement` types it. */
|
|
50
|
+
interface PlainHandlers {
|
|
51
|
+
readonly commands: Readonly<Record<string, Handler>>;
|
|
52
|
+
readonly create?: CreateHandler;
|
|
53
|
+
readonly files?: {
|
|
54
|
+
readonly authorize: (ctx: ErasedContext, request: FileRequest) => boolean;
|
|
55
|
+
};
|
|
56
|
+
readonly jobs?: Readonly<Record<string, JobHandler>>;
|
|
57
|
+
readonly migrations?: Migrations$1;
|
|
58
|
+
readonly queries?: Readonly<Record<string, Handler>>;
|
|
59
|
+
readonly results?: Readonly<Record<string, ResultHandler>>;
|
|
60
|
+
readonly state?: StateUpgrade;
|
|
61
|
+
}
|
|
62
|
+
declare const sqlOf: (scope: CommandScope, run: ReadRun) => Sql;
|
|
63
|
+
/** What a handler threw: a declared error fails the call, anything else is a defect. */
|
|
64
|
+
declare const failureOf: (errors: readonly DeclaredError[], cause: unknown) => Effect.Effect<never, PlainFailure>;
|
|
65
|
+
/** Runs a handler: a declared error it throws fails the call; anything else it throws, or a Promise it returns, is a defect. */
|
|
66
|
+
declare const settle: <Returned>(errors: readonly DeclaredError[], call: () => Returned) => Effect.Effect<Returned, PlainFailure>;
|
|
67
|
+
/** The durable deferred a plain job waits on with `steps.waitFor(name)` and a command fills with `ctx.signal`. */
|
|
68
|
+
declare const signalNamed: (name: string) => DurableDeferred.DurableDeferred<typeof Schema.Json>;
|
|
69
|
+
interface Contexting {
|
|
70
|
+
readonly run: Run;
|
|
71
|
+
readonly scope: CommandScope;
|
|
72
|
+
readonly state: StateFields;
|
|
73
|
+
readonly update: (patch: StateFields) => void;
|
|
74
|
+
}
|
|
75
|
+
declare const commandContext: ({ run, scope, state, update }: Contexting) => ErasedContext;
|
|
76
|
+
declare const migrationsOf: (migrations: Migrations$1) => Migrations;
|
|
77
|
+
declare const jobResultOf: (exit: Exit.Exit<Value, PlainFailure>) => JobResult<Value, PlainFailure>;
|
|
78
|
+
export declare const PlainHandlerBridge: {
|
|
79
|
+
readonly commandContext: typeof commandContext;
|
|
80
|
+
readonly failureOf: typeof failureOf;
|
|
81
|
+
readonly jobResultOf: typeof jobResultOf;
|
|
82
|
+
readonly migrationsOf: typeof migrationsOf;
|
|
83
|
+
readonly settle: typeof settle;
|
|
84
|
+
readonly signalNamed: typeof signalNamed;
|
|
85
|
+
readonly sqlOf: typeof sqlOf;
|
|
86
|
+
};
|
|
87
|
+
export type { CommandScope, ErasedContext, ErasedSteps, JobHandler, PlainHandlers, ReadRun, Run, Value };
|