Hyperlinkv0.9.0-beta.0

Hyperlink

Hyperlink.lookupClientconstsrc/Hyperlink.ts:5843

Lookup-resolved nodeless client (D7/D4) — you do not pass a Node; Lookup chooses the dial target. Contrast client(Tag, node), where you name the Node.

Resolution order: Lookup.Identityresolve(tag.key), else Lookup.DirectorynodesServing(tag.key).

Fail-closed by default: missing or more than one directory row → LookupClientError. When N>1, a live Lookup.Advice prefer that matches a directory row wins before D4 { pick }. Opt into soft pick with { pick: "first" } or a sync (rows) => DirectoryEntry. Identity resolve ignores advice / pick (unique by key).

Hot-rebind: after the initial resolve, watches Lookup.Directorychanges and rebuilds the dial when the chosen endpoint moves (dialChanged) or membership for this service key changes — same plane as directory peersLayer. A brief gap keeps the previous client (calls may fail until the new row appears).

Bake name sketch was unsafeLookupClient (“trust Lookup or die”); bare lookupClient(Tag) keeps that fail-closed contract when advice is absent/stale.

// Sole endpoint (identity winner or one directory row):
Hyperlink.lookupClient(Mail).pipe(Layer.provide(Lookup.layer))

// Coordinator published advice — bare client honors prefer:
yield* Lookup.advise({ serviceKey: Mail.key, prefer: "fleet/Mail#w2" })
Hyperlink.lookupClient(Mail)

// N>1 replicas — opt-in pick when no advice (still fail on 0):
Hyperlink.lookupClient(Mail, { pick: "first" })

// You already know an addressed Node — client auto-connects:
Hyperlink.client(Mail, East)
clientsNodeclientLookup.IdentityLookup.DirectoryLookupClientErrorLookup.AdvicepeersLayer
Source src/Hyperlink.ts:5843132 lines
export const lookupClient = <Self, S extends Spec>(
  tag: HyperlinkTag<Self, S>,
  options?: LookupClientOptions,
): Layer.Layer<
  Self,
  LookupClientError,
  LookupIdentity | LookupDirectory | LookupAdvice
> =>
  Layer.effect(
    tag,
    Effect.gen(function* () {
      const Lookup = yield* Effect.promise(() => import("./Lookup"));
      const identity = yield* Lookup.Identity;
      const directory = yield* Lookup.Directory;
      const serviceKey = tag[wireKeySym];

      const resolve = Effect.gen(function* () {
        const resolved = yield* identity.resolve(
          new Lookup.ResolveRequest({ key: tag.key }),
        );
        if (Option.isSome(resolved)) {
          return resolved.value satisfies LookupDialEndpoint;
        }
        const entries = yield* Lookup.nodesServing(tag);
        if (entries.length === 0) {
          return yield* new LookupClientError({
            tag: tag.key,
            reason: "missing",
            count: 0,
          });
        }
        if (entries.length === 1) {
          return entries[0]!;
        }
        const prefer = yield* Lookup.preferred(tag.key);
        if (Option.isSome(prefer)) {
          const advised = entries.find((row) => row.nodeKey === prefer.value);
          if (advised !== undefined) {
            return advised;
          }
        }
        const pick = options?.pick;
        if (pick === undefined) {
          return yield* new LookupClientError({
            tag: tag.key,
            reason: "ambiguous",
            count: entries.length,
          });
        }
        return pick === "first" ? entries[0]! : pick(entries);
      });

      let clientScope: Scope.Closeable | undefined;
      const holder: { current: ServiceOf<S, Self> } = {
        current: null as unknown as ServiceOf<S, Self>,
      };

      const install = (endpoint: LookupDialEndpoint): Effect.Effect<void> =>
        Effect.gen(function* () {
          if (clientScope !== undefined) {
            yield* Scope.close(clientScope, Exit.void);
          }
          const scope = yield* Scope.make();
          clientScope = scope;
          const ctx = yield* Layer.build(
            clientLayerForEndpoint(tag, endpoint),
          ).pipe(Scope.provide(scope));
          holder.current = Context.get(ctx, tag);
        });

      const endpoint = yield* resolve;
      yield* install(endpoint);
      let currentKey = lookupDialKey(endpoint);
      let currentNodeKey = endpoint.nodeKey;

      yield* directory.changes.pipe(
        Stream.runForEach((event) =>
          Effect.gen(function* () {
            if (event._tag === "DirectoryRemoved") {
              if (event.nodeKey !== currentNodeKey) return;
            } else {
              const servesUs =
                event.entry.serves.includes(serviceKey) ||
                event.entry.serves.includes(tag.key);
              const isCurrent = event.entry.nodeKey === currentNodeKey;
              if (!servesUs && !isCurrent) return;
              // Same-dial refresh for our current node — nothing to rebuild.
              if (
                isCurrent &&
                servesUs &&
                event.previous !== undefined &&
                !event.dialChanged
              ) {
                return;
              }
            }
            const next = yield* Effect.option(resolve);
            if (Option.isNone(next)) return;
            const nextKey = lookupDialKey(next.value);
            if (nextKey === currentKey) return;
            const installed = yield* Effect.exit(install(next.value));
            if (Exit.isSuccess(installed)) {
              currentKey = nextKey;
              currentNodeKey = next.value.nodeKey;
              return;
            }
            yield* Effect.logWarning(
              "lookupClient rebind failed; keeping prior dial",
            ).pipe(
              Effect.annotateLogs({
                "lookupClient.tag": tag.key,
                "lookupClient.node": next.value.nodeKey,
              }),
            );
          }),
        ),
        Effect.forkScoped,
      );

      yield* Effect.addFinalizer(() =>
        clientScope === undefined
          ? Effect.void
          : Scope.close(clientScope, Exit.void),
      );

      return makeLiveLookupService(tag, holder);
    }),
  ) as Layer.Layer<
    Self,
    LookupClientError,
    LookupIdentity | LookupDirectory | LookupAdvice
  >;
Referenced by 1 symbols