npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@flowpure/edlink-effect-sdk

v1.0.7

Published

Edlink SDK built with Effect-TS — functional, type-safe, stream-based

Readme

Edlink Effect SDK

npm version license types node

Type-safe, functional SDK for the Edlink v2 API — built on Effect-TS.

Every list endpoint returns a lazy Stream with cursor-based pagination. Every response is schema-validated at the boundary. Errors are tagged for precise catchTag recovery. Ships .d.ts types — no @types/ package needed.


Install

pnpm add @flowpure/edlink-effect-sdk effect @effect/platform

Note: effect and @effect/platform are peer-level requirements. Add a platform-specific runtime package for your environment (see Runtime Compatibility).

Quick Start

import { Effect, Stream, Chunk } from "effect";
import { NodeRuntime } from "@effect/platform-node";
import { EdlinkClient, EdlinkLive } from "@flowpure/edlink-effect-sdk";

const program = Effect.gen(function* () {
  const client = yield* EdlinkClient;

  // Fetch up to 3 pages of classes (default)
  const classes = yield* client.classes
    .list()
    .pipe(Stream.runCollect, Effect.map(Chunk.toArray));

  console.log(`Fetched ${classes.length} classes`);
});

program.pipe(Effect.provide(EdlinkLive), NodeRuntime.runMain);

Set EDLINK_CLIENT_SECRET in your environment — that's the only required variable. The SDK reads it via Effect's Config module (auto-redacted as Secret.Secret in logs).


Runtime Compatibility

The SDK itself is runtime-agnostic — it uses FetchHttpClient (global fetch) and ESM. Only the runner changes per platform.

Node.js (>= 18)

pnpm add @effect/platform-node
import { NodeRuntime } from "@effect/platform-node";
program.pipe(Effect.provide(EdlinkLive), NodeRuntime.runMain);

Bun

bun add @effect/platform-bun
import { BunRuntime } from "@effect/platform-bun";
program.pipe(Effect.provide(EdlinkLive), BunRuntime.runMain);

Deno

import { Effect } from "npm:effect";
import { EdlinkClient, EdlinkLive } from "npm:@flowpure/edlink-effect-sdk";

// Use Effect.runPromise directly — Deno has global fetch
Effect.runPromise(program.pipe(Effect.provide(EdlinkLive)));

API Overview

The EdlinkClient service exposes 13 namespaced sub-services + an events stream:

const client = yield* EdlinkClient;

client.districts.list();                                    // → Stream<District>
client.schools.fetch(id);                                   // → Effect<School>
client.people.listEnrollments(personId);                    // → Stream<Enrollment>

client.assignments.create(classId, body);                   // → Effect<Assignment>
client.assignments.update({ classId, assignmentId, body }); // → Effect<Assignment>
client.assignments.delete(classId, assignmentId);           // → Effect<void>

Supported Entities

| Entity | List | Fetch | Create | Update | Delete | |---|:---:|:---:|:---:|:---:|:---:| | Districts | ✓ | ✓ | | | | | Schools | ✓ | ✓ | | | | | Courses | ✓ | ✓ | | | | | Sessions | ✓ | ✓ | | | | | Sections | ✓ | ✓ | | | | | Classes | ✓ | ✓ | | | | | People | ✓ | ✓ | | | | | Enrollments | ✓ | ✓ | | | | | Agents | ✓ | ✓ | | | | | Licenses | ✓ | | | | | | Assignments | ✓ | ✓ | ✓ | ✓ | ✓ | | Categories | ✓ | ✓ | ✓ | ✓ | ✓ | | Submissions | ✓ | ✓ | ✓ | ✓ | | | Events | ✓ | | | | |

Many entities support nested listing — for example:

  • client.schools.listClasses(schoolId) / listCourses / listTeachers / listStudents
  • client.classes.listSections(classId) / listEnrollments / listPeople
  • client.people.listDistricts(personId) / listSchools / listClasses / listAgents
  • client.sections.listEnrollments(sectionId) / listTeachers / listStudents
  • client.districts.listAdministrators(districtId)

All nested endpoints also return Stream with configurable pagination.


Working with Streams

All list* methods return Stream.Stream<Entity, EdlinkApiError | EdlinkDecodeError> — a lazy, pull-based stream. Pages are only fetched when downstream consumers demand more items. This is a core Effect Stream pattern.

Collect to an Array

Best for small datasets. Pulls all pages into memory at once.

const schools = yield* client.schools
  .list()
  .pipe(Stream.runCollect, Effect.map(Chunk.toArray));
// schools: School[]

Process One-by-One (Constant Memory)

Best for large datasets. Each item is processed and discarded — memory stays flat.

yield* Stream.runForEach(client.classes.list(), (cls) =>
  Effect.log(`${cls.id}: ${cls.name}`)
);

Transform Pipelines

Compose Stream operators for filtering, mapping, and batching before consumption.

const activeTeachers = client.people
  .list()
  .pipe(
    Stream.filter((p) => p.roles.includes("teacher")),
    Stream.take(100),
  );

const names = yield* activeTeachers.pipe(
  Stream.map((p) => p.display_name),
  Stream.runCollect,
  Effect.map(Chunk.toArray),
);

Tip: Streams are lazy — Stream.filter and Stream.take don't fetch extra pages once the downstream limit is reached.


Pagination

Every list* method accepts an optional PaginationConfig — a discriminated union with three strategies:

1. Cap by Pages (default)

client.districts.list({ type: "pages", maxPages: 5 })

Stops after N pages. Default is 3 pages if no config is provided (controlled by EDLINK_DEFAULT_MAX_PAGES).

2. Cap by Record Count

client.classes.list({ type: "records", maxRecords: 50 })

Stops at exactly N records — the SDK trims the last page if it would overshoot. Good for pagination UIs and batch processing with predictable output sizes.

3. Fetch Everything

client.enrollments.list({ type: "all" })

Fetches every available record. Use with caution on entities that may have thousands of rows. Combine with Stream.runForEach for constant-memory processing on large datasets.

How It Works Under the Hood

The SDK uses Stream.unfoldEffect with a cursor-based loop. Each iteration:

  1. Checks if the pagination limit has been reached
  2. Fetches the next page via HTTP
  3. Decodes the response through Effect Schema
  4. Emits items and advances the cursor

Pages are fetched lazily — if you Stream.take(10) from a large dataset, only enough pages to fill 10 items are requested.


Examples

The repo includes 7 runnable examples covering the most common patterns. Clone the repo, set up .env.local, and run them to see the SDK in action.

Setup

git clone https://github.com/tvsudhir2/edlink-effect-sdk.git
cd edlink-effect-sdk && pnpm install

cp .env.example .env.local
# Edit .env.local → set EDLINK_CLIENT_SECRET
# For examples 5–8, also set CLASS_ID
# For examples 7–8, also set ASSIGNMENT_ID

Fetching Data (Examples 1–3)

| # | Run | What It Does | Best For | Limitations | |---|---|---|---|---| | 1 | pnpm ex-1 | Fetch classes with default 3-page limit; collects to an array | First-time setup, quick sanity check that your credentials work | Loads all items into memory; uses default pagination only | | 2 | pnpm ex-2 | Fetch exactly 50 classes using { type: "records", maxRecords: 50 } | Batch processing, pagination UIs, predictable output sizes | Still collects everything in memory | | 3 | pnpm ex-3 | Process classes one-by-one via Stream.runForEach with a plain let counter | Large datasets, ETL pipelines, memory-constrained environments | Slightly more complex code; still uses default 3-page limit (pass { type: "all" } for unlimited streaming) | | 4 | pnpm ex-4 | Process classes concurrently via Stream.mapEffect with a Ref counter | High-throughput pipelines, enriching records via external API calls | Requires Ref for atomic counting across concurrent fibers |

Choosing between 1 / 2 / 3 / 4:

  • Need a quick array of items? → Example 1
  • Need a specific number of records? → Example 2
  • Need to process a large dataset sequentially without loading everything into memory? → Example 3
  • Need maximum throughput by processing many items concurrently? → Example 4

CRUD Operations (Examples 4–7)

| # | Run | What It Does | Best For | Limitations | |---|---|---|---|---| | 5 | pnpm ex-5 | List assignments for a class using client.assignments.list(classId) | Viewing class assignments, reading nested/scoped resources | Requires CLASS_ID env var; collects all results into memory | | 6 | pnpm ex-6 | Create a new assignment with a full request body (title, points, due date, etc.) | LMS integrations that create homework/quizzes programmatically | The request body is passed as a plain object — see the example for all available fields | | 7 | pnpm ex-7 | Fetch-then-update pattern: reads an assignment, patches its title/points/due date | Extending deadlines, adjusting point values, partial updates | Requires both CLASS_ID and ASSIGNMENT_ID; two sequential API calls | | 8 | pnpm ex-8 | Verify-then-delete pattern: fetches an assignment to confirm it exists, then deletes it | Cleaning up drafts or cancelled assignments | Requires both CLASS_ID and ASSIGNMENT_ID; no undo — deletion is permanent |

Choosing between 5 / 6 / 7 / 8:

  • Need to read entities scoped to a parent? → Example 5
  • Need to create a new resource? → Example 6
  • Need to partially update a resource? → Example 7
  • Need to delete a resource? → Example 8

What the Examples Don't Cover

  • Error handling — none of the examples use Effect.catchTag for recovery. See Error Handling below for patterns.
  • User API (OAuth2) — no runnable examples yet. See User API for usage.
  • { type: "all" } pagination — Example 3 uses default 3-page limit. Pass { type: "all" } as the pagination config to stream everything.

User API (OAuth2)

For per-user actions (e.g. fetching a student's own profile), the SDK provides an OAuth2 authorization code flow via EdlinkUserClient.

import { Effect } from "effect";
import { EdlinkUserClient, EdlinkUserLive } from "@flowpure/edlink-effect-sdk";

const program = Effect.gen(function* () {
  const userClient = yield* EdlinkUserClient;

  // 1. Generate the authorization URL — redirect your user here
  const authUrl = userClient.authorize(["roster:read"]);

  // 2. After consent, exchange the callback code for tokens
  const tokenResponse = yield* userClient.handleCallback(code);

  // 3. Fetch the user's profile (auto-refreshes token if expired)
  const profile = yield* userClient.getProfile(userId);
});

program.pipe(Effect.provide(EdlinkUserLive));

Required env vars: EDLINK_CLIENT_ID, EDLINK_CLIENT_SECRET, EDLINK_REDIRECT_URI

Token storage is pluggable. The SDK ships InMemoryTokenStoreLive for development. For production, provide your own Layer<TokenStore> backed by Redis, a database, or any persistent store:

import { Layer } from "effect";
import { FetchHttpClient } from "@effect/platform";
import { EdlinkUserClientLive, EdlinkUserConfig } from "@flowpure/edlink-effect-sdk";

const MyUserLive = EdlinkUserClientLive.pipe(
  Layer.provide(EdlinkUserConfig.Live),
  Layer.provide(FetchHttpClient.layer),
  Layer.provide(MyRedisTokenStoreLive), // your implementation
);

No runnable examples yet — contributions welcome.


Error Handling

The SDK exposes two tagged error types. Both extend Data.TaggedError, so you can use Effect's catchTag for selective recovery.

| Error | When | |---|---| | EdlinkApiError | HTTP failure, non-OK status, network error | | EdlinkDecodeError | API response doesn't match the expected schema |

import { Effect } from "effect";
import { EdlinkApiError, EdlinkDecodeError } from "@flowpure/edlink-effect-sdk";

yield* client.schools.fetch(id).pipe(
  Effect.catchTag("EdlinkApiError", (err) =>
    Effect.logError(`API error: ${err.message}`)
  ),
  Effect.catchTag("EdlinkDecodeError", (err) =>
    Effect.logError(`Unexpected response shape: ${err.message}`)
  ),
);

Schema validation happens at the boundary — if the Edlink API returns malformed data, you get an EdlinkDecodeError immediately instead of a silent type mismatch downstream.


Configuration

All config is loaded via Effect's Config module — zero process.env usage.

Graph API

| Variable | Required | Default | Description | |---|---|---|---| | EDLINK_CLIENT_SECRET | yes | — | Bearer token for the Edlink API (stored as Secret.Secret) | | EDLINK_API_BASE_URL | no | https://ed.link/api | API base URL | | EDLINK_DEFAULT_MAX_PAGES | no | 3 | Default page limit when no PaginationConfig is given |

User API (OAuth2)

| Variable | Required | Default | Description | |---|---|---|---| | EDLINK_CLIENT_ID | yes | — | OAuth2 client ID | | EDLINK_CLIENT_SECRET | yes | — | OAuth2 client secret | | EDLINK_REDIRECT_URI | yes | — | OAuth2 redirect URI |

Tip: For local development, pass env vars via tsx --env-file=.env.local (like the examples do). In production, use your platform's native secret management.


TypeScript

  • Ships with .d.ts declarations — no separate @types/ package needed.
  • Compiled to ES2022 with NodeNext module resolution.
  • ESM only — no CommonJS build.
  • Strict mode enabled: strict, exactOptionalPropertyTypes, noUncheckedIndexedAccess.

Contributing

Contributions, bug reports, and feature requests are welcome. Please open an issue on GitHub before submitting a PR.

git clone https://github.com/tvsudhir2/edlink-effect-sdk.git
cd edlink-effect-sdk && pnpm install
pnpm build

Changelog

See GitHub Releases for version history.

License

MIT