import type { CollectionConfig, VirtualRowProps } from '@tanstack/react-db'; import type { StandardSchemaV1 } from '@standard-schema/spec'; import { Schema } from 'effect'; import type { Effect } from 'effect'; import type { DecodedEntity, EntityMetaSchema, DecodedSingleEntity, } from '../../../core/index.js'; import type { AnyESchema, AnyEntityESchema, ESchemaIdField, } from '../../../eschema/index.js'; /** * TanStack DB row form of an entity: value fields hoisted to the top level with * meta nested under `_meta`, plus the runtime virtual props ($synced, $origin). * * Virtual props are added at runtime by @tanstack/db on every read, but the * `useLiveQuery(() => collection)` overload types `data` as the bare item type and * drops them. The props are optional so they surface on reads without being * required on writes. The collection schema validates this latest decoded shape. */ export type CollectionItem = T & { _meta?: typeof EntityMetaSchema.Type; } & Partial>; export type CollectionItemSchema = StandardSchemaV1< CollectionItem, CollectionItem >; export const makeCollectionItemSchema = ( schema: S, ): CollectionItemSchema => { const isDecoded = Schema.is(Schema.toType(schema.schema)); return { '~standard': { version: 1, vendor: 'std-toolkit/sync', types: { input: null as unknown as CollectionItem, output: null as unknown as CollectionItem, }, validate: (input) => { if (input === null || typeof input !== 'object') { return { issues: [{ message: 'CollectionItem must be an object' }] }; } const meta = (input as { readonly _meta?: unknown })._meta; if ( meta !== null && typeof meta === 'object' && Object.hasOwn(meta, '_v') ) { return { issues: [{ message: 'CollectionItem meta must not contain _v' }], }; } const value = stripMeta(input); return Object.hasOwn(value, '_v') || !isDecoded(value) ? { issues: [ { message: `CollectionItem does not match schema "${schema.name}"`, }, ], } : { value: input }; }, }, }; }; /** * An old→new cursor fetch source. A worker or repair capability declares one * only when it consumes that direction. */ export type ForwardFetch = (ctx: { cursor: DecodedEntity | null; }) => Effect.Effect[], E, R>; /** * Pass-through TanStack collection options, with the fields the engine owns * (id, getKey, schema, sync wiring, mutation handlers, utils) removed. */ export type StdCollectionOptions = Omit< CollectionConfig, string>, | 'id' | 'getKey' | 'schema' | 'syncMode' | 'sync' | 'rowUpdateMode' | 'onInsert' | 'onUpdate' | 'onDelete' | 'utils' >; /** * Payload for a keyed update: the complete value before the mutation plus the * partial updates (the id field itself excluded from the updatable fields). */ export type UpdatePayload< TItem extends object, TSchema extends AnyEntityESchema, > = { current: TItem; updates: Partial>>; }; export type DeletePayload = { current: TItem; }; export type UpdateChanges< TItem extends object, TSchema extends AnyEntityESchema, > = UpdatePayload['updates']; export const stripMeta = ( item: CollectionItem, ): TItem => { const { _meta: _ignoredMeta, $synced: _ignoredSynced, $origin: _ignoredOrigin, ...value } = item; return value as TItem; }; export const stripMetaPartial = ( item: Partial>, ): Partial => { const { _meta: _ignoredMeta, $synced: _ignoredSynced, $origin: _ignoredOrigin, ...value } = item; return value as Partial; }; export const toEntity = ( entity: DecodedSingleEntity, ): DecodedEntity => ({ value: entity.value, meta: { ...entity.meta, _d: false }, });