显示 HN:LazyPromise = Observable – 信号
Show HN: LazyPromise = Observable – Signals

原始链接: https://github.com/lazy-promise/lazy-promise

## LazyPromise:Promises & Observables 的轻量级替代方案 LazyPromise 是一种处理异步操作的新方法,旨在实现简单和控制。它解决了 Promises 和 RxJS Observables 的缺点,提供了一种旨在避免常见陷阱(如钻石问题和不必要的微任务调度)的原始类型。 主要特性包括: * **惰性:** LazyPromise 在显式订阅之前不会执行,如果不存在订阅者,可以通过 teardown 函数进行取消。 * **控制:** 它避免了强制性的微任务,使开发者能够精确控制执行顺序。 * **类型化错误:** 支持可选但推荐的类型化错误处理,以实现健壮的代码。 * **熟悉 API:** 模仿 Promise API,使用 `pipe` 替换 `.then`、`.catch` 和 `.finally`。 * **取消:** Promises 在解析之前可以被取消。 LazyPromise 使用 `pipe` 函数来链式操作,并提供用于转换为/从标准 Promises (`eager`、`lazy`) 的实用工具。它还包括一个“failure”通道来处理意外的、未类型化的错误,以及 SolidJS 的实验性绑定。本质上,LazyPromise 旨在成为异步工作流程的一种更易于管理和可预测的替代方案。

Hacker News 新闻 | 过去 | 评论 | 提问 | 展示 | 招聘 | 提交 登录 Show HN: LazyPromise = Observable – Signals (github.com/lazy-promise) 3 分,作者 ivan7237d 2小时前 | 隐藏 | 过去 | 收藏 | 讨论 一种类似 Promise 的基础类型,具有延迟/可取消性,具有类型化的错误,并且同步发出而不是在微任务队列上。 从某种程度上说,LazyPromise 就像你获取一个 Observable 并使其不可能被用于 Signals 构建的目的那样。 指南 | 常见问题 | 列表 | API | 安全 | 法律 | 申请 YC | 联系方式 搜索:
相关文章

原文

A LazyPromise is like a Promise, with three differences:

The ingredients that went into the cauldron were as follows:

  • A primitive-based approach: make the simplest possible primitive for the job without attempting to think of all possible use-cases.

  • The good and bad parts of the experience of using RxJS. You can't beat Observable for simplicity, but you've got the diamond problem and undesirable behavior in the case of sync re-entry. LazyPromise is what you get if you take an Observable, make it impossible to misuse it for what the Signals were built to do, and then take advantage of the reduced scope to gracefully handle re-entry.

  • Desire to avoid mandatory microtasks. A native promise would guarantee that when you do promise.then(foo); bar();, foo will run after bar, but this guarantee comes with a cost: if for example you have two async functions that each await a few resolved promises, which of them will finish last will depend on which one has more awaits in it (this breaks modularity). Without microtasks, you're in full control over what runs in what order.

  • Practical need for typed errors.

npm install @lazy-promise/core pipe-function

In the above snippet, pipe-function package provides the pipe function, but there is nothing special about it and you can use the same function from another library. pipe(x, foo, bar) is bar(foo(x)).

You create a LazyPromise much like you call the Promise constructor, except you can optionally return a teardown function, for example:

const lazyPromise = createLazyPromise<0, "oops">((resolve, reject) => {
  const timeoutId = setTimeout(() => {
    if (Math.random() > 0.5) {
      resolve(0);
    } else {
      reject("oops");
    }
  }, 1000);
  return () => {
    clearTimeout(timeoutId);
  };
});

Unlike a promise, a lazy promise doesn't do anything until you subscribe to it:

// `unsubscribe` is an idempotent `() => void` function.
const unsubscribe = lazyPromise.subscribe(handleValue, handleError);

Besides being lazy, LazyPromise is cancelable: if the subscriber count goes down to zero before the promise has had time to fire, the teardown function will be called and we'll be back to square one.

If a lazy promise does fire, then like a regular promise it will remember forever the value or error, and give it to whoever tries to subscribe in the future.

Instead of dot-chaining LazyPromise uses pipes, and there are small naming differences, but that aside, LazyPromise API mirrors that of Promise:

Promise api LazyPromise equivalent
promise.then(foo) pipe(lazyPromise, map(foo))
promise.catch(foo) pipe(lazyPromise, catchRejection(foo))
promise.finally(foo) pipe(lazyPromise, finalize(foo))
Promise.resolve(value) resolved(value)
Promise.reject(error) rejected(error)
new Promise<never>(() => {}) never
Promise.all(...) all(...)
Promise.any(...) any(...)
Promise.race(...) race(...)
x instanceof Promise isLazyPromise(x)
Promise<Value> LazyPromise<Value, Error>
Awaited<T> LazyPromiseValue<T>, LazyPromiseError<T>

Your typical code could look something like this (types of all values and errors will be inferred, and callbacks are guaranteed to not be called once you unsubscribe):

pipe(
  // Create a LazyPromise<Value, Error>.
  callAnApiEndpoint(params),
  // Handle some errors.
  catchRejection(error => {
    // To turn the error into a value, return that value.

    // To turn the error into another error, return `rejected(newError)`, which
    // will have type LazyPromise<never, NewError>.

    // To perform some side effect and have the resulting promise never fire,
    // return `never` which has type LazyPromise<never, never>.
    ...
  }),
  // The return value of the callback is treated the same way as for `catchRejection`,
  // so again, you can return either a value or a LazyPromise.
  map(value => ...),
).subscribe(
  // This handler is always optional.
  (value) => { ... },
  // The type system will only want you to provide this handler if by now the
  // type of `error` is other than `never`.
  (error) => { ... },
);

A few random items to know

  • There are utility functions eager and lazy that convert to and from a regular promise. eager takes a LazyPromise and an optional AbortSignal, and returns a Promise, lazy takes a function async (abortSignal) => ... and returns a LazyPromise.

  • The teardown function will not be called if the promise settles (it's either-or).

  • Illegal operations, such as settling an already settled lazy promise, throw an error rather than failing silently.

  • An easy way to tell whether a lazy promise has settled synchronously when you subscribed is to check if the unsubscribe handle === noopUnsubscribe.

Since the type system doesn't know what errors a function can throw, you don't reject a lazy promise by throwing an error, but only by calling reject. It's still possible however that an error will be thrown due to a bug, and for that there exists a third "failure" channel, which is much like the rejection channel, but deals with untyped errors, for instance failed assertions.

// `handleFailure` is always optional and has signature `(error: unknown) => void`.
lazyPromise.subscribe(handleValue, handleError, handleFailure);

Besides throwing in the callbacks you pass to createLazyPromise, lazy, map, etc., you can also fail a lazy promise using the fail handle:

// `fail` has signature `(error: unknown) => void`.
const lazyPromise = createLazyPromise((resolve, reject, fail) => {
  // Throwing here is the same as calling `fail`.

  // If you throw in setTimeout, LazyPromise will have no way of
  // knowing about it, so `fail` has to be called explicitly.
  setTimeout(() => {
    try {
      ...
    } catch (error) {
      fail(error);
    }
  });
});

There are catchFailure and failed utilities analogous to catchRejection and rejected.

The failure channel also makes typed errors an optional feature: you can easily use the library with all your promises typed as LazyPromise<Value, never>.

Experimental SolidJS bindings

https://github.com/lazy-promise/lazy-promise/tree/main/packages/solid-js

联系我们 contact @ memedata.com