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

justlink

v1.2.1

Published

Type-safe RPC for Workers, threads, and beyond — zero deps, local-feel calls.

Downloads

517

Readme

justlink

English Documentation

类型安全的 Worker 与线程 RPC 库——零依赖,本地调用般的体验。

灵感来源于 comlinkminlink

这是什么?

Worker 能帮你把耗时的计算放到后台线程,但和 Worker 通信是一件很痛苦的事——你需要手动 postMessage / onmessage,手动序列化数据,手动追踪状态,代码很快就会变成一团面条。

justlink 让你像调用本地函数一样调用 Worker 里的方法。 你只需要定义一个对象,justlink 会自动帮你处理所有通信细节。

// 定义(在 worker 中)
const impl = {
    fibonacci(n: number) {
        // 这个计算会跑在 Worker 线程,不会卡 UI
        return n <= 1 ? n : fibonacci(n - 1) + fibonacci(n - 2);
    },
};

// 使用(在主线程中)
const api = wrap<Impl>(new MyWorker());
const result = await api.fibonacci(40); // 就这么简单!

没有 postMessage,没有 onmessage,没有手动序列化。只需要 exposewrap,两个函数搞定。

为什么选 justlink?

| | justlink | 原生 postMessage | Comlink | | ---------------- | ---------------------------------- | ------------------- | ------- | | 代码量 | 极少(2 个函数) | 需要大量样板代码 | 较少 | | TypeScript | ✅ 完整类型推导 + 自动补全 | ❌ 需要手动定义类型 | ✅ | | 依赖数 | 0 | 0 | 1 | | 嵌套对象 | ✅ 自动代理 | ❌ 需要手动处理 | ✅ | | Transferable | ✅ 自动检测 | ❌ 需要手动列举 | ✅ | | $eval | ✅ 可在 Worker 内执行任意逻辑 | ❌ | ❌ | | Node.js | ✅ worker_threads | ✅ | ❌ | | 内存模式 | ✅ 主线程/非 Worker 场景 | ❌ | ❌ | | 事件 | ✅ $on/$off/$once + emit() | ❌ | ❌ |

安装

npm install justlink

导入方式

justlink 提供三个入口——根据你的运行环境选择:

| 导入路径 | 适用场景 | | ------------------ | -------------------------------- | | justlink/browser | Web Worker(self / Worker) | | justlink/node | worker_threadsparentPort) | | justlink/memory | 进程内(无需真实 Worker) |

import { expose, wrap } from 'justlink/browser';
import { expose, wrap } from 'justlink/node';
import { expose, wrap, createMemoryPair } from 'justlink/memory';
import type { RemoteApi } from 'justlink/browser';

core 模块(justlink/core)提供底层 API,用于构建自定义 Adapter:

import { createExpose, createWrap, type Adapter, type EmitFn, type EventMap, type RemoteApi } from 'justlink/core';

快速开始(5 分钟)

只需要 3 步,就能让 Worker 跑起来。

第 1 步:创建 worker 实现

新建 worker-impl.ts,写一个普通的对象,包含你想在 Worker 中执行的方法:

// worker-impl.ts
let count = 0;

export const impl = {
    // 普通方法
    greet(name: string) {
        return `你好, ${name}!`;
    },

    // 有状态的方法
    getCount() {
        return count;
    },
    increment(n = 1) {
        return (count += n);
    },

    // 嵌套对象也支持
    nested: {
        add(a: number, b: number) {
            return a + b;
        },
    },
};

// 导出类型——宿主端需要用到
export type Impl = typeof impl;

💡 关键点: 你只需要写一个普通对象,不需要继承任何基类,不需要注册任何装饰器。

第 2 步:在 Worker 中暴露它

浏览器(Vite)

// worker.ts
import { expose } from 'justlink/browser';
import { impl } from './worker-impl';

expose(self, impl); // 一行搞定!

Node.js

// worker.ts
import { parentPort } from 'node:worker_threads';
import { expose } from 'justlink/node';
import { impl } from './worker-impl';

expose(parentPort!, impl); // 一行搞定!

第 3 步:在主线程中使用

浏览器(Vite)

// main.ts
import { wrap } from 'justlink/browser';
import type { Impl } from './worker-impl';
import MyWorker from './worker?worker'; // Vite 的 Worker 导入语法

// 创建 worker,包装成类型安全的 api
const api = wrap<Impl>(new MyWorker());

// 🎉 就像调用本地函数一样!
console.log(await api.greet('世界')); // "你好, 世界!"
console.log(await api.increment(5)); // 5
console.log(await api.nested.add(1, 2)); // 3

Node.js

// main.ts
import { Worker } from 'node:worker_threads';
import { wrap } from 'justlink/node';
import type { Impl } from './worker-impl';

const worker = new Worker('./worker.js');
const api = wrap<Impl>(worker);

// 🎉 用法完全一样!
console.log(await api.greet('世界')); // "你好, 世界!"
console.log(await api.increment(5)); // 5

💡 核心概念: expose 是在 Worker 端"暴露"实现,wrap 是在主线程端"包装"成代理对象。两个函数配合使用,就完成了整个通信链路。

第 4 步:监听事件(可选)

如果 Worker 需要向主线程推送实时更新,可以使用 工厂模式 配合 emit

// worker.ts — 工厂函数接收类型安全的 emit
import { expose, type EmitFn, type EventMap } from 'justlink/browser';

// 声明事件类型,两端共享
type MyEvents = {
    tick: [timestamp: number];
};

expose(self, (emit: EmitFn<MyEvents>) => ({
    startTimer() {
        setInterval(() => emit('tick', Date.now()), 1000);
    },
}));
// main.ts — 订阅事件(类型自动推导)
const api = wrap<Impl, MyEvents>(new MyWorker());

api.$on('tick', timestamp => {
    console.log('tick:', timestamp); // number
});
api.$on('typo', ...); // ❌ 编译错误

💡 $on 返回一个取消订阅函数。使用 $once 订阅单次事件,$off 移除特定 handler。

完整示例:在 Worker 中做计算

下面是一个更实际的例子——把计算密集型任务放到 Worker 中:

// worker-impl.ts
export const impl = {
    // 计算斐波那契数列——耗时操作,适合放到 Worker
    fibonacci(n: number): number {
        return n <= 1 ? n : this.fibonacci(n - 1) + this.fibonacci(n - 2);
    },

    // 处理数组
    sum(arr: number[]): number {
        return arr.reduce((a, b) => a + b, 0);
    },

    // 返回复杂对象
    analyze(text: string) {
        return {
            length: text.length,
            words: text.split(/\s+/).length,
            chars: new Set(text).size,
        };
    },
};

export type Impl = typeof impl;
// main.ts
const api = wrap<Impl>(new MyWorker());

// 计算密集型任务——不会卡 UI
const result = await api.fibonacci(40);

// 传递大数组——自动 transfer
const data = new Array(1000000).fill(0).map(() => Math.random());
const total = await api.sum(data);

// 获取复杂对象
const stats = await api.analyze('hello world');
console.log(stats.length); // 11
console.log(stats.words); // 2
console.log(stats.chars); // 9

API 参考

expose(ctx, impl)

在 Worker 端调用。 把一个普通对象暴露给主线程。

| 参数 | 说明 | | ------ | ----------------------------------------------------------- | | ctx | Worker 上下文。浏览器中是 self,Node.js 中是 parentPort | | impl | 要暴露的对象,包含属性和方法 |

wrap<Impl, Events>(ctx): RemoteApi<Impl, Events>

在主线程端调用。 把 Worker 包装成一个类型安全的代理对象。

| 参数 | 说明 | | ----- | ------------------------------------------------------------------------- | | ctx | Worker 实例。浏览器中是 new Worker(...),Node.js 中是 new Worker(...) |

返回值 api 上有以下特殊方法:

| 方法 | 说明 | 示例 | | ---------------------------- | -------------------- | ------------------------------ | | api.$get(key) | 读取远程属性 | await api.$get('name') | | api.$exec(method, ...args) | 动态调用方法 | await api.$exec('add', 1, 2) | | api.$eval(callback, deps?) | 在 Worker 内执行回调 | 见下方 $eval 章节 | | api.$on(event, handler) | 订阅事件 | api.$on('tick', handler) | | api.$off(event, handler) | 移除事件 handler | api.$off('tick', handler) | | api.$once(event, handler) | 订阅单次事件 | api.$once('tick', handler) | | api.$terminate() | 终止远程端 | await api.$terminate() |

💡 大多数情况下你不需要这些方法——直接 await api.methodName(args) 就够了。这些是"逃生舱",用于动态方法名等特殊场景。

$terminate() — 终止远程端

调用 $terminate() 后,远程端会被关闭,所有 pending 请求都会被 reject。后续的调用会立即 reject,不会进入无尽的 Promise pending 状态:

await api.$terminate();

// 以下调用都会立即 reject:
await api.someMethod(); // → reject: "The remote peer has been terminated"
await api.$get('x');    // → reject
await api.$eval(...);   // → reject

💡 这是一个安全机制——防止在已终止的远程端上意外挂起。

createMemoryPair()

import { createMemoryPair } from 'justlink/memory'

创建一对 host / worker 上下文,通过进程内直接调用通信,无需真实的 Worker。

典型场景:你封装了一套基于 justlink 的 API,希望它在 Worker 内外都能使用:

import { createMemoryPair, expose, wrap } from 'justlink/memory';
import { expose as browserExpose, wrap as browserWrap } from 'justlink/browser';

// 无论在哪个环境,api 的用法完全一样
let api: ImplApi;

if (isWorker) {
    // Worker 环境:走 postMessage
    browserExpose(self, impl);
    api = browserWrap<Impl>(self);
} else {
    // 主线程 / Node 主进程:进程内直接调用
    const { host, worker } = createMemoryPair();
    expose(worker, impl);
    api = wrap<Impl>(host);
}

// 以下代码在两种环境下行为完全一致
console.log(await api.someMethod());

进阶用法

$eval — 在 Worker 内执行代码

普通调用(api.methodName())是"在主线程调用,Worker 执行"。但有时候你需要"在 Worker 内部操作"——比如一次往返中同时读取多个属性。$eval 就是为此设计的:

// ❌ 普通方式:3 次往返通信
const a = await api.a;
const b = await api.nested.b;
const c = await api.nested.c;

// ✅ $eval:1 次往返通信
const result = await api.$eval(ref => {
    return ref.a + ref.nested.b + ref.nested.c;
});

传递外部数据到 Worker

$eval 的回调会被序列化发送到 Worker 内部执行,所以不能使用外部变量。需要用第二个参数传递:

// 把 Uint8Array 传到 Worker 中处理
const data = new Uint8Array([1, 2, 3]);
const sum = await api.$eval(
    (ref, arr) => ref.processData(arr),
    [data], // deps 数组中的值会被自动 transfer
);

字符串形式

Function.prototype.toString() 不可用或返回无法解析的内容时——例如函数已被编译为 V8 字节码(代码缓存、快照),或运行环境覆写了 toString(JS 沙箱、Proxy 包装)——可以直接传字符串表达式或代码体:

// 表达式 — 隐式 return
await api.$eval<number>('ref.add(a, b)', { a: 1, b: 2 });
await api.$eval('ref.foo + ref.bar');

// 代码体 — 多行逻辑,需要显式 return
await api.$eval<number>('const x = ref.add(a, b); return x * 2;', { a: 3, b: 4 });

字符串形式会自动根据 args 对象的 key 包装成 (ref, ...keys) => ... 箭头函数,wire 协议完全一致——expose 端无需任何改动。

  • 表达式 vs 代码体判断规则:; 或换行 → 代码体(需要 return);否则 → 表达式(隐式 return)。以声明语句开头(constletreturnifthrow 等)也会触发代码体模式。
  • args 的 key = 变量名: args 对象的 key 会成为函数形参,必须与字符串中的变量名完全一致。
  • Transferable 自动检测: args 中的值会自动检测 transferable,与函数形式的 deps 行为一致。

⚠️ 注意: 两种形式都不支持闭包捕获外部变量。函数形式通过 .toString() 序列化;字符串形式无需序列化。通过第二个参数传值——函数形式传数组(deps),字符串形式传对象(args)。

Transferable 对象

justlink 会自动检测并转移以下类型——无需手动管理 transferList

  • ArrayBuffer
  • 类型数组(Uint8ArrayFloat32Array 等)
  • OffscreenCanvas
  • ImageBitmap
  • MessagePort

💡 什么是 Transfer? 普通的数据传递是"复制",Transfer 是"移动"——所有权从发送方转移到接收方,发送方的视图会失效。好处是零拷贝,性能更好。

错误处理

Worker 中抛出的错误会自动传递到主线程:

const api = wrap<Impl>(new MyWorker());

try {
    await api.willThrow(); // 假设 Worker 中这个方法会抛错
} catch (err) {
    console.error(err.message); // 错误信息会正确传递过来
}

嵌套对象与返回值

Worker 方法返回对象时,justlink 会自动代理,你可以继续调用返回对象上的方法:

const counter = await api.createCounter(0);
await counter.inc(); // 1
await counter.inc(); // 2
await counter.get(); // 2

// 继续深入嵌套
const obj = await api.getNestedObject();
await obj.child.deepMethod();

普通对象 vs 代理对象

justlink 根据返回值是否包含函数来决定传输方式:

包含函数的对象 → 返回远程代理,每次访问触发 postMessage 通信:

// Worker 返回 { count: 1, inc() {...} }
const obj = await api.createCounter(0);
await obj.inc(); // 触发一次 postMessage
await obj.count; // 再触发一次 postMessage

纯数据对象(不含函数)structured clone,返回普通 JS 对象,零开销:

// Worker 返回 { x: 1, y: [2, 3] }
const point = await api.getPoint();
// point 是普通对象——直接使用,同步读取
console.log(point.x); // 1 — 无 postMessage
console.log(point.y); // [2, 3]

💡 提示: 如果返回值意外包含了函数字段,它会被视为远程对象。如果不需要,可以在返回前移除。

$get 的返回值

// 纯数据 → 普通对象
const data = await api.$get('config');
console.log(data.theme); // 'dark' — 同步读取

// 包含函数 → 远程代理
const counter = await api.$get('counter');
await counter.inc(); // postMessage 往返

$eval 的返回值

// 返回纯数据 → 普通值
const sum = await api.$eval(ref => ref.a + ref.nested.a); // 4

// 返回包含函数的对象 → 远程代理
const counter = await api.$eval(ref => ref.createCounter(0));
await counter.inc(); // postMessage 往返

事件 — Worker → 主线程

普通的 RPC 是由主线程发起的:你调用一个方法,然后等待结果。但有时候 Worker 需要主动向你推送数据 — 实时更新、进度通知、周期性数据。这就是 事件 的用途。

Worker 端 — 工厂模式

expose 的第二个参数支持工厂函数。工厂接收类型安全的 emit 函数,返回实现对象:

// worker.ts
import { expose, type EmitFn, type EventMap } from 'justlink/browser';

// 声明事件映射,两端共享
type MyEvents = {
    tick: [timestamp: number];
    progress: [data: { file: string; percent: number }];
};

expose(self, (emit: EmitFn<MyEvents>) => ({
    // 方法可以在返回值的同时发射事件
    processFile(name: string) {
        emit('progress', { file: name, percent: 0 }); // ✅
        emit('progress', { file: name, percent: 100 });
        return { done: true };
    },

    // 周期性事件
    startTimer() {
        setInterval(() => emit('tick', Date.now()), 1000);
    },
}));

主线程 — 订阅

const api = wrap<Impl, MyEvents>(new MyWorker());

// $on — 订阅(返回取消订阅函数)
const unsub = api.$on('tick', timestamp => {
    console.log('tick:', timestamp); // number
});

// $once — 只触发一次,然后自动移除
api.$once('progress', data => {
    console.log('第一次进度:', data);
});

// $off — 移除特定 handler
const handler = (data: unknown) => {
    /* ... */
};
api.$on('progress', handler);
api.$off('progress', handler);

// 取消订阅
unsub();

工厂 vs 纯对象

| 模式 | 适用场景 | | ---------------------------------- | -------------------------- | | expose(ctx, { ... }) | 简单的实现,不需要推送事件 | | expose(ctx, (emit) => ({ ... })) | 方法需要向主线程推送事件 |

工厂模式向后兼容 — 使用纯对象的现有代码无需任何更改。

事件参数传输

事件参数走与 RPC 响应相同的传输管道:

  • 原始类型直接克隆
  • 包含函数的对象变为远程代理
  • Transferable 对象(ArrayBuffer、Uint8Array 等)自动检测

自定义传输层

核心的 createExpose / createWrap 函数接受泛型的 Adapter 元组,可以对接任意传输层:

import { createExpose, createWrap, type Adapter } from 'justlink/core';

const myAdapter: Adapter<MyContext> = [
    // emit(ctx, data, transferList?) — 向对方发送数据
    (ctx, data, transferList) => {
        /* ... */
    },
    // listen(ctx, handler) — 注册接收数据的回调
    (ctx, handler) => {
        /* ... */
    },
    // terminate(ctx) — 清理上下文
    ctx => Promise.resolve(),
];

export const expose = createExpose(myAdapter);
export const wrap = createWrap(myAdapter);

导入路径一览

import { expose, wrap } from 'justlink/browser'; // 浏览器 Web Worker
import { expose, wrap } from 'justlink/node'; // Node.js worker_threads
import { createMemoryPair, expose, wrap } from 'justlink/memory'; // 进程内通信
import { createExpose, createWrap } from 'justlink/core'; // 自定义传输层

Agent Skills

justlink 附带了面向 AI 编程助手的 skills——可复用的知识,帮助 agent 生成正确的 justlink 代码。

npx skills add oyzhen/justlink

| Skill | 描述 | | -------------------- | ------------------------------------------------------------------------ | | wrap-to-remote-api | 在 Worker 中暴露一个普通对象,并包装为 RemoteApi 代理 | | universal-worker | 自动检测环境运行 Worker;不可用时通过 justlink/memory 降级为主进程执行 |

限制与注意事项

$eval 函数形式闭包不生效

$eval 函数回调在 Worker 内部被 .toString() 序列化后执行——闭包中的外部变量无法传递。请使用 deps 数组或字符串形式传参。

__ref 对象

包含函数的对象无法被结构化克隆,将被存储为远程引用(ref)。如果你的纯数据对象恰巧含有 __ref 属性,justlink 会错误地将其视为远程引用描述符。如有冲突请换用其他属性名前缀。

环境要求

  • TypeScript 5.0+
  • 浏览器适配器需要支持 ?worker 导入的打包工具(如 Vite、Webpack 等)

许可证

MIT