feat(data): schema type-constructors + sideloaded service schemas driving createLazy - #193
Conversation
…ptor validation Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…a.ToType blob → Blob
Move IsValidWith{Partial,Complete}Descriptor out of is-valid.ts so the base
validator stays descriptor-agnostic. Fix Schema.ToType to resolve { type: "blob" }
to Blob instead of falling through to any; add a Blob-as-state-and-return test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the inline bump one-liner with scripts/bump.mjs, which sets the anchor version on the root and every publishable package (private !== true) and skips private sample/app packages. Keyed off the same `private` field that governs publishing, so there is no package list to maintain. Revert the private samples bumped in this PR back to 0.10.6. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
| * is absent; an observe factory (`bar(args): Observe`) when present. | ||
| */ | ||
| export interface StateDescriptor { | ||
| readonly schema: Schema; |
There was a problem hiding this comment.
suggestion: Building on the note I sent yesterday — this is the spot where dynamic schemas would land. Today schema (here, and parameters on ActionDescriptor) is static, but for agent tools the valid inputs often depend on live state: which models a user is entitled to, or options that change once a model is picked. Could the descriptor let a schema be either static or dynamic — e.g. also accepting an Observe<Schema> (Schema is Data, so it still round-trips) — so both the static shape and the runtime-narrowed one are first-class? Happy to be the first to exercise it on our side.
There was a problem hiding this comment.
concerning the dynamic schema. The services only expose static typed api endpoints. Those static endpoints can provide observability and invocation of dynamic things, naturally, but they can't expose statically typed constraints on each.
The service descriptor is merely a runtime description of the strongly typed service interface. So it also cannot provide dynamic schema.
That said, you can still support dynamic interactions, just like how you can today across the strongly typed service interfaces.
| /** A callable invoked for its effect or async result. */ | ||
| export interface ActionDescriptor { | ||
| readonly parameters: readonly Schema[]; | ||
| readonly result: "promise" | "generator" | "void"; |
There was a problem hiding this comment.
addressed offline. We don't need yet, but may want a way to indicate expected max duration of a promise or async generator.
…uctors Extend Schema + Schema.ToType to express data-adjacent types so a schema can describe a service surface, not just data. Nestable (e.g. AsyncGenerator<Data> as a function parameter). Confined by usage, like blob/typed-buffer. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Collapse the bespoke Service.Descriptor into a plain object Schema (ToService =
Schema.ToType). Schemas are published beside the service (MyService.schema), not
attached to instances, so the base Service stays minimal. Rename the descriptor
validators to IsValidWith{Partial,Complete}Schema over Schema.ToType. Rework
createLazy to take a sideloaded schema, deriving each member's wrapper from its
schema type, gated by IsValidWithCompleteSchema.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- memberKind: a function schema with a present-but-unrecognized `returns` now
throws instead of silently defaulting to void (was dropping the result).
- Constrain createLazy's schema to observe/function members (LazyMemberSchema),
so nested-object or malformed-returns members are a compile error rather than
a runtime throw — the gate and runtime dispatch can no longer disagree.
- Guard the lazy serviceName rename against an undefined real serviceName.
- Pass Promise/AsyncGenerator through DeepReadonly (avoid structural expansion).
- Soften createLazy's "completely describes" wording re: {}-as-any members.
- Delete the README's false IsDataService backwards-compat section.
- Add compile-time tests for the nested-object and malformed-returns rejections.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The lazy AsyncGenerator wrapper could resurrect after termination: return()/throw() called before the first next() didn't latch, so a later next() would start the real generator and yield. Latch a `done` flag on return/throw/natural-completion, and add [Symbol.asyncDispose] (terminates via return) for `await using`. Red/green tested. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ection
Re-add `schema?: Schema` to the base Service so a factory may attach a service's
authored schema to the instance (runtime introspection), excluded from IsValid
like `serviceName`. Fix both IsValidProperty variants (async-data-service and
ui-service) to exclude base-Service metadata keys when recursing into nested
objects — otherwise validating a nested `Service & {...}` recurses into the
self-referential Schema and errors. createLazy exposes the schema on the lazy
instance before load.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Additive optional `external?: { agent?; link? }` on the function schema node,
read at runtime to gate invocation from untrusted channels. Metadata only —
Schema.ToType ignores it. Channels have opposite default polarity: `link`
(deeplink/URL) is a default-deny whitelist; `agent` is a default-allow blacklist.
resolveExternalInvocation() is the single source of truth for that polarity.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…JSDoc Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a flat named export (alongside the Schema namespace) so consumers can
import { resolveExternalInvocation } from "@adobe/data/schema".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…sdoc Silences the doc-gen warning; SchemaMismatch is intentionally internal. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The named SchemaMismatch type was referenced by createLazy's public signature, so typedoc warned it wasn't documented. Inline the marker object (keeping the __createLazyError message) so there's no named symbol to warn about, without exporting an internal type. Enforcement unchanged (46 createLazy tests pass). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…t standard Rename external.ts -> resolve-external-invocation.ts so the filename maps deterministically to its single export (do-bar.ts -> doBar), and drop the non-standard flat re-export from schema/index.ts — reach it via the Schema namespace (Schema.resolveExternalInvocation), per data-ai global/namespace.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move parameters/returns/external off the flat Schema and under
`{ type: "function", signature: { parameters, returns, external } }`, so these
members live only on function schemas. Schema.ToType reads signature.parameters/
returns; resolveExternalInvocation reads signature.external; createLazy's
memberKind + LazyMemberSchema read signature.returns. Migrated all function-schema
literals in tests, example, and docs. Absent signature ⇒ () => void.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…(TS2321) #193 inserted the observe/promise/generator/function branches BEFORE the data branches in Schema.ToType, so every data-schema ToType (run per component by FromSchemas across a plugin combine chain) evaluated 4 extra conditionals deeper. On a ~5-deep chain already near TS's stack-depth ceiling that tipped it into TS2321 (excessive stack depth). Move the constructor branches AFTER the data branches so data schemas return to the pre-#193 nesting depth; only service schemas (never in the Plugin hot path) fall through. Verified: fixes a downstream deep-plugin consumer's TS2321 with no behavior change (to-type tests pass). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…azy gate from IsValid (#195) * feat(data): decouple createLazy gate from IsValid (schema-match only) createLazy now gates on whether the schema completely and correctly describes the service's members (EquivalentTypes<Schema.ToType<S>, members>) — the actual correctness condition for building the wrappers — and no longer requires the service to be a valid AsyncDataService. This lets services that legitimately return non-Data (e.g. fetch(): Promise<Response>) be lazily chunk-loaded, while IsValid stays a separate definition-site concern. Collapses the two failure modes into one clear schema-mismatch error. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore(data): bump publishable packages to 0.10.8 0.10.7 was already published; bump the continued work (schema type-constructors, sideloaded schemas, schema-driven createLazy, external policy) to 0.10.8. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(data): move ToType type-constructor branches after data branches (TS2321) #193 inserted the observe/promise/generator/function branches BEFORE the data branches in Schema.ToType, so every data-schema ToType (run per component by FromSchemas across a plugin combine chain) evaluated 4 extra conditionals deeper. On a ~5-deep chain already near TS's stack-depth ceiling that tipped it into TS2321 (excessive stack depth). Move the constructor branches AFTER the data branches so data schemas return to the pre-#193 nesting depth; only service schemas (never in the Plugin hot path) fall through. Verified: fixes a downstream deep-plugin consumer's TS2321 with no behavior change (to-type tests pass). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Description
Teaches
Schemato describe data-adjacent types (reactive values, async values, streams, callables), so a service's whole surface can be described by one plain, serializableSchema. Service schemas are sideloaded — published beside the service and passed explicitly — and now driveAsyncDataService.createLazy.This started as a bespoke
ServiceDescriptor(states/actions/services buckets) and collapsed, over the course of the work, into "a service is just an objectSchema" onceSchema.ToTypelearned to express these types. The bespoke descriptor, itsToServicetransform, and theService.descriptorinstance slot were all removed.1. Schema type-constructors (
schema.ts,to-type.ts)Four new, nestable
typeconstructors, mapped bySchema.ToType:ToType{ type: "observe", value: S }Observe<ToType<S>>{ type: "promise", value: S }Promise<ToType<S>>{ type: "generator", value: S }AsyncGenerator<ToType<S>>{ type: "function", signature: { parameters: S[], returns?: S } }(...args) => ToType<returns>(absentreturns⇒ void; absentsignature⇒() => void; absentvalue⇒ any)They compose and nest (e.g. an
AsyncGenerator<Data>parameter inside an object argument), which the plain-Data-only argument model could not express. Like the existingblob/typed-buffertypes, they are confined by usage — they belong in service schemas, not ECS component / typed-buffer schemas (there is deliberately noDataSchemasplit:Schemaalready produces non-immutable / non-Datatypes such asTypedBuffer, and consumers already handle-or-throw per context).The function constructor's members (
parameters,returns, and the invocation-policyexternal) are grouped under a nestedsignatureso they live only on function schemas rather than on everySchemanode.signature.external?: { agent?: boolean; link?: boolean }is optional invocation-policy metadata — who may invoke the function from an untrusted channel — with deliberately opposite default polarity:link(deeplink/URL, least trusted) is a default-deny whitelist (invocable only whenlink === true);agent(acting on the user's behalf) is a default-allow blacklist (invocable unlessagent === false). It is metadata only:Schema.ToTypeignores it (a function differing only inexternalderives the same signature), and it does not interact with the schema validators orcreateLazy.resolveExternalInvocation(schema)(reached asSchema.resolveExternalInvocation) is the single source of truth for resolving that polarity — call sites must not re-derive it.2. Sideloaded service schemas (
service.ts,is-valid-with-*-schema.ts)Schema;ToServiceis justSchema.ToType.MyService.schemavia the namespace pattern) and passed explicitly. The baseServicealso carries an optionalschema?: Schemaslot so a factory may attach it to the instance for runtime introspection — excluded fromIsValidlikeserviceName(bothIsValidPropertyvariants now skip base-Servicemetadata keys when recursing into nested objects, so validating a nestedService & {…}doesn't recurse into the self-referentialSchema).IsValidWithPartialSchema<T, S>— service members INCLUDE everythingSdescribes (subset).IsValidWithCompleteSchema<T, S>— service members are EXACTLY whatSdescribes.3.
createLazydriven by a schema (create-lazy.ts)createLazynow takes{ load, schema, preload? }. Each member's runtime wrapper is derived from its schema (observe, orfunctionclassified byreturns). The gate is schema-match only —EquivalentTypes<Schema.ToType<S>, members>, the actual correctness condition for building the wrappers — and is deliberately decoupled fromIsValid: a service that legitimately returns non-Data(e.g. afetch(): Promise<Response>port) can still be lazily chunk-loaded, while async-data-service conformance stays a separate definition-site concern. ALazyMemberSchemaconstraint rejects unsupported members (nested-object, malformedreturns) at compile time rather than at runtime, so the type-level gate and the runtime dispatch cannot disagree.Drive-by fixes
Schema.ToTypeblob mapping.{ type: "blob" }resolved toany(its test passed only vacuously); it now resolves toBlob, guarded by a regression test.pnpm run bumpnow runsscripts/bump.mjs, which bumps the root anchor + every publishable package (private !== true) and skips private sample/app packages, keyed off the sameprivatefield that governspnpm -r publish. Removes the per-PR churn of ~9 unpublishedpackage.jsons.AsyncGeneratorwrapper could resurrect after termination (return()/throw()before the firstnext()didn't latch); it now latches a terminal state and supports[Symbol.asyncDispose].Test evidence
pnpm run lint— clean.pnpm run typecheck(all caches cleared, whole monorepo) — exit 0, 0 errors.check:workspace— OK.True/False/EquivalentTypes+@ts-expect-error): every constructor incl. the nested-AsyncGenerator-in-object-arg driver; the Blob-as-Blob-not-anyregression guard; partial/complete schema matching; createLazy inference + the compile-time rejection of nested-object / malformed-returnsmembers.create-lazy.test.ts, vitest): 46 passed — all wrapper kinds, the generator terminal-state / async-dispose red→green tests, and the lazy instance exposing itsschemabefore load; fullsrc/servicesuite 120 passed.TS2344);@ts-expect-errordirectives all consumed.returnsand on nested-object members) are fixed and covered by new compile-time tests, along with the minor/nit findings.Related PRs
None.
Jira ticket
N/A — exploratory addition to
@adobe/data.Checklist
pnpm run typecheck— exit 0).pnpm run lint— clean);pnpm run check:workspace— OK.create-lazy.test.ts(44 passing, incl. red→green generator tests).scripts/bump.mjs).README.md+create-lazy.mdmigrated to the schema API; removed a falseIsDataServicebackwards-compat note.createLazy's input changed frompropertiestoschema(pre-1.0), all callers migrated.