Skip to content

refactor: make provider imports go one way, so no file imports a file that imports it back - #25

Merged
K-Mistele merged 1 commit into
mainfrom
one-way-provider-imports
Oct 16, 2026
Merged

K-Mistele merged 1 commit into
mainfrom
one-way-provider-imports

Conversation

@K-Mistele

Copy link
Copy Markdown
Contributor

Why

Each provider's API service returned objects whose methods call the API service back. For example, GitHubApi.postIssueComment returned a GitHubIssueComment, and that comment's addReaction() calls GitHubApi. So the API service file and the objects file each imported the other.

In Linear and Slack this crashed at startup, depending on which file loaded first. #24 patched over it with second tags that share the service's key (LinearApiHandle, SlackApiHandle). This PR removes that workaround and fixes the cause.

What changed

Each provider's files now fall into three groups, and each group imports only from the groups above it. These are groups of files, not Effect Layers; how services are built and provided doesn't change.

  1. Plain data: refs, ids, and the records the API returns. No methods.
  2. The API service (GitHubApi, LinearApi, SlackApi): takes and returns plain data only, and never imports the objects.
  3. Callback objects (GitHubIssue, GitHubIssueComment, LinearFile, SlackMessage, …): wrap plain data. Their methods call the API service and wrap what it returns.
  • The API services return records, such as GitHubIssueCommentRecord, LinearCommentRecord and SlackMessageRecord, instead of objects with methods.
  • Callback code doesn't change. issue.postComment(), thread.listMessages() and file.download() still return objects.
  • Wrapping a record: objects with exactly the record's fields use X.make(record). Objects that hold other objects have a static X.fromRecord(record) (LinearComment, SlackMessage).
  • The second tags are deleted: LinearApiHandle and SlackApiHandle.
  • Slack: SlackModels.ts held both plain data and objects, so SlackMessage and SlackFile move to their own files. This also fixes a startup crash that was already on main: importing SlackModels first.
  • Where plain data now lives:
    • Linear: LinearFileModels.ts and LinearResourceRecords.ts.
    • GitHub: GitHubModels.ts.
  • Alchemy Cloudflare example: the helpers that read comments returned by GitHubApi now take the record types. The tests' fake APIs return records.

Breaking changes

Code that calls an API service directly gets records back:

  • GitHub:
    • listIssueComments and listPullRequestComments return GitHubIssueCommentRecords.
    • listPullRequestReviewComments returns GitHubReviewCommentRecords.
    • postIssueComment and postPullRequestComment return GitHubIssueCommentRecord.
    • postPullRequestReviewComment and replyToReviewComment return GitHubReviewCommentRecord.
    • updateComment returns GitHubCommentRecord.
    • listCheckRunsForRef returns GitHubCheckRunRecords.
    • resolveActionsJob returns GitHubActionsJobRecord | null.
  • Linear:
    • listIssueComments returns LinearCommentRecord[].
    • listIssueAttachments returns LinearIssueAttachmentRecord[].
    • createComment and updateComment return LinearCommentRecord.
    • createReaction returns LinearReactionRecord.
    • createAttachment, updateAttachment and uploadAttachment return LinearIssueAttachmentRecord.
    • uploadFile returns LinearFileRecord.
  • Slack:
    • listThreadMessages and listChannelMessagesBeforeThread return SlackMessageRecords.
    • getMessage returns SlackMessageRecord.
    • postToThread, postToChannel and stream return SlackSentMessageRecord.
    • uploadFileToChannel and uploadFileToThread return SlackFileRecord.
  • Moved names: some plain schemas moved files, such as LinearFileRef (now in LinearFileModels) and LinearFileError (now in LinearApiContract). All are still exported from each package's root.

Checks

  • bun run check (lint, typecheck, typed-lint) and bun run format: pass.
  • bun run test: 742 tests pass.
  • Backend suites: Redis passes 87 tests, Postgres 86.
  • Release validation (build, prepare, validate, publish dry run): passes.
  • Import cycles: none in any package's src or in the example.
  • Load order: every GitHub, Linear and Slack module loads when imported on its own.

🤖 Generated with Claude Code

… that imports it back

Each provider's API service (GitHubApi, LinearApi, SlackApi) returned objects whose methods call the
API service back: postIssueComment returned a GitHubIssueComment, whose addReaction() calls GitHubApi.
So the API service files and the objects files imported each other. In Linear and Slack that crashed at
startup depending on which file loaded first, and was patched over with second tags sharing the
services' keys (LinearApiHandle, SlackApiHandle).

Now each provider's files fall into three groups, and each imports only from the ones before it:
- Plain data: refs, ids, and the plain records the API returns. No methods.
- The API service: takes and returns plain data only, and never imports the objects.
- Callback objects (GitHubIssue, GitHubIssueComment, LinearFile, SlackMessage, ...): wrap plain data;
  their methods call the API service and wrap what it returns.

- The API services now return records (`GitHubIssueCommentRecord`, `LinearCommentRecord`,
  `SlackMessageRecord`, ...) instead of objects with methods. Callback code is unchanged:
  `issue.postComment()`, `thread.listMessages()`, and `file.download()` still return objects.
- Objects with exactly a record's fields are built with `X.make(record)`; objects that hold other
  objects have a static `X.fromRecord(record)` (`LinearComment`, `SlackMessage`).
- LinearApiHandle and SlackApiHandle are deleted; the objects use the real tags.
- Slack's SlackModels.ts held both plain data and objects; SlackMessage and SlackFile move to their own
  files. This also fixes a startup crash that was already on main (importing SlackModels first).
- Linear's plain file and resource data move to LinearFileModels.ts and LinearResourceRecords.ts;
  GitHub's plain data the API returns moves to GitHubModels.ts.
- The Alchemy Cloudflare example's helpers that read comments from GitHubApi now take records.

No package's source has an import cycle, and every provider module loads on its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
HumanLayer-Session: https://app.dev.codelayer.gg/sessions/01a1418a-45ea-7a36-87bf-d2e8c4e465fe
@K-Mistele
K-Mistele merged commit 9b74803 into main Oct 16, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant