@renderinc/sdk 0.2.0 → 0.2.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.
Files changed (56) hide show
  1. package/CHANGELOG.md +9 -2
  2. package/README.md +32 -22
  3. package/dist/experimental/experimental.d.ts +6 -2
  4. package/dist/experimental/experimental.d.ts.map +1 -1
  5. package/dist/experimental/experimental.js +9 -3
  6. package/dist/experimental/index.d.ts +2 -2
  7. package/dist/experimental/index.d.ts.map +1 -1
  8. package/dist/experimental/index.js +6 -5
  9. package/dist/experimental/object/api.d.ts +11 -0
  10. package/dist/experimental/object/api.d.ts.map +1 -0
  11. package/dist/experimental/object/api.js +44 -0
  12. package/dist/experimental/object/client.d.ts +21 -0
  13. package/dist/experimental/object/client.d.ts.map +1 -0
  14. package/dist/experimental/object/client.js +127 -0
  15. package/dist/experimental/object/index.d.ts +5 -0
  16. package/dist/experimental/object/index.d.ts.map +1 -0
  17. package/dist/experimental/object/index.js +8 -0
  18. package/dist/experimental/object/types.d.ts +49 -0
  19. package/dist/experimental/object/types.d.ts.map +1 -0
  20. package/dist/generated/schema.d.ts +131 -3
  21. package/dist/generated/schema.d.ts.map +1 -1
  22. package/dist/workflows/uds.d.ts.map +1 -1
  23. package/dist/workflows/uds.js +26 -51
  24. package/examples/client/main.ts +1 -1
  25. package/examples/client/package-lock.json +4 -4
  26. package/examples/client/package.json +1 -1
  27. package/examples/task/main.ts +1 -1
  28. package/examples/task/package-lock.json +5 -6
  29. package/examples/task/package.json +1 -1
  30. package/package.json +9 -8
  31. package/src/errors.test.ts +75 -0
  32. package/src/experimental/experimental.ts +30 -7
  33. package/src/experimental/index.ts +18 -18
  34. package/src/experimental/{blob → object}/api.ts +7 -7
  35. package/src/experimental/object/client.test.ts +138 -0
  36. package/src/experimental/{blob → object}/client.ts +57 -57
  37. package/src/experimental/object/index.ts +22 -0
  38. package/src/experimental/object/types.test.ts +87 -0
  39. package/src/experimental/{blob → object}/types.ts +30 -30
  40. package/src/generated/schema.ts +217 -9
  41. package/src/utils/get-base-url.test.ts +58 -0
  42. package/src/workflows/client/client.test.ts +68 -0
  43. package/src/workflows/types.test.ts +52 -0
  44. package/src/workflows/uds.ts +29 -69
  45. package/tsconfig.json +1 -1
  46. package/{vite.config.ts → vitest.config.ts} +1 -0
  47. package/dist/workflows/client/errors.d.ts +0 -25
  48. package/dist/workflows/client/errors.d.ts.map +0 -1
  49. package/dist/workflows/client/errors.js +0 -56
  50. package/dist/workflows/client/schema.d.ts +0 -9322
  51. package/dist/workflows/client/schema.d.ts.map +0 -1
  52. package/dist/workflows/client/workflows.d.ts +0 -15
  53. package/dist/workflows/client/workflows.d.ts.map +0 -1
  54. package/dist/workflows/client/workflows.js +0 -63
  55. package/src/experimental/blob/index.ts +0 -22
  56. /package/dist/{workflows/client/schema.js → experimental/object/types.js} +0 -0
@@ -27,6 +27,28 @@ export interface paths {
27
27
  patch?: never;
28
28
  trace?: never;
29
29
  };
30
+ "/blueprints/validate": {
31
+ parameters: {
32
+ query?: never;
33
+ header?: never;
34
+ path?: never;
35
+ cookie?: never;
36
+ };
37
+ get?: never;
38
+ put?: never;
39
+ /**
40
+ * Validate Blueprint
41
+ * @description Validate a `render.yaml` Blueprint file without creating or modifying any resources. This endpoint checks the syntax and structure of the Blueprint, validates that all required fields are present, and returns a plan indicating the resources that would be created.
42
+ *
43
+ * Requests to this endpoint use `Content-Type: multipart/form-data`. The provided Blueprint file cannot exceed 10MB in size.
44
+ */
45
+ post: operations["validate-blueprint"];
46
+ delete?: never;
47
+ options?: never;
48
+ head?: never;
49
+ patch?: never;
50
+ trace?: never;
51
+ };
30
52
  "/owners/{ownerId}/members/{userId}": {
31
53
  parameters: {
32
54
  query?: never;
@@ -512,7 +534,7 @@ export interface paths {
512
534
  put?: never;
513
535
  /**
514
536
  * Create service
515
- * @description Create a service.
537
+ * @description Creates a new Render service in the specified workspace with the specified configuration.
516
538
  */
517
539
  post: operations["create-service"];
518
540
  delete?: never;
@@ -3262,6 +3284,31 @@ export interface paths {
3262
3284
  patch?: never;
3263
3285
  trace?: never;
3264
3286
  };
3287
+ "/blobs/{ownerId}/{region}": {
3288
+ parameters: {
3289
+ query?: never;
3290
+ header?: never;
3291
+ path: {
3292
+ /** @description The ID of the workspace to return resources for */
3293
+ ownerId: components["parameters"]["ownerIdPathParam"];
3294
+ /** @description The region to return resources for */
3295
+ region: components["parameters"]["regionPathParam"];
3296
+ };
3297
+ cookie?: never;
3298
+ };
3299
+ /**
3300
+ * List blobs
3301
+ * @description List blobs in the specified region for a workspace.
3302
+ */
3303
+ get: operations["list-blobs"];
3304
+ put?: never;
3305
+ post?: never;
3306
+ delete?: never;
3307
+ options?: never;
3308
+ head?: never;
3309
+ patch?: never;
3310
+ trace?: never;
3311
+ };
3265
3312
  "/blobs/{ownerId}/{region}/{key}": {
3266
3313
  parameters: {
3267
3314
  query?: never;
@@ -3603,23 +3650,25 @@ export interface components {
3603
3650
  };
3604
3651
  servicePOST: {
3605
3652
  type: components["schemas"]["serviceType"];
3653
+ /** @description The service's name. Must be unique within the workspace. */
3606
3654
  name: string;
3655
+ /** @description The ID of the workspace the service belongs to. Obtain your workspace's ID from its Settings page in the Render Dashboard. */
3607
3656
  ownerId: string;
3608
3657
  /**
3609
- * @description Do not include the branch in the repo string. You can instead supply a 'branch' parameter.
3658
+ * @description The service's repository URL. Do not specify a branch in this string (use the `branch` parameter instead).
3610
3659
  * @example https://github.com/render-examples/flask-hello-world
3611
3660
  */
3612
3661
  repo?: string;
3613
3662
  autoDeploy?: components["schemas"]["autoDeploy"];
3614
3663
  autoDeployTrigger?: components["schemas"]["autoDeployTrigger"];
3615
- /** @description If left empty, this will fall back to the default branch of the repository */
3664
+ /** @description The repo branch to pull, build, and deploy. If omitted, uses the repository's default branch. */
3616
3665
  branch?: string;
3617
3666
  image?: components["schemas"]["image"];
3618
3667
  buildFilter?: components["schemas"]["buildFilter"];
3619
3668
  rootDir?: string;
3620
3669
  envVars?: components["schemas"]["envVarInputArray"];
3621
3670
  secretFiles?: components["schemas"]["secretFileInput"][];
3622
- /** @description The ID of the environment the service is associated with */
3671
+ /** @description The ID of the environment the service belongs to, if any. Obtain an environment's ID from its Settings page in the Render Dashboard. */
3623
3672
  environmentId?: string;
3624
3673
  serviceDetails?: components["schemas"]["staticSiteDetailsPOST"] | components["schemas"]["webServiceDetailsPOST"] | components["schemas"]["privateServiceDetailsPOST"] | components["schemas"]["backgroundWorkerDetailsPOST"] | components["schemas"]["cronJobDetailsPOST"];
3625
3674
  };
@@ -3656,7 +3705,11 @@ export interface components {
3656
3705
  maintenanceMode?: components["schemas"]["maintenanceMode"];
3657
3706
  /** @description Defaults to 1 */
3658
3707
  numInstances?: number;
3659
- plan?: components["schemas"]["paidPlan"];
3708
+ /**
3709
+ * @description The instance type to use. If omitted, defaults to `starter` when creating a new service.
3710
+ * @default starter
3711
+ */
3712
+ plan: components["schemas"]["plan"];
3660
3713
  preDeployCommand?: string;
3661
3714
  pullRequestPreviewsEnabled?: components["schemas"]["pullRequestPreviewsEnabled"];
3662
3715
  previews?: components["schemas"]["previews"];
@@ -3742,7 +3795,7 @@ export interface components {
3742
3795
  envSpecificDetails?: components["schemas"]["envSpecificDetailsPATCH"];
3743
3796
  healthCheckPath?: string;
3744
3797
  maintenanceMode?: components["schemas"]["maintenanceMode"];
3745
- plan?: components["schemas"]["paidPlan"];
3798
+ plan?: components["schemas"]["plan"];
3746
3799
  preDeployCommand?: string;
3747
3800
  pullRequestPreviewsEnabled?: components["schemas"]["pullRequestPreviewsEnabled"];
3748
3801
  previews?: components["schemas"]["previews"];
@@ -3807,7 +3860,7 @@ export interface components {
3807
3860
  plan?: components["schemas"]["plan"];
3808
3861
  };
3809
3862
  /**
3810
- * @description The instance type to use for the preview instance. Note that base services with any paid instance type can't create preview instances with the `free` instance type.
3863
+ * @description The instance type to use. Note that base services on any paid instance type can't create preview instances with the `free` instance type.
3811
3864
  * @example starter
3812
3865
  * @enum {string}
3813
3866
  */
@@ -4250,6 +4303,18 @@ export interface components {
4250
4303
  redisPlan: "free" | "starter" | "standard" | "pro" | "pro_plus" | "custom";
4251
4304
  /** @enum {string} */
4252
4305
  databaseStatus: "creating" | "available" | "unavailable" | "config_restart" | "suspended" | "maintenance_scheduled" | "maintenance_in_progress" | "recovery_failed" | "recovery_in_progress" | "unknown" | "updating_instance";
4306
+ /**
4307
+ * @description Controls deployment behavior when triggering a deploy.
4308
+ *
4309
+ * - `deploy_only`: Deploy the last successful build without rebuilding (minimizes downtime)
4310
+ * - `build_and_deploy`: Build new code and deploy it (default behavior when not specified)
4311
+ *
4312
+ * **Note:** `deploy_only` cannot be combined with `commitId`, `imageUrl` or `clearCache` parameters,
4313
+ * as those are build related fields.
4314
+ * @default build_and_deploy
4315
+ * @enum {string}
4316
+ */
4317
+ DeployMode: "deploy_only" | "build_and_deploy";
4253
4318
  projectWithCursor: {
4254
4319
  project: components["schemas"]["project"];
4255
4320
  cursor: components["schemas"]["cursor"];
@@ -4526,7 +4591,7 @@ export interface components {
4526
4591
  /** @enum {string} */
4527
4592
  databaseRole: "primary" | "replica";
4528
4593
  /**
4529
- * @description Defaults to "starter"
4594
+ * @description Defaults to `starter` when creating a new database.
4530
4595
  * @default starter
4531
4596
  * @enum {string}
4532
4597
  */
@@ -4603,6 +4668,48 @@ export interface components {
4603
4668
  /** Format: date-time */
4604
4669
  lastSync?: string;
4605
4670
  };
4671
+ validateBlueprintRequest: {
4672
+ /**
4673
+ * @description The ID of the workspace to validate against. Obtain your workspace ID from its Settings page in the Render Dashboard.
4674
+ * @example tea-cjnxpkdhshc73d12t9i0
4675
+ */
4676
+ ownerId: string;
4677
+ /**
4678
+ * Format: binary
4679
+ * @description The render.yaml file to validate, as a binary file.
4680
+ */
4681
+ file: string;
4682
+ };
4683
+ validationError: {
4684
+ /** @description The path to the field with the error (e.g., `services[0].plan`) */
4685
+ path?: string;
4686
+ /** @description The error message */
4687
+ error: string;
4688
+ /** @description The line number in the YAML file (1-indexed) */
4689
+ line?: number;
4690
+ /** @description The column number in the YAML file (1-indexed) */
4691
+ column?: number;
4692
+ };
4693
+ validationPlanSummary: {
4694
+ /** @description The names of services that would be created as part of the Blueprint. */
4695
+ services?: string[];
4696
+ /** @description The names of Render Postgres databases that would be created as part of the Blueprint. */
4697
+ databases?: string[];
4698
+ /** @description The names of Render Key Value instances that would be created as part of the Blueprint. */
4699
+ keyValue?: string[];
4700
+ /** @description The names of environment groups that would be created as part of the Blueprint. */
4701
+ envGroups?: string[];
4702
+ /** @description The total number of actions that would be performed by the Blueprint. In addition to created resources, this includes modifications to individual configuration fields. */
4703
+ totalActions?: number;
4704
+ };
4705
+ validateBlueprintResponse: {
4706
+ /** @description If `true`, the Blueprint validated successfully. If `false`, at least one validation error occurred. */
4707
+ valid: boolean;
4708
+ /** @description A list of validation errors. Only present if `valid` is `false`. */
4709
+ errors?: components["schemas"]["validationError"][];
4710
+ /** @description A summary of the resources that would be created as part of the Blueprint. Only present if `valid` is `true`. */
4711
+ plan?: components["schemas"]["validationPlanSummary"];
4712
+ };
4606
4713
  resourceRef: {
4607
4714
  id: string;
4608
4715
  name: string;
@@ -5182,7 +5289,7 @@ export interface components {
5182
5289
  * @description Provider to send metrics to
5183
5290
  * @enum {string}
5184
5291
  */
5185
- otelProviderType: "BETTER_STACK" | "GRAFANA" | "DATADOG" | "NEW_RELIC" | "HONEYCOMB" | "SIGNOZ" | "CUSTOM";
5292
+ otelProviderType: "BETTER_STACK" | "GRAFANA" | "DATADOG" | "NEW_RELIC" | "HONEYCOMB" | "SIGNOZ" | "GROUNDSOURCE" | "CUSTOM";
5186
5293
  metricsStream: {
5187
5294
  /** @description The ID of the owner */
5188
5295
  ownerId: string;
@@ -5493,6 +5600,34 @@ export interface components {
5493
5600
  retries: number;
5494
5601
  attempts: components["schemas"]["TaskAttemptDetails"][];
5495
5602
  };
5603
+ blobMetadata: {
5604
+ /**
5605
+ * @description The blob's object key
5606
+ * @example workflow-data/task-output.json
5607
+ */
5608
+ key: string;
5609
+ /**
5610
+ * Format: int64
5611
+ * @description Size of the blob in bytes
5612
+ * @example 1048576
5613
+ */
5614
+ sizeBytes: number;
5615
+ /**
5616
+ * Format: date-time
5617
+ * @description When the blob was last modified (ISO 8601)
5618
+ * @example 2026-01-15T12:30:00Z
5619
+ */
5620
+ lastModified: string;
5621
+ /**
5622
+ * @description MIME type of the blob
5623
+ * @example application/json
5624
+ */
5625
+ contentType: string;
5626
+ };
5627
+ blobWithCursor: {
5628
+ cursor: string;
5629
+ blob: components["schemas"]["blobMetadata"];
5630
+ };
5496
5631
  getBlobOutput: {
5497
5632
  /**
5498
5633
  * Format: uri
@@ -5984,6 +6119,35 @@ export interface operations {
5984
6119
  503: components["responses"]["503ServiceUnavailable"];
5985
6120
  };
5986
6121
  };
6122
+ "validate-blueprint": {
6123
+ parameters: {
6124
+ query?: never;
6125
+ header?: never;
6126
+ path?: never;
6127
+ cookie?: never;
6128
+ };
6129
+ requestBody: {
6130
+ content: {
6131
+ "multipart/form-data": components["schemas"]["validateBlueprintRequest"];
6132
+ };
6133
+ };
6134
+ responses: {
6135
+ /** @description Validation complete */
6136
+ 200: {
6137
+ headers: {
6138
+ [name: string]: unknown;
6139
+ };
6140
+ content: {
6141
+ "application/json": components["schemas"]["validateBlueprintResponse"];
6142
+ };
6143
+ };
6144
+ 400: components["responses"]["400BadRequest"];
6145
+ 401: components["responses"]["401Unauthorized"];
6146
+ 403: components["responses"]["403Forbidden"];
6147
+ 429: components["responses"]["429RateLimit"];
6148
+ 500: components["responses"]["500InternalServerError"];
6149
+ };
6150
+ };
5987
6151
  "remove-workspace-member": {
5988
6152
  parameters: {
5989
6153
  query?: never;
@@ -7357,6 +7521,14 @@ export interface operations {
7357
7521
  * The host, repository, and image name all must match the currently configured image for the service.
7358
7522
  */
7359
7523
  imageUrl?: string;
7524
+ /**
7525
+ * @description Deployment mode controlling build and deploy behavior.
7526
+ *
7527
+ * Defaults to `build_and_deploy` when not specified.
7528
+ *
7529
+ * **Validation:** `deploy_mode` cannot be combined with `commitId` or `imageUrl` or `clearCache`.
7530
+ */
7531
+ deployMode?: components["schemas"]["DeployMode"];
7360
7532
  };
7361
7533
  };
7362
7534
  };
@@ -12619,6 +12791,42 @@ export interface operations {
12619
12791
  503: components["responses"]["503ServiceUnavailable"];
12620
12792
  };
12621
12793
  };
12794
+ "list-blobs": {
12795
+ parameters: {
12796
+ query?: {
12797
+ /** @description The position in the result list to start from when fetching paginated results. For details, see [Pagination](https://api-docs.render.com/reference/pagination). */
12798
+ cursor?: components["parameters"]["cursorParam"];
12799
+ /** @description The maximum number of items to return. For details, see [Pagination](https://api-docs.render.com/reference/pagination). */
12800
+ limit?: components["parameters"]["limitParam"];
12801
+ };
12802
+ header?: never;
12803
+ path: {
12804
+ /** @description The ID of the workspace to return resources for */
12805
+ ownerId: components["parameters"]["ownerIdPathParam"];
12806
+ /** @description The region to return resources for */
12807
+ region: components["parameters"]["regionPathParam"];
12808
+ };
12809
+ cookie?: never;
12810
+ };
12811
+ requestBody?: never;
12812
+ responses: {
12813
+ /** @description OK */
12814
+ 200: {
12815
+ headers: {
12816
+ [name: string]: unknown;
12817
+ };
12818
+ content: {
12819
+ "application/json": components["schemas"]["blobWithCursor"][];
12820
+ };
12821
+ };
12822
+ 401: components["responses"]["401Unauthorized"];
12823
+ 403: components["responses"]["403Forbidden"];
12824
+ 404: components["responses"]["404NotFound"];
12825
+ 429: components["responses"]["429RateLimit"];
12826
+ 500: components["responses"]["500InternalServerError"];
12827
+ 503: components["responses"]["503ServiceUnavailable"];
12828
+ };
12829
+ };
12622
12830
  "get-blob": {
12623
12831
  parameters: {
12624
12832
  query?: never;
@@ -0,0 +1,58 @@
1
+ import { getBaseUrl } from "./get-base-url.js";
2
+
3
+ describe("getBaseUrl", () => {
4
+ const originalEnv = process.env;
5
+
6
+ beforeEach(() => {
7
+ process.env = { ...originalEnv };
8
+ process.env.RENDER_USE_LOCAL_DEV = undefined;
9
+ process.env.RENDER_LOCAL_DEV_URL = undefined;
10
+ });
11
+
12
+ afterEach(() => {
13
+ process.env = originalEnv;
14
+ });
15
+
16
+ it("returns production URL by default", () => {
17
+ expect(getBaseUrl()).toBe("https://api.render.com");
18
+ });
19
+
20
+ it("returns custom baseUrl when provided", () => {
21
+ expect(getBaseUrl({ baseUrl: "https://custom.api.com" })).toBe("https://custom.api.com");
22
+ });
23
+
24
+ it("returns local dev URL when useLocalDev is true", () => {
25
+ expect(getBaseUrl({ useLocalDev: true })).toBe("http://localhost:8120");
26
+ });
27
+
28
+ it("respects custom localDevUrl", () => {
29
+ expect(getBaseUrl({ useLocalDev: true, localDevUrl: "http://localhost:9000" })).toBe(
30
+ "http://localhost:9000",
31
+ );
32
+ });
33
+
34
+ it("respects RENDER_USE_LOCAL_DEV env var", () => {
35
+ process.env.RENDER_USE_LOCAL_DEV = "true";
36
+ expect(getBaseUrl()).toBe("http://localhost:8120");
37
+ });
38
+
39
+ it("respects RENDER_LOCAL_DEV_URL env var", () => {
40
+ process.env.RENDER_USE_LOCAL_DEV = "true";
41
+ process.env.RENDER_LOCAL_DEV_URL = "http://localhost:7777";
42
+ expect(getBaseUrl()).toBe("http://localhost:7777");
43
+ });
44
+
45
+ it("option overrides env var for localDevUrl", () => {
46
+ process.env.RENDER_USE_LOCAL_DEV = "true";
47
+ process.env.RENDER_LOCAL_DEV_URL = "http://localhost:7777";
48
+ expect(getBaseUrl({ useLocalDev: true, localDevUrl: "http://localhost:9999" })).toBe(
49
+ "http://localhost:9999",
50
+ );
51
+ });
52
+
53
+ it("ignores baseUrl option when in local dev mode", () => {
54
+ expect(getBaseUrl({ useLocalDev: true, baseUrl: "https://custom.api.com" })).toBe(
55
+ "http://localhost:8120",
56
+ );
57
+ });
58
+ });
@@ -0,0 +1,68 @@
1
+ import type { Client } from "openapi-fetch";
2
+ import { AbortError, ClientError, ServerError } from "../../errors.js";
3
+ import type { paths } from "../../generated/schema.js";
4
+ import { WorkflowsClient } from "./client.js";
5
+
6
+ describe("WorkflowsClient", () => {
7
+ describe("runTask", () => {
8
+ it("throws AbortError if signal already aborted", async () => {
9
+ // No API calls occur; client throws before using the client
10
+ const mockApiClient = {} as unknown as Client<paths>;
11
+ const client = new WorkflowsClient(mockApiClient, "http://test", "token");
12
+
13
+ const controller = new AbortController();
14
+ controller.abort();
15
+ await expect(client.runTask("task-1", ["data"], controller.signal)).rejects.toBeInstanceOf(
16
+ AbortError,
17
+ );
18
+ });
19
+ });
20
+
21
+ describe("handleApiError (via getTaskRun)", () => {
22
+ it("throws ServerError for 5xx status", async () => {
23
+ const mockApiClient = {
24
+ GET: vi.fn().mockResolvedValue({
25
+ error: { message: "Internal error" },
26
+ response: { status: 500 },
27
+ }),
28
+ } as unknown as Client<paths>;
29
+
30
+ const client = new WorkflowsClient(mockApiClient, "http://test", "token");
31
+
32
+ await expect(client.getTaskRun("run-123")).rejects.toBeInstanceOf(ServerError);
33
+ await expect(client.getTaskRun("run-123")).rejects.toMatchObject({
34
+ statusCode: 500,
35
+ });
36
+ });
37
+
38
+ it("throws ClientError for 4xx status", async () => {
39
+ const mockApiClient = {
40
+ GET: vi.fn().mockResolvedValue({
41
+ error: { message: "Not found" },
42
+ response: { status: 404 },
43
+ }),
44
+ } as unknown as Client<paths>;
45
+
46
+ const client = new WorkflowsClient(mockApiClient, "http://test", "token");
47
+
48
+ await expect(client.getTaskRun("run-123")).rejects.toBeInstanceOf(ClientError);
49
+ await expect(client.getTaskRun("run-123")).rejects.toMatchObject({
50
+ statusCode: 404,
51
+ });
52
+ });
53
+ });
54
+
55
+ describe("listTaskRuns", () => {
56
+ it("throws ClientError for 400 status", async () => {
57
+ const mockApiClient = {
58
+ GET: vi.fn().mockResolvedValue({
59
+ error: { message: "Bad request" },
60
+ response: { status: 400 },
61
+ }),
62
+ } as unknown as Client<paths>;
63
+
64
+ const client = new WorkflowsClient(mockApiClient, "http://test", "token");
65
+ await expect(client.listTaskRuns({ taskId: ["task-1"] })).rejects.toBeInstanceOf(ClientError);
66
+ });
67
+ });
68
+ });
@@ -0,0 +1,52 @@
1
+ import type { TaskContext, TaskFunction, TaskResult } from "./types.js";
2
+
3
+ describe("TaskFunction type", () => {
4
+ it("accepts typed args and returns typed result", () => {
5
+ expectTypeOf<TaskFunction<[string, number], boolean>>().toExtend<
6
+ (a: string, b: number) => boolean | Promise<boolean>
7
+ >();
8
+ });
9
+
10
+ it("defaults to any[] args and any result", () => {
11
+ expectTypeOf<TaskFunction>().toExtend<(...args: any[]) => any>();
12
+ });
13
+
14
+ it("allows async return type", () => {
15
+ const asyncFn: TaskFunction<[string], number> = async (s) => s.length;
16
+ expectTypeOf(asyncFn).returns.toExtend<number | Promise<number>>();
17
+ });
18
+
19
+ it("allows sync return type", () => {
20
+ const syncFn: TaskFunction<[string], number> = (s) => s.length;
21
+ expectTypeOf(syncFn).returns.toExtend<number | Promise<number>>();
22
+ });
23
+ });
24
+
25
+ describe("TaskResult type", () => {
26
+ it("get() returns Promise<T>", () => {
27
+ expectTypeOf<TaskResult<string>>().toHaveProperty("get");
28
+ expectTypeOf<TaskResult<string>["get"]>().returns.toEqualTypeOf<Promise<string>>();
29
+ });
30
+
31
+ it("preserves generic parameter", () => {
32
+ type NumberResult = TaskResult<number>;
33
+ type StringResult = TaskResult<string>;
34
+ expectTypeOf<NumberResult["get"]>().returns.toEqualTypeOf<Promise<number>>();
35
+ expectTypeOf<StringResult["get"]>().returns.toEqualTypeOf<Promise<string>>();
36
+ });
37
+ });
38
+
39
+ describe("TaskContext type", () => {
40
+ it("has executeTask method", () => {
41
+ expectTypeOf<TaskContext>().toHaveProperty("executeTask");
42
+ });
43
+
44
+ it("executeTask is callable", () => {
45
+ type ExecuteTask = TaskContext["executeTask"];
46
+ expectTypeOf<ExecuteTask>().toBeCallableWith(
47
+ {} as TaskFunction<[string], number>,
48
+ "taskName",
49
+ "arg",
50
+ );
51
+ });
52
+ });
@@ -1,8 +1,6 @@
1
- import { createConnection } from "node:net";
1
+ import http from "node:http";
2
2
  import { getUserAgent } from "../version.js";
3
3
 
4
- const CONTENT_LENGTH_REGEX = /Content-Length:\s*(\d+)/i;
5
-
6
4
  import type {
7
5
  CallbackRequest,
8
6
  GetInputResponse,
@@ -97,83 +95,45 @@ export class UDSClient {
97
95
  */
98
96
  private async request<T>(path: string, method: string, body?: any): Promise<T> {
99
97
  return new Promise((resolve, reject) => {
100
- const client = createConnection({ path: this.socketPath }, () => {
101
- const bodyStr = body ? JSON.stringify(body) : "";
102
- const userAgent = getUserAgent();
103
- const request = `${method} ${path} HTTP/1.1\r\nHost: unix\r\nContent-Length: ${bodyStr.length}\r\nContent-Type: application/json\r\nUser-Agent: ${userAgent}\r\n\r\n${bodyStr}`;
104
- client.write(request);
105
- });
106
-
107
- let data = "";
108
- let contentLength: number | null = null;
109
- let headersParsed = false;
110
- let bodyStartIndex = -1;
111
-
112
- client.on("data", (chunk) => {
113
- data += chunk.toString();
114
-
115
- // Check if we have received the full response
116
- if (!headersParsed) {
117
- const headerEndIndex = data.indexOf("\r\n\r\n");
118
- if (headerEndIndex !== -1) {
119
- headersParsed = true;
120
- bodyStartIndex = headerEndIndex + 4;
121
-
122
- // Parse Content-Length header
123
- const headers = data.substring(0, headerEndIndex);
124
- const contentLengthMatch = headers.match(CONTENT_LENGTH_REGEX);
125
- if (contentLengthMatch) {
126
- contentLength = Number.parseInt(contentLengthMatch[1], 10);
127
- }
128
- }
129
- }
130
-
131
- // Check if we have received the complete body
132
- if (headersParsed && contentLength !== null) {
133
- const bodyReceived = data.length - bodyStartIndex;
134
- if (bodyReceived >= contentLength) {
135
- // We have the complete response, close the connection
136
- client.end();
137
- }
138
- }
139
- });
140
-
141
- client.on("end", () => {
142
- try {
143
- // Parse HTTP response
144
- const lines = data.split("\r\n");
145
- const statusLine = lines[0];
146
- const statusCode = Number.parseInt(statusLine.split(" ")[1], 10);
98
+ const req = http.request(
99
+ {
100
+ socketPath: this.socketPath,
101
+ path: path,
102
+ method: method,
103
+ headers: {
104
+ "Content-Length": body ? JSON.stringify(body).length : 0,
105
+ "Content-Type": "application/json",
106
+ "User-Agent": getUserAgent(),
107
+ },
108
+ },
109
+ async (res) => {
110
+ const chunks: Buffer[] = [];
111
+ for await (const chunk of res) chunks.push(chunk);
112
+ const responseBody = Buffer.concat(chunks).toString();
147
113
 
148
- if (statusCode >= 400) {
149
- reject(new Error(`HTTP ${statusCode}: ${data}`));
114
+ if (res.statusCode && res.statusCode >= 400) {
115
+ reject(new Error(`HTTP ${res.statusCode}: ${responseBody}`));
150
116
  return;
151
117
  }
152
118
 
153
- // Find empty line (separates headers from body)
154
- const emptyLineIndex = lines.indexOf("");
155
- if (emptyLineIndex === -1) {
119
+ if (responseBody.length === 0) {
156
120
  resolve(undefined as T);
157
121
  return;
158
122
  }
159
123
 
160
- const bodyLines = lines.slice(emptyLineIndex + 1);
161
- const responseBody = bodyLines.join("\r\n").trim();
162
-
163
- if (!responseBody) {
164
- resolve(undefined as T);
165
- return;
124
+ try {
125
+ resolve(JSON.parse(responseBody));
126
+ } catch (error) {
127
+ reject(error);
166
128
  }
167
-
168
- resolve(JSON.parse(responseBody));
169
- } catch (error) {
170
- reject(error);
171
- }
172
- });
173
-
174
- client.on("error", (error) => {
129
+ },
130
+ );
131
+ req.on("error", (error) => {
175
132
  reject(error);
176
133
  });
134
+
135
+ // Write the body to the request
136
+ req.end(body ? JSON.stringify(body) : "");
177
137
  });
178
138
  }
179
139
  }
package/tsconfig.json CHANGED
@@ -16,7 +16,7 @@
16
16
  "resolveJsonModule": true,
17
17
  "experimentalDecorators": true,
18
18
  "emitDecoratorMetadata": true,
19
- "types": ["vitest/globals"]
19
+ "types": ["node", "vitest/globals"]
20
20
  },
21
21
  "include": ["src/**/*"],
22
22
  "exclude": ["node_modules", "dist"]
@@ -2,6 +2,7 @@ import { defineConfig } from "vitest/config";
2
2
 
3
3
  export default defineConfig({
4
4
  test: {
5
+ include: ["src/**/*.test.ts"],
5
6
  globals: true,
6
7
  },
7
8
  });