mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
feat(chord): lifetime "host" is checked by the type system
serve(Type, factory, { lifetime: "host" }) compiles only for a local type whose input has no required fields, and
not together with linger; the runtime check stays as a backstop. ServeOptions is a union (ReferencedServeOptions,
HostServeOptions), picked per contract by ServeOptionsFor<C>.
This commit is contained in:
parent
b042997178
commit
301617df7b
7 changed files with 76 additions and 29 deletions
|
|
@ -13,6 +13,7 @@
|
|||
- A derivation is new at every reload (computed at the commit) instead of kept with its function replaced. A listener whose state source a reload replaced subscribes again and gets the current value again: listeners on state must reconcile. Listeners on state subscribe when they activate, so they get the value current then
|
||||
- `method(args?, result?)`: without arguments a method is called `ref.name(ctx?)` and implemented `(ctx) => …`; without a result it is void (callers get `undefined`). Descriptions record both by leaving `args`/`result` out (fingerprints of such methods change), `call` frames omit `args` and `result` frames `value` for them. Coverage requires both sides to agree on arguments; a void consumer accepts a result
|
||||
- `FacetApi` has a `signal` (below): code that builds a `FacetApi` by hand, for example to wrap `serve`, must pass it on
|
||||
- `serve(type, factory, { lifetime: "host" })` compiles only for a local type whose input has no required fields, and without `linger`; `serve` still refuses the rest at runtime. `ServeOptions` is a union of `ReferencedServeOptions` and `HostServeOptions`, and `serve` takes `ServeOptionsFor<C>`
|
||||
|
||||
### Added
|
||||
|
||||
|
|
|
|||
|
|
@ -487,7 +487,8 @@ host.serve(Index, indexFactory, { lifetime: "host" }); // one objec
|
|||
**`lifetime: "host"`** is for local types with an empty input (or only defaults): the type has exactly one object,
|
||||
created for the host principal when its facet loads. Every `get` reaches that object, whoever asks; a `get` with
|
||||
another input is invalid. References others hold do not change its lifetime, and it cannot `linger`. Only such
|
||||
objects may register into registries.
|
||||
objects may register into registries. TypeScript refuses `lifetime: "host"` for a remote type, for an input with a
|
||||
required field and together with `linger`; `serve` checks the same at runtime, for callers without types.
|
||||
|
||||
## Registries
|
||||
|
||||
|
|
|
|||
|
|
@ -137,7 +137,8 @@ Inside the factory, placeholders must not be used (`resource.value` throws `not_
|
|||
| `"referenced"` (default) | at the first `get` of its address | after the last reference is released, plus `linger` | allowed (default 0) |
|
||||
| `"host"` | when its facet loads (or at `serve`); loading waits until it is live | when the host closes, or a reload removes its facet | refused |
|
||||
|
||||
- `"host"` is refused at `serve` for remote types, for an input that does not accept `{}`, and together with `linger`.
|
||||
- `"host"` is refused at `serve` for remote types, for an input that does not accept `{}`, and together with `linger`;
|
||||
the types (`ServeOptionsFor`) refuse the same at compile time, where such an input is one with a required field.
|
||||
The type has exactly one object: the empty input (defaults applied), created for the host principal. Every `get`
|
||||
reaches it, whatever its principal (the factory sees the host principal); a `get` with another input is
|
||||
`invalid/input`. Other references do not change its lifetime: the host keeps its own until it closes.
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import {
|
|||
RefImpl,
|
||||
type RefOwner,
|
||||
type ServeOptions,
|
||||
type ServeOptionsFor,
|
||||
type Service,
|
||||
} from "./object.ts";
|
||||
import { enqueue } from "./publish.ts";
|
||||
|
|
@ -42,8 +43,16 @@ export const hostPrincipal: Principal = Object.freeze({ host: true });
|
|||
* keeps literal `variant`s in factory returns.
|
||||
*/
|
||||
export interface Serve {
|
||||
<D extends RemoteDef>(type: RemoteContract<D>, factory: Factory<RemoteContract<D>>, options?: ServeOptions): void;
|
||||
<D extends LocalDef>(type: LocalContract<D>, factory: Factory<LocalContract<D>>, options?: ServeOptions): void;
|
||||
<D extends RemoteDef>(
|
||||
type: RemoteContract<D>,
|
||||
factory: Factory<RemoteContract<D>>,
|
||||
options?: ServeOptionsFor<RemoteContract<D>>,
|
||||
): void;
|
||||
<D extends LocalDef>(
|
||||
type: LocalContract<D>,
|
||||
factory: Factory<LocalContract<D>>,
|
||||
options?: ServeOptionsFor<LocalContract<D>>,
|
||||
): void;
|
||||
}
|
||||
|
||||
type AnyServe = (type: AnyContract, factory: Factory<AnyContract>, options?: ServeOptions) => void;
|
||||
|
|
|
|||
|
|
@ -49,7 +49,16 @@ export {
|
|||
type WebSocketLinkOptions,
|
||||
webSocketLink,
|
||||
} from "./link.ts";
|
||||
export type { Factory, Lifetime, Self, ServeOptions, StateOptions } from "./object.ts";
|
||||
export type {
|
||||
Factory,
|
||||
HostServeOptions,
|
||||
Lifetime,
|
||||
ReferencedServeOptions,
|
||||
Self,
|
||||
ServeOptions,
|
||||
ServeOptionsFor,
|
||||
StateOptions,
|
||||
} from "./object.ts";
|
||||
export { defineRegistry, type Registry, type RegistryTable, type RegistryView } from "./registry.ts";
|
||||
export type { ReloadOptions, ReloadReport } from "./reload.ts";
|
||||
export {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {
|
|||
type ImplementationOf,
|
||||
type InputOf,
|
||||
LOCAL_VARIANT,
|
||||
type LocalContract,
|
||||
type MemberDescription,
|
||||
type Ref,
|
||||
takesNoArgs,
|
||||
|
|
@ -24,21 +25,42 @@ const noop = (): void => {};
|
|||
|
||||
// ─── Services ────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface ServeOptions {
|
||||
/**
|
||||
* How long objects of the type live. `"referenced"` (default): created at the first `get` of an address, disposed
|
||||
* after the last reference is released (plus `linger`). `"host"`: one object, created when its facet loads (loading
|
||||
* waits until it is live) and kept until the host closes or a reload removes its facet. Only local types with an
|
||||
* empty input may have it; every `get` reaches that one object, created for the host principal. Only such objects
|
||||
* may register into registries.
|
||||
*/
|
||||
readonly lifetime?: Lifetime;
|
||||
/** Milliseconds an object stays alive after its last reference is released (`lifetime: "referenced"` only). */
|
||||
/**
|
||||
* `serve` options. `lifetime` says how long objects of the type live. `"referenced"` (default): created at the first
|
||||
* `get` of an address, disposed after the last reference is released (plus `linger`). `"host"`: one object, created when
|
||||
* its facet loads (loading waits until it is live) and kept until the host closes or a reload removes its facet. Only
|
||||
* local types whose input accepts `{}` may have it (`ServeOptionsFor` checks that at compile time, `serve` again at
|
||||
* runtime); every `get` reaches that one object, created for the host principal. Only such objects may register into
|
||||
* registries.
|
||||
*/
|
||||
export type ServeOptions = ReferencedServeOptions | HostServeOptions;
|
||||
|
||||
export interface ReferencedServeOptions {
|
||||
readonly lifetime?: "referenced";
|
||||
/** Milliseconds an object stays alive after its last reference is released. */
|
||||
readonly linger?: number;
|
||||
/** Adapters for consumers built against older contract versions. */
|
||||
readonly accepts?: Accepts;
|
||||
}
|
||||
|
||||
export interface HostServeOptions {
|
||||
readonly lifetime: "host";
|
||||
/** An object of lifetime `"host"` lives as long as the host: it does not linger. */
|
||||
readonly linger?: undefined;
|
||||
/** Adapters for consumers built against older contract versions. */
|
||||
readonly accepts?: Accepts;
|
||||
}
|
||||
|
||||
/**
|
||||
* The options `serve` accepts for `C`: `lifetime: "host"` only for a local type whose input has no required fields
|
||||
* (the type's one object has the empty input).
|
||||
*/
|
||||
export type ServeOptionsFor<C extends AnyContract> = C extends LocalContract
|
||||
? Record<never, never> extends InputOf<C>
|
||||
? ServeOptions
|
||||
: ReferencedServeOptions
|
||||
: ReferencedServeOptions;
|
||||
|
||||
export type Lifetime = "referenced" | "host";
|
||||
|
||||
export type Factory<C extends AnyContract> = (
|
||||
|
|
|
|||
|
|
@ -388,23 +388,27 @@ describe("host: lifetime", () => {
|
|||
members: { seen: opaque<unknown>("t.seen@1") },
|
||||
});
|
||||
|
||||
it('serve refuses a lifetime "host" the type cannot have', () => {
|
||||
it('serve refuses a lifetime "host" the type cannot have, at compile time and at runtime', () => {
|
||||
const host = quiet();
|
||||
const Remote = remote("t.remote-store", { version: 1, input: Type.Object({}), variants: { main: {} } });
|
||||
assert.throws(
|
||||
() => host.serve(Remote, () => ({ variant: "main" }), { lifetime: "host" } as never),
|
||||
/needs a local type/,
|
||||
);
|
||||
const Keyed = local("t.keyed", { version: 1, input: Type.Object({ id: Type.String() }) });
|
||||
assert.throws(() => host.serve(Keyed, () => ({}), { lifetime: "host" }), /input that accepts \{\}/);
|
||||
assert.throws(
|
||||
() => host.serve(Store, () => ({ seen: null }), { lifetime: "host", linger: 10 }),
|
||||
/does not linger/,
|
||||
);
|
||||
assert.throws(
|
||||
() => host.serve(Store, () => ({ seen: null }), { lifetime: "forever" } as never),
|
||||
/unknown lifetime/,
|
||||
);
|
||||
// Each refused call is also a type error; the runtime check is the backstop for untyped callers.
|
||||
const refused: [() => void, RegExp][] = [
|
||||
// @ts-expect-error: a remote type is never of lifetime "host"
|
||||
[() => host.serve(Remote, () => ({ variant: "main" }), { lifetime: "host" }), /needs a local type/],
|
||||
// @ts-expect-error: the one object has the empty input, and `id` is required
|
||||
[() => host.serve(Keyed, () => ({}), { lifetime: "host" }), /input that accepts \{\}/],
|
||||
// @ts-expect-error: an object of lifetime "host" does not linger
|
||||
[() => host.serve(Store, () => ({ seen: null }), { lifetime: "host", linger: 10 }), /does not linger/],
|
||||
// @ts-expect-error: no such lifetime
|
||||
[() => host.serve(Store, () => ({ seen: null }), { lifetime: "forever" }), /unknown lifetime/],
|
||||
];
|
||||
for (const [serve, error] of refused) assert.throws(serve, error);
|
||||
// Allowed: a local type whose input has only optional fields, and any type with the default lifetime.
|
||||
const Optional = local("t.optional", { version: 1, input: Type.Object({ id: Type.Optional(Type.String()) }) });
|
||||
host.serve(Optional, () => ({}), { lifetime: "host" });
|
||||
host.serve(Keyed, () => ({}), { lifetime: "referenced", linger: 10 });
|
||||
host.serve(Remote, () => ({ variant: "main" }), { linger: 10 });
|
||||
});
|
||||
|
||||
it("every get reaches the one object, created at serve for the host principal; another input is invalid", async () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue