@diphyx/harlemify 5.0.0 → 5.2.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/README.md CHANGED
@@ -2,16 +2,16 @@
2
2
 
3
3
  > Factory-driven state management for Nuxt powered by [Harlem](https://harlemjs.com/)
4
4
 
5
- ## Features
5
+ Define your data **shape** once with Zod — get typed **models**, computed **views**, and async **actions** with a single `createStore` call.
6
6
 
7
- - **Single Factory** - Define shape, model, view, and action in one `createStore` call — fully typed end to end
8
- - **Zod Shapes** - Schema-first design with built-in validation, type inference, and identifier metadata
9
- - **Reactive Models** - `one()` and `many()` state containers with `set`, `patch`, `add`, `remove`, `reset` mutations
10
- - **Computed Views** - Derive read-only state from models with `from()` and `merge()` auto-tracked by Vue
11
- - **API & Handler Actions** - Declarative HTTP actions with auto-commit, or custom handlers with full model/view access
12
- - **Action Metadata** - Every action exposes `loading`, `status`, `error`, `data`, and `reset()` out of the box
13
- - **Concurrency Control** - Block, skip, cancel, or allow parallel calls per action
14
- - **SSR Ready** - Server-side rendering with automatic state hydration
7
+ - **Schema-first** Define your data shape once, get TypeScript types and validation automatically
8
+ - **Reactive state** Single items and collections with built-in mutations
9
+ - **Computed views** Derived read-only state that updates when models change
10
+ - **API integration** Declarative HTTP actions that fetch and commit data in one step
11
+ - **Status tracking** Every action exposes loading, error, and status reactively
12
+ - **Concurrency control** Block, skip, cancel, or allow parallel calls per action
13
+ - **Vue composables** Reactive helpers for actions, models, and views in components
14
+ - **SSR ready** Server-side rendering with automatic state hydration
15
15
 
16
16
  ## Install
17
17
 
@@ -19,37 +19,23 @@
19
19
  npm install @diphyx/harlemify
20
20
  ```
21
21
 
22
- ## Setup
23
-
24
22
  ```typescript
25
23
  // nuxt.config.ts
26
24
  export default defineNuxtConfig({
27
25
  modules: ["@diphyx/harlemify"],
28
- harlemify: {
29
- action: {
30
- endpoint: "https://api.example.com",
31
- },
32
- },
33
26
  });
34
27
  ```
35
28
 
36
29
  ## Usage
37
30
 
38
31
  ```typescript
39
- // stores/user.ts
40
- import { createStore, shape, ModelManyMode, type ShapeInfer } from "@diphyx/harlemify";
41
-
42
- const userShape = shape((factory) => {
43
- return {
44
- id: factory.number().meta({
45
- identifier: true,
46
- }),
47
- name: factory.string(),
48
- email: factory.email(),
49
- };
50
- });
51
-
52
- export type User = ShapeInfer<typeof userShape>;
32
+ const userShape = shape((factory) => ({
33
+ id: factory.number().meta({
34
+ identifier: true,
35
+ }),
36
+ name: factory.string(),
37
+ email: factory.email(),
38
+ }));
53
39
 
54
40
  export const userStore = createStore({
55
41
  name: "users",
@@ -65,13 +51,17 @@ export const userStore = createStore({
65
51
  users: from("list"),
66
52
  };
67
53
  },
68
- action({ api, handler }) {
54
+ action({ api }) {
69
55
  return {
70
- list: api.get({ url: "/users" }, { model: "list", mode: ModelManyMode.SET }),
71
- create: api.post({ url: "/users" }, { model: "list", mode: ModelManyMode.ADD }),
72
- clear: handler(async ({ model }) => {
73
- model.list.reset();
74
- }),
56
+ list: api.get(
57
+ {
58
+ url: "/users",
59
+ },
60
+ {
61
+ model: "list",
62
+ mode: ModelManyMode.SET,
63
+ },
64
+ ),
75
65
  };
76
66
  },
77
67
  });
@@ -79,15 +69,15 @@ export const userStore = createStore({
79
69
 
80
70
  ```vue
81
71
  <script setup>
82
- const { view, action } = userStore;
72
+ const { execute, loading } = useStoreAction(userStore, "list");
73
+ const { data } = useStoreView(userStore, "users");
83
74
 
84
- await action.list();
75
+ await execute();
85
76
  </script>
86
77
 
87
78
  <template>
88
- <div v-if="action.list.loading.value">Loading...</div>
89
- <ul v-else>
90
- <li v-for="user in view.users.value" :key="user.id">{{ user.name }}</li>
79
+ <ul v-if="!loading">
80
+ <li v-for="user in data.value" :key="user.id">{{ user.name }}</li>
91
81
  </ul>
92
82
  </template>
93
83
  ```
package/dist/module.d.mts CHANGED
@@ -4,13 +4,13 @@ interface RuntimeModelConfig {
4
4
  identifier?: string;
5
5
  }
6
6
 
7
+ interface RuntimeViewConfig {
8
+ clone?: ViewClone;
9
+ }
7
10
  declare enum ViewClone {
8
11
  SHALLOW = "shallow",
9
12
  DEEP = "deep"
10
13
  }
11
- interface RuntimeViewConfig {
12
- clone?: ViewClone;
13
- }
14
14
 
15
15
  interface RuntimeActionConfig {
16
16
  endpoint?: string;
package/dist/module.d.ts CHANGED
@@ -4,13 +4,13 @@ interface RuntimeModelConfig {
4
4
  identifier?: string;
5
5
  }
6
6
 
7
+ interface RuntimeViewConfig {
8
+ clone?: ViewClone;
9
+ }
7
10
  declare enum ViewClone {
8
11
  SHALLOW = "shallow",
9
12
  DEEP = "deep"
10
13
  }
11
- interface RuntimeViewConfig {
12
- clone?: ViewClone;
13
- }
14
14
 
15
15
  interface RuntimeActionConfig {
16
16
  endpoint?: string;
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": ">=3.0.0 || >=4.0.0"
6
6
  },
7
- "version": "5.0.0",
7
+ "version": "5.2.0",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "0.8.4",
10
10
  "unbuild": "unknown"
@@ -1,4 +1,17 @@
1
- import { type Ref } from "vue";
2
- import { ActionStatus } from "../core/types/action.js";
3
- export declare function useIsolatedActionStatus(): Ref<ActionStatus>;
1
+ import { type ComputedRef, type Ref } from "vue";
2
+ import { ActionStatus, type ActionCall, type ActionCallOptions } from "../core/types/action.js";
3
+ export interface UseStoreActionOptions {
4
+ isolated?: boolean;
5
+ }
6
+ export type UseStoreAction<T> = {
7
+ execute: (options?: ActionCallOptions) => Promise<T>;
8
+ error: Readonly<Ref<Error | null>>;
9
+ status: Readonly<Ref<ActionStatus>>;
10
+ loading: ComputedRef<boolean>;
11
+ reset: () => void;
12
+ };
4
13
  export declare function useIsolatedActionError(): Ref<Error | null>;
14
+ export declare function useIsolatedActionStatus(): Ref<ActionStatus>;
15
+ export declare function useStoreAction<A extends Record<string, ActionCall<any>>, K extends keyof A & string, T = Awaited<ReturnType<A[K]>>>(store: {
16
+ action: A;
17
+ }, key: K, options?: UseStoreActionOptions): UseStoreAction<T>;
@@ -1,8 +1,54 @@
1
- import { ref } from "vue";
2
- import { ActionStatus } from "../core/types/action.js";
1
+ import { ref, computed } from "vue";
2
+ import {
3
+ ActionStatus
4
+ } from "../core/types/action.js";
5
+ export function useIsolatedActionError() {
6
+ return ref(null);
7
+ }
3
8
  export function useIsolatedActionStatus() {
4
9
  return ref(ActionStatus.IDLE);
5
10
  }
6
- export function useIsolatedActionError() {
7
- return ref(null);
11
+ export function useStoreAction(store, key, options) {
12
+ const action = store.action[key];
13
+ if (!action) {
14
+ throw new Error(`Action "${key}" not found in store`);
15
+ }
16
+ let error;
17
+ let status;
18
+ let loading;
19
+ let reset;
20
+ if (options?.isolated) {
21
+ const isolatedError = useIsolatedActionError();
22
+ const isolatedStatus = useIsolatedActionStatus();
23
+ error = isolatedError;
24
+ status = isolatedStatus;
25
+ loading = computed(() => {
26
+ return isolatedStatus.value === ActionStatus.PENDING;
27
+ });
28
+ reset = () => {
29
+ isolatedStatus.value = ActionStatus.IDLE;
30
+ isolatedError.value = null;
31
+ };
32
+ } else {
33
+ error = action.error;
34
+ status = action.status;
35
+ loading = action.loading;
36
+ reset = action.reset;
37
+ }
38
+ function execute(callOptions = {}) {
39
+ if (options?.isolated) {
40
+ callOptions.bind = {
41
+ status,
42
+ error
43
+ };
44
+ }
45
+ return action(callOptions);
46
+ }
47
+ return {
48
+ execute,
49
+ error,
50
+ status,
51
+ loading,
52
+ reset
53
+ };
8
54
  }
@@ -0,0 +1,22 @@
1
+ import type { ModelCall, ModelOneCall, ModelManyCall, ModelOneCommit, ModelManyCommit, StoreModel, ModelDefinitions } from "../core/types/model.js";
2
+ export interface UseStoreModelOptions {
3
+ debounce?: number;
4
+ throttle?: number;
5
+ }
6
+ type UseStoreModelOne<C extends ModelOneCommit<any>> = {
7
+ set: C["set"];
8
+ reset: C["reset"];
9
+ patch: C["patch"];
10
+ };
11
+ type UseStoreModelMany<C extends ModelManyCommit<any>> = {
12
+ set: C["set"];
13
+ reset: C["reset"];
14
+ patch: C["patch"];
15
+ add: C["add"];
16
+ remove: C["remove"];
17
+ };
18
+ export type UseStoreModel<M extends ModelCall<any> = ModelCall<any>> = M extends ModelManyCall<infer S> ? UseStoreModelMany<ModelManyCommit<S>> : M extends ModelOneCall<infer S> ? UseStoreModelOne<ModelOneCommit<S>> : never;
19
+ export declare function useStoreModel<M extends StoreModel<ModelDefinitions>, K extends keyof M & string>(store: {
20
+ model: M;
21
+ }, key: K, options?: UseStoreModelOptions): UseStoreModel<M[K]>;
22
+ export {};
@@ -0,0 +1,32 @@
1
+ import { debounce, throttle } from "../core/utils/base.js";
2
+ function isMany(model) {
3
+ return "add" in model && typeof model.add === "function";
4
+ }
5
+ function wrapCommit(commit, options) {
6
+ if (options?.debounce) {
7
+ return debounce(commit, options.debounce);
8
+ }
9
+ if (options?.throttle) {
10
+ return throttle(commit, options.throttle);
11
+ }
12
+ return commit;
13
+ }
14
+ export function useStoreModel(store, key, options) {
15
+ const model = store.model[key];
16
+ if (!model) {
17
+ throw new Error(`Model "${key}" not found in store`);
18
+ }
19
+ let output = {
20
+ set: wrapCommit(model.set.bind(model), options),
21
+ reset: wrapCommit(model.reset.bind(model), options),
22
+ patch: wrapCommit(model.patch.bind(model), options)
23
+ };
24
+ if (isMany(model)) {
25
+ output = {
26
+ ...output,
27
+ add: wrapCommit(model.add.bind(model), options),
28
+ remove: wrapCommit(model.remove.bind(model), options)
29
+ };
30
+ }
31
+ return output;
32
+ }
@@ -0,0 +1,33 @@
1
+ import { type ComputedRef, type WatchStopHandle } from "vue";
2
+ import type { ViewCall } from "../core/types/view.js";
3
+ export interface UseStoreViewTrackOptions {
4
+ deep?: boolean;
5
+ immediate?: boolean;
6
+ debounce?: number;
7
+ throttle?: number;
8
+ }
9
+ export interface UseStoreViewOptions<T> {
10
+ proxy?: boolean;
11
+ default?: T;
12
+ }
13
+ export type UseStoreViewData<T> = {
14
+ value: T;
15
+ } & (T extends Record<string, unknown> ? {
16
+ [K in keyof T]: T[K];
17
+ } : Record<string, unknown>);
18
+ export type UseStoreViewProxy<T> = {
19
+ data: UseStoreViewData<T>;
20
+ track: (handler: (value: T) => void, options?: UseStoreViewTrackOptions) => WatchStopHandle;
21
+ };
22
+ export type UseStoreViewComputed<T> = {
23
+ data: ComputedRef<T>;
24
+ track: (handler: (value: T) => void, options?: UseStoreViewTrackOptions) => WatchStopHandle;
25
+ };
26
+ export declare function useStoreView<V extends Record<string, ViewCall>, K extends keyof V & string, T = V[K] extends ComputedRef<infer R> ? R : unknown>(store: {
27
+ view: V;
28
+ }, key: K, options: UseStoreViewOptions<T> & {
29
+ proxy: false;
30
+ }): UseStoreViewComputed<T>;
31
+ export declare function useStoreView<V extends Record<string, ViewCall>, K extends keyof V & string, T = V[K] extends ComputedRef<infer R> ? R : unknown>(store: {
32
+ view: V;
33
+ }, key: K, options?: UseStoreViewOptions<T>): UseStoreViewProxy<T>;
@@ -0,0 +1,54 @@
1
+ import { computed, watch } from "vue";
2
+ import { debounce, throttle, toReactiveProxy } from "../core/utils/base.js";
3
+ function resolveDefault(view, defaultValue) {
4
+ if (defaultValue === void 0) {
5
+ return view;
6
+ }
7
+ return computed(() => {
8
+ const value = view.value;
9
+ if (value == null) {
10
+ return defaultValue;
11
+ }
12
+ return value;
13
+ });
14
+ }
15
+ function resolveData(source, proxy) {
16
+ if (proxy !== false) {
17
+ return toReactiveProxy(source);
18
+ }
19
+ return source;
20
+ }
21
+ export function useStoreView(store, key, options) {
22
+ if (!store.view[key]) {
23
+ throw new Error(`View "${key}" not found in store`);
24
+ }
25
+ const source = resolveDefault(store.view[key], options?.default);
26
+ const data = resolveData(source, options?.proxy);
27
+ function resolveCallback(callback, callbackOptions) {
28
+ if (callbackOptions?.debounce) {
29
+ return debounce(callback, callbackOptions.debounce);
30
+ }
31
+ if (callbackOptions?.throttle) {
32
+ return throttle(callback, callbackOptions.throttle);
33
+ }
34
+ return callback;
35
+ }
36
+ function track(handler, trackOptions) {
37
+ const callback = resolveCallback(handler, trackOptions);
38
+ const stop = watch(
39
+ source,
40
+ (value) => {
41
+ callback(value);
42
+ },
43
+ {
44
+ deep: trackOptions?.deep,
45
+ immediate: trackOptions?.immediate
46
+ }
47
+ );
48
+ return stop;
49
+ }
50
+ return {
51
+ data,
52
+ track
53
+ };
54
+ }
@@ -1,46 +1,69 @@
1
+ import { wrapBaseDefinition } from "../utils/base.js";
1
2
  import {
2
3
  ActionApiMethod
3
4
  } from "../types/action.js";
4
5
  export function createActionFactory(config, logger) {
5
6
  function apiCall(request, commit) {
6
- const mergedRequest = {
7
- endpoint: config?.endpoint,
8
- headers: config?.headers,
9
- query: config?.query,
10
- timeout: config?.timeout,
11
- concurrent: config?.concurrent,
12
- ...request
13
- };
14
- let key = "";
15
- return {
16
- get key() {
17
- return key;
7
+ return wrapBaseDefinition({
8
+ request: {
9
+ endpoint: config?.endpoint,
10
+ headers: config?.headers,
11
+ query: config?.query,
12
+ timeout: config?.timeout,
13
+ concurrent: config?.concurrent,
14
+ ...request
18
15
  },
19
- setKey(value) {
20
- key = value;
21
- },
22
- request: mergedRequest,
23
16
  commit,
24
17
  logger
25
- };
18
+ });
26
19
  }
27
20
  function apiGet(request, commit) {
28
21
  return apiCall({ ...request, method: ActionApiMethod.GET }, commit);
29
22
  }
30
23
  function apiHead(request, commit) {
31
- return apiCall({ ...request, method: ActionApiMethod.HEAD }, commit);
24
+ return apiCall(
25
+ {
26
+ ...request,
27
+ method: ActionApiMethod.HEAD
28
+ },
29
+ commit
30
+ );
32
31
  }
33
32
  function apiPost(request, commit) {
34
- return apiCall({ ...request, method: ActionApiMethod.POST }, commit);
33
+ return apiCall(
34
+ {
35
+ ...request,
36
+ method: ActionApiMethod.POST
37
+ },
38
+ commit
39
+ );
35
40
  }
36
41
  function apiPut(request, commit) {
37
- return apiCall({ ...request, method: ActionApiMethod.PUT }, commit);
42
+ return apiCall(
43
+ {
44
+ ...request,
45
+ method: ActionApiMethod.PUT
46
+ },
47
+ commit
48
+ );
38
49
  }
39
50
  function apiPatch(request, commit) {
40
- return apiCall({ ...request, method: ActionApiMethod.PATCH }, commit);
51
+ return apiCall(
52
+ {
53
+ ...request,
54
+ method: ActionApiMethod.PATCH
55
+ },
56
+ commit
57
+ );
41
58
  }
42
59
  function apiDelete(request, commit) {
43
- return apiCall({ ...request, method: ActionApiMethod.DELETE }, commit);
60
+ return apiCall(
61
+ {
62
+ ...request,
63
+ method: ActionApiMethod.DELETE
64
+ },
65
+ commit
66
+ );
44
67
  }
45
68
  const api = Object.assign(apiCall, {
46
69
  get: apiGet,
@@ -50,18 +73,15 @@ export function createActionFactory(config, logger) {
50
73
  patch: apiPatch,
51
74
  delete: apiDelete
52
75
  });
53
- function handler(callback) {
54
- let key = "";
55
- return {
56
- get key() {
57
- return key;
58
- },
59
- setKey(value) {
60
- key = value;
61
- },
76
+ function handler(callback, options) {
77
+ return wrapBaseDefinition({
62
78
  callback,
79
+ options: {
80
+ concurrent: config?.concurrent,
81
+ ...options
82
+ },
63
83
  logger
64
- };
84
+ });
65
85
  }
66
86
  return {
67
87
  api,
@@ -1,16 +1,10 @@
1
+ import { wrapBaseDefinition } from "../utils/base.js";
1
2
  import {
2
3
  ModelKind
3
4
  } from "../types/model.js";
4
5
  export function createModelFactory(config, logger) {
5
6
  function one(shape, options) {
6
- let key = "";
7
- return {
8
- get key() {
9
- return key;
10
- },
11
- setKey(value) {
12
- key = value;
13
- },
7
+ return wrapBaseDefinition({
14
8
  shape,
15
9
  kind: ModelKind.OBJECT,
16
10
  options: {
@@ -18,17 +12,10 @@ export function createModelFactory(config, logger) {
18
12
  ...options
19
13
  },
20
14
  logger
21
- };
15
+ });
22
16
  }
23
17
  function many(shape, options) {
24
- let key = "";
25
- return {
26
- get key() {
27
- return key;
28
- },
29
- setKey(value) {
30
- key = value;
31
- },
18
+ return wrapBaseDefinition({
32
19
  shape,
33
20
  kind: ModelKind.ARRAY,
34
21
  options: {
@@ -36,7 +23,7 @@ export function createModelFactory(config, logger) {
36
23
  ...options
37
24
  },
38
25
  logger
39
- };
26
+ });
40
27
  }
41
28
  return {
42
29
  one,
@@ -1,13 +1,7 @@
1
+ import { wrapBaseDefinition } from "../utils/base.js";
1
2
  export function createViewFactory(config, logger) {
2
3
  function from(model, resolver, options) {
3
- let key = "";
4
- const definition = {
5
- get key() {
6
- return key;
7
- },
8
- setKey(value) {
9
- key = value;
10
- },
4
+ return wrapBaseDefinition({
11
5
  model: [model],
12
6
  resolver,
13
7
  options: {
@@ -15,18 +9,10 @@ export function createViewFactory(config, logger) {
15
9
  ...options
16
10
  },
17
11
  logger
18
- };
19
- return definition;
12
+ });
20
13
  }
21
14
  function merge(models, resolver, options) {
22
- let key = "";
23
- return {
24
- get key() {
25
- return key;
26
- },
27
- setKey(value) {
28
- key = value;
29
- },
15
+ return wrapBaseDefinition({
30
16
  models,
31
17
  resolver,
32
18
  options: {
@@ -34,7 +20,7 @@ export function createViewFactory(config, logger) {
34
20
  ...options
35
21
  },
36
22
  logger
37
- };
23
+ });
38
24
  }
39
25
  return {
40
26
  from,
@@ -52,18 +52,21 @@ export interface ActionApiDefinition<MD extends ModelDefinitions, VD extends Vie
52
52
  request: ActionApiRequest<MD, VD>;
53
53
  commit?: ActionApiCommit<MD>;
54
54
  }
55
+ export interface ActionHandlerOptions {
56
+ payload?: unknown;
57
+ concurrent?: ActionConcurrent;
58
+ }
55
59
  export type ActionHandlerCallback<MD extends ModelDefinitions, VD extends ViewDefinitions<MD>, R = void> = (context: {
56
60
  model: StoreModel<MD>;
57
61
  view: StoreView<MD, VD>;
62
+ payload: unknown;
58
63
  }) => Promise<R>;
59
64
  export interface ActionHandlerDefinition<MD extends ModelDefinitions, VD extends ViewDefinitions<MD>, R = void> extends BaseDefinition {
60
65
  callback: ActionHandlerCallback<MD, VD, R>;
66
+ options?: ActionHandlerOptions;
61
67
  }
62
68
  export type ActionDefinition<MD extends ModelDefinitions, VD extends ViewDefinitions<MD>> = ActionApiDefinition<MD, VD> | ActionHandlerDefinition<MD, VD, unknown>;
63
69
  export type ActionDefinitions<MD extends ModelDefinitions, VD extends ViewDefinitions<MD>> = Record<string, ActionDefinition<MD, VD>>;
64
- export type StoreAction<MD extends ModelDefinitions, VD extends ViewDefinitions<MD>, AD extends ActionDefinitions<MD, VD>> = {
65
- [K in keyof AD]: ActionCall;
66
- };
67
70
  export interface ActionApiFactory<MD extends ModelDefinitions, VD extends ViewDefinitions<MD>> {
68
71
  (request: ActionApiRequest<MD, VD>, commit?: ActionApiCommit<MD>): ActionApiDefinition<MD, VD>;
69
72
  get(request: ActionApiRequestShortcut<MD, VD>, commit?: ActionApiCommit<MD>): ActionApiDefinition<MD, VD>;
@@ -74,7 +77,7 @@ export interface ActionApiFactory<MD extends ModelDefinitions, VD extends ViewDe
74
77
  delete(request: ActionApiRequestShortcut<MD, VD>, commit?: ActionApiCommit<MD>): ActionApiDefinition<MD, VD>;
75
78
  }
76
79
  export interface ActionHandlerFactory<MD extends ModelDefinitions, VD extends ViewDefinitions<MD>> {
77
- <R>(callback: ActionHandlerCallback<MD, VD, R>): ActionHandlerDefinition<MD, VD, R>;
80
+ <R>(callback: ActionHandlerCallback<MD, VD, R>, options?: ActionHandlerOptions): ActionHandlerDefinition<MD, VD, R>;
78
81
  }
79
82
  export interface ActionFactory<MD extends ModelDefinitions, VD extends ViewDefinitions<MD>> {
80
83
  api: ActionApiFactory<MD, VD>;
@@ -84,8 +87,9 @@ export interface ActionCallBindOptions {
84
87
  status?: Ref<ActionStatus>;
85
88
  error?: Ref<Error | null>;
86
89
  }
87
- export interface ActionCallCommitOptions {
88
- mode?: ModelOneMode | ModelManyMode;
90
+ export interface ActionCallBaseOptions {
91
+ concurrent?: ActionConcurrent;
92
+ bind?: ActionCallBindOptions;
89
93
  }
90
94
  export interface ActionResolvedApi {
91
95
  url: string;
@@ -100,7 +104,10 @@ export interface ActionCallTransformerOptions {
100
104
  request?: (api: ActionResolvedApi) => ActionResolvedApi;
101
105
  response?: (data: unknown) => unknown;
102
106
  }
103
- export interface ActionCallOptions {
107
+ export interface ActionCallCommitOptions {
108
+ mode?: ModelOneMode | ModelManyMode;
109
+ }
110
+ export interface ActionApiCallOptions extends ActionCallBaseOptions {
104
111
  params?: Record<string, string>;
105
112
  headers?: Record<string, string>;
106
113
  query?: Record<string, unknown>;
@@ -108,15 +115,30 @@ export interface ActionCallOptions {
108
115
  timeout?: number;
109
116
  signal?: AbortSignal;
110
117
  transformer?: ActionCallTransformerOptions;
111
- concurrent?: ActionConcurrent;
112
- bind?: ActionCallBindOptions;
113
118
  commit?: ActionCallCommitOptions;
114
119
  }
115
- export interface ActionCall<T = void> {
116
- (options?: ActionCallOptions): Promise<T>;
117
- readonly loading: ComputedRef<boolean>;
118
- readonly status: Readonly<Ref<ActionStatus>>;
120
+ export interface ActionResolvedHandler<MD extends ModelDefinitions, VD extends ViewDefinitions<MD>> {
121
+ model: StoreModel<MD>;
122
+ view: StoreView<MD, VD>;
123
+ payload: unknown;
124
+ }
125
+ export interface ActionHandlerCallOptions extends ActionCallBaseOptions {
126
+ payload?: unknown;
127
+ }
128
+ export type ActionCallOptions = ActionApiCallOptions | ActionHandlerCallOptions;
129
+ export interface ActionCallBase {
119
130
  readonly error: Readonly<Ref<Error | null>>;
120
- readonly data: DeepReadonly<T> | null;
131
+ readonly status: Readonly<Ref<ActionStatus>>;
132
+ readonly loading: ComputedRef<boolean>;
121
133
  reset: () => void;
122
134
  }
135
+ export interface ActionApiCall<T = void> extends ActionCallBase {
136
+ (options?: ActionApiCallOptions): Promise<T>;
137
+ }
138
+ export interface ActionHandlerCall<T = void> extends ActionCallBase {
139
+ (options?: ActionHandlerCallOptions): Promise<T>;
140
+ }
141
+ export type ActionCall<T = void> = ActionApiCall<T> | ActionHandlerCall<T>;
142
+ export type StoreAction<MD extends ModelDefinitions, VD extends ViewDefinitions<MD>, AD extends ActionDefinitions<MD, VD>> = {
143
+ [K in keyof AD]: AD[K] extends ActionApiDefinition<MD, VD> ? ActionApiCall : AD[K] extends ActionHandlerDefinition<MD, VD, infer R> ? ActionHandlerCall<R> : never;
144
+ };
@@ -1,11 +1,11 @@
1
1
  import type { ModelDefinitions, ModelFactory, StoreModel } from "./model.js";
2
2
  import type { ViewDefinitions, ViewFactory, StoreView } from "./view.js";
3
- import type { ActionDefinition, ActionDefinitions, ActionFactory, StoreAction } from "./action.js";
4
- export interface StoreConfig<MD extends ModelDefinitions, VD extends ViewDefinitions<MD>, _AD extends ActionDefinitions<MD, VD>> {
3
+ import type { ActionDefinitions, ActionFactory, StoreAction } from "./action.js";
4
+ export interface StoreConfig<MD extends ModelDefinitions, VD extends ViewDefinitions<MD>, AD extends ActionDefinitions<MD, VD>> {
5
5
  name: string;
6
6
  model: (factory: ModelFactory) => MD;
7
7
  view: (factory: ViewFactory<MD>) => VD;
8
- action: (factory: ActionFactory<MD, VD>) => Record<string, ActionDefinition<MD, VD>>;
8
+ action: (factory: ActionFactory<MD, VD>) => AD;
9
9
  }
10
10
  export interface Store<MD extends ModelDefinitions, VD extends ViewDefinitions<MD>, AD extends ActionDefinitions<MD, VD>> {
11
11
  model: StoreModel<MD>;
@@ -1,13 +1,13 @@
1
1
  import type { ComputedRef } from "vue";
2
2
  import type { BaseDefinition } from "./base.js";
3
3
  import type { ModelDefinitions, ModelDefinitionInfer, ModelDefinitionInferTuple } from "./model.js";
4
+ export interface RuntimeViewConfig {
5
+ clone?: ViewClone;
6
+ }
4
7
  export declare enum ViewClone {
5
8
  SHALLOW = "shallow",
6
9
  DEEP = "deep"
7
10
  }
8
- export interface RuntimeViewConfig {
9
- clone?: ViewClone;
10
- }
11
11
  export interface ViewDefinitionOptions {
12
12
  clone?: ViewClone;
13
13
  }
@@ -77,13 +77,22 @@ function resolveApiSignal(options, abortController) {
77
77
  }
78
78
  return abortController.signal;
79
79
  }
80
- function resolveCommitTarget(commit, model) {
80
+ function resolveHandlerPayload(definition, options) {
81
+ if (options?.payload !== void 0) {
82
+ return options.payload;
83
+ }
84
+ if (definition.options?.payload !== void 0) {
85
+ return definition.options.payload;
86
+ }
87
+ return void 0;
88
+ }
89
+ function resolveApiCommitTarget(commit, model) {
81
90
  if (commit) {
82
91
  return model[commit.model];
83
92
  }
84
93
  return void 0;
85
94
  }
86
- function resolveCommitMode(commit, options) {
95
+ function resolveApiCommitMode(commit, options) {
87
96
  if (commit) {
88
97
  if (options?.commit?.mode) {
89
98
  return options.commit.mode;
@@ -92,14 +101,17 @@ function resolveCommitMode(commit, options) {
92
101
  }
93
102
  return void 0;
94
103
  }
95
- function resolveCommitValue(commit, data) {
104
+ function resolveApiCommitValue(commit, data) {
96
105
  if (typeof commit.value === "function") {
97
106
  return commit.value(data);
98
107
  }
99
108
  return data;
100
109
  }
101
110
  function isApiDefinition(definition) {
102
- return "request" in definition;
111
+ return !("callback" in definition);
112
+ }
113
+ function isHandlerDefinition(definition) {
114
+ return "callback" in definition;
103
115
  }
104
116
  function resolveConcurrent(definition, options) {
105
117
  if (options?.concurrent) {
@@ -108,6 +120,9 @@ function resolveConcurrent(definition, options) {
108
120
  if (isApiDefinition(definition) && definition.request.concurrent) {
109
121
  return definition.request.concurrent;
110
122
  }
123
+ if (isHandlerDefinition(definition) && definition.options?.concurrent) {
124
+ return definition.options.concurrent;
125
+ }
111
126
  return ActionConcurrent.BLOCK;
112
127
  }
113
128
  async function executeApi(definition, api, options) {
@@ -147,15 +162,13 @@ async function executeApi(definition, api, options) {
147
162
  throw fetchError;
148
163
  }
149
164
  }
150
- async function executeHandler(definition, model, view) {
165
+ async function executeHandler(definition, handler) {
151
166
  try {
152
167
  definition.logger?.debug("Action handler phase", {
153
168
  action: definition.key
154
169
  });
155
- return await definition.callback({
156
- model,
157
- view
158
- });
170
+ const data = await definition.callback(handler);
171
+ return data;
159
172
  } catch (error) {
160
173
  if (isError(error, ActionApiError, ActionHandlerError)) {
161
174
  throw error;
@@ -186,7 +199,7 @@ function executeCommit(definition, target, mode, data) {
186
199
  if (!isEmptyRecord(target.aliases())) {
187
200
  data = resolveAliasInbound(data, target.aliases());
188
201
  }
189
- const value = resolveCommitValue(definition.commit, data);
202
+ const value = resolveApiCommitValue(definition.commit, data);
190
203
  target.commit(mode, value, definition.commit.options);
191
204
  } catch (error) {
192
205
  const commitError = toError(error, ActionCommitError);
@@ -204,7 +217,6 @@ export function createAction(definition, model, view) {
204
217
  });
205
218
  let currentController = null;
206
219
  let abortController = null;
207
- let globalData = null;
208
220
  const globalError = ref(null);
209
221
  const globalStatus = ref(ActionStatus.IDLE);
210
222
  const loading = computed(() => {
@@ -242,10 +254,10 @@ export function createAction(definition, model, view) {
242
254
  activeError.value = null;
243
255
  currentController = (async () => {
244
256
  try {
245
- let data;
257
+ let data = void 0;
246
258
  if (isApiDefinition(definition)) {
247
- const target = resolveCommitTarget(definition.commit, model);
248
- const mode = resolveCommitMode(definition.commit, options);
259
+ const target = resolveApiCommitTarget(definition.commit, model);
260
+ const mode = resolveApiCommitMode(definition.commit, options);
249
261
  const url = resolveApiUrl(definition, view, options);
250
262
  const method = resolveApiMethod(definition, view);
251
263
  const headers = resolveApiHeaders(definition, view, options);
@@ -267,10 +279,14 @@ export function createAction(definition, model, view) {
267
279
  options
268
280
  );
269
281
  executeCommit(definition, target, mode, data);
270
- } else {
271
- data = await executeHandler(definition, model, view);
282
+ } else if (isHandlerDefinition(definition)) {
283
+ const payload = resolveHandlerPayload(definition, options);
284
+ data = await executeHandler(definition, {
285
+ model,
286
+ view,
287
+ payload
288
+ });
272
289
  }
273
- globalData = data;
274
290
  activeStatus.value = ActionStatus.SUCCESS;
275
291
  definition.logger?.debug("Action success", {
276
292
  action: definition.key
@@ -288,22 +304,18 @@ export function createAction(definition, model, view) {
288
304
  return currentController;
289
305
  }
290
306
  const action = Object.assign(execute, {
291
- get loading() {
292
- return loading;
293
- },
294
307
  get error() {
295
308
  return readonly(globalError);
296
309
  },
297
310
  get status() {
298
311
  return readonly(globalStatus);
299
312
  },
300
- get data() {
301
- return globalData;
313
+ get loading() {
314
+ return loading;
302
315
  },
303
316
  reset() {
304
317
  globalError.value = null;
305
318
  globalStatus.value = ActionStatus.IDLE;
306
- globalData = null;
307
319
  }
308
320
  });
309
321
  return action;
@@ -1,4 +1,16 @@
1
+ import type { BaseDefinition } from "../types/base.js";
2
+ export declare function wrapBaseDefinition<T extends Omit<BaseDefinition, "key" | "setKey">>(definition: T): T & BaseDefinition;
1
3
  export declare function trimStart(value: string, char: string): string;
2
4
  export declare function trimEnd(value: string, char: string): string;
5
+ export declare function isObject(value: unknown): value is object;
3
6
  export declare function isPlainObject(value: unknown): value is Record<string, unknown>;
4
7
  export declare function isEmptyRecord(record: Record<string, unknown> | undefined): record is undefined;
8
+ type ReferenceProxy<T> = {
9
+ value: T;
10
+ } & Record<string | symbol, unknown>;
11
+ export declare function toReactiveProxy<T>(reference: {
12
+ value: T;
13
+ }): ReferenceProxy<T>;
14
+ export declare function debounce<T extends (...args: any[]) => any>(callback: T, delay: number): T;
15
+ export declare function throttle<T extends (...args: any[]) => any>(callback: T, delay: number): T;
16
+ export {};
@@ -1,11 +1,33 @@
1
+ export function wrapBaseDefinition(definition) {
2
+ let key = "";
3
+ return Object.defineProperties(definition, {
4
+ key: {
5
+ get() {
6
+ return key;
7
+ },
8
+ enumerable: true,
9
+ configurable: true
10
+ },
11
+ setKey: {
12
+ value(value) {
13
+ key = value;
14
+ },
15
+ enumerable: true,
16
+ configurable: true
17
+ }
18
+ });
19
+ }
1
20
  export function trimStart(value, char) {
2
21
  return value.replace(new RegExp(`^${char}+`), "");
3
22
  }
4
23
  export function trimEnd(value, char) {
5
24
  return value.replace(new RegExp(`${char}+$`), "");
6
25
  }
26
+ export function isObject(value) {
27
+ return value != null && typeof value === "object";
28
+ }
7
29
  export function isPlainObject(value) {
8
- if (!value || typeof value !== "object") {
30
+ if (!isObject(value)) {
9
31
  return false;
10
32
  }
11
33
  if (Array.isArray(value)) {
@@ -22,3 +44,85 @@ export function isEmptyRecord(record) {
22
44
  }
23
45
  return false;
24
46
  }
47
+ export function toReactiveProxy(reference) {
48
+ function get(_target, prop) {
49
+ if (prop === "value") {
50
+ return reference.value;
51
+ }
52
+ if (!isObject(reference.value)) {
53
+ return void 0;
54
+ }
55
+ return reference.value[prop];
56
+ }
57
+ function has(_target, prop) {
58
+ if (prop === "value") {
59
+ return true;
60
+ }
61
+ if (!isObject(reference.value)) {
62
+ return false;
63
+ }
64
+ return prop in reference.value;
65
+ }
66
+ function ownKeys() {
67
+ if (!isObject(reference.value)) {
68
+ return [];
69
+ }
70
+ return Reflect.ownKeys(reference.value);
71
+ }
72
+ function getOwnPropertyDescriptor(_target, prop) {
73
+ if (!isObject(reference.value) || !(prop in reference.value)) {
74
+ return void 0;
75
+ }
76
+ return {
77
+ configurable: true,
78
+ enumerable: true,
79
+ value: reference.value[prop]
80
+ };
81
+ }
82
+ return new Proxy(
83
+ {},
84
+ {
85
+ get,
86
+ has,
87
+ ownKeys,
88
+ getOwnPropertyDescriptor
89
+ }
90
+ );
91
+ }
92
+ export function debounce(callback, delay) {
93
+ let timer = null;
94
+ return (...args) => {
95
+ if (timer) {
96
+ clearTimeout(timer);
97
+ }
98
+ timer = setTimeout(() => {
99
+ timer = null;
100
+ callback(...args);
101
+ }, delay);
102
+ };
103
+ }
104
+ export function throttle(callback, delay) {
105
+ let lastCall = 0;
106
+ let timer = null;
107
+ return (...args) => {
108
+ const now = Date.now();
109
+ const remaining = delay - (now - lastCall);
110
+ if (remaining <= 0) {
111
+ if (timer) {
112
+ clearTimeout(timer);
113
+ timer = null;
114
+ }
115
+ lastCall = now;
116
+ callback(...args);
117
+ return;
118
+ }
119
+ if (timer) {
120
+ return;
121
+ }
122
+ timer = setTimeout(() => {
123
+ lastCall = Date.now();
124
+ timer = null;
125
+ callback(...args);
126
+ }, remaining);
127
+ };
128
+ }
@@ -7,7 +7,12 @@ export type { ModelOneCommitOptions, ModelManyCommitOptions } from "./core/types
7
7
  export { ViewClone } from "./core/types/view.js";
8
8
  export type { ViewDefinitionOptions } from "./core/types/view.js";
9
9
  export { ActionStatus, ActionConcurrent, ActionApiMethod } from "./core/types/action.js";
10
- export type { ActionCall, ActionCallOptions, ActionCallTransformerOptions, ActionCallBindOptions, ActionCallCommitOptions, ActionResolvedApi, } from "./core/types/action.js";
10
+ export type { ActionCall, ActionApiCall, ActionHandlerCall, ActionCallOptions, ActionCallBaseOptions, ActionApiCallOptions, ActionHandlerCallOptions, ActionCallTransformerOptions, ActionCallBindOptions, ActionCallCommitOptions, ActionHandlerOptions, ActionResolvedApi, } from "./core/types/action.js";
11
11
  export { ActionApiError, ActionHandlerError, ActionCommitError, ActionConcurrentError } from "./core/utils/error.js";
12
- export { useIsolatedActionStatus, useIsolatedActionError } from "./composables/action.js";
12
+ export { useIsolatedActionStatus, useIsolatedActionError, useStoreAction } from "./composables/action.js";
13
+ export type { UseStoreActionOptions, UseStoreAction } from "./composables/action.js";
14
+ export { useStoreModel } from "./composables/model.js";
15
+ export type { UseStoreModelOptions, UseStoreModel } from "./composables/model.js";
16
+ export { useStoreView } from "./composables/view.js";
17
+ export type { UseStoreViewOptions, UseStoreViewProxy, UseStoreViewComputed, UseStoreViewData, UseStoreViewTrackOptions, } from "./composables/view.js";
13
18
  export type { RuntimeConfig } from "./config.js";
@@ -4,4 +4,6 @@ export { ModelKind, ModelOneMode, ModelManyMode } from "./core/types/model.js";
4
4
  export { ViewClone } from "./core/types/view.js";
5
5
  export { ActionStatus, ActionConcurrent, ActionApiMethod } from "./core/types/action.js";
6
6
  export { ActionApiError, ActionHandlerError, ActionCommitError, ActionConcurrentError } from "./core/utils/error.js";
7
- export { useIsolatedActionStatus, useIsolatedActionError } from "./composables/action.js";
7
+ export { useIsolatedActionStatus, useIsolatedActionError, useStoreAction } from "./composables/action.js";
8
+ export { useStoreModel } from "./composables/model.js";
9
+ export { useStoreView } from "./composables/view.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diphyx/harlemify",
3
- "version": "5.0.0",
3
+ "version": "5.2.0",
4
4
  "description": "API state management for Nuxt powered by Harlem",
5
5
  "keywords": [
6
6
  "nuxt",