@cosmicdrift/kumiko-headless 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@cosmicdrift/kumiko-headless",
3
+ "version": "0.1.0",
4
+ "description": "Headless UI logic for Kumiko — Dispatcher contract, Form-Controller, View-Model, Nav-Resolver. Plattform- und React-frei; jeder Renderer (renderer, renderer-web, renderer-native, …) komponiert darauf.",
5
+ "license": "BUSL-1.1",
6
+ "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/cosmicdriftgamestudio/kumiko-framework.git",
10
+ "directory": "packages/headless"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/cosmicdriftgamestudio/kumiko-framework/issues"
14
+ },
15
+ "homepage": "https://kumiko.so",
16
+ "type": "module",
17
+ "kumiko": {
18
+ "runtime": "client"
19
+ },
20
+ "exports": {
21
+ ".": "./src/index.ts",
22
+ "./dispatcher": "./src/dispatcher/index.ts"
23
+ },
24
+ "dependencies": {
25
+ "@cosmicdrift/kumiko-framework": "workspace:*",
26
+ "zod": "^4.3.6"
27
+ },
28
+ "publishConfig": {
29
+ "registry": "https://registry.npmjs.org",
30
+ "access": "public"
31
+ },
32
+ "files": [
33
+ "src",
34
+ "README.md",
35
+ "LICENSE"
36
+ ]
37
+ }
@@ -0,0 +1,123 @@
1
+ import { describe, expect, test, vi } from "vitest";
2
+ import type {
3
+ AssetResolver,
4
+ ButtonProps,
5
+ LocaleResolver,
6
+ PrimitiveCommonProps,
7
+ PrimitivesContract,
8
+ SelectProps,
9
+ TextInputProps,
10
+ } from "../index";
11
+
12
+ // These aren't unit tests — they're compile-time contract guards in
13
+ // runtime clothing. Building a small fake that satisfies each contract
14
+ // ensures a future refactor that drops or renames a required field
15
+ // breaks here loudly instead of in some downstream renderer.
16
+
17
+ describe("Asset / Locale / Primitives contracts", () => {
18
+ test("AssetResolver — a minimal in-memory resolver compiles and runs", () => {
19
+ const table = new Map<string, { uri: string; alt?: string }>([
20
+ ["app:asset:logo", { uri: "/static/logo.svg", alt: "Kumiko" }],
21
+ ]);
22
+ const resolver: AssetResolver = {
23
+ resolve(qn) {
24
+ const found = table.get(qn);
25
+ return found ? { uri: found.uri, alt: found.alt } : null;
26
+ },
27
+ };
28
+
29
+ expect(resolver.resolve("app:asset:logo")?.uri).toBe("/static/logo.svg");
30
+ expect(resolver.resolve("app:asset:missing")).toBeNull();
31
+ });
32
+
33
+ test("LocaleResolver — subscribe/unsubscribe + translate with params", () => {
34
+ const listeners = new Set<() => void>();
35
+ const resolver: LocaleResolver = {
36
+ translate(key, params) {
37
+ // Minimal translation: "errors.value.minimum" + {min:3} → "≥3"
38
+ if (key === "errors.value.minimum" && params?.["min"] !== undefined) {
39
+ return `≥${params["min"]}`;
40
+ }
41
+ return key;
42
+ },
43
+ locale: () => "de-AT",
44
+ timeZone: () => "Europe/Vienna",
45
+ subscribe(listener) {
46
+ listeners.add(listener);
47
+ return () => listeners.delete(listener);
48
+ },
49
+ };
50
+
51
+ expect(resolver.translate("errors.value.minimum", { min: 3 })).toBe("≥3");
52
+ expect(resolver.locale()).toBe("de-AT");
53
+
54
+ const listener = vi.fn();
55
+ const unsubscribe = resolver.subscribe(listener);
56
+ for (const l of listeners) l();
57
+ expect(listener).toHaveBeenCalledTimes(1);
58
+ unsubscribe();
59
+ for (const l of listeners) l();
60
+ expect(listener).toHaveBeenCalledTimes(1);
61
+ });
62
+
63
+ test("PrimitivesContract — a fake renderer's primitives satisfy the type", () => {
64
+ // The renderer-side wiring creates a concrete object where each
65
+ // member is a component. From ui-core's vantage they're opaque
66
+ // values typed as `unknown` — we only care that all 11 keys are
67
+ // present.
68
+ const primitives: PrimitivesContract = {
69
+ TextInput: { kind: "text-input" },
70
+ NumberInput: { kind: "number-input" },
71
+ Select: { kind: "select" },
72
+ Toggle: { kind: "toggle" },
73
+ DatePicker: { kind: "date-picker" },
74
+ Button: { kind: "button" },
75
+ Modal: { kind: "modal" },
76
+ Toast: { kind: "toast" },
77
+ Badge: { kind: "badge" },
78
+ Card: { kind: "card" },
79
+ Icon: { kind: "icon" },
80
+ };
81
+
82
+ // Exhaustiveness — missing a key means the contract drifted without
83
+ // updating this test (and, by implication, every primitives-* impl).
84
+ expect(Object.keys(primitives)).toHaveLength(11);
85
+ });
86
+
87
+ test("PrimitiveCommonProps propagate to concrete props (TextInput, Select, Button)", () => {
88
+ // Compile-time assertion: PrimitiveCommonProps members narrow correctly
89
+ // on each derived prop type. Runtime side just checks the object
90
+ // shape we'd hand to a primitive component.
91
+ const textProps: TextInputProps = {
92
+ id: "title",
93
+ name: "title",
94
+ disabled: false,
95
+ readOnly: false,
96
+ required: true,
97
+ label: "Title",
98
+ value: "hello",
99
+ onChange: () => {},
100
+ type: "text",
101
+ };
102
+ const selectProps: SelectProps<"a" | "b"> = {
103
+ label: "Pick one",
104
+ value: "a",
105
+ onChange: () => {},
106
+ options: [
107
+ { value: "a", label: "A" },
108
+ { value: "b", label: "B" },
109
+ ],
110
+ };
111
+ const buttonProps: ButtonProps = {
112
+ label: "Save",
113
+ onPress: () => {},
114
+ variant: "primary",
115
+ };
116
+
117
+ const common: PrimitiveCommonProps = { label: "Title" };
118
+ expect(common.label).toBe("Title");
119
+ expect(textProps.value).toBe("hello");
120
+ expect(selectProps.options).toHaveLength(2);
121
+ expect(buttonProps.variant).toBe("primary");
122
+ });
123
+ });
@@ -0,0 +1,48 @@
1
+ // Asset-Resolver — the layer between "qualified asset name" and "URL the
2
+ // renderer can point an <img>/<Image> at". An asset (logo, illustration,
3
+ // icon bundle) is declared by a feature with a qualified name
4
+ // ("admin:asset:hero-logo") and resolved at render-time against the
5
+ // current tenant / theme / platform.
6
+ //
7
+ // Why a resolver indirection:
8
+ // - Tenant-theming (different logo per tenant) without the feature
9
+ // knowing what tenant it's rendering for.
10
+ // - Dark-mode / light-mode variants picked up from the current theme
11
+ // context.
12
+ // - Web vs mobile different asset bundles (e.g. PNG vs SVG).
13
+ //
14
+ // The app registers a concrete resolver on startup and hands it to the
15
+ // renderer via context. Feature code calls `useAsset("admin:asset:logo")`
16
+ // (renderer-side hook) → internally `AssetResolver.resolve(qn)`.
17
+
18
+ // Render-context the resolver may consult. Everything here is optional;
19
+ // the default resolver ignores fields it doesn't use.
20
+ export type AssetResolveContext = {
21
+ readonly tenantId?: string;
22
+ readonly theme?: "light" | "dark";
23
+ readonly platform?: "web" | "ios" | "android";
24
+ readonly locale?: string;
25
+ };
26
+
27
+ export type AssetResolution = {
28
+ // Final URL / URI — web uses https://, Expo uses local file:// or
29
+ // remote https://, SSR may get a data: URI for inlined critical
30
+ // assets.
31
+ readonly uri: string;
32
+ // Optional pre-computed dimensions so the renderer can reserve layout
33
+ // space without a first-paint reflow (fixes the image-loads-then-
34
+ // layout-jumps UX on mobile).
35
+ readonly width?: number;
36
+ readonly height?: number;
37
+ // Alt-text to apply when the asset renders into an <img>. The i18n
38
+ // layer resolved it to a string before the resolver returned.
39
+ readonly alt?: string;
40
+ };
41
+
42
+ export type AssetResolver = {
43
+ // Returns null when the asset isn't known; renderer shows a fallback
44
+ // (missing-image placeholder) instead of crashing. Kumiko's standard
45
+ // renderer logs a warn when it sees null so features catch typos in
46
+ // the asset qualified name early.
47
+ resolve(qn: string, ctx?: AssetResolveContext): AssetResolution | null;
48
+ };
@@ -0,0 +1,23 @@
1
+ export type {
2
+ AssetResolution,
3
+ AssetResolveContext,
4
+ AssetResolver,
5
+ } from "./asset";
6
+ export type { LocaleResolver } from "./locale";
7
+ export type {
8
+ BadgeProps,
9
+ ButtonProps,
10
+ CardProps,
11
+ DatePickerProps,
12
+ IconProps,
13
+ ModalProps,
14
+ NumberInputProps,
15
+ PrimitiveCommonProps,
16
+ PrimitivesContract,
17
+ SelectOption,
18
+ SelectProps,
19
+ TextInputProps,
20
+ ToastIntent,
21
+ ToastProps,
22
+ ToggleProps,
23
+ } from "./primitives";
@@ -0,0 +1,33 @@
1
+ // Locale-Resolver — translation lookup + language/region info the
2
+ // renderer needs. Sits in front of i18next (or whatever i18n library
3
+ // the app wires up). ui-core calls it generically so form-controller,
4
+ // view-model, and nav-resolver all produce already-translated strings
5
+ // to hand to the renderer.
6
+ //
7
+ // The framework already has an i18n layer (@cosmicdrift/kumiko-framework/i18n) that
8
+ // features register translations against. This contract is what ui-core
9
+ // consumes — the app's entrypoint instantiates the resolver from a
10
+ // framework i18next instance and passes it to the renderer via context.
11
+
12
+ export type LocaleResolver = {
13
+ // Resolve an i18n key to a localized string. `params` are interpolated
14
+ // (`{name}` → params.name) — same semantics as i18next t().
15
+ translate(key: string, params?: Readonly<Record<string, unknown>>): string;
16
+ // Current locale in BCP-47 form (e.g. "de-AT", "en-US"). Feature code
17
+ // that does manual formatting (e.g. a custom number renderer) reads
18
+ // this instead of calling translate() on a placeholder.
19
+ locale(): string;
20
+ // Preferred time-zone for the current user. Time-zone-aware fields
21
+ // (Kumiko's locatedTimestamp) render in this zone unless the entity
22
+ // overrides it with a per-row locatedBy reference.
23
+ timeZone(): string;
24
+ // Subscribe to locale/timezone changes. Hooks (useTranslation) rely
25
+ // on this so a user switching language mid-session triggers a
26
+ // re-render across every subscribed consumer.
27
+ subscribe(listener: () => void): () => void;
28
+ // Optional — when present, the resolver is stateful and callers (like
29
+ // a language-picker UI) can trigger a locale change that will be
30
+ // broadcast via `subscribe`. Static resolvers (e.g. server-side
31
+ // render with a fixed locale) omit it.
32
+ setLocale?(locale: string): void;
33
+ };
@@ -0,0 +1,158 @@
1
+ // Primitives-Contract — the minimal set of UI building blocks every
2
+ // renderer must provide. A "primitive" is a unit the standard-renderers
3
+ // (entityList, entityEdit, etc.) call into without knowing which library
4
+ // implements it. On web that's shadcn; on mobile it's react-native-paper;
5
+ // both packages (primitives-shadcn, primitives-paper) implement this
6
+ // contract and the app wires one of them into the renderer via context.
7
+ //
8
+ // Why a contract here (in ui-core) instead of in each renderer:
9
+ // ui-core's view-model builder and form-controller produce metadata
10
+ // (field-type, validation-state, label, placeholder) that the renderer
11
+ // translates into primitive invocations. Having the contract shape in
12
+ // ui-core means the metadata can carry references to primitives by name
13
+ // ("text-input", "select") and the translation is a pure map-lookup on
14
+ // the renderer side, not a switch statement that re-derives UI intent
15
+ // from entity-schema.
16
+ //
17
+ // Runtime-free: every primitive is expressed as its input contract only
18
+ // (what props it takes). Concrete components live in each primitives-*
19
+ // package and depend on React / React Native / etc.; ui-core never
20
+ // imports them.
21
+
22
+ // Common props every primitive accepts. Renderers pass down the
23
+ // current field-state (visible/readonly/required from FormController)
24
+ // through these — a primitive that honours them doesn't need to know
25
+ // about Kumiko's form-controller, it just renders according to flags.
26
+ export type PrimitiveCommonProps = {
27
+ readonly id?: string;
28
+ readonly name?: string;
29
+ readonly disabled?: boolean;
30
+ readonly readOnly?: boolean;
31
+ readonly required?: boolean;
32
+ // Accessible label / placeholder / helper text. These are already
33
+ // localized strings — the renderer resolves i18n keys via useTranslation
34
+ // before handing them to the primitive.
35
+ readonly label?: string;
36
+ readonly placeholder?: string;
37
+ readonly helperText?: string;
38
+ // One or more issue messages to display. Localised; the renderer
39
+ // picked them up from FormSnapshot.errors and ran them through
40
+ // i18n.
41
+ readonly errors?: readonly string[];
42
+ };
43
+
44
+ export type TextInputProps = PrimitiveCommonProps & {
45
+ readonly value: string;
46
+ readonly onChange: (next: string) => void;
47
+ readonly onBlur?: () => void;
48
+ readonly type?: "text" | "email" | "url" | "tel" | "password";
49
+ readonly maxLength?: number;
50
+ readonly autoComplete?: string;
51
+ };
52
+
53
+ export type NumberInputProps = PrimitiveCommonProps & {
54
+ readonly value: number | null;
55
+ readonly onChange: (next: number | null) => void;
56
+ readonly onBlur?: () => void;
57
+ readonly min?: number;
58
+ readonly max?: number;
59
+ readonly step?: number;
60
+ };
61
+
62
+ export type SelectOption<TValue extends string = string> = {
63
+ readonly value: TValue;
64
+ readonly label: string;
65
+ };
66
+
67
+ export type SelectProps<TValue extends string = string> = PrimitiveCommonProps & {
68
+ readonly value: TValue | null;
69
+ readonly onChange: (next: TValue | null) => void;
70
+ readonly options: readonly SelectOption<TValue>[];
71
+ };
72
+
73
+ export type ToggleProps = PrimitiveCommonProps & {
74
+ readonly value: boolean;
75
+ readonly onChange: (next: boolean) => void;
76
+ };
77
+
78
+ export type DatePickerProps = PrimitiveCommonProps & {
79
+ // ISO-8601 date string (YYYY-MM-DD) or null. The primitive translates
80
+ // to/from the native date widget on each platform; ui-core stays
81
+ // platform-free by using the serialized string form.
82
+ readonly value: string | null;
83
+ readonly onChange: (next: string | null) => void;
84
+ readonly minDate?: string;
85
+ readonly maxDate?: string;
86
+ };
87
+
88
+ // Container primitives — layout / feedback that renderers compose but
89
+ // don't directly bind to a form field. Props stay minimal: anything that
90
+ // feels like "more variants" (destructive button, soft button, ghost
91
+ // button) gets a `variant` prop the primitive interprets.
92
+
93
+ export type ButtonProps = {
94
+ readonly onPress: () => void;
95
+ readonly label: string;
96
+ readonly variant?: "primary" | "secondary" | "destructive" | "ghost";
97
+ readonly disabled?: boolean;
98
+ readonly loading?: boolean;
99
+ // Left/right icons addressed by name through the Icon primitive.
100
+ readonly leftIcon?: string;
101
+ readonly rightIcon?: string;
102
+ };
103
+
104
+ export type ModalProps = {
105
+ readonly open: boolean;
106
+ readonly onClose: () => void;
107
+ readonly title?: string;
108
+ readonly description?: string;
109
+ };
110
+
111
+ export type ToastIntent = "info" | "success" | "warning" | "error";
112
+ export type ToastProps = {
113
+ readonly intent: ToastIntent;
114
+ readonly title: string;
115
+ readonly description?: string;
116
+ readonly onDismiss?: () => void;
117
+ };
118
+
119
+ export type BadgeProps = {
120
+ readonly label: string;
121
+ readonly intent?: ToastIntent;
122
+ };
123
+
124
+ export type CardProps = {
125
+ readonly title?: string;
126
+ readonly description?: string;
127
+ };
128
+
129
+ // Icons are referenced by string name so ui-core doesn't own an icon set.
130
+ // primitives-shadcn maps "check" → `<Check />` from lucide-react;
131
+ // primitives-paper maps "check" → `<Icon source="check" />`. The set of
132
+ // names is deliberately open — renderer-level code can introduce feature-
133
+ // specific icons without a ui-core round-trip.
134
+ export type IconProps = {
135
+ readonly name: string;
136
+ readonly size?: number;
137
+ };
138
+
139
+ // The full contract. A primitives package implements each member — the
140
+ // renderer consumes them from a single context object (PrimitivesProvider).
141
+ // Adding a new primitive to the contract is a breaking change for all
142
+ // primitives-* packages; removing one is not (renderers just stop using
143
+ // it). Keep the surface small: something that only one or two renderers
144
+ // need (masked input, signature pad, chart) should live in the feature
145
+ // module, not here.
146
+ export type PrimitivesContract<TPrimitive = unknown> = {
147
+ readonly TextInput: TPrimitive;
148
+ readonly NumberInput: TPrimitive;
149
+ readonly Select: TPrimitive;
150
+ readonly Toggle: TPrimitive;
151
+ readonly DatePicker: TPrimitive;
152
+ readonly Button: TPrimitive;
153
+ readonly Modal: TPrimitive;
154
+ readonly Toast: TPrimitive;
155
+ readonly Badge: TPrimitive;
156
+ readonly Card: TPrimitive;
157
+ readonly Icon: TPrimitive;
158
+ };
@@ -0,0 +1,160 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { createStore } from "../../store";
3
+ import type {
4
+ BatchResult,
5
+ Command,
6
+ Dispatcher,
7
+ DispatcherStatus,
8
+ PendingFile,
9
+ PendingWrite,
10
+ QueryResult,
11
+ WriteResult,
12
+ } from "../types";
13
+
14
+ // A minimal fake dispatcher — proves the Dispatcher interface is sufficient
15
+ // to implement a synchronous in-memory client, and pins the public shape of
16
+ // WriteResult / QueryResult / BatchResult so a PR that renames or reshapes
17
+ // a field breaks this file loud.
18
+ //
19
+ // The fake records every call, so the test can assert on the exact
20
+ // sequence without re-exercising real HTTP. Block 2's form-controller uses
21
+ // this fake as its default test double.
22
+ function createFakeDispatcher(options?: {
23
+ readonly writeResponses?: Record<string, WriteResult>;
24
+ readonly queryResponses?: Record<string, QueryResult>;
25
+ }): Dispatcher & {
26
+ readonly calls: ReadonlyArray<{
27
+ kind: "write" | "query" | "batch";
28
+ type?: string;
29
+ payload?: unknown;
30
+ }>;
31
+ setStatus(next: DispatcherStatus): void;
32
+ } {
33
+ const calls: Array<{ kind: "write" | "query" | "batch"; type?: string; payload?: unknown }> = [];
34
+ const statusStore = createStore<DispatcherStatus>("online");
35
+ const pendingWritesStore: PendingWrite[] = [];
36
+ const pendingFilesStore: PendingFile[] = [];
37
+
38
+ return {
39
+ calls,
40
+ async write<TData>(type: string, payload: unknown) {
41
+ calls.push({ kind: "write", type, payload });
42
+ const response = options?.writeResponses?.[type];
43
+ return (response ?? {
44
+ isSuccess: true,
45
+ data: { id: `fake-${calls.length}` },
46
+ }) as WriteResult<TData>;
47
+ },
48
+ async query<TData>(type: string, payload: unknown) {
49
+ calls.push({ kind: "query", type, payload });
50
+ const response = options?.queryResponses?.[type];
51
+ return (response ?? { isSuccess: true, data: null }) as QueryResult<TData>;
52
+ },
53
+ async batch(commands) {
54
+ calls.push({ kind: "batch", payload: commands });
55
+ const results: WriteResult[] = commands.map((_, i) => ({
56
+ isSuccess: true as const,
57
+ data: { id: `fake-batch-${i}` },
58
+ }));
59
+ const result: BatchResult = { isSuccess: true, results };
60
+ return result;
61
+ },
62
+ statusStore,
63
+ pendingWrites: () => pendingWritesStore,
64
+ pendingFiles: () => pendingFilesStore,
65
+ setStatus(next) {
66
+ statusStore.setState(next);
67
+ },
68
+ };
69
+ }
70
+
71
+ describe("Dispatcher contract", () => {
72
+ test("write() records the call and returns the configured response", async () => {
73
+ const disp = createFakeDispatcher({
74
+ writeResponses: {
75
+ "app:write:task:create": { isSuccess: true, data: { id: "t-1", title: "hello" } },
76
+ },
77
+ });
78
+
79
+ const result = await disp.write("app:write:task:create", { title: "hello" });
80
+
81
+ expect(result.isSuccess).toBe(true);
82
+ if (result.isSuccess) {
83
+ expect((result.data as { id: string }).id).toBe("t-1");
84
+ }
85
+ expect(disp.calls).toHaveLength(1);
86
+ expect(disp.calls[0]).toEqual({
87
+ kind: "write",
88
+ type: "app:write:task:create",
89
+ payload: { title: "hello" },
90
+ });
91
+ });
92
+
93
+ test("write() failure shape carries code + message + optional field issues", async () => {
94
+ const disp = createFakeDispatcher({
95
+ writeResponses: {
96
+ "app:write:task:create": {
97
+ isSuccess: false,
98
+ error: {
99
+ code: "validation_error",
100
+ httpStatus: 400,
101
+ i18nKey: "errors.validation.failed",
102
+ message: "Validation failed",
103
+ details: {
104
+ fields: [
105
+ { path: "title", code: "too_small", i18nKey: "errors.validation.too_small" },
106
+ ],
107
+ },
108
+ },
109
+ },
110
+ },
111
+ });
112
+
113
+ const result = await disp.write("app:write:task:create", { title: "" });
114
+
115
+ expect(result.isSuccess).toBe(false);
116
+ if (!result.isSuccess) {
117
+ expect(result.error.code).toBe("validation_error");
118
+ expect(result.error.details?.fields?.[0]?.path).toBe("title");
119
+ }
120
+ });
121
+
122
+ test("batch() returns one result per command, in order", async () => {
123
+ const disp = createFakeDispatcher();
124
+ const commands: readonly Command[] = [
125
+ { type: "app:write:task:create", payload: { title: "a" } },
126
+ { type: "app:write:task:create", payload: { title: "b" } },
127
+ { type: "app:write:task:create", payload: { title: "c" } },
128
+ ];
129
+
130
+ const result = await disp.batch(commands);
131
+
132
+ expect(result.isSuccess).toBe(true);
133
+ if (result.isSuccess) {
134
+ expect(result.results).toHaveLength(3);
135
+ for (const r of result.results) {
136
+ expect(r.isSuccess).toBe(true);
137
+ }
138
+ }
139
+ });
140
+
141
+ test("statusStore fires on transitions and unsubscribes cleanly", () => {
142
+ const disp = createFakeDispatcher();
143
+ const seen: DispatcherStatus[] = [];
144
+
145
+ const unsubscribe = disp.statusStore.subscribe(() => seen.push(disp.statusStore.getSnapshot()));
146
+ disp.setStatus("offline");
147
+ disp.setStatus("syncing");
148
+ unsubscribe();
149
+ disp.setStatus("online");
150
+
151
+ expect(seen).toEqual(["offline", "syncing"]);
152
+ });
153
+
154
+ test("pending queues are empty by default (live-dispatcher semantic)", () => {
155
+ const disp = createFakeDispatcher();
156
+
157
+ expect(disp.pendingWrites()).toEqual([]);
158
+ expect(disp.pendingFiles()).toEqual([]);
159
+ });
160
+ });
@@ -0,0 +1,14 @@
1
+ export type {
2
+ BatchResult,
3
+ Command,
4
+ Dispatcher,
5
+ DispatcherError,
6
+ DispatcherStatus,
7
+ FieldIssue,
8
+ PendingFile,
9
+ PendingWrite,
10
+ QueryOpts,
11
+ QueryResult,
12
+ WriteOpts,
13
+ WriteResult,
14
+ } from "./types";