effect-cf 0.26.0 → 0.28.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.
Files changed (67) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +8 -6
  3. package/dist/{Artifacts-DcoXcRP5.d.mts → Artifacts-DnHQA-Ax.d.mts} +42 -25
  4. package/dist/Artifacts-rq3hUKQE.mjs +257 -0
  5. package/dist/{Binding-COCtoJXa.d.mts → Binding-xJJJPmiD.d.mts} +6 -5
  6. package/dist/ComputerArtifacts.d.mts +55 -0
  7. package/dist/ComputerArtifacts.mjs +95 -0
  8. package/dist/ComputerWorkspace.d.mts +279 -0
  9. package/dist/ComputerWorkspace.mjs +577 -0
  10. package/dist/ComputerWorkspaceHost.d.mts +1 -1
  11. package/dist/ComputerWorkspaceHost.mjs +18 -16
  12. package/dist/{WorkflowBinding-D5iWzCUa.d.mts → DurableObjectState-B3uFY1eM.d.mts} +90 -141
  13. package/dist/{DurableObjectState-CEh0DVw7.mjs → DurableObjectState-BjkK2kwH.mjs} +4 -49
  14. package/dist/ErrorMessage-BM0K-Nik.mjs +46 -0
  15. package/dist/{Hyperdrive-6p59yyVk.d.mts → Hyperdrive-z-9fFz15.d.mts} +2 -2
  16. package/dist/HyperdrivePg.d.mts +2 -2
  17. package/dist/Vitest.d.mts +6 -4
  18. package/dist/Vitest.mjs +2 -1
  19. package/dist/WorkflowBinding-wZ-yGHow.d.mts +74 -0
  20. package/dist/index.d.mts +92 -398
  21. package/dist/index.mjs +323 -1105
  22. package/package.json +12 -1
  23. package/src/AiGateway.ts +62 -7
  24. package/src/AnalyticsEngine.ts +17 -13
  25. package/src/Artifacts.ts +111 -47
  26. package/src/Binding.ts +34 -23
  27. package/src/BrowserRendering.ts +72 -33
  28. package/src/CloudflareOtlp.ts +8 -5
  29. package/src/ComputerArtifacts.ts +17 -9
  30. package/src/ComputerWorkspace.ts +202 -98
  31. package/src/ComputerWorkspaceHost.ts +55 -27
  32. package/src/ContainerNamespace.ts +8 -9
  33. package/src/D1.ts +8 -14
  34. package/src/DurableObject.ts +27 -11
  35. package/src/DurableObjectAlarm.ts +17 -5
  36. package/src/DurableObjectDefinition.ts +89 -72
  37. package/src/DurableObjectNamespace.ts +26 -12
  38. package/src/DurableObjectRpcWebSocket.ts +9 -10
  39. package/src/DurableObjectState.ts +1 -0
  40. package/src/DurableObjectStorage.ts +5 -2
  41. package/src/DurableObjectWebSocket.ts +4 -1
  42. package/src/Email.ts +26 -25
  43. package/src/Hyperdrive.ts +4 -10
  44. package/src/Images.ts +12 -14
  45. package/src/Kv.ts +15 -17
  46. package/src/Queue.ts +2 -1
  47. package/src/QueueBinding.ts +7 -12
  48. package/src/QueueDefinition.ts +7 -2
  49. package/src/R2.ts +33 -24
  50. package/src/Rpc.ts +15 -21
  51. package/src/RpcDefinition.ts +33 -38
  52. package/src/ServiceBinding.ts +26 -8
  53. package/src/Vectorize.ts +45 -18
  54. package/src/Vitest.ts +9 -3
  55. package/src/WebTransport.ts +11 -5
  56. package/src/Worker.ts +91 -21
  57. package/src/WorkerDefinition.ts +143 -63
  58. package/src/WorkersAi.ts +50 -28
  59. package/src/Workflow.ts +66 -13
  60. package/src/WorkflowBinding.ts +18 -25
  61. package/src/WorkflowDefinition.ts +4 -2
  62. package/src/index.ts +0 -2
  63. package/src/internal/Entrypoint.ts +29 -13
  64. package/src/internal/ErrorMessage.ts +7 -6
  65. package/src/internal/RpcInvocation.ts +34 -11
  66. package/src/internal/Runtime.ts +33 -22
  67. package/src/internal/WorkerContext.ts +3 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "effect-cf",
3
- "version": "0.26.0",
3
+ "version": "0.28.0",
4
4
  "description": "Effect-native primitives for Cloudflare Workers and bindings.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -27,6 +27,16 @@
27
27
  "import": "./dist/HyperdrivePg.mjs",
28
28
  "default": "./dist/HyperdrivePg.mjs"
29
29
  },
30
+ "./computer-artifacts": {
31
+ "types": "./dist/ComputerArtifacts.d.mts",
32
+ "import": "./dist/ComputerArtifacts.mjs",
33
+ "default": "./dist/ComputerArtifacts.mjs"
34
+ },
35
+ "./computer-workspace": {
36
+ "types": "./dist/ComputerWorkspace.d.mts",
37
+ "import": "./dist/ComputerWorkspace.mjs",
38
+ "default": "./dist/ComputerWorkspace.mjs"
39
+ },
30
40
  "./computer-workspace-host": {
31
41
  "types": "./dist/ComputerWorkspaceHost.d.mts",
32
42
  "import": "./dist/ComputerWorkspaceHost.mjs",
@@ -61,6 +71,7 @@
61
71
  "@effect/vitest": "catalog:",
62
72
  "@platformatic/vfs": "^0.4.0",
63
73
  "effect": "catalog:",
74
+ "esbuild": "^0.28.1",
64
75
  "vite-plus": "catalog:",
65
76
  "vitest": "catalog:"
66
77
  },
package/src/AiGateway.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Context, Data, Effect, type Layer } from "effect";
1
+ import { Context, Data, Effect, type Layer, Schema as S } from "effect";
2
2
  import type { AiGateway as CloudflareAiGateway } from "@cloudflare/workers-types";
3
3
 
4
4
  import * as Binding from "./Binding";
@@ -37,6 +37,46 @@ export type AiGatewayProvider = NonNullable<Parameters<AiGatewayBinding["getUrl"
37
37
  export type AiGatewayUniversalRequest = Exclude<AiGatewayRunData, ReadonlyArray<unknown>>;
38
38
  export type AiGatewayHeaders = NonNullable<AiGatewayUniversalRequest["headers"]>;
39
39
  export type AiGatewayRunOptions = Parameters<CloudflareAiGateway["run"]>[1];
40
+ type AiGatewayBindingCandidate = Parameters<typeof isWorkersAiBinding>[0];
41
+
42
+ const AiGatewayMetadata = S.Record(S.String, S.Union([S.String, S.Number, S.Boolean, S.Null]));
43
+ const AiGatewayHttpMetadata = S.Union([AiGatewayMetadata, S.fromJsonString(AiGatewayMetadata)]);
44
+ const AiGatewayLogResponse = S.Struct({
45
+ success: S.Literal(true),
46
+ result: S.Struct({
47
+ id: S.String,
48
+ provider: S.String,
49
+ model: S.String,
50
+ model_type: S.optional(S.String),
51
+ path: S.String,
52
+ duration: S.Number,
53
+ request_type: S.optional(S.String),
54
+ request_content_type: S.optional(S.String),
55
+ status_code: S.Number,
56
+ response_content_type: S.optional(S.String),
57
+ success: S.Boolean,
58
+ cached: S.Boolean,
59
+ tokens_in: S.optional(S.Number),
60
+ tokens_out: S.optional(S.Number),
61
+ metadata: S.optional(AiGatewayHttpMetadata),
62
+ step: S.optional(S.Number),
63
+ cost: S.optional(S.Number),
64
+ custom_cost: S.optional(S.Boolean),
65
+ request_size: S.Number,
66
+ request_head: S.optional(S.String),
67
+ request_head_complete: S.Boolean,
68
+ response_size: S.Number,
69
+ response_head: S.optional(S.String),
70
+ response_head_complete: S.Boolean,
71
+ created_at: S.DateFromString,
72
+ }),
73
+ });
74
+ const AiGatewayPatchLogResponse = S.Struct({
75
+ success: S.Literal(true),
76
+ result: S.Unknown,
77
+ });
78
+ const decodeAiGatewayLogResponse = S.decodeUnknownPromise(AiGatewayLogResponse);
79
+ const decodeAiGatewayPatchLogResponse = S.decodeUnknownPromise(AiGatewayPatchLogResponse);
40
80
 
41
81
  export interface AiGatewayFetchOptions {
42
82
  readonly provider?: AiGatewayProvider;
@@ -131,7 +171,13 @@ const providerUrl = (baseUrl: string, path = "") => {
131
171
  const gatewayUrl = (definition: { readonly accountId: string; readonly gatewayId: string }) =>
132
172
  `https://gateway.ai.cloudflare.com/v1/${definition.accountId}/${definition.gatewayId}/`;
133
173
 
134
- export const isAiGatewayBinding = (value: unknown): value is WorkersAiBinding =>
174
+ const ensureSuccessfulResponse = (response: Response): void => {
175
+ if (!response.ok) {
176
+ throw new Error(`AI Gateway HTTP request failed with status ${response.status}`);
177
+ }
178
+ };
179
+
180
+ export const isAiGatewayBinding = (value: AiGatewayBindingCandidate): value is WorkersAiBinding =>
135
181
  isWorkersAiBinding(value);
136
182
 
137
183
  export const makeClient = (definition: AiGatewayDefinition, gateway: AiGatewayBinding) =>
@@ -144,6 +190,7 @@ export const makeClient = (definition: AiGatewayDefinition, gateway: AiGatewayBi
144
190
  ) =>
145
191
  tryAiGatewayPromise(definition.binding, "run", () =>
146
192
  gateway.run(
193
+ // SAFETY: spreading the readonly batch produces the mutable array accepted by the binding.
147
194
  (Array.isArray(data) ? [...data] : data) as
148
195
  | AiGatewayUniversalRequest
149
196
  | Array<AiGatewayUniversalRequest>,
@@ -225,6 +272,7 @@ export const makeHttpClient = (
225
272
  Array.isArray(options.extraHeaders) ||
226
273
  options.extraHeaders instanceof Headers
227
274
  ) {
275
+ // SAFETY: the preceding branches retain only HeadersInit-compatible header containers.
228
276
  return options?.extraHeaders as HeadersInit | undefined;
229
277
  }
230
278
 
@@ -266,14 +314,17 @@ export const makeHttpClient = (
266
314
  ),
267
315
  ),
268
316
  patchLog: Effect.fn("AiGateway.patchLog")((logId: string, data: AiGatewayPatchLog) =>
269
- tryAiGatewayPromise(binding, "patchLog", (signal) =>
270
- request(providerUrl(gatewayUrl(definition), `logs/${logId}`).href, {
317
+ tryAiGatewayPromise(binding, "patchLog", async (signal) => {
318
+ const response = await request(providerUrl(gatewayUrl(definition), `logs/${logId}`).href, {
271
319
  method: "PATCH",
272
320
  body: JSON.stringify(data),
273
321
  headers: { "content-type": "application/json" },
274
322
  signal,
275
- }).then(() => undefined),
276
- ),
323
+ });
324
+
325
+ ensureSuccessfulResponse(response);
326
+ await decodeAiGatewayPatchLogResponse(await response.json());
327
+ }),
277
328
  ),
278
329
  getLog: Effect.fn("AiGateway.getLog")((logId: string) =>
279
330
  tryAiGatewayPromise(binding, "getLog", async (signal) => {
@@ -281,7 +332,10 @@ export const makeHttpClient = (
281
332
  signal,
282
333
  });
283
334
 
284
- return (await response.json()) as AiGatewayLog;
335
+ ensureSuccessfulResponse(response);
336
+ const payload = await decodeAiGatewayLogResponse(await response.json());
337
+
338
+ return payload.result;
285
339
  }),
286
340
  ),
287
341
  rawUnsafe: Effect.fail(
@@ -315,6 +369,7 @@ export const Tag =
315
369
 
316
370
  const makeLayer = (definition: LayerOptions) => layer(tag, definition);
317
371
 
372
+ // SAFETY: these are exactly the members required by TagClass, attached to the matching service tag.
318
373
  return Object.assign(tag, {
319
374
  id,
320
375
  layer: makeLayer,
@@ -10,6 +10,7 @@ import {
10
10
  Effect,
11
11
  Layer,
12
12
  Option,
13
+ Predicate,
13
14
  Result,
14
15
  Schema as S,
15
16
  } from "effect";
@@ -321,7 +322,7 @@ const byteLength = (value: AnalyticsEngineFieldValue) => {
321
322
  return 0;
322
323
  }
323
324
 
324
- return typeof value === "string" ? textEncoder.encode(value).byteLength : value.byteLength;
325
+ return Predicate.isString(value) ? textEncoder.encode(value).byteLength : value.byteLength;
325
326
  };
326
327
 
327
328
  const lengthViolation = (
@@ -420,11 +421,15 @@ const schemaViolation = (
420
421
 
421
422
  const toAnalyticsEngineDataPoint = (
422
423
  dataPoint: S.Schema.Type<typeof AnalyticsEngineDataPointSchema>,
423
- ): AnalyticsEngineDataPoint => ({
424
- ...(dataPoint.indexes === undefined ? {} : { indexes: [...dataPoint.indexes] }),
425
- ...(dataPoint.doubles === undefined ? {} : { doubles: [...dataPoint.doubles] }),
426
- ...(dataPoint.blobs === undefined ? {} : { blobs: [...dataPoint.blobs] }),
427
- });
424
+ ): AnalyticsEngineDataPoint => {
425
+ const result: AnalyticsEngineDataPoint = {};
426
+
427
+ if (dataPoint.indexes !== undefined) result.indexes = [...dataPoint.indexes];
428
+ if (dataPoint.doubles !== undefined) result.doubles = [...dataPoint.doubles];
429
+ if (dataPoint.blobs !== undefined) result.blobs = [...dataPoint.blobs];
430
+
431
+ return result;
432
+ };
428
433
 
429
434
  const validateDataPoint = (
430
435
  binding: string,
@@ -678,13 +683,10 @@ const makeQueryClientWith = (
678
683
  export const makeQueryClient = (definition: AnalyticsEngineQueryDefinition) =>
679
684
  Effect.map(HttpClient.HttpClient, (httpClient) => makeQueryClientWith(definition, httpClient));
680
685
 
681
- export const isAnalyticsEngineDataset = (value: unknown): value is AnalyticsEngineBinding => {
682
- if (typeof value !== "object" || value === null) {
683
- return false;
684
- }
685
-
686
- return typeof Reflect.get(value, "writeDataPoint") === "function";
687
- };
686
+ export const isAnalyticsEngineDataset = <Candidate>(
687
+ value: Candidate,
688
+ ): value is Candidate & AnalyticsEngineBinding =>
689
+ Predicate.hasProperty(value, "writeDataPoint") && Predicate.isFunction(value.writeDataPoint);
688
690
 
689
691
  export const makeClient =
690
692
  (definition: AnalyticsEngineDefinition, defaults?: AnalyticsEngineWritePolicy) =>
@@ -793,6 +795,7 @@ export const Tag =
793
795
 
794
796
  const makeLayer = (definition: LayerOptions) => layer(tag, definition);
795
797
 
798
+ // SAFETY: these are exactly the members required by TagClass, attached to the matching service tag.
796
799
  return Object.assign(tag, {
797
800
  id,
798
801
  layer: makeLayer,
@@ -811,6 +814,7 @@ export const QueryTag =
811
814
  const makeLayerFetchConfig = (config?: Config.Config<AnalyticsEngineQueryDefinition>) =>
812
815
  layerFetchConfig(tag, config);
813
816
 
817
+ // SAFETY: these are exactly the members required by QueryTagClass, attached to the matching service tag.
814
818
  return Object.assign(tag, {
815
819
  id,
816
820
  layer: makeLayer,
package/src/Artifacts.ts CHANGED
@@ -6,7 +6,7 @@ import type {
6
6
  ArtifactsTokenInfo as CloudflareArtifactsTokenInfo,
7
7
  ArtifactsTokenListResult as CloudflareArtifactsTokenListResult,
8
8
  } from "@cloudflare/workers-types";
9
- import { Context, Data, Effect, type Layer } from "effect";
9
+ import { Context, Data, Effect, type Layer, Predicate, Schema } from "effect";
10
10
 
11
11
  import * as Binding from "./Binding";
12
12
  import type { WorkerEnvironment } from "./Environment";
@@ -31,6 +31,7 @@ export type ArtifactsOperation =
31
31
  | "getToken"
32
32
  | "revokeToken"
33
33
  | "fork"
34
+ | "info"
34
35
  | "log"
35
36
  | "readCommit"
36
37
  | "readTree"
@@ -136,34 +137,37 @@ export type ArtifactsTokenInfo = CloudflareArtifactsTokenInfo;
136
137
  /** Repository token list result. */
137
138
  export type ArtifactsTokenListResult = CloudflareArtifactsTokenListResult;
138
139
 
139
- /**
140
- * Commit history returned by `log()`.
141
- *
142
- * Cloudflare currently documents this generated type by name but does not
143
- * publish its fields in the binding reference or Workers type declarations.
144
- */
145
- export interface ArtifactsLogResult {
146
- readonly [field: string]: unknown;
140
+ /** Identity recorded for a Git commit author or committer. */
141
+ export interface ArtifactsCommitIdentity {
142
+ readonly name: string;
143
+ readonly email: string;
147
144
  }
148
145
 
149
- /**
150
- * Parsed Git commit returned by `readCommit()`.
151
- *
152
- * Cloudflare currently documents this generated type by name but does not
153
- * publish its fields in the binding reference or Workers type declarations.
154
- */
146
+ /** Parsed Git commit returned by `readCommit()` and `log()`. */
155
147
  export interface ArtifactsCommit {
156
- readonly [field: string]: unknown;
148
+ readonly hash: string;
149
+ readonly treeHash: string;
150
+ readonly message: string;
151
+ readonly author: ArtifactsCommitIdentity;
152
+ readonly committer: ArtifactsCommitIdentity;
153
+ readonly parents: ReadonlyArray<string>;
154
+ /** Unix timestamp in seconds. */
155
+ readonly authoredAt: number;
156
+ /** Unix timestamp in seconds. */
157
+ readonly committedAt: number;
157
158
  }
158
159
 
160
+ /** Bare commit array returned by `log()`. */
161
+ export type ArtifactsLogResult = ReadonlyArray<ArtifactsCommit>;
162
+
159
163
  /**
160
164
  * Parsed Git tree returned by `readTree()`.
161
165
  *
162
166
  * Cloudflare currently documents this generated type by name but does not
163
167
  * publish its fields in the binding reference or Workers type declarations.
164
168
  */
165
- export interface ArtifactsTree {
166
- readonly [field: string]: unknown;
169
+ export interface ArtifactsTree<Field = unknown> {
170
+ readonly [field: string]: Field;
167
171
  }
168
172
 
169
173
  /** Options for `Artifacts.create()`. */
@@ -228,7 +232,15 @@ export interface ArtifactsLogOptions {
228
232
  }
229
233
 
230
234
  /** Runtime repository handle, including methods absent from current shipped Workers types. */
231
- export interface ArtifactsRepoBinding extends ArtifactsRepoInfo {
235
+ export interface ArtifactsRepoBinding extends Partial<ArtifactsRepoInfo> {
236
+ /**
237
+ * Read repository metadata.
238
+ *
239
+ * Remote bindings expose method-only RPC handles and provide metadata only
240
+ * through this method. It is optional solely for compatibility with older
241
+ * local handles that expose metadata as data properties.
242
+ */
243
+ readonly info?: () => Promise<ArtifactsRepoInfo>;
232
244
  /**
233
245
  * Create a Git token for the repository.
234
246
  *
@@ -262,8 +274,15 @@ export interface ArtifactsBinding {
262
274
  readonly delete: (name: RepoName) => Promise<boolean>;
263
275
  }
264
276
 
265
- /** Effect wrapper around a repository handle returned by `Artifacts.get()`. */
277
+ /**
278
+ * Effect wrapper around a repository handle returned by `Artifacts.get()`.
279
+ *
280
+ * The inherited metadata is the snapshot read while resolving the client.
281
+ * Use `info()` to refresh it from the repository handle.
282
+ */
266
283
  export interface ArtifactsRepoClient extends ArtifactsRepoInfo {
284
+ /** Read and validate the repository's current metadata. */
285
+ readonly info: () => Effect.Effect<ArtifactsRepoInfo, ArtifactsOperationError>;
267
286
  /**
268
287
  * Create a Git token for the repository.
269
288
  *
@@ -319,6 +338,11 @@ export type LayerOptions = {
319
338
  readonly binding: string;
320
339
  };
321
340
 
341
+ interface MutableArtifactsErrorDetails {
342
+ code?: ArtifactsOperationError["code"];
343
+ numericCode?: number;
344
+ }
345
+
322
346
  export interface TagClass<Self, Id extends string> extends Context.ServiceClass<
323
347
  Self,
324
348
  Id,
@@ -337,17 +361,18 @@ export interface TagClass<Self, Id extends string> extends Context.ServiceClass<
337
361
  const artifactsErrorDetails = (
338
362
  cause: unknown,
339
363
  ): Pick<ArtifactsOperationError, "code" | "numericCode"> => {
340
- if (typeof cause !== "object" || cause === null) {
364
+ if (!Predicate.isObject(cause)) {
341
365
  return {};
342
366
  }
343
367
 
344
- const code = Reflect.get(cause, "code");
345
- const numericCode = Reflect.get(cause, "numericCode");
368
+ const code = Predicate.hasProperty(cause, "code") ? cause.code : undefined;
369
+ const numericCode = Predicate.hasProperty(cause, "numericCode") ? cause.numericCode : undefined;
370
+ const details: MutableArtifactsErrorDetails = {};
346
371
 
347
- return {
348
- ...(typeof code === "string" ? { code } : {}),
349
- ...(typeof numericCode === "number" ? { numericCode } : {}),
350
- };
372
+ if (Predicate.isString(code)) details.code = code;
373
+ if (Predicate.isNumber(numericCode)) details.numericCode = numericCode;
374
+
375
+ return details;
351
376
  };
352
377
 
353
378
  const artifactsError = (binding: string, operation: ArtifactsOperation, cause: unknown) =>
@@ -372,13 +397,53 @@ const spanOptions = (binding: string, operation: ArtifactsOperation) => ({
372
397
  attributes: { binding, operation },
373
398
  });
374
399
 
375
- const hasFunction = (value: object, key: string): boolean =>
376
- typeof Reflect.get(value, key) === "function";
400
+ const hasFunction = <Candidate, Key extends string>(
401
+ value: Candidate,
402
+ key: Key,
403
+ ): value is Candidate & Record<Key, (...args: ReadonlyArray<any>) => any> =>
404
+ Predicate.hasProperty(value, key) && Predicate.isFunction(value[key]);
405
+
406
+ const ArtifactsRepoInfoSchema = Schema.Struct({
407
+ id: Schema.String,
408
+ name: Schema.String,
409
+ description: Schema.NullOr(Schema.String),
410
+ defaultBranch: Schema.String,
411
+ createdAt: Schema.String,
412
+ updatedAt: Schema.String,
413
+ lastPushAt: Schema.NullOr(Schema.String),
414
+ source: Schema.NullOr(Schema.String),
415
+ readOnly: Schema.Boolean,
416
+ remote: Schema.String,
417
+ });
418
+
419
+ const decodeRepoInfo = (
420
+ binding: string,
421
+ value: ArtifactsRepoInfo | ArtifactsRepoBinding,
422
+ ): Effect.Effect<ArtifactsRepoInfo, ArtifactsOperationError> =>
423
+ Schema.decodeUnknownEffect(ArtifactsRepoInfoSchema)(value).pipe(
424
+ Effect.mapError((cause) => artifactsError(binding, "info", cause)),
425
+ );
426
+
427
+ const readRepoInfo = (
428
+ binding: string,
429
+ repo: ArtifactsRepoBinding,
430
+ ): Effect.Effect<ArtifactsRepoInfo, ArtifactsOperationError> => {
431
+ if (hasFunction(repo, "info")) {
432
+ return tryArtifactsPromise(binding, "info", () => repo.info()).pipe(
433
+ Effect.flatMap((info) => decodeRepoInfo(binding, info)),
434
+ Effect.withSpan("Artifacts.info", spanOptions(binding, "info")),
435
+ );
436
+ }
437
+
438
+ return decodeRepoInfo(binding, repo).pipe(
439
+ Effect.withSpan("Artifacts.info", spanOptions(binding, "info")),
440
+ );
441
+ };
377
442
 
378
443
  /** Tests whether a value exposes the complete documented Artifacts repo handle API. */
379
- export const isArtifactsRepoBinding = (value: unknown): value is ArtifactsRepoBinding =>
380
- typeof value === "object" &&
381
- value !== null &&
444
+ export const isArtifactsRepoBinding = <Candidate>(
445
+ value: Candidate,
446
+ ): value is Candidate & ArtifactsRepoBinding =>
382
447
  hasFunction(value, "createToken") &&
383
448
  hasFunction(value, "listTokens") &&
384
449
  hasFunction(value, "revokeToken") &&
@@ -388,26 +453,22 @@ export const isArtifactsRepoBinding = (value: unknown): value is ArtifactsRepoBi
388
453
  hasFunction(value, "readTree");
389
454
 
390
455
  /** Tests whether a value exposes the complete documented Artifacts namespace API. */
391
- export const isArtifactsBinding = (value: unknown): value is ArtifactsBinding =>
392
- typeof value === "object" &&
393
- value !== null &&
456
+ export const isArtifactsBinding = <Candidate>(
457
+ value: Candidate,
458
+ ): value is Candidate & ArtifactsBinding =>
394
459
  hasFunction(value, "create") &&
395
460
  hasFunction(value, "get") &&
396
461
  hasFunction(value, "import") &&
397
462
  hasFunction(value, "list") &&
398
463
  hasFunction(value, "delete");
399
464
 
400
- const wrapRepo = (binding: string, repo: ArtifactsRepoBinding): ArtifactsRepoClient => ({
401
- id: repo.id,
402
- name: repo.name,
403
- description: repo.description,
404
- defaultBranch: repo.defaultBranch,
405
- createdAt: repo.createdAt,
406
- updatedAt: repo.updatedAt,
407
- lastPushAt: repo.lastPushAt,
408
- source: repo.source,
409
- readOnly: repo.readOnly,
410
- remote: repo.remote,
465
+ const wrapRepo = (
466
+ binding: string,
467
+ repo: ArtifactsRepoBinding,
468
+ info: ArtifactsRepoInfo,
469
+ ): ArtifactsRepoClient => ({
470
+ ...info,
471
+ info: () => readRepoInfo(binding, repo),
411
472
  createToken: Effect.fn(
412
473
  "Artifacts.createToken",
413
474
  spanOptions(binding, "createToken"),
@@ -463,7 +524,9 @@ export const makeClient =
463
524
  tryArtifactsPromise(definition.binding, "get", () => artifacts.get(name)).pipe(
464
525
  Effect.flatMap((repo) =>
465
526
  isArtifactsRepoBinding(repo)
466
- ? Effect.succeed(wrapRepo(definition.binding, repo))
527
+ ? readRepoInfo(definition.binding, repo).pipe(
528
+ Effect.map((info) => wrapRepo(definition.binding, repo, info)),
529
+ )
467
530
  : Effect.fail(
468
531
  artifactsError(
469
532
  definition.binding,
@@ -512,6 +575,7 @@ export const Tag =
512
575
 
513
576
  const makeLayer = (definition: LayerOptions) => layer(tag, definition);
514
577
 
578
+ // SAFETY: these are exactly the members required by TagClass, attached to the matching service tag.
515
579
  return Object.assign(tag, {
516
580
  id,
517
581
  layer: makeLayer,
package/src/Binding.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Context, Data, Effect, Layer } from "effect";
1
+ import { Context, Data, Effect, Layer, Predicate } from "effect";
2
2
 
3
3
  import { WorkerEnvironment, type WorkerEnv } from "./Environment";
4
4
 
@@ -28,22 +28,25 @@ export interface ValidationOptions {
28
28
 
29
29
  const defaultExpected = "Cloudflare binding resource";
30
30
 
31
- const isPropertyTarget = (value: unknown): value is object =>
32
- (typeof value === "object" || typeof value === "function") && value !== null;
31
+ type BindingCandidate = Parameters<typeof Predicate.isUnknown>[0];
32
+ type PropertyTarget = { readonly constructor?: Function };
33
33
 
34
- const getObjectName = (value: object | Function): string => {
34
+ const isPropertyTarget = (value: BindingCandidate): value is PropertyTarget =>
35
+ Predicate.isObjectOrArray(value) || Predicate.isFunction(value);
36
+
37
+ const getObjectName = (value: PropertyTarget): string => {
35
38
  const tag = (() => {
36
39
  try {
37
40
  return Object.prototype.toString.call(value).slice("[object ".length, -1);
38
41
  } catch {
39
- return typeof value;
42
+ return Predicate.isFunction(value) ? "function" : "object";
40
43
  }
41
44
  })();
42
45
  const constructorName = (() => {
43
46
  try {
44
47
  return "constructor" in value &&
45
- typeof value.constructor === "function" &&
46
- typeof value.constructor.name === "string"
48
+ Predicate.isFunction(value.constructor) &&
49
+ Predicate.isString(value.constructor.name)
47
50
  ? value.constructor.name
48
51
  : undefined;
49
52
  } catch {
@@ -62,7 +65,7 @@ const getObjectName = (value: object | Function): string => {
62
65
  return tag;
63
66
  };
64
67
 
65
- const propertyNames = (value: object | Function): ReadonlyArray<string> => {
68
+ const propertyNames = (value: PropertyTarget): ReadonlyArray<string> => {
66
69
  const names = new Set<string>();
67
70
 
68
71
  for (const target of [value, Object.getPrototypeOf(value)] as const) {
@@ -82,21 +85,27 @@ const propertyNames = (value: object | Function): ReadonlyArray<string> => {
82
85
  return [...names].filter((name) => name !== "constructor").sort();
83
86
  };
84
87
 
85
- const isMethod = (value: object | Function, name: string): boolean => {
88
+ const isMethod = (value: PropertyTarget, name: string): boolean => {
86
89
  try {
87
- return typeof Reflect.get(value, name) === "function";
90
+ return Predicate.hasProperty(value, name) && Predicate.isFunction(value[name]);
88
91
  } catch {
89
92
  return false;
90
93
  }
91
94
  };
92
95
 
93
- const describeActual = (value: unknown): string => {
96
+ const describeActual = (value: BindingCandidate): string => {
94
97
  if (value === null) {
95
98
  return "null";
96
99
  }
97
100
 
98
- if (typeof value !== "object" && typeof value !== "function") {
99
- return typeof value;
101
+ if (!isPropertyTarget(value)) {
102
+ if (Predicate.isString(value)) return "string";
103
+ if (Predicate.isNumber(value)) return "number";
104
+ if (Predicate.isBoolean(value)) return "boolean";
105
+ if (Predicate.isBigInt(value)) return "bigint";
106
+ if (Predicate.isSymbol(value)) return "symbol";
107
+
108
+ return "undefined";
100
109
  }
101
110
 
102
111
  const names = propertyNames(value);
@@ -117,7 +126,7 @@ const describeActual = (value: unknown): string => {
117
126
  const getBinding = <Resource>(
118
127
  env: WorkerEnv,
119
128
  binding: string,
120
- isResource: (value: unknown) => value is Resource,
129
+ isResource: (value: BindingCandidate) => value is Resource,
121
130
  options?: ValidationOptions,
122
131
  ): Effect.Effect<Resource, BindingNotFoundError | BindingValidationError> =>
123
132
  Effect.gen(function* () {
@@ -134,7 +143,7 @@ const getBinding = <Resource>(
134
143
  );
135
144
  }
136
145
 
137
- const resource = Reflect.get(env, binding);
146
+ const resource = Predicate.hasProperty(env, binding) ? env[binding] : undefined;
138
147
 
139
148
  if (resource === undefined) {
140
149
  return yield* Effect.fail(
@@ -191,7 +200,7 @@ export interface BindingService<Self, Id extends string, Service> extends Contex
191
200
  const makeBindingLayer = <Self, Resource, Service>(
192
201
  tag: Context.Service<Self, Service>,
193
202
  binding: string,
194
- isResource: (value: unknown) => value is Resource,
203
+ isResource: (value: BindingCandidate) => value is Resource,
195
204
  wrap: ((resource: Resource) => Service) | undefined,
196
205
  options: ValidationOptions | undefined,
197
206
  ): Layer.Layer<Self, BindingNotFoundError | BindingValidationError, WorkerEnvironment> =>
@@ -201,28 +210,29 @@ const makeBindingLayer = <Self, Resource, Service>(
201
210
  const env = yield* WorkerEnvironment;
202
211
  const resource = yield* getBinding(env, binding, isResource, options);
203
212
 
204
- return wrap === undefined ? (resource as unknown as Service) : wrap(resource);
213
+ // SAFETY: the overload without wrap fixes Service to Resource; the other branch invokes wrap.
214
+ return wrap === undefined ? (resource as Resource & Service) : wrap(resource);
205
215
  }),
206
216
  );
207
217
 
208
218
  export function layer<Self, Resource>(
209
219
  tag: Context.Service<Self, Resource>,
210
220
  binding: string,
211
- isResource: (value: unknown) => value is Resource,
221
+ isResource: (value: BindingCandidate) => value is Resource,
212
222
  wrap?: undefined,
213
223
  options?: ValidationOptions,
214
224
  ): Layer.Layer<Self, BindingNotFoundError | BindingValidationError, WorkerEnvironment>;
215
225
  export function layer<Self, Resource, Service>(
216
226
  tag: Context.Service<Self, Service>,
217
227
  binding: string,
218
- isResource: (value: unknown) => value is Resource,
228
+ isResource: (value: BindingCandidate) => value is Resource,
219
229
  wrap: (resource: Resource) => Service,
220
230
  options?: ValidationOptions,
221
231
  ): Layer.Layer<Self, BindingNotFoundError | BindingValidationError, WorkerEnvironment>;
222
232
  export function layer<Self, Resource, Service = Resource>(
223
233
  tag: Context.Service<Self, Service>,
224
234
  binding: string,
225
- isResource: (value: unknown) => value is Resource,
235
+ isResource: (value: BindingCandidate) => value is Resource,
226
236
  wrap?: (resource: Resource) => Service,
227
237
  options?: ValidationOptions,
228
238
  ): Layer.Layer<Self, BindingNotFoundError | BindingValidationError, WorkerEnvironment> {
@@ -233,27 +243,28 @@ export const Service = <Self>() => {
233
243
  function makeService<Id extends string, Resource>(
234
244
  id: Id,
235
245
  binding: string,
236
- isResource: (value: unknown) => value is Resource,
246
+ isResource: (value: BindingCandidate) => value is Resource,
237
247
  wrap?: undefined,
238
248
  options?: ValidationOptions,
239
249
  ): BindingService<Self, Id, Resource>;
240
250
  function makeService<Id extends string, Resource, Service>(
241
251
  id: Id,
242
252
  binding: string,
243
- isResource: (value: unknown) => value is Resource,
253
+ isResource: (value: BindingCandidate) => value is Resource,
244
254
  wrap: (resource: Resource) => Service,
245
255
  options?: ValidationOptions,
246
256
  ): BindingService<Self, Id, Service>;
247
257
  function makeService<Id extends string, Resource, Service = Resource>(
248
258
  id: Id,
249
259
  binding: string,
250
- isResource: (value: unknown) => value is Resource,
260
+ isResource: (value: BindingCandidate) => value is Resource,
251
261
  wrap?: (resource: Resource) => Service,
252
262
  options?: ValidationOptions,
253
263
  ): BindingService<Self, Id, Service> {
254
264
  const tag = Context.Service<Self, Service>()(`effect-cf/Binding/${id}` as const);
255
265
  const serviceLayer = makeBindingLayer(tag, binding, isResource, wrap, options);
256
266
 
267
+ // SAFETY: the assigned metadata and layer exactly implement BindingService for this tag.
257
268
  return Object.assign(tag, {
258
269
  [TypeId]: TypeId,
259
270
  id,