effect-cf 0.6.0 → 0.7.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.
@@ -1,5 +1,6 @@
1
- import { Effect, Schema as S, type Layer, type Scope } from "effect";
1
+ import { Context, Effect, Schema as S, type Layer, type Scope } from "effect";
2
2
 
3
+ import * as Binding from "./Binding";
3
4
  import type { WorkerEnvironment } from "./Environment";
4
5
  import * as QueueEntrypoint from "./Queue";
5
6
  import * as QueueBinding from "./QueueBinding";
@@ -19,15 +20,6 @@ export namespace Definition {
19
20
  export type Any = Definition<string, RpcDefinition.ServiceFreeSchema>;
20
21
  }
21
22
 
22
- declare const BindingServiceTypeId: unique symbol;
23
-
24
- export interface BindingService<Id extends string, Self extends Definition.Any> {
25
- readonly [BindingServiceTypeId]: {
26
- readonly id: Id;
27
- readonly definition: Self;
28
- };
29
- }
30
-
31
23
  export type Handler<ROut, Self extends Definition.Any> = (
32
24
  batch: QueueEntrypoint.QueueBatch<S.Schema.Type<Self["message"]>>,
33
25
  ) => Effect.Effect<
@@ -44,6 +36,44 @@ export interface Options<ROut, Self extends Definition.Any> extends Omit<
44
36
  readonly rpc?: never;
45
37
  }
46
38
 
39
+ export type LayerOptions = {
40
+ readonly binding: string;
41
+ };
42
+
43
+ export interface TagClass<
44
+ Self,
45
+ Id extends string,
46
+ Message extends RpcDefinition.ServiceFreeSchema,
47
+ > extends Context.ServiceClass<Self, Id, QueueBinding.QueueBindingClient<Message>> {
48
+ readonly id: Id;
49
+ readonly message: Message;
50
+ readonly make: <ROut, LayerError>(
51
+ layer: Layer.Layer<ROut, LayerError, ExecutionContext | WorkerContext | WorkerEnvironment>,
52
+ options: Options<ROut, Definition<Id, Message>>,
53
+ ) => WorkerEntrypoint.WorkerClass<Record<never, never>, ROut>;
54
+ readonly layer: (
55
+ options: LayerOptions,
56
+ ) => Layer.Layer<
57
+ Self,
58
+ Binding.BindingNotFoundError | Binding.BindingValidationError,
59
+ WorkerEnvironment
60
+ >;
61
+ readonly send: (
62
+ message: S.Schema.Type<Message>,
63
+ options?: QueueBinding.QueueSendOptions,
64
+ ) => Effect.Effect<void, QueueBinding.QueueOperationError | S.SchemaError, Self>;
65
+ readonly sendBatch: (
66
+ messages: Iterable<QueueBinding.MessageSendRequest<S.Schema.Type<Message>>>,
67
+ options?: QueueBinding.QueueSendBatchOptions,
68
+ ) => Effect.Effect<void, QueueBinding.QueueOperationError | S.SchemaError, Self>;
69
+ readonly metrics: () => Effect.Effect<
70
+ QueueBinding.QueueMetrics,
71
+ QueueBinding.QueueOperationError,
72
+ Self
73
+ >;
74
+ readonly unsafeRaw: () => Effect.Effect<globalThis.Queue<S.Codec.Encoded<Message>>, never, Self>;
75
+ }
76
+
47
77
  const makeDefinition = <Id extends string, Message extends RpcDefinition.ServiceFreeSchema>(
48
78
  id: Id,
49
79
  definition: { readonly message: Message },
@@ -63,49 +93,65 @@ const makeDefinition = <Id extends string, Message extends RpcDefinition.Service
63
93
  ...options,
64
94
  queue: wrapHandler(queueDefinition, options.queue),
65
95
  }),
66
- Binding:
67
- <Self>() =>
68
- <BindingId extends string>(
69
- bindingId: BindingId,
70
- binding: Omit<QueueBinding.QueueBindingDefinition<Message>, "message">,
71
- ) =>
72
- QueueBinding.Service<Self>()<BindingId, Message>(bindingId, {
73
- ...binding,
74
- message: definition.message,
75
- }),
76
- binding: <BindingId extends string>(
77
- bindingId: BindingId,
78
- binding: Omit<QueueBinding.QueueBindingDefinition<Message>, "message">,
79
- ) =>
80
- QueueBinding.Service<BindingService<BindingId, SelfDefinition>>()<BindingId, Message>(
81
- bindingId,
82
- {
83
- ...binding,
84
- message: definition.message,
85
- },
86
- ),
87
96
  });
88
97
  };
89
98
 
90
- export const make = makeDefinition;
99
+ export const make = <Id extends string, Message extends RpcDefinition.ServiceFreeSchema>(
100
+ id: Id,
101
+ definition: { readonly message: Message },
102
+ ) => Tag<Definition<Id, Message>>()(id, definition);
91
103
 
92
104
  export const Tag =
93
- <_Self>() =>
105
+ <Self>() =>
94
106
  <Id extends string, Message extends RpcDefinition.ServiceFreeSchema>(
95
107
  id: Id,
96
108
  definition: { readonly message: Message },
97
109
  ) => {
98
110
  const queueDefinition = makeDefinition(id, definition);
111
+ const tag = Context.Service<Self, QueueBinding.QueueBindingClient<Message>>()(id);
112
+
113
+ const layer = (binding: LayerOptions) =>
114
+ QueueBinding.layer(tag, {
115
+ ...binding,
116
+ message: definition.message,
117
+ });
118
+
119
+ const send = Effect.fnUntraced(function* (
120
+ message: S.Schema.Type<Message>,
121
+ options?: QueueBinding.QueueSendOptions,
122
+ ) {
123
+ const queue = yield* tag;
124
+ yield* queue.send(message, options);
125
+ });
99
126
 
100
- abstract class QueueDefinitionClass {
101
- static readonly id = queueDefinition.id;
102
- static readonly message = queueDefinition.message;
103
- static readonly make = queueDefinition.make;
104
- static readonly Binding = queueDefinition.Binding;
105
- static readonly binding = queueDefinition.binding;
106
- }
127
+ const sendBatch = Effect.fnUntraced(function* (
128
+ messages: Iterable<QueueBinding.MessageSendRequest<S.Schema.Type<Message>>>,
129
+ options?: QueueBinding.QueueSendBatchOptions,
130
+ ) {
131
+ const queue = yield* tag;
132
+ yield* queue.sendBatch(messages, options);
133
+ });
134
+
135
+ const metrics = Effect.fnUntraced(function* () {
136
+ const queue = yield* tag;
137
+ return yield* queue.metrics();
138
+ });
139
+
140
+ const unsafeRaw = Effect.fnUntraced(function* () {
141
+ const queue = yield* tag;
142
+ return yield* queue.unsafeRaw;
143
+ });
107
144
 
108
- return QueueDefinitionClass as (abstract new () => object) & typeof queueDefinition;
145
+ return Object.assign(tag, {
146
+ id: queueDefinition.id,
147
+ message: queueDefinition.message,
148
+ make: queueDefinition.make,
149
+ layer,
150
+ send,
151
+ sendBatch,
152
+ metrics,
153
+ unsafeRaw,
154
+ }) as TagClass<Self, Id, Message>;
109
155
  };
110
156
 
111
157
  export const Queue = Tag;
@@ -1,4 +1,4 @@
1
- import { Data, Effect, type Scope } from "effect";
1
+ import { Context, Data, Effect, type Scope } from "effect";
2
2
 
3
3
  import * as Binding from "./Binding";
4
4
  import * as CloudflareRpc from "./Rpc";
@@ -74,6 +74,10 @@ type ServiceMethodSuccess<Api, Method extends keyof Api> = RpcInvocation.AsyncMe
74
74
  Api,
75
75
  Method
76
76
  >;
77
+ type ServiceMethodCloudflareReturn<
78
+ Api,
79
+ Method extends keyof Api,
80
+ > = RpcInvocation.AsyncMethodCloudflareReturn<Api, Method>;
77
81
 
78
82
  type ServiceCall<R, Api> = <Method extends ServiceMethodKey<Api>>(
79
83
  method: Method,
@@ -98,31 +102,274 @@ export type ServiceBindingEffectClient<
98
102
  Api extends object,
99
103
  Definition extends WorkerDefinition.Definition.Any | undefined = undefined,
100
104
  > = DirectMethods<never, Definition> & {
105
+ /**
106
+ * Forwards an HTTP request to the bound Worker service.
107
+ *
108
+ * Use this when the service binding is acting as an HTTP origin rather than a
109
+ * Cloudflare RPC target.
110
+ *
111
+ * @example
112
+ * ```ts
113
+ * import { Effect } from "effect";
114
+ *
115
+ * const program = Effect.gen(function* () {
116
+ * const api = yield* ApiWorker;
117
+ * return yield* api.fetch(new Request("https://internal.example/users"));
118
+ * });
119
+ * ```
120
+ */
101
121
  readonly fetch: (
102
122
  input: RequestInfo | URL,
103
123
  init?: RequestInit,
104
124
  ) => Effect.Effect<globalThis.Response, ServiceBindingFetchError>;
125
+ /**
126
+ * Invokes a Worker RPC method and returns Cloudflare's raw RPC result.
127
+ *
128
+ * This preserves Cloudflare RPC behavior such as promise-like pipelining and
129
+ * transferable / disposable result objects. It does not resolve the returned
130
+ * promise-like value and it does not decode definition-backed success schemas.
131
+ *
132
+ * Most application code should use {@link call} instead.
133
+ *
134
+ * @example
135
+ * ```ts
136
+ * import { Effect } from "effect";
137
+ *
138
+ * const program = Effect.gen(function* () {
139
+ * const counter = yield* CounterService;
140
+ *
141
+ * const result = yield* counter.rpc("increment", 41);
142
+ * const value = yield* Effect.promise(() => result);
143
+ *
144
+ * return value;
145
+ * });
146
+ * ```
147
+ */
105
148
  readonly rpc: <Method extends ServiceMethodKey<Api>>(
106
149
  method: Method,
107
150
  ...args: ServiceMethodArgs<Api, Method>
108
- ) => Effect.Effect<ServiceMethodSuccess<Api, Method>, ServiceBindingRpcError>;
151
+ ) => Effect.Effect<ServiceMethodCloudflareReturn<Api, Method>, ServiceBindingRpcError>;
152
+ /**
153
+ * Invokes a Worker RPC method, resolves Cloudflare's RPC result, and decodes
154
+ * the success value when the binding was created from a definition.
155
+ *
156
+ * This is the normal choice when application code wants the final typed value.
157
+ *
158
+ * @example
159
+ * ```ts
160
+ * import { Effect } from "effect";
161
+ *
162
+ * const program = Effect.gen(function* () {
163
+ * const counter = yield* CounterService;
164
+ * const value = yield* counter.call("increment", 41);
165
+ *
166
+ * return value;
167
+ * });
168
+ * ```
169
+ */
109
170
  readonly call: <Method extends ServiceMethodKey<Api>>(
110
171
  method: Method,
111
172
  ...args: ServiceMethodArgs<Api, Method>
112
173
  ) => Effect.Effect<ServiceMethodSuccess<Api, Method>, ServiceBindingRpcError>;
174
+ /**
175
+ * Invokes a Worker RPC method in the current `Scope`, resolves Cloudflare's RPC
176
+ * result, decodes definition-backed success values, and disposes the resolved
177
+ * result when the scope closes if it implements `Symbol.dispose`.
178
+ *
179
+ * Use this for RPC methods that return Cloudflare RPC resources or other
180
+ * disposable objects whose lifetime should be tied to an Effect scope.
181
+ *
182
+ * @example
183
+ * ```ts
184
+ * import { Effect } from "effect";
185
+ *
186
+ * const program = Effect.scoped(
187
+ * Effect.gen(function* () {
188
+ * const files = yield* FileService;
189
+ * const handle = yield* files.scopedCall("open", "report.csv");
190
+ *
191
+ * return yield* handle.read();
192
+ * }),
193
+ * );
194
+ * ```
195
+ */
113
196
  readonly scopedCall: <Method extends ServiceMethodKey<Api>>(
114
197
  method: Method,
115
198
  ...args: ServiceMethodArgs<Api, Method>
116
199
  ) => Effect.Effect<Awaited<ServiceMethodSuccess<Api, Method>>, unknown, Scope.Scope>;
117
200
  };
118
201
 
202
+ export type ServiceBindingStaticClient<
203
+ R,
204
+ Api extends object,
205
+ Definition extends WorkerDefinition.Definition.Any | undefined = undefined,
206
+ > = DirectMethods<R, Definition> & {
207
+ readonly fetch: (
208
+ input: RequestInfo | URL,
209
+ init?: RequestInit,
210
+ ) => Effect.Effect<globalThis.Response, ServiceBindingFetchError, R>;
211
+ readonly rpc: <Method extends ServiceMethodKey<Api>>(
212
+ method: Method,
213
+ ...args: ServiceMethodArgs<Api, Method>
214
+ ) => Effect.Effect<ServiceMethodCloudflareReturn<Api, Method>, ServiceBindingRpcError, R>;
215
+ readonly call: <Method extends ServiceMethodKey<Api>>(
216
+ method: Method,
217
+ ...args: ServiceMethodArgs<Api, Method>
218
+ ) => Effect.Effect<ServiceMethodSuccess<Api, Method>, ServiceBindingRpcError, R>;
219
+ readonly scopedCall: <Method extends ServiceMethodKey<Api>>(
220
+ method: Method,
221
+ ...args: ServiceMethodArgs<Api, Method>
222
+ ) => Effect.Effect<Awaited<ServiceMethodSuccess<Api, Method>>, unknown, Scope.Scope | R>;
223
+ };
224
+
225
+ export const isServiceBindingClient = <Api extends object>(
226
+ value: unknown,
227
+ ): value is ServiceBindingClient<Api> =>
228
+ typeof value === "object" && value !== null && typeof Reflect.get(value, "fetch") === "function";
229
+
230
+ export const makeClient = <
231
+ Api extends object,
232
+ const Definition extends WorkerDefinition.Definition.Any | undefined = undefined,
233
+ >(
234
+ definition: ServiceBindingDefinition<Definition>,
235
+ ): ((service: ServiceBindingClient<Api>) => ServiceBindingEffectClient<Api, Definition>) => {
236
+ return (service: ServiceBindingClient<Api>) => {
237
+ const fetch = (input: RequestInfo | URL, init?: RequestInit) =>
238
+ Effect.tryPromise({
239
+ try: () => service.fetch(input, init),
240
+ catch: (cause) => new ServiceBindingFetchError({ binding: definition.binding, cause }),
241
+ });
242
+
243
+ const rpc = <Method extends ServiceMethodKey<Api>>(
244
+ method: Method,
245
+ ...args: ServiceMethodArgs<Api, Method>
246
+ ) =>
247
+ Effect.gen(function* () {
248
+ const methodName = String(method);
249
+ const encodedArgs =
250
+ definition.definition === undefined
251
+ ? args
252
+ : yield* RpcDefinition.encodeArgs(
253
+ definition.definition,
254
+ methodName as RpcDefinition.Definition.MethodNames<NonNullable<Definition>>,
255
+ args as never,
256
+ ).pipe(
257
+ Effect.mapError(
258
+ (cause) =>
259
+ new ServiceBindingRpcError({
260
+ binding: definition.binding,
261
+ method: methodName,
262
+ cause,
263
+ }),
264
+ ),
265
+ );
266
+
267
+ return yield* RpcInvocation.invokeRpcMethod(
268
+ service,
269
+ method,
270
+ encodedArgs as ServiceMethodArgs<Api, Method>,
271
+ (cause) =>
272
+ new ServiceBindingRpcError({
273
+ binding: definition.binding,
274
+ method: methodName,
275
+ cause,
276
+ }),
277
+ );
278
+ });
279
+
280
+ const decodeSuccess = <Method extends ServiceMethodKey<Api>>(
281
+ methodName: string,
282
+ value: Awaited<ServiceMethodCloudflareReturn<Api, Method>>,
283
+ ) =>
284
+ Effect.gen(function* () {
285
+ if (definition.definition === undefined) {
286
+ return value as ServiceMethodSuccess<Api, Method>;
287
+ }
288
+
289
+ const decoded = yield* RpcDefinition.decodeSuccess(
290
+ definition.definition,
291
+ methodName as RpcDefinition.Definition.MethodNames<NonNullable<Definition>>,
292
+ value,
293
+ ).pipe(
294
+ Effect.mapError(
295
+ (cause) =>
296
+ new ServiceBindingRpcError({
297
+ binding: definition.binding,
298
+ method: methodName,
299
+ cause,
300
+ }),
301
+ ),
302
+ );
303
+
304
+ return decoded as ServiceMethodSuccess<Api, Method>;
305
+ });
306
+
307
+ const call = <Method extends ServiceMethodKey<Api>>(
308
+ method: Method,
309
+ ...args: ServiceMethodArgs<Api, Method>
310
+ ) =>
311
+ Effect.gen(function* () {
312
+ const methodName = String(method);
313
+ const value = yield* CloudflareRpc.resolve(yield* rpc(method, ...args)).pipe(
314
+ Effect.mapError(
315
+ (cause) =>
316
+ new ServiceBindingRpcError({
317
+ binding: definition.binding,
318
+ method: methodName,
319
+ cause,
320
+ }),
321
+ ),
322
+ );
323
+
324
+ return yield* decodeSuccess<Method>(methodName, value);
325
+ });
326
+
327
+ const scopedCall = <Method extends ServiceMethodKey<Api>>(
328
+ method: Method,
329
+ ...args: ServiceMethodArgs<Api, Method>
330
+ ) =>
331
+ Effect.gen(function* () {
332
+ const methodName = String(method);
333
+ const result = yield* rpc(method, ...args);
334
+ const value = yield* CloudflareRpc.scoped(result);
335
+ return yield* decodeSuccess<Method>(methodName, value);
336
+ });
337
+
338
+ const directMethods = makeDirectMethods<never, Api, Definition>(definition.definition, call);
339
+
340
+ return Object.assign(directMethods, {
341
+ fetch,
342
+ rpc,
343
+ call,
344
+ scopedCall,
345
+ }) as ServiceBindingEffectClient<Api, Definition>;
346
+ };
347
+ };
348
+
349
+ export const layer = <
350
+ Self,
351
+ Api extends object,
352
+ const Definition extends WorkerDefinition.Definition.Any | undefined = undefined,
353
+ >(
354
+ tag: Context.Service<Self, ServiceBindingEffectClient<Api, Definition>>,
355
+ definition: ServiceBindingDefinition<Definition>,
356
+ ) =>
357
+ Binding.layer(
358
+ tag,
359
+ definition.binding,
360
+ (value): value is ServiceBindingClient<Api> => isServiceBindingClient<Api>(value),
361
+ makeClient<Api, Definition>(definition),
362
+ );
363
+
119
364
  /**
120
365
  * Creates a typed Effect service for a Worker service binding.
121
366
  *
122
367
  * Returned value includes:
123
368
  * - a Context tag for dependency injection
124
369
  * - `fetch(...)` for raw HTTP forwarding
125
- * - `call(...)` for generic RPC invocation
370
+ * - `rpc(...)` for raw Cloudflare RPC results
371
+ * - `call(...)` for resolved and decoded RPC results
372
+ * - `scopedCall(...)` for scoped and decoded disposable RPC results
126
373
  * - direct RPC methods when `definition` is provided
127
374
  *
128
375
  * @example
@@ -134,10 +381,11 @@ export type ServiceBindingEffectClient<
134
381
  * }),
135
382
  * });
136
383
  *
137
- * const CounterService = Counter.binding("COUNTER", { binding: "COUNTER" });
384
+ * const CounterLive = Counter.layer({ binding: "COUNTER" });
138
385
  *
139
386
  * const program = Effect.gen(function* () {
140
- * const next = yield* CounterService.increment(1);
387
+ * const counter = yield* Counter;
388
+ * const next = yield* counter.increment(1);
141
389
  * return next;
142
390
  * });
143
391
  * ```
@@ -153,119 +401,11 @@ export const Service =
153
401
  ) => {
154
402
  type ServiceApi = ApiOrDefinition<Api, Definition>;
155
403
 
156
- const makeClient = (
157
- service: ServiceBindingClient<ServiceApi>,
158
- ): ServiceBindingEffectClient<ServiceApi, Definition> => {
159
- const fetch = (input: RequestInfo | URL, init?: RequestInit) =>
160
- Effect.tryPromise({
161
- try: () => service.fetch(input, init),
162
- catch: (cause) => new ServiceBindingFetchError({ binding: definition.binding, cause }),
163
- });
164
-
165
- const rpc = <Method extends ServiceMethodKey<ServiceApi>>(
166
- method: Method,
167
- ...args: ServiceMethodArgs<ServiceApi, Method>
168
- ) =>
169
- Effect.gen(function* () {
170
- const methodName = String(method);
171
- const encodedArgs =
172
- definition.definition === undefined
173
- ? args
174
- : yield* RpcDefinition.encodeArgs(
175
- definition.definition,
176
- methodName as RpcDefinition.Definition.MethodNames<NonNullable<Definition>>,
177
- args as never,
178
- ).pipe(
179
- Effect.mapError(
180
- (cause) =>
181
- new ServiceBindingRpcError({
182
- binding: definition.binding,
183
- method: methodName,
184
- cause,
185
- }),
186
- ),
187
- );
188
-
189
- return yield* RpcInvocation.invokeRpcMethod(
190
- service,
191
- method,
192
- encodedArgs as ServiceMethodArgs<ServiceApi, Method>,
193
- (cause) =>
194
- new ServiceBindingRpcError({
195
- binding: definition.binding,
196
- method: methodName,
197
- cause,
198
- }),
199
- );
200
- });
201
-
202
- const call = <Method extends ServiceMethodKey<ServiceApi>>(
203
- method: Method,
204
- ...args: ServiceMethodArgs<ServiceApi, Method>
205
- ) =>
206
- Effect.gen(function* () {
207
- const methodName = String(method);
208
- const value = yield* CloudflareRpc.resolve(yield* rpc(method, ...args)).pipe(
209
- Effect.mapError(
210
- (cause) =>
211
- new ServiceBindingRpcError({
212
- binding: definition.binding,
213
- method: methodName,
214
- cause,
215
- }),
216
- ),
217
- );
218
-
219
- if (definition.definition === undefined) {
220
- return value as ServiceMethodSuccess<ServiceApi, Method>;
221
- }
222
-
223
- const decoded = yield* RpcDefinition.decodeSuccess(
224
- definition.definition,
225
- methodName as RpcDefinition.Definition.MethodNames<NonNullable<Definition>>,
226
- value,
227
- ).pipe(
228
- Effect.mapError(
229
- (cause) =>
230
- new ServiceBindingRpcError({
231
- binding: definition.binding,
232
- method: methodName,
233
- cause,
234
- }),
235
- ),
236
- );
237
-
238
- return decoded as ServiceMethodSuccess<ServiceApi, Method>;
239
- });
240
-
241
- const scopedCall = <Method extends ServiceMethodKey<ServiceApi>>(
242
- method: Method,
243
- ...args: ServiceMethodArgs<ServiceApi, Method>
244
- ) =>
245
- Effect.gen(function* () {
246
- const result = yield* rpc(method, ...args);
247
- return yield* CloudflareRpc.scoped(result);
248
- });
249
-
250
- const directMethods = makeDirectMethods<never, ServiceApi, Definition>(
251
- definition.definition,
252
- call,
253
- );
254
-
255
- return Object.assign(directMethods, {
256
- fetch,
257
- rpc,
258
- call,
259
- scopedCall,
260
- }) as ServiceBindingEffectClient<ServiceApi, Definition>;
261
- };
262
-
263
404
  const tag = Binding.Service<Self>()(
264
405
  id,
265
406
  definition.binding,
266
- (value): value is ServiceBindingClient<ServiceApi> =>
267
- typeof value === "object" && value !== null && "fetch" in value,
268
- makeClient,
407
+ (value): value is ServiceBindingClient<ServiceApi> => isServiceBindingClient(value),
408
+ makeClient<ServiceApi, Definition>(definition),
269
409
  );
270
410
 
271
411
  const fetch = (input: RequestInfo | URL, init?: RequestInit) =>
@@ -324,7 +464,11 @@ export const Service =
324
464
  };
325
465
  };
326
466
 
327
- const makeDirectMethods = <R, Api, Definition extends WorkerDefinition.Definition.Any | undefined>(
467
+ export const makeDirectMethods = <
468
+ R,
469
+ Api,
470
+ Definition extends WorkerDefinition.Definition.Any | undefined,
471
+ >(
328
472
  rpcDefinition: Definition | undefined,
329
473
  call: ServiceCall<R, Api>,
330
474
  ): DirectMethods<R, Definition> => {