MCPcopy Index your code
hub / github.com/dcocchia/Ity

github.com/dcocchia/Ity @v4.0.0

Chat with this repo
repository ↗ · DeepWiki ↗ · release v4.0.0 ↗ · + Follow
951 symbols 2,649 edges 94 files 0 documented · 0%
What it actually does AI analysis from the code graph — generated when you open this
loading…
README

Ity

A tiny dependency-free reactive app kernel for small browser apps, embeddable widgets, progressive SPAs, and local-first browser applications.

Ity 3 keeps the original Ity goal: do useful client-side app work without a framework stack, runtime dependencies, or a large bundle. The core stays platform-native and explicit: signals, computed values, effects, tagged HTML templates, DOM rendering, Web Components, and a modern router. V3 adds a small set of app-scale primitives and optional companion modules without trying to recreate React inside the core.

Why Ity 3?

  • Tiny runtime, no production dependencies in the core or companion modules.
  • Fine-grained reactive state with signal, computed, effect, and store.
  • Async UI primitives with resource, action, form, and formState.
  • Safe-by-default DOM templates: dynamic values become text unless explicitly marked with unsafeHTML.
  • Native Web Component support via component().
  • SPA routing with URLPattern support when available and a regex fallback when it is not.
  • Keyed structural rendering with repeat().
  • Static and hydration-friendly output through renderToString() and hydrate().
  • Scoped dependency flow through createScope(), ctx.provide(), and ctx.inject().
  • Runtime observability through observeRuntime().
  • Optional View Transition integration for same-document route/render updates.
  • Optional app-scale companion modules: ity/query, ity/forms, and ity/react.
  • V1-compatible Model, Collection, View, Application, and SelectorObject.

What's New In 3.0.0

Ity 3.0.0 expands the kernel without changing its native-first design:

  • repeat(items, key, render) for keyed structural list rendering.
  • hydrate() plus morph-based render() updates for progressive enhancement and SSR-style flows.
  • createScope() and scope-aware components and routers.
  • observeRuntime() for signals, resources, actions, and router activity.
  • Router.resource() and Router.action() for route-scoped async work.
  • Companion modules:
  • ity/query for cache-backed async queries and optimistic mutations.
  • ity/forms for nested form state, field arrays, explicit form.sync(), and richer submit controllers.
  • ity/react for wrapping Ity custom elements inside React trees.
  • The deep Examples/OperationsWorkbench/index.html example now uses the v3 kernel and companion modules together.

What's New In 4.0.0

Ity 4.0.0 is a security-hardening major release:

  • Safe template bindings now block inline handler attributes and strip unsafe URL schemes such as javascript: and vbscript: from URL-bearing attributes and properties.
  • HTML-parsing sinks such as srcdoc and .innerHTML now require an explicit unsafeHTML(...) boundary so trusted markup stays deliberate.
  • SelectorObject string insertion is now text-by-default. Use Ity.unsafeHTML(...) if you intentionally want append, prepend, html, before, or after to parse trusted HTML strings.
  • SSR output from renderToString() now uses the same sink protections as the browser renderer, which removes a client/server consistency gap for unsafe attrs.
  • The Operations Workbench example sanitizer now blocks additional rich-text attack vectors such as srcdoc, xlink:href, and protocol-based data: navigation payloads.

Installation

npm install ity
import Ity, { signal, computed, html, render } from "ity";
import { createQueryClient, query } from "ity/query";
import { createFormKit } from "ity/forms";

The package ships ESM, CommonJS, browser IIFE, minified IIFE, source maps, and TypeScript declarations.

ity, ity/query, and ity/forms have no runtime package dependencies. ity/react is optional and expects react to already exist in the consuming application.

Examples

The repository includes small focused demos plus a deeper production-style app:

Run them locally with:

npm install
npm run examples:serve

Quick Start

import { signal, computed, html, render } from "ity";

const count = signal(0);
const doubled = computed(() => count() * 2);

render(() => html`
  <button @click=${() => count.update((n) => n + 1)}>
    Count: ${count}
  </button>


Doubled: ${doubled}


`, "#app");

When count changes, only the reactive render effect reruns. Dynamic text is inserted as text, not parsed as HTML.

Signals

Signals are callable values.

const name = Ity.signal("Ada");

name();               // "Ada"
name.set("Grace");    // "Grace"
name("Hedy");         // callable setter
name.update((v) => v.toUpperCase());
name.peek();          // read without dependency tracking

computed

const first = Ity.signal("Ada");
const last = Ity.signal("Lovelace");
const full = Ity.computed(() => `${first()} ${last()}`);

full(); // "Ada Lovelace"

Computed values are lazy and cached. They invalidate when dependencies change.

effect

const stop = Ity.effect((onCleanup) => {
  const controller = new AbortController();
  onCleanup(() => controller.abort());

  console.log("Current value", full());
});

stop();

batch and untrack

Ity.batch(() => {
  first.set("Grace");
  last.set("Hopper");
});

const snapshot = Ity.untrack(() => full());

batch coalesces dependent effects. untrack reads without subscribing the current computation.

store

const state = Ity.store({ name: "Ada", count: 1 });

state.name; // reactive read
state.count = 2;
state.$patch({ name: "Grace" });
state.$snapshot(); // plain object

const unsubscribe = state.$subscribe((value) => {
  console.log(value);
}, { immediate: true });

store tracks object structure as well as values. Effects and subscribers that read $snapshot() rerun when keys are added or deleted.

Async UI

resource

resource() models loadable async data.

const user = Ity.resource(async ({ signal, previous, refreshId }) => {
  const response = await fetch(`/api/user?refresh=${refreshId}`, { signal });
  if (!response.ok) throw new Error("Failed to load user");
  return response.json() as Promise<{ name: string }>;
}, {
  initialValue: undefined,
  keepPrevious: true,
  onError(error) {
    console.error(error);
  }
});

Ity.render(() => Ity.html`
  ${user.loading() && Ity.html`

Loading...

`}
  ${user.error() && Ity.html`

${user.error().message}

`}
  ${user.data() && Ity.html`<h1>${user.data().name}</h1>`}
  <button @click=${() => user.refresh()}>Refresh</button>
`, "#app");

Each refresh aborts the previous in-flight refresh. Stale completions are ignored, failures are captured in error instead of being thrown from refresh(), and mutate(value) can update local state optimistically.

action

action() models async writes and other user-triggered effects.

const save = Ity.action(async (payload: { name: string }) => {
  const response = await fetch("/api/user", {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify(payload)
  });
  if (!response.ok) throw new Error("Save failed");
  return response.json();
});

Ity.render(() => Ity.html`
  <button ?disabled=${save.pending()} @click=${() => save({ name: "Ada" })}>
    ${save.pending() ? "Saving..." : "Save"}
  </button>
`, "#app");

Actions expose data, error, pending, pendingCount, status, submit(), run(), with(), from(), and reset().

form

form() wraps action() for native forms.

const signup = Ity.form(async (data) => {
  const response = await fetch("/signup", {
    method: "POST",
    body: data
  });
  if (!response.ok) throw new Error("Signup failed");
  return response.json();
}, { resetOnSuccess: true });

Ity.render(() => Ity.html`
  <form @submit=${signup.onSubmit}>
    <input name="email" type="email" required>
    <button ?disabled=${signup.pending()}>Join</button>
    ${signup.error() && Ity.html`

${signup.error().message}

`}
  </form>
`, "#app");

For direct DOM event wiring that should stay inside the controller error model, prefer signup.handleSubmit over signup.onSubmit.

formState

formState() adds field-level state on top of native forms.

const draft = Ity.formState({
  title: "",
  ownerId: "ava",
  urgent: false
}, {
  validators: {
    title(value) {
      return value.trim() ? null : "Title is required.";
    }
  }
});

const saveDraft = draft.submit(async (values) => {
  return fetch("/tasks", {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify(values)
  });
});

Ity.render(() => Ity.html`
  <form @submit=${saveDraft.handleSubmit}>
    <input bind=${draft.bind("title", { name: "taskTitle" })}>
    <select bind=${draft.bind("ownerId", { type: "select" })}></select>
    <label>
      <input type="checkbox" bind=${draft.bind("urgent", { type: "checkbox" })}>
      Urgent
    </label>
    ${draft.errors.title && Ity.html`

${draft.errors.title}

`}
  </form>
`, "#app");

formState() exposes values, initialValues, errors, touched, dirty, valid, field(name), bind(name), set(), reset(), validate(), markTouched(), and submit().

V3 Bridges

repeat

Use repeat() when list items need keyed identity:

const tasks = Ity.signal([
  { id: "a", title: "Draft launch brief" },
  { id: "b", title: "Ship release notes" }
]);

Ity.render(() => Ity.html`
  <ul>
    ${Ity.repeat(tasks(), (task) => task.id, (task) => Ity.html`
      <li>${task.title}</li>
    `)}
  </ul>
`, "#app");

hydrate

hydrate() attaches bindings to existing markup instead of replacing it:

Ity.hydrate(() => Ity.html`
  <button @click=${() => console.log("ready")}>Hydrated</button>
`, "#app");

createScope and observeRuntime

Scopes let components and routers share services without a framework-wide context object. Runtime observation gives a lightweight event stream for debugging and tooling.

const scope = Ity.createScope({ name: "app" });
scope.provide("apiBase", "/api");

const stop = Ity.observeRuntime((event) => {
  console.log(event.type, event.name);
});

ity/query

Use ity/query when async data should be cached and invalidated:

import { createQueryClient, query, mutation } from "ity/query";

const client = createQueryClient();
const user = query(client, ["user", "42"], async () => {
  const response = await fetch("/api/users/42");
  return response.json();
});

const saveUser = mutation(client, async (payload) => {
  const response = await fetch("/api/users/42", {
    method: "POST",
    body: JSON.stringify(payload)
  });
  return response.json();
}, {
  invalidate: [["user", "42"]]
});

ity/forms

ity/forms is the richer companion to core formState(). Use it when the form needs nested paths, field arrays, or explicit syncing before structural mutations:

import { createFormKit } from "ity/forms";

const draft = createFormKit({
  title: "",
  checklist: [{ label: "" }]
});

const checklist = draft.array("checklist");

ity/react

Use ity/react when Ity custom elements need to live inside a React tree:

import { wrapCustomElement } from "ity/react";

const UserCard = wrapCustomElement("user-card", {
  events: {
    onChoose: "choose"
  }
});

ity/react is a bridge, not part of the dependency-free core. Consumers using that entrypoint should already have react installed.

HTML Templates

html creates a template result. Use render to mount it.

const title = Ity.signal("Dashboard");

Ity.render(() => Ity.html`
  <section class=${["panel", "primary"]}>
    <h1>${title}</h1>
    <input .value=${title}>
    <button ?disabled=${false} @click=${() => title.set("Updated")}>
      Rename
    </button>
  </section>
`, "#app");

Supported bindings:

  • Text/content: ${value}.
  • Events: @click=${handler} or @click=${[handler, options]}.
  • Properties: .value=${value}.
  • Boolean attributes: ?disabled=${condition}.
  • Attributes: href=${url}, class=${["a", "b"]}, class=${{ active: true }}.
  • Style objects: style=${{ color: "red" }}.
  • Nested templates, DOM nodes, arrays, and signals.

Unsafe HTML

Dynamic values are safe text by default. If you intentionally want to parse a string as HTML, mark that boundary explicitly:

Ity.html`<article>${Ity.unsafeHTML(trustedHtmlString)}</article>`;

Only pass trusted content to unsafeHTML. If your application allows rich HTML from a less trusted source, wire in your sanitizer:

const htmlPolicy = Ity.createConfig({
  sanitizeHTML(value) {
    return DOMPurify.sanitize(value);
  }
});

Ity.render(() => Ity.html`
  <article>${Ity.unsafeHTML(userProvidedHtml)}</article>
`, "#app", { config: htmlPolicy });

You can still sanitize one boundary without changing configuration:

Ity.unsafeHTML(markdownHtml, { sanitize: sanitizeMarkdownOutput });

Ity.configure({ sanitizeHTML }) still exists for process-wide setup, but createConfig() is the better fit for multi-app pages, tests, and SSR.

Ity does not bundle a sanitizer. Sanitization policy depends on the content source and threat model, and most production apps already standardize that choice separately.

Safe bindings also block inline ha

Extension points exported contracts — how you extend this code

ReactiveObserver (Interface)
(no doc) [4 implementers]
Ity.ts
QueryClient (Interface)
(no doc) [2 implementers]
query.ts
EffectHandle (Interface)
(no doc) [2 implementers]
types/Ity.d.ts
OperationsWorkbenchApp (Interface)
(no doc) [2 implementers]
Examples/OperationsWorkbench/operationsWorkbenchApp.ts
QueryClient (Interface)
(no doc) [1 implementers]
types/query.d.ts
ReactWrapperOptions (Interface)
(no doc)
react.ts
FormKitOptions (Interface)
(no doc)
forms.ts
ReactWrapperOptions (Interface)
(no doc)
types/react.d.ts

Core symbols most depended-on inside this repo

set
called by 228
types/Ity.d.ts
setupDOM
called by 196
test/helpers.ts
html
called by 158
Ity.ts
push
called by 85
types/forms.d.ts
peek
called by 80
types/Ity.d.ts
get
called by 73
types/Ity.d.ts
map
called by 71
Ity.ts
bind
called by 70
types/Ity.d.ts

Shape

Function 436
Method 331
Interface 144
Class 40

Languages

TypeScript100%

Modules by API surface

Ity.ts408 symbols
Examples/OperationsWorkbench/operationsWorkbenchApp.ts139 symbols
types/Ity.d.ts101 symbols
forms.ts67 symbols
query.ts43 symbols
types/forms.d.ts28 symbols
scripts/perf-bench.mjs18 symbols
types/query.d.ts17 symbols
test/libraryCoverage.test.ts14 symbols
Examples/Router/routerApp.ts11 symbols
Examples/Calculator/calculatorApp.ts11 symbols
test/operationsWorkbenchExample.test.ts9 symbols

For agents

$ claude mcp add Ity \
  -- python -m otcore.mcp_server <graph>

⬇ download graph artifact