ilha 0.6.1 → 0.7.1

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/README.md CHANGED
@@ -121,11 +121,11 @@ ilha
121
121
  .render(({ derived }) => {
122
122
  if (derived.user.loading) return `<p>Loading…</p>`;
123
123
  if (derived.user.error) return `<p>Error: ${derived.user.error.message}</p>`;
124
- return `<p>${derived.user.value.name}</p>`;
124
+ return `<p>${derived.user().name}</p>`;
125
125
  });
126
126
  ```
127
127
 
128
- Each derived value exposes `{ loading, value, error }`.
128
+ Read with `derived.name()` like state. Each accessor also exposes `loading`, `value`, and `error` for async work. Write `derived.name(value)` for optimistic UI.
129
129
 
130
130
  ---
131
131
 
@@ -239,12 +239,15 @@ The handler receives an `EffectContext`:
239
239
  ```ts
240
240
  {
241
241
  state: IslandState;
242
+ derived: IslandDerived;
242
243
  input: TInput;
243
244
  host: Element;
244
245
  signal: AbortSignal; // aborts when the effect re-runs OR the island unmounts
245
246
  }
246
247
  ```
247
248
 
249
+ Reading `derived.name()` subscribes the effect. Writing `derived.name(value)` does not — use writes for optimistic UI without pinning the effect to every derived update.
250
+
248
251
  **Cancelling async work with `ctx.signal`** — unlike `.on()`, race-cancellation is the **default** behaviour for effects (no opt-in modifier needed) because dependency changes invariably make the previous run stale. Pass `signal` to async work to bail out of stale invocations without needing a manual cleanup function:
249
252
 
250
253
  ```ts
package/dist/index.d.ts CHANGED
@@ -105,6 +105,13 @@ interface DerivedValue<T> {
105
105
  value: T | undefined;
106
106
  error: Error | undefined;
107
107
  }
108
+ type DerivedAccessor<T> = {
109
+ readonly loading: boolean;
110
+ readonly value: T | undefined;
111
+ readonly error: Error | undefined;
112
+ (): T | undefined;
113
+ (value: T): void;
114
+ };
108
115
  type DerivedFnContext<TInput, TStateMap extends Record<string, unknown>> = {
109
116
  state: IslandState<TStateMap>;
110
117
  input: TInput;
@@ -115,7 +122,7 @@ interface DerivedEntry<TInput, TStateMap extends Record<string, unknown>> {
115
122
  key: string;
116
123
  fn: DerivedFn<TInput, TStateMap, unknown>;
117
124
  }
118
- type IslandDerived<TDerivedMap extends Record<string, unknown>> = { readonly [K in keyof TDerivedMap]: DerivedValue<TDerivedMap[K]> };
125
+ type IslandDerived<TDerivedMap extends Record<string, unknown>> = { readonly [K in keyof TDerivedMap]: DerivedAccessor<TDerivedMap[K]> };
119
126
  type ExternalSignal<T = unknown> = SignalAccessor<T>;
120
127
  type SignalAccessor<T> = MarkedSignalAccessor<T>;
121
128
  type MergeState<TStateMap extends Record<string, unknown>, K extends string, V> = Omit<TStateMap, K> & Record<K, V>;
@@ -147,8 +154,9 @@ type RenderContext<TInput, TStateMap extends Record<string, unknown>, TDerivedMa
147
154
  derived: IslandDerived<TDerivedMap>;
148
155
  input: TInput;
149
156
  };
150
- type EffectContext<TInput, TStateMap extends Record<string, unknown>> = {
157
+ type EffectContext<TInput, TStateMap extends Record<string, unknown>, TDerivedMap extends Record<string, unknown> = Record<never, never>> = {
151
158
  state: IslandState<TStateMap>;
159
+ derived: IslandDerived<TDerivedMap>;
152
160
  input: TInput;
153
161
  host: Element;
154
162
  /**
@@ -211,8 +219,8 @@ interface OnEntry<TInput, TStateMap extends Record<string, unknown>, TDerivedMap
211
219
  abortable: boolean;
212
220
  handler: (ctx: HandlerContext<TInput, TStateMap, TDerivedMap>) => void | Promise<void>;
213
221
  }
214
- interface EffectEntry<TInput, TStateMap extends Record<string, unknown>> {
215
- fn: (ctx: EffectContext<TInput, TStateMap>) => (() => void) | void;
222
+ interface EffectEntry<TInput, TStateMap extends Record<string, unknown>, TDerivedMap extends Record<string, unknown>> {
223
+ fn: (ctx: EffectContext<TInput, TStateMap, TDerivedMap>) => (() => void) | void;
216
224
  }
217
225
  /** Where the error originated. `"on"` covers sync throws and async rejections
218
226
  * from `.on()` handlers; `"effect"` covers sync throws from `.effect()` runs
@@ -248,7 +256,7 @@ interface BuilderConfig<TInput, TStateMap extends Record<string, unknown>, TDeri
248
256
  states: StateEntry<TInput>[];
249
257
  deriveds: DerivedEntry<TInput, TStateMap>[];
250
258
  ons: OnEntry<TInput, TStateMap, TDerivedMap>[];
251
- effects: EffectEntry<TInput, TStateMap>[];
259
+ effects: EffectEntry<TInput, TStateMap, TDerivedMap>[];
252
260
  onMounts: OnMountEntry<TInput, TStateMap, TDerivedMap>[];
253
261
  onErrors: OnErrorEntry<TInput, TStateMap, TDerivedMap>[];
254
262
  transition: TransitionOptions | null;
@@ -262,7 +270,7 @@ declare class IlhaBuilder<TInput extends Record<string, unknown>, TStateMap exte
262
270
  state<V = undefined, K extends string = string>(key: K, init?: StateInit<TInput, V> | undefined): IlhaBuilder<TInput, MergeState<TStateMap, K, V>, TDerivedMap>;
263
271
  derived<K extends string, V>(key: K, fn: DerivedFn<TInput, TStateMap, V>): IlhaBuilder<TInput, TStateMap, TDerivedMap & Record<K, V>>;
264
272
  on<S extends string>(selectorOrCombined: S, handler: (ctx: S extends `${string}@${infer E}:${string}` ? HandlerContextFor<TInput, TStateMap, E, TDerivedMap> : S extends `${string}@${infer E}` ? HandlerContextFor<TInput, TStateMap, E, TDerivedMap> : HandlerContext<TInput, TStateMap, TDerivedMap>) => void | Promise<void>): IlhaBuilder<TInput, TStateMap, TDerivedMap>;
265
- effect(fn: (ctx: EffectContext<TInput, TStateMap>) => (() => void) | void): IlhaBuilder<TInput, TStateMap, TDerivedMap>;
273
+ effect(fn: (ctx: EffectContext<TInput, TStateMap, TDerivedMap>) => (() => void) | void): IlhaBuilder<TInput, TStateMap, TDerivedMap>;
266
274
  onMount(fn: (ctx: OnMountContext<TInput, TStateMap, TDerivedMap>) => (() => void) | void): IlhaBuilder<TInput, TStateMap, TDerivedMap>;
267
275
  onError(fn: (ctx: ErrorContext<TInput, TStateMap, TDerivedMap>) => void): IlhaBuilder<TInput, TStateMap, TDerivedMap>;
268
276
  transition(opts: TransitionOptions): IlhaBuilder<TInput, TStateMap, TDerivedMap>;
@@ -297,4 +305,4 @@ declare const mount: typeof mountAll;
297
305
  declare const from: typeof ilhaFrom;
298
306
  declare const context: typeof ilhaContext;
299
307
  //#endregion
300
- export { DerivedValue, ErrorContext, ErrorSource, ExternalSignal, Fragment, HandlerContext, HandlerContextFor, HydratableOptions, ISLAND_MOUNT_INTERNAL, Island, IslandDerived, IslandState, KeyedIsland, MountOptions, MountResult, OnMountContext, RawHtml, SignalAccessor, batch, context, css, ilha as default, from, html, ilhaSignal, ilhaSignal as signal, jsx, jsxDEV, jsxs, mount, raw, untrack };
308
+ export { DerivedAccessor, DerivedValue, EffectContext, ErrorContext, ErrorSource, ExternalSignal, Fragment, HandlerContext, HandlerContextFor, HydratableOptions, ISLAND_MOUNT_INTERNAL, Island, IslandDerived, IslandState, KeyedIsland, MountOptions, MountResult, OnMountContext, RawHtml, SignalAccessor, batch, context, css, ilha as default, from, html, ilhaSignal, ilhaSignal as signal, jsx, jsxDEV, jsxs, mount, raw, untrack };
package/dist/index.js CHANGED
@@ -465,6 +465,7 @@ function ilhaJsx(type, props, maybeKey, ...restChildren) {
465
465
  if (isRawHtml(out)) return out;
466
466
  if (typeof out === "string" && isIsland(type)) return ilhaRaw(emitIslandSlot(type, componentProps, slotKey));
467
467
  if (typeof out === "string") return ilhaHtml`${out}`;
468
+ if (typeof out === "object" && out !== null && Object.getPrototypeOf(out) === Object.prototype && out[Symbol.for("ilha.renderPart")] === true && typeof out.toString === "function") return ilhaRaw(out.toString());
468
469
  return ilhaHtml`${out}`;
469
470
  }
470
471
  return renderJsxElement(type, props, normalizedChildren);
@@ -676,6 +677,38 @@ function batch(fn) {
676
677
  endBatch();
677
678
  }
678
679
  }
680
+ function createDerivedAccessor(read, write) {
681
+ const accessor = markSignalAccessor((...args) => {
682
+ if (args.length > 0) {
683
+ if (write) write(args[0]);
684
+ else if (__DEV__) warn("derived values are read-only");
685
+ return;
686
+ }
687
+ return read().value;
688
+ });
689
+ return new Proxy(accessor, { get(target, prop, receiver) {
690
+ if (prop === "loading" || prop === "value" || prop === "error") return read()[prop];
691
+ return Reflect.get(target, prop, receiver);
692
+ } });
693
+ }
694
+ function defaultDerivedAccessor() {
695
+ return createDerivedAccessor(() => ({
696
+ loading: false,
697
+ value: void 0,
698
+ error: void 0
699
+ }));
700
+ }
701
+ function buildDerivedAccessors(envelopes) {
702
+ const accessors = /* @__PURE__ */ new Map();
703
+ for (const [key, envelope] of Object.entries(envelopes)) accessors.set(key, createDerivedAccessor(() => envelope, (value) => {
704
+ envelope.loading = false;
705
+ envelope.value = value;
706
+ envelope.error = void 0;
707
+ }));
708
+ return new Proxy({}, { get(_, key) {
709
+ return accessors.get(key) ?? defaultDerivedAccessor();
710
+ } });
711
+ }
679
712
  function createDerivedProxy(entries, state, input, derivedSnapshot) {
680
713
  const envelopes = /* @__PURE__ */ new Map();
681
714
  for (const entry of entries) {
@@ -719,14 +752,21 @@ function createDerivedProxy(entries, state, input, derivedSnapshot) {
719
752
  const env = signal(initialEnvelope);
720
753
  envelopes.set(entry.key, env);
721
754
  }
755
+ const accessors = /* @__PURE__ */ new Map();
756
+ for (const entry of entries) {
757
+ const env = envelopes.get(entry.key);
758
+ accessors.set(entry.key, createDerivedAccessor(() => env(), (value) => {
759
+ const prevSub = setActiveSub(void 0);
760
+ env({
761
+ loading: false,
762
+ value,
763
+ error: void 0
764
+ });
765
+ setActiveSub(prevSub);
766
+ }));
767
+ }
722
768
  const proxy = new Proxy({}, { get(_, key) {
723
- const env = envelopes.get(key);
724
- if (!env) return {
725
- loading: false,
726
- value: void 0,
727
- error: void 0
728
- };
729
- return env();
769
+ return accessors.get(key) ?? defaultDerivedAccessor();
730
770
  } });
731
771
  const setup = () => {
732
772
  const stops = [];
@@ -1183,17 +1223,18 @@ var IlhaBuilder = class IlhaBuilder {
1183
1223
  }
1184
1224
  });
1185
1225
  if (!results.some((r) => r.result instanceof Promise) || sync) {
1186
- const derived = {};
1187
- for (const r of results) if (r.result instanceof Promise) derived[r.key] = {
1226
+ const envelopes = {};
1227
+ for (const r of results) if (r.result instanceof Promise) envelopes[r.key] = {
1188
1228
  loading: true,
1189
1229
  value: void 0,
1190
1230
  error: void 0
1191
1231
  };
1192
- else derived[r.key] = {
1232
+ else envelopes[r.key] = {
1193
1233
  loading: false,
1194
1234
  value: r.result,
1195
1235
  error: void 0
1196
1236
  };
1237
+ const derived = buildDerivedAccessors(envelopes);
1197
1238
  const prevSub = setActiveSub(void 0);
1198
1239
  try {
1199
1240
  const { html } = renderWithCtx(() => fn({
@@ -1227,8 +1268,9 @@ var IlhaBuilder = class IlhaBuilder {
1227
1268
  };
1228
1269
  }
1229
1270
  })).then(async (resolved) => {
1230
- const derived = {};
1231
- for (const r of resolved) derived[r.key] = r.envelope;
1271
+ const envelopes = {};
1272
+ for (const r of resolved) envelopes[r.key] = r.envelope;
1273
+ const derived = buildDerivedAccessors(envelopes);
1232
1274
  const prevSub = setActiveSub(void 0);
1233
1275
  try {
1234
1276
  const { html } = await renderWithCtx(() => fn({
@@ -1537,6 +1579,7 @@ Element: ${host.outerHTML.slice(0, 120)}`);
1537
1579
  try {
1538
1580
  userCleanup = entry.fn({
1539
1581
  state,
1582
+ derived,
1540
1583
  input,
1541
1584
  host,
1542
1585
  signal: runSignal
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ilha",
3
- "version": "0.6.1",
3
+ "version": "0.7.1",
4
4
  "description": "A tiny, framework-free island architecture library",
5
5
  "license": "MIT",
6
6
  "author": "Ryuz <ryuzer@proton.me>",