@diphyx/harlemify 4.0.1 → 5.0.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.
Files changed (42) hide show
  1. package/README.md +15 -19
  2. package/dist/module.d.mts +5 -0
  3. package/dist/module.d.ts +5 -0
  4. package/dist/module.json +1 -1
  5. package/dist/runtime/composables/action.d.ts +2 -2
  6. package/dist/runtime/core/layers/action.d.ts +3 -2
  7. package/dist/runtime/core/layers/action.js +37 -69
  8. package/dist/runtime/core/layers/model.js +14 -0
  9. package/dist/runtime/core/layers/shape.d.ts +2 -2
  10. package/dist/runtime/core/layers/shape.js +3 -2
  11. package/dist/runtime/core/layers/view.d.ts +2 -2
  12. package/dist/runtime/core/layers/view.js +27 -5
  13. package/dist/runtime/core/store.d.ts +5 -23
  14. package/dist/runtime/core/store.js +8 -28
  15. package/dist/runtime/core/types/action.d.ts +78 -119
  16. package/dist/runtime/core/types/action.js +0 -16
  17. package/dist/runtime/core/types/base.d.ts +6 -0
  18. package/dist/runtime/core/types/base.js +0 -0
  19. package/dist/runtime/core/types/model.d.ts +47 -32
  20. package/dist/runtime/core/types/model.js +14 -0
  21. package/dist/runtime/core/types/shape.d.ts +30 -5
  22. package/dist/runtime/core/types/store.d.ts +14 -0
  23. package/dist/runtime/core/types/store.js +0 -0
  24. package/dist/runtime/core/types/view.d.ts +35 -24
  25. package/dist/runtime/core/types/view.js +5 -0
  26. package/dist/runtime/core/utils/action.d.ts +4 -4
  27. package/dist/runtime/core/utils/action.js +219 -203
  28. package/dist/runtime/core/utils/base.d.ts +4 -0
  29. package/dist/runtime/core/utils/base.js +24 -0
  30. package/dist/runtime/core/utils/error.d.ts +21 -0
  31. package/dist/runtime/core/utils/error.js +36 -0
  32. package/dist/runtime/core/utils/model.d.ts +3 -11
  33. package/dist/runtime/core/utils/model.js +104 -110
  34. package/dist/runtime/core/utils/shape.d.ts +6 -3
  35. package/dist/runtime/core/utils/shape.js +218 -14
  36. package/dist/runtime/core/utils/store.d.ts +8 -0
  37. package/dist/runtime/core/utils/store.js +35 -0
  38. package/dist/runtime/core/utils/view.d.ts +3 -4
  39. package/dist/runtime/core/utils/view.js +35 -14
  40. package/dist/runtime/index.d.ts +8 -4
  41. package/dist/runtime/index.js +4 -9
  42. package/package.json +2 -1
@@ -3,263 +3,279 @@ import { ref, computed, readonly, toValue, nextTick } from "vue";
3
3
  import {
4
4
  ActionApiMethod,
5
5
  ActionStatus,
6
- ActionConcurrent,
7
- DEFINITION
6
+ ActionConcurrent
8
7
  } from "../types/action.js";
9
- import { createCommitter, executeCommit } from "./model.js";
10
- class ApiError extends Error {
11
- name = "ActionApiError";
12
- constructor(message, options) {
13
- super(message);
14
- this.status = options?.status;
15
- this.statusText = options?.statusText;
16
- this.data = options?.data;
8
+ import { trimStart, trimEnd, isEmptyRecord, isPlainObject } from "./base.js";
9
+ import {
10
+ ActionApiError,
11
+ ActionHandlerError,
12
+ ActionCommitError,
13
+ ActionConcurrentError,
14
+ isError,
15
+ toError
16
+ } from "./error.js";
17
+ import { resolveAliasInbound, resolveAliasOutbound } from "./shape.js";
18
+ function resolveValue(value, view, fallback) {
19
+ if (typeof value === "function") {
20
+ return value(view) || fallback;
17
21
  }
22
+ return toValue(value) || fallback;
18
23
  }
19
- class HandleError extends Error {
20
- name = "ActionHandleError";
21
- constructor(cause) {
22
- super(cause.message);
23
- this.cause = cause;
24
+ function resolveApiUrl(definition, view, options) {
25
+ const endpoint = trimEnd(definition.request.endpoint ?? "", "/");
26
+ let path = resolveValue(definition.request.url, view);
27
+ if (options?.params) {
28
+ for (const [key, value] of Object.entries(options.params)) {
29
+ path = path.replace(`:${key}`, encodeURIComponent(value));
30
+ }
24
31
  }
25
- }
26
- class CommitError extends Error {
27
- name = "ActionCommitError";
28
- constructor(cause) {
29
- super(cause.message);
30
- this.cause = cause;
32
+ if (endpoint) {
33
+ return `${endpoint}/${trimStart(path, "/")}`;
31
34
  }
35
+ return path;
36
+ }
37
+ function resolveApiHeaders(definition, view, options) {
38
+ const initial = resolveValue(definition.request.headers, view, {});
39
+ const custom = options?.headers ?? {};
40
+ return defu(custom, initial);
32
41
  }
33
- class ConcurrentError extends Error {
34
- name = "ActionConcurrentError";
35
- constructor() {
36
- super("Action is already pending");
42
+ function resolveApiQuery(definition, view, options) {
43
+ const initial = resolveValue(definition.request.query, view, {});
44
+ const custom = options?.query ?? {};
45
+ return defu(custom, initial);
46
+ }
47
+ function resolveApiBody(definition, view, target, options) {
48
+ if (definition.request.method === ActionApiMethod.GET || definition.request.method === ActionApiMethod.HEAD) {
49
+ return void 0;
37
50
  }
51
+ const initial = resolveValue(definition.request.body, view, {});
52
+ const custom = options?.body ?? {};
53
+ const body = defu(custom, initial);
54
+ if (!isPlainObject(body)) {
55
+ return body;
56
+ }
57
+ if (!isEmptyRecord(target?.aliases())) {
58
+ return resolveAliasOutbound(body, target.aliases());
59
+ }
60
+ return body;
38
61
  }
39
- function createApiError(message, options) {
40
- return new ApiError(message, options);
62
+ function resolveApiMethod(definition, view) {
63
+ return resolveValue(definition.request.method, view, ActionApiMethod.GET);
41
64
  }
42
- function createHandleError(cause) {
43
- return new HandleError(cause);
65
+ function resolveApiTimeout(definition, view, options) {
66
+ if (options?.timeout) {
67
+ return options.timeout;
68
+ }
69
+ if (definition.request.timeout) {
70
+ return resolveValue(definition.request.timeout, view);
71
+ }
72
+ return void 0;
44
73
  }
45
- function createCommitError(cause) {
46
- return new CommitError(cause);
74
+ function resolveApiSignal(options, abortController) {
75
+ if (options?.signal) {
76
+ return options.signal;
77
+ }
78
+ return abortController.signal;
47
79
  }
48
- function createConcurrentError() {
49
- return new ConcurrentError();
80
+ function resolveCommitTarget(commit, model) {
81
+ if (commit) {
82
+ return model[commit.model];
83
+ }
84
+ return void 0;
50
85
  }
51
- export function buildCommitMethod(definition) {
52
- function commit(model, mode, value, options) {
53
- return {
54
- [DEFINITION]: {
55
- ...definition,
56
- commit: {
57
- model,
58
- mode,
59
- value,
60
- options
61
- }
62
- }
63
- };
86
+ function resolveCommitMode(commit, options) {
87
+ if (commit) {
88
+ if (options?.commit?.mode) {
89
+ return options.commit.mode;
90
+ }
91
+ return commit.mode;
64
92
  }
65
- return commit;
93
+ return void 0;
66
94
  }
67
- function resolveApiValue(value, view, fallback) {
68
- if (typeof value === "function") {
69
- const handler = value;
70
- return handler(view) || fallback;
95
+ function resolveCommitValue(commit, data) {
96
+ if (typeof commit.value === "function") {
97
+ return commit.value(data);
71
98
  }
72
- return toValue(value) || fallback;
99
+ return data;
73
100
  }
74
- function resolveApiUrl(definition, view) {
75
- const endpoint = (definition.endpoint ?? "").replace(/\/+$/, "");
76
- const path = resolveApiValue(definition.url, view);
77
- if (endpoint) {
78
- return `${endpoint}/${path.replace(/^\/+/, "")}`;
101
+ function isApiDefinition(definition) {
102
+ return "request" in definition;
103
+ }
104
+ function resolveConcurrent(definition, options) {
105
+ if (options?.concurrent) {
106
+ return options.concurrent;
79
107
  }
80
- return path;
108
+ if (isApiDefinition(definition) && definition.request.concurrent) {
109
+ return definition.request.concurrent;
110
+ }
111
+ return ActionConcurrent.BLOCK;
81
112
  }
82
- function resolveApiHeaders(definition, view, payload) {
83
- const initial = resolveApiValue(definition.headers, view, {});
84
- const custom = resolveApiValue(payload?.headers, view, {});
85
- return defu(custom, initial);
113
+ async function executeApi(definition, api, options) {
114
+ try {
115
+ definition.logger?.debug("Action API request", {
116
+ action: definition.key,
117
+ method: api.method,
118
+ url: api.url
119
+ });
120
+ if (options?.transformer?.request) {
121
+ api = options.transformer.request(api);
122
+ }
123
+ const response = await $fetch(api.url, {
124
+ method: api.method,
125
+ headers: api.headers,
126
+ query: api.query,
127
+ body: api.body,
128
+ timeout: api.timeout,
129
+ signal: api.signal,
130
+ responseType: "json"
131
+ });
132
+ definition.logger?.debug("Action API response received", {
133
+ action: definition.key,
134
+ method: api.method,
135
+ url: api.url
136
+ });
137
+ if (options?.transformer?.response) {
138
+ return options.transformer.response(response);
139
+ }
140
+ return response;
141
+ } catch (error) {
142
+ const fetchError = toError(error, ActionApiError);
143
+ definition.logger?.error("Action API error", {
144
+ action: definition.key,
145
+ error: fetchError.message
146
+ });
147
+ throw fetchError;
148
+ }
86
149
  }
87
- function resolveApiQuery(definition, view, payload) {
88
- const initial = resolveApiValue(definition.query, view, {});
89
- const custom = resolveApiValue(payload?.query, view, {});
90
- return defu(custom, initial);
150
+ async function executeHandler(definition, model, view) {
151
+ try {
152
+ definition.logger?.debug("Action handler phase", {
153
+ action: definition.key
154
+ });
155
+ return await definition.callback({
156
+ model,
157
+ view
158
+ });
159
+ } catch (error) {
160
+ if (isError(error, ActionApiError, ActionHandlerError)) {
161
+ throw error;
162
+ }
163
+ const handlerError = toError(error, ActionHandlerError);
164
+ definition.logger?.error("Action handler error", {
165
+ action: definition.key,
166
+ error: handlerError.message
167
+ });
168
+ throw handlerError;
169
+ }
91
170
  }
92
- function resolveApiBody(definition, view, payload) {
93
- if (definition.method === ActionApiMethod.GET || definition.method === ActionApiMethod.HEAD) {
94
- return void 0;
171
+ function executeCommit(definition, target, mode, data) {
172
+ if (!definition.commit) {
173
+ return;
174
+ }
175
+ if (!target || !mode) {
176
+ throw new ActionCommitError({
177
+ message: `Model "${definition.commit.model}" is not defined`
178
+ });
179
+ }
180
+ try {
181
+ definition.logger?.debug("Action commit phase", {
182
+ action: definition.key,
183
+ target,
184
+ mode
185
+ });
186
+ if (!isEmptyRecord(target.aliases())) {
187
+ data = resolveAliasInbound(data, target.aliases());
188
+ }
189
+ const value = resolveCommitValue(definition.commit, data);
190
+ target.commit(mode, value, definition.commit.options);
191
+ } catch (error) {
192
+ const commitError = toError(error, ActionCommitError);
193
+ definition.logger?.error("Action commit error", {
194
+ action: definition.key,
195
+ error: commitError.message
196
+ });
197
+ throw commitError;
95
198
  }
96
- const initial = resolveApiValue(definition.body, view, {});
97
- const custom = resolveApiValue(payload?.body, view, {});
98
- return defu(custom, initial);
99
199
  }
100
- export function createAction(definition, mutations, view, key) {
200
+ export function createAction(definition, model, view) {
201
+ definition.logger?.debug("Registering action", {
202
+ action: definition.key,
203
+ type: isApiDefinition(definition) ? "api" : "handler"
204
+ });
205
+ let currentController = null;
206
+ let abortController = null;
207
+ let globalData = null;
101
208
  const globalError = ref(null);
102
209
  const globalStatus = ref(ActionStatus.IDLE);
103
210
  const loading = computed(() => {
104
211
  return globalStatus.value === ActionStatus.PENDING;
105
212
  });
106
- let data = null;
107
- let currentController = null;
108
- let abortController = null;
109
- async function execute(payload) {
213
+ async function execute(options) {
110
214
  await nextTick();
215
+ const concurrent = resolveConcurrent(definition, options);
111
216
  if (loading.value) {
112
- const concurrent = payload?.concurrent ?? definition.api?.concurrent ?? ActionConcurrent.BLOCK;
113
217
  switch (concurrent) {
114
218
  case ActionConcurrent.BLOCK: {
115
219
  definition.logger?.error("Action blocked by concurrent guard", {
116
- action: key
220
+ action: definition.key
117
221
  });
118
- throw createConcurrentError();
222
+ throw new ActionConcurrentError();
119
223
  }
120
224
  case ActionConcurrent.SKIP: {
121
225
  definition.logger?.warn("Action skipped by concurrent guard", {
122
- action: key
226
+ action: definition.key
123
227
  });
124
228
  return currentController;
125
229
  }
126
230
  case ActionConcurrent.CANCEL: {
127
231
  definition.logger?.warn("Action cancelling previous execution", {
128
- action: key
232
+ action: definition.key
129
233
  });
130
234
  abortController?.abort();
131
235
  }
132
236
  }
133
237
  }
134
238
  abortController = new AbortController();
135
- const activeStatus = payload?.bind?.status ?? globalStatus;
136
- const activeError = payload?.bind?.error ?? globalError;
239
+ const activeStatus = options?.bind?.status ?? globalStatus;
240
+ const activeError = options?.bind?.error ?? globalError;
137
241
  activeStatus.value = ActionStatus.PENDING;
138
242
  activeError.value = null;
139
243
  currentController = (async () => {
140
244
  try {
141
- let result;
142
- const committer = createCommitter(mutations);
143
- if (definition.api) {
144
- let response;
145
- try {
146
- const apiPayload = {
147
- ...payload,
148
- signal: payload?.signal ?? abortController.signal
149
- };
150
- const url = resolveApiUrl(definition.api, view);
151
- definition.logger?.debug("Action API request", {
152
- action: key,
153
- method: definition.api.method,
154
- url
155
- });
156
- response = await $fetch(url, {
157
- method: definition.api.method,
158
- headers: resolveApiHeaders(definition.api, view, apiPayload),
159
- query: resolveApiQuery(definition.api, view, apiPayload),
160
- body: resolveApiBody(definition.api, view, apiPayload),
161
- timeout: apiPayload?.timeout ?? definition.api.timeout,
162
- signal: apiPayload?.signal
163
- });
164
- definition.logger?.debug("Action API response received", {
165
- action: key,
166
- method: definition.api.method,
167
- url
168
- });
169
- } catch (error) {
170
- const errorMessage = error?.message ?? "API request failed";
171
- const errorOptions = {
172
- status: error?.status ?? error?.response?.status,
173
- statusText: error?.statusText ?? error?.response?.statusText,
174
- data: error?.data ?? error?.response?._data
175
- };
176
- definition.logger?.error("Action API error", {
177
- action: key,
178
- error: errorMessage
179
- });
180
- throw createApiError(errorMessage, errorOptions);
181
- }
182
- if (definition.handle) {
183
- const handler = definition.handle;
184
- try {
185
- definition.logger?.debug("Action handle phase", {
186
- action: key
187
- });
188
- result = await handler({
189
- view,
190
- commit: committer,
191
- async api() {
192
- return response;
193
- }
194
- });
195
- } catch (handleError) {
196
- if (handleError instanceof ApiError || handleError instanceof HandleError) {
197
- throw handleError;
198
- }
199
- definition.logger?.error("Action handle error", {
200
- action: key,
201
- error: handleError?.message
202
- });
203
- throw createHandleError(handleError);
204
- }
205
- } else {
206
- result = response;
207
- }
208
- } else if (definition.handle) {
209
- const handler = definition.handle;
210
- try {
211
- definition.logger?.debug("Action handle phase", {
212
- action: key
213
- });
214
- result = await handler({
215
- view,
216
- commit: committer
217
- });
218
- } catch (handleError) {
219
- if (handleError instanceof HandleError) {
220
- throw handleError;
221
- }
222
- definition.logger?.error("Action handle error", {
223
- action: key,
224
- error: handleError?.message
225
- });
226
- throw createHandleError(handleError);
227
- }
245
+ let data;
246
+ if (isApiDefinition(definition)) {
247
+ const target = resolveCommitTarget(definition.commit, model);
248
+ const mode = resolveCommitMode(definition.commit, options);
249
+ const url = resolveApiUrl(definition, view, options);
250
+ const method = resolveApiMethod(definition, view);
251
+ const headers = resolveApiHeaders(definition, view, options);
252
+ const query = resolveApiQuery(definition, view, options);
253
+ const body = resolveApiBody(definition, view, target, options);
254
+ const timeout = resolveApiTimeout(definition, view, options);
255
+ const signal = resolveApiSignal(options, abortController);
256
+ data = await executeApi(
257
+ definition,
258
+ {
259
+ url,
260
+ method,
261
+ headers,
262
+ query,
263
+ body,
264
+ timeout,
265
+ signal
266
+ },
267
+ options
268
+ );
269
+ executeCommit(definition, target, mode, data);
228
270
  } else {
229
- result = void 0;
230
- }
231
- if (payload?.transformer) {
232
- result = payload.transformer(result);
233
- }
234
- if (definition.commit) {
235
- try {
236
- definition.logger?.debug("Action commit phase", {
237
- action: key,
238
- model: definition.commit.model,
239
- mode: definition.commit.mode
240
- });
241
- executeCommit(
242
- {
243
- ...definition.commit,
244
- mode: payload?.commit?.mode ?? definition.commit.mode
245
- },
246
- mutations,
247
- result
248
- );
249
- } catch (commitError) {
250
- definition.logger?.error("Action commit error", {
251
- action: key,
252
- error: commitError?.message
253
- });
254
- throw createCommitError(commitError);
255
- }
271
+ data = await executeHandler(definition, model, view);
256
272
  }
257
- data = result;
273
+ globalData = data;
258
274
  activeStatus.value = ActionStatus.SUCCESS;
259
275
  definition.logger?.debug("Action success", {
260
- action: key
276
+ action: definition.key
261
277
  });
262
- return result;
278
+ return data;
263
279
  } catch (actionError) {
264
280
  activeError.value = actionError;
265
281
  activeStatus.value = ActionStatus.ERROR;
@@ -282,12 +298,12 @@ export function createAction(definition, mutations, view, key) {
282
298
  return readonly(globalStatus);
283
299
  },
284
300
  get data() {
285
- return data;
301
+ return globalData;
286
302
  },
287
303
  reset() {
288
304
  globalError.value = null;
289
305
  globalStatus.value = ActionStatus.IDLE;
290
- data = null;
306
+ globalData = null;
291
307
  }
292
308
  });
293
309
  return action;
@@ -0,0 +1,4 @@
1
+ export declare function trimStart(value: string, char: string): string;
2
+ export declare function trimEnd(value: string, char: string): string;
3
+ export declare function isPlainObject(value: unknown): value is Record<string, unknown>;
4
+ export declare function isEmptyRecord(record: Record<string, unknown> | undefined): record is undefined;
@@ -0,0 +1,24 @@
1
+ export function trimStart(value, char) {
2
+ return value.replace(new RegExp(`^${char}+`), "");
3
+ }
4
+ export function trimEnd(value, char) {
5
+ return value.replace(new RegExp(`${char}+$`), "");
6
+ }
7
+ export function isPlainObject(value) {
8
+ if (!value || typeof value !== "object") {
9
+ return false;
10
+ }
11
+ if (Array.isArray(value)) {
12
+ return false;
13
+ }
14
+ return true;
15
+ }
16
+ export function isEmptyRecord(record) {
17
+ if (!record) {
18
+ return true;
19
+ }
20
+ if (Object.keys(record).length === 0) {
21
+ return true;
22
+ }
23
+ return false;
24
+ }
@@ -0,0 +1,21 @@
1
+ export declare class ActionApiError extends Error {
2
+ name: "ActionApiError";
3
+ status: number;
4
+ statusText: string;
5
+ data: unknown;
6
+ constructor(source: any);
7
+ }
8
+ export declare class ActionHandlerError extends Error {
9
+ name: "ActionHandlerError";
10
+ constructor(source: any);
11
+ }
12
+ export declare class ActionCommitError extends Error {
13
+ name: "ActionCommitError";
14
+ constructor(source: any);
15
+ }
16
+ export declare class ActionConcurrentError extends Error {
17
+ name: "ActionConcurrentError";
18
+ constructor();
19
+ }
20
+ export declare function isError(error: unknown, ...types: (abstract new (...args: never[]) => Error)[]): error is Error;
21
+ export declare function toError<T extends Error = Error>(error: unknown, ErrorType?: new (source: unknown) => T): T;
@@ -0,0 +1,36 @@
1
+ export class ActionApiError extends Error {
2
+ name = "ActionApiError";
3
+ constructor(source) {
4
+ super(source.message || "API request failed");
5
+ this.status = source?.status ?? source?.response?.status ?? 500;
6
+ this.statusText = source?.statusText ?? source?.response?.statusText ?? "Internal Server Error";
7
+ this.data = source?.data ?? source?.response?._data ?? null;
8
+ }
9
+ }
10
+ export class ActionHandlerError extends Error {
11
+ name = "ActionHandlerError";
12
+ constructor(source) {
13
+ super(source.message || "Action handler failed");
14
+ }
15
+ }
16
+ export class ActionCommitError extends Error {
17
+ name = "ActionCommitError";
18
+ constructor(source) {
19
+ super(source.message || "Action commit failed");
20
+ }
21
+ }
22
+ export class ActionConcurrentError extends Error {
23
+ name = "ActionConcurrentError";
24
+ constructor() {
25
+ super("Action is already pending");
26
+ }
27
+ }
28
+ export function isError(error, ...types) {
29
+ return types.some((ErrorType) => error instanceof ErrorType);
30
+ }
31
+ export function toError(error, ErrorType) {
32
+ if (ErrorType) {
33
+ return new ErrorType(error);
34
+ }
35
+ return error instanceof Error ? error : new Error(String(error));
36
+ }
@@ -1,12 +1,4 @@
1
1
  import type { Store as SourceStore, BaseState } from "@harlem/core";
2
- import { type Model, type ModelStateOf, type MutationsOneOptions, type MutationsManyOptions, type Mutations } from "../types/model.js";
3
- import { type ActionCommitter, ActionOneMode, ActionManyMode } from "../types/action.js";
4
- export declare function initializeState<M extends Model>(model: M): ModelStateOf<M>;
5
- export declare function createMutations<M extends Model>(source: SourceStore<BaseState>, model: M): Mutations<M>;
6
- export declare function executeCommit<M extends Model>(definition: {
7
- model: keyof M;
8
- mode: ActionOneMode | ActionManyMode;
9
- value?: unknown;
10
- options?: MutationsOneOptions | MutationsManyOptions;
11
- }, mutations: Mutations<M>, result?: unknown): void;
12
- export declare function createCommitter<M extends Model>(mutations: Mutations<M>): ActionCommitter<M>;
2
+ import type { Shape } from "../types/shape.js";
3
+ import { type ModelDefinition, type ModelCall } from "../types/model.js";
4
+ export declare function createModel<S extends Shape>(definition: ModelDefinition<S>, source: SourceStore<BaseState>): ModelCall<S>;