@process.co/element-types 0.0.18 → 0.0.20

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.
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Compile-only: embedded `defineApp` in signal `props` (not emitted).
3
+ */
4
+ import {
5
+ defineApp,
6
+ defineSignal,
7
+ type DeriveEmbeddedAppPropInstance,
8
+ type DeriveSignalInstance,
9
+ } from './index';
10
+
11
+ const httpApp = defineApp({
12
+ type: 'app',
13
+ app: 'http',
14
+ noAuth: true,
15
+ propDefinitions: {
16
+ httpRequest: {
17
+ type: 'http_request',
18
+ label: 'HTTP Request Configuration',
19
+ },
20
+ },
21
+ } as const);
22
+
23
+ const _signal = defineSignal({
24
+ type: 'signal',
25
+ props: {
26
+ httpInterface: { type: '$.interface.http' },
27
+ http: httpApp,
28
+ },
29
+ async run() {
30
+ this.http.httpRequest.execute;
31
+ this.httpInterface.deferHttpResponse;
32
+ },
33
+ });
34
+
35
+ export type _HttpOnThis = DeriveSignalInstance<typeof _signal>['http'];
36
+ export type _HttpRuntime = DeriveEmbeddedAppPropInstance<typeof httpApp>;
37
+ type _assertHttpProp = _HttpOnThis extends _HttpRuntime ? true : false;
38
+ const _httpPropCheck: _assertHttpProp = true;
39
+ type _assertNotDefinition = 'type' extends keyof _HttpOnThis ? false : true;
40
+ const _notDefinitionCheck: _assertNotDefinition = true;
41
+ type _assertHttpRequest = _HttpOnThis['httpRequest'] extends { execute: () => Promise<unknown> }
42
+ ? true
43
+ : false;
44
+ const _httpRequestCheck: _assertHttpRequest = true;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Compile-only checks for defineSignal hook vs run `this` (not emitted).
3
+ */
4
+ import {
5
+ defineSignal,
6
+ type SignalRunHostServices,
7
+ type SignalSaveHostParameters,
8
+ } from './index';
9
+
10
+ const _webhook = defineSignal({
11
+ type: 'signal',
12
+ props: {
13
+ httpInterface: { type: '$.interface.http' },
14
+ cacheMaxAge: { type: '$.interface.duration', default: 86400 },
15
+ authType: { type: 'string', default: 'none' },
16
+ },
17
+ hooks: {
18
+ async save({ $ }) {
19
+ const _draft: boolean = $.isDraft;
20
+ void _draft;
21
+ await $.http.configureResponseCaching({
22
+ maxAge: this.cacheMaxAge,
23
+ varyBy: '*',
24
+ });
25
+ // @ts-expect-error — `run` host `$` is not available in hooks.save
26
+ $.enforceSchema;
27
+ // @ts-expect-error — `$emit` is only on `run` `this`, not hooks
28
+ this.$emit;
29
+ // @ts-expect-error — no runtime HTTP interface on hook `this`
30
+ await this.httpInterface.respond({ status: 200, body: {} });
31
+ },
32
+ },
33
+ async run({ $, event }) {
34
+ void event;
35
+ $.enforceSchema;
36
+ // @ts-expect-error — save-only host `$` is not available in run
37
+ $.http;
38
+ await this.httpInterface.deferHttpResponse(30_000);
39
+ },
40
+ });
41
+
42
+ export type _webhookType = typeof _webhook;
43
+
44
+ type _saveParams = Parameters<NonNullable<typeof _webhook.hooks>['save']>[0];
45
+ type _saveDollar = _saveParams['$'];
46
+ type _assertSaveDollar = _saveDollar extends SignalSaveHostParameters['$'] ? true : false;
47
+ const _saveDollarCheck: _assertSaveDollar = true;
48
+ type _saveDollarNotRun = SignalRunHostServices extends _saveDollar ? false : true;
49
+ const _saveDollarNotRunCheck: _saveDollarNotRun = true;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * HTTP response replay cache — policy at save, vary key at runtime.
3
+ */
4
+
5
+ export type HttpRequestCacheMode = 'cache-only' | 'stale-while-revalidate';
6
+
7
+ /** Materialized at save → elementData.$httpRequestCachePolicy */
8
+ export type HttpRequestCachePolicy = {
9
+ mode: HttpRequestCacheMode;
10
+ ttlSeconds: number;
11
+ refreshAfterSeconds?: number;
12
+ vary: HttpRequestCacheVary;
13
+ /** When true, webhook loads trigger so author can call setRequestVaryKey. */
14
+ needsRuntimeVaryKey?: boolean;
15
+ };
16
+
17
+ export type HttpRequestCacheVary = {
18
+ headers?: Array<{ name: string; lowerCase?: boolean }>;
19
+ clientIp?: boolean;
20
+ pathTemplate?: string;
21
+ query?: string[];
22
+ body?: BodyVaryProjection;
23
+ };
24
+
25
+ /** Boolean leaves select fields; nested objects recurse. */
26
+ export type BodyVaryProjection = {
27
+ [key: string]: boolean | BodyVaryProjection;
28
+ };
29
+
30
+ /** Editor wire for $.interface.cacheVaryInfo */
31
+ export type CacheVaryInfoWire = {
32
+ headers?: Array<{ name: string }>;
33
+ query?: string[];
34
+ clientIp?: boolean;
35
+ pathTemplate?: string;
36
+ bodyPaths?: BodyVaryProjection;
37
+ customRuntimeKey?: boolean;
38
+ };
39
+
40
+ /** Seconds (TTL). $.interface.duration */
41
+ export type DurationWire = number;
42
+
43
+ /** Save-only: hooks.save → $.http.configureResponseCaching */
44
+ export type ConfigureResponseCachingOptions = {
45
+ maxAge: number;
46
+ varyBy: HttpRequestCacheVary | CacheVaryInfoWire | '*' | string[];
47
+ mode?: HttpRequestCacheMode;
48
+ refreshAfterSeconds?: number;
49
+ needsRuntimeVaryKey?: boolean;
50
+ };
51
+
52
+ export const HTTP_REQUEST_CACHE_POLICY_KEY = '$httpRequestCachePolicy' as const;
53
+
54
+ export const REPLAY_BINDING_RANGE = '$replayBinding' as const;
55
+
56
+ export const REPLAY_META_RANGE = '$replayMeta' as const;
package/src/index.ts CHANGED
@@ -21,6 +21,7 @@ import type {
21
21
  ISlotStaticInstanceDefinition,
22
22
  ISlotDefinition,
23
23
  } from './slot-definition';
24
+ import { ConfigureResponseCachingOptions } from './http-request-cache';
24
25
 
25
26
  export type { ISlotInstanceDefinition, ISlotStaticInstanceDefinition, ISlotDefinition };
26
27
 
@@ -60,6 +61,19 @@ export {
60
61
  isPlatformBoundLoaderType,
61
62
  } from './platform-loader-type';
62
63
 
64
+ export {
65
+ HTTP_REQUEST_CACHE_POLICY_KEY,
66
+ REPLAY_BINDING_RANGE,
67
+ REPLAY_META_RANGE,
68
+ type BodyVaryProjection,
69
+ type CacheVaryInfoWire,
70
+ type ConfigureResponseCachingOptions,
71
+ type DurationWire,
72
+ type HttpRequestCacheMode,
73
+ type HttpRequestCachePolicy,
74
+ type HttpRequestCacheVary,
75
+ } from './http-request-cache';
76
+
63
77
  // Base types for module definitions
64
78
  export type ModuleDefinition = {
65
79
  type: string;
@@ -110,7 +124,7 @@ export type SignalEventShape = {
110
124
  * When {@link HttpInterfaceSchemaWire.validation} is true, the runtime does **not** validate against
111
125
  * those JSON blobs alone: it loads the ESM at {@link HttpInterfaceSchemaWire.compiledValidatorKey}
112
126
  * (default export = Zod schema) and runs full `safeParse` on the payload via the runner-bound
113
- * {@link SignalHostServices.enforceSchema} RPC (see {@link setSignalEmitValidationHost}).
127
+ * {@link SignalRunHostServices.enforceSchema} RPC (see {@link setSignalEmitValidationHost}).
114
128
  */
115
129
  export type HttpInterfaceSchemaWire = {
116
130
  /**
@@ -141,9 +155,13 @@ export type HttpInterfaceSchemaWire = {
141
155
  coerceLeafPrimitives?: boolean | 'auto';
142
156
  };
143
157
 
144
- /** Host-backed RPC surface passed as `params.$` to signal `run` (parallel to action `ActionRunOptions.$`). */
145
- export type SignalHostServices = {
158
+ /**
159
+ * Host `params.$` during signal **`run`** (live webhook / test execution).
160
+ * Not passed to `hooks.save` — use {@link SignalSaveHookHostServices} there.
161
+ */
162
+ export type SignalRunHostServices = {
146
163
  export: (category: string, message: string) => void | Promise<void>;
164
+
147
165
  $transitionToSlot: (slots: Array<SlotTransitionDefinition>) => void | Promise<void>;
148
166
 
149
167
  /**
@@ -158,12 +176,54 @@ export type SignalHostServices = {
158
176
 
159
177
  /**
160
178
  * Wire for the primary `$.interface.schema` property (same persisted object the publish
161
- * pipeline attaches `compiledValidatorKey` to). Use with {@link SignalHostServices.enforceSchema},
179
+ * pipeline attaches `compiledValidatorKey` to). Use with {@link SignalRunHostServices.enforceSchema},
162
180
  * e.g. `await $.enforceSchema($.interfaceEmitSchema, toEmit)`.
163
181
  */
164
182
  interfaceEmitSchema?: HttpInterfaceSchemaWire;
165
183
  };
166
184
 
185
+ /** @deprecated Use {@link SignalRunHostServices} for `run`; hook `$` types are separate. */
186
+ export type SignalHostServices = SignalRunHostServices;
187
+
188
+ /** Shared on all signal hook `params.$` surfaces. */
189
+ export type SignalHookHostContext = {
190
+ /**
191
+ * `true` when the hook runs during draft/editor materialization;
192
+ * `false` on publish / production hook runs.
193
+ */
194
+ isDraft: boolean;
195
+ };
196
+
197
+ /** Host `params.$` during **`hooks.save`**. */
198
+ export type SignalSaveHookHostServices = SignalHookHostContext & {
199
+ http: {
200
+ configureResponseCaching: (
201
+ options: ConfigureResponseCachingOptions,
202
+ ) => Promise<void> | void;
203
+ };
204
+ };
205
+
206
+ /** @deprecated Use {@link SignalSaveHookHostServices} */
207
+ export type HookSaveHostServices = SignalSaveHookHostServices;
208
+
209
+ /** Host `params.$` during `hooks.activate` / `hooks.deactivate`. */
210
+ export type SignalLifecycleHookHostServices = SignalHookHostContext &
211
+ Pick<SignalRunHostServices, 'export'>;
212
+
213
+ /** Inputs used to resolve {@link SignalHookHostContext.isDraft} in the API runner. */
214
+ export type SignalHookDraftContextInput = {
215
+ isDraft?: boolean;
216
+ executionContext?: 'editor' | 'production' | 'test' | 'webhook' | (string & {});
217
+ };
218
+
219
+ /** Resolve `$.isDraft` for hook invocations (explicit flag wins; else `executionContext === 'editor'`). */
220
+ export function resolveSignalHookIsDraft(ctx: SignalHookDraftContextInput): boolean {
221
+ if (typeof ctx.isDraft === 'boolean') {
222
+ return ctx.isDraft;
223
+ }
224
+ return ctx.executionContext === 'editor';
225
+ }
226
+
167
227
  /** One row from a failed Zod `safeParse` (host / `validateEmitPayload`). */
168
228
  export type SchemaValidationIssue = {
169
229
  path: string;
@@ -193,7 +253,7 @@ export type EnforceSchemaResult<T extends unknown = unknown> =
193
253
  export const PROCESS_CO_ENFORCE_SCHEMA_HOST_PAYLOAD_MARKER = 'enforceSchema' as const;
194
254
 
195
255
  export type SignalRunOptions = {
196
- $: SignalHostServices;
256
+ $: SignalRunHostServices;
197
257
  event: SignalEventShape;
198
258
  };
199
259
 
@@ -206,10 +266,10 @@ const SIGNAL_EMIT_VALIDATION_HOST = Symbol.for('process.co.signalEmitValidationH
206
266
 
207
267
  /**
208
268
  * Host shape accepted from the runner RPC bridge (`$.enforceSchema` may be typed as
209
- * returning `Promise<unknown>` while {@link SignalHostServices} uses a generic `T`).
269
+ * returning `Promise<unknown>` while {@link SignalRunHostServices} uses a generic `T`).
210
270
  */
211
271
  export type SignalEmitValidationHostBinding =
212
- | Pick<SignalHostServices, 'enforceSchema'>
272
+ | Pick<SignalRunHostServices, 'enforceSchema'>
213
273
  | {
214
274
  enforceSchema?: (
215
275
  inputSchema: HttpInterfaceSchemaWire | undefined,
@@ -217,8 +277,8 @@ export type SignalEmitValidationHostBinding =
217
277
  ) => Promise<unknown>;
218
278
  };
219
279
 
220
- function getSignalEmitValidationHostBinding(): Pick<SignalHostServices, 'enforceSchema'> | undefined {
221
- return (globalThis as Record<symbol, Pick<SignalHostServices, 'enforceSchema'> | undefined>)[
280
+ function getSignalEmitValidationHostBinding(): Pick<SignalRunHostServices, 'enforceSchema'> | undefined {
281
+ return (globalThis as Record<symbol, Pick<SignalRunHostServices, 'enforceSchema'> | undefined>)[
222
282
  SIGNAL_EMIT_VALIDATION_HOST
223
283
  ];
224
284
  }
@@ -232,11 +292,11 @@ function getSignalEmitValidationHostBinding(): Pick<SignalHostServices, 'enforce
232
292
  export function setSignalEmitValidationHost(
233
293
  host: SignalEmitValidationHostBinding | undefined,
234
294
  ): void {
235
- const g = globalThis as Record<symbol, Pick<SignalHostServices, 'enforceSchema'> | undefined>;
295
+ const g = globalThis as Record<symbol, Pick<SignalRunHostServices, 'enforceSchema'> | undefined>;
236
296
  if (host === undefined) {
237
297
  delete g[SIGNAL_EMIT_VALIDATION_HOST];
238
298
  } else {
239
- g[SIGNAL_EMIT_VALIDATION_HOST] = host as Pick<SignalHostServices, 'enforceSchema'>;
299
+ g[SIGNAL_EMIT_VALIDATION_HOST] = host as Pick<SignalRunHostServices, 'enforceSchema'>;
240
300
  }
241
301
  }
242
302
 
@@ -663,10 +723,12 @@ type PropDefinitionInput<TType = unknown> = BasePropDefinition & {
663
723
  };
664
724
 
665
725
  export type HttpInterfaceType = {
726
+
666
727
  /**
667
728
  * Full HTTP response (status / headers / body). Sending `body` completes the exchange for typical requests.
668
729
  */
669
730
  respond: (response: HTTPResponse) => Promise<any> | void;
731
+
670
732
  /**
671
733
  * Incremental write or SSE frame; does not complete the exchange. Pair with {@link end} or a terminal {@link respond} where applicable.
672
734
  */
@@ -680,6 +742,12 @@ export type HttpInterfaceType = {
680
742
  */
681
743
  deferHttpResponse: (timeoutMs?: number, options?: HttpDeferResponseOptions) => void;
682
744
 
745
+ /**
746
+ * Optional runtime vary suffix (hashed and appended to the HTTP base scenario key).
747
+ * Call when saved `$httpRequestCachePolicy.needsRuntimeVaryKey` is true.
748
+ */
749
+ setRequestVaryKey: (value: string) => void;
750
+
683
751
  /**
684
752
  * Append one JSON value to an incremental stream (`ndjson` or `jsonArray` defer modes). Each call is one line (NDJSON) or one array element (json-array).
685
753
  */
@@ -693,6 +761,7 @@ export type HttpInterfaceType = {
693
761
  flow: FlowFunctions;
694
762
  end: () => void;
695
763
  execute: () => Promise<{ headers?: Record<string, string>;[key: string]: any }>
764
+
696
765
  };
697
766
 
698
767
 
@@ -729,12 +798,32 @@ type PropTypeFromTypeValue<U, T = unknown> =
729
798
  : U extends "integer" ? number
730
799
  : U extends "$.interface.schema" ? HttpInterfaceSchemaWire
731
800
  : U extends "$.interface.http" ? HttpInterfaceType
801
+ : U extends "$.interface.duration" ? import('./http-request-cache').DurationWire
802
+ : U extends "$.interface.cacheVaryInfo" ? import('./http-request-cache').CacheVaryInfoWire
732
803
  : unknown;
733
804
 
805
+ /**
806
+ * Runtime shape of an embedded app prop (`props: { http: httpApp }` on a signal/action).
807
+ * Maps `propDefinitions` / `props` / `methods` to instance fields — not the `defineApp` module metadata (`type`, `app`, …).
808
+ */
809
+ export type DeriveEmbeddedAppPropInstance<T extends { type: 'app' }> = Spread<
810
+ (T extends { propDefinitions: Record<string, any> }
811
+ ? { [K in keyof T['propDefinitions']]: PropType<T['propDefinitions'][K]> }
812
+ : {}) &
813
+ (T extends { props: Record<string, any> }
814
+ ? { [K in keyof T['props']]: PropType<T['props'][K]> }
815
+ : {}) &
816
+ (T extends { methods: Record<string, any> }
817
+ ? { [K in keyof T['methods']]: T['methods'][K] }
818
+ : {})
819
+ >;
820
+
734
821
  // Utility type for transforming prop definitions to their runtime types
735
822
  export type PropType<T> =
736
- // 1. If T is an app definition, derive its instance type
737
- T extends { props: Record<string, any>; methods: Record<string, any> }
823
+ // 1. Embedded app module runtime prop surface (not the definition object)
824
+ T extends { type: 'app' }
825
+ ? DeriveEmbeddedAppPropInstance<T>
826
+ : T extends { props: Record<string, any>; methods: Record<string, any> }
738
827
  ? DeriveAppInstance<T>
739
828
  // 2. If T is a propDefinition, resolve from propDefinitions or props
740
829
  : T extends { propDefinition: readonly [infer App, infer PropName] }
@@ -801,24 +890,69 @@ export type DeriveAppInstance<T> =
801
890
  { [K in keyof T as K extends "props" | "propDefinitions" | "methods" ? never : K]: T[K] }
802
891
  >;
803
892
 
893
+ /** True when a prop definition resolves to `$.interface.http` (runtime-only on `run`). */
894
+ type IsHttpInterfacePropDef<P> =
895
+ P extends { type: '$.interface.http' }
896
+ ? true
897
+ : P extends { propDefinition: readonly [infer App, infer PropName] }
898
+ ? App extends { propDefinitions: Record<string, any> }
899
+ ? PropName extends keyof App['propDefinitions']
900
+ ? App['propDefinitions'][PropName] extends { type: '$.interface.http' }
901
+ ? true
902
+ : false
903
+ : false
904
+ : App extends { props: Record<string, any> }
905
+ ? PropName extends keyof App['props']
906
+ ? App['props'][PropName] extends { type: '$.interface.http' }
907
+ ? true
908
+ : false
909
+ : false
910
+ : false
911
+ : false;
912
+
804
913
  export type DeriveSignalInstance<T> =
805
914
  Spread<
806
- Omit<T, "props" | "propDefinitions" | "methods"> &
915
+ Omit<T, SignalInstanceExcludedKeys> &
807
916
  (T extends { props: Record<string, any> }
808
- ? { [K in keyof T["props"]]: PropType<T["props"][K]> }
917
+ ? { [K in keyof T['props']]: PropType<T['props'][K]> }
809
918
  : {}) &
810
919
  (T extends { propDefinitions: Record<string, any> }
811
- ? { [K in keyof T["propDefinitions"]]: PropType<T["propDefinitions"][K]> }
920
+ ? { [K in keyof T['propDefinitions']]: PropType<T['propDefinitions'][K]> }
812
921
  : {}) &
813
- // Add $emit to all signal instances
814
922
  EmitFunction &
815
923
  (T extends { methods: Record<string, any> }
816
- ? { [K in keyof T["methods"]]: T["methods"][K] }
924
+ ? { [K in keyof T['methods']]: T['methods'][K] }
817
925
  : {}) &
818
- // Also include all direct methods on the object
819
- { [K in keyof T as K extends "props" | "propDefinitions" | "methods" ? never : K]: T[K] }
926
+ {
927
+ [K in keyof T as K extends SignalInstanceExcludedKeys ? never : K]: T[K];
928
+ }
820
929
  >;
821
930
 
931
+ /** Module definition keys that are not instance fields on `this` in `run` or hooks. */
932
+ type SignalInstanceExcludedKeys =
933
+ | 'props'
934
+ | 'propDefinitions'
935
+ | 'methods'
936
+ | 'run'
937
+ | 'hooks';
938
+
939
+ /** Prop names on `T` that are `$.interface.http` (excluded from hook `this`). */
940
+ type HttpInterfacePropKeys<T> =
941
+ T extends { props: infer P extends Record<string, unknown> }
942
+ ? keyof {
943
+ [K in keyof P as IsHttpInterfacePropDef<P[K]> extends true ? K : never]: true;
944
+ }
945
+ : never;
946
+
947
+ /**
948
+ * `this` inside signal hooks: instance props minus `$.interface.http` and `$emit`.
949
+ * Use `params.$.http.configureResponseCaching` in `save`.
950
+ */
951
+ export type DeriveSignalHookInstance<T> = Omit<
952
+ DeriveSignalInstance<T>,
953
+ HttpInterfacePropKeys<T> | keyof EmitFunction
954
+ >;
955
+
822
956
  // In your element-types
823
957
  export type PropDefinitionType<App, PropName extends string> =
824
958
  App extends { propDefinitions: Record<string, any> }
@@ -827,21 +961,37 @@ export type PropDefinitionType<App, PropName extends string> =
827
961
  : unknown
828
962
  : unknown;
829
963
 
830
- // --- Add this helper above DeriveActionInstance ---
831
-
832
- // Enhanced action instance type
964
+ /** Module definition keys that are not instance fields on `this` in `run`. */
965
+ type ActionInstanceExcludedKeys =
966
+ | 'props'
967
+ | 'propDefinitions'
968
+ | 'methods'
969
+ | 'run'
970
+ | 'type'
971
+ | 'name'
972
+ | 'description'
973
+ | 'icon'
974
+ | 'noAuth'
975
+ | 'slots'
976
+ | 'hasNew'
977
+ | 'initValue';
978
+
979
+ /** Runtime `this` for action `run` (prop values via {@link PropType}, including embedded apps). */
833
980
  export type DeriveActionInstance<T> =
834
981
  Spread<
835
- Omit<T, "props" | "propDefinitions" | "methods"> &
982
+ Omit<T, ActionInstanceExcludedKeys> &
836
983
  (T extends { props: Record<string, any> }
837
- ? { [K in keyof T["props"]]: PropType<T["props"][K]> }
984
+ ? { [K in keyof T['props']]: PropType<T['props'][K]> }
838
985
  : {}) &
839
986
  (T extends { propDefinitions: Record<string, any> }
840
- ? { [K in keyof T["propDefinitions"]]: PropType<T["propDefinitions"][K]> }
987
+ ? { [K in keyof T['propDefinitions']]: PropType<T['propDefinitions'][K]> }
841
988
  : {}) &
842
989
  (T extends { methods: Record<string, any> }
843
- ? { [K in keyof T["methods"]]: T["methods"][K] }
844
- : {})
990
+ ? { [K in keyof T['methods']]: T['methods'][K] }
991
+ : {}) &
992
+ {
993
+ [K in keyof T as K extends ActionInstanceExcludedKeys ? never : K]: T[K];
994
+ }
845
995
  >;
846
996
 
847
997
  // Helper type to create a module with proper this context
@@ -878,7 +1028,10 @@ export type ActionInstance<A extends Action> = DeriveActionInstance<A>;
878
1028
  export type SignalInstance<S extends Signal> = DeriveSignalInstance<S>;
879
1029
 
880
1030
  export type SignalMethod<S extends Signal> = (this: SignalInstance<S>, params: SignalRunOptions) => Promise<unknown>;
881
- export type ActionMethod<A extends Action> = (this: ActionInstance<A>, params: { $: any }) => Promise<unknown>;
1031
+ export type ActionMethod<A extends Action> = (
1032
+ this: ActionInstance<A>,
1033
+ params: ActionRunOptions,
1034
+ ) => Promise<unknown>;
882
1035
 
883
1036
  export type PropStringDefinitionTypes = "text" | "html" | "markdown" | "json" | "xml" | "yaml" | "csv" | "tsv" | "css" | "sql" | "email" | "emailList" | "urlList" | "url" | "base64" | "javascript";
884
1037
 
@@ -905,22 +1058,112 @@ export function defineApp<const T extends object>(app: T & ThisType<DeriveAppIns
905
1058
  return app;
906
1059
  }
907
1060
 
908
- // Helper to provide ThisType context for action definitions
909
- export function defineAction<const T extends
910
- { props?: Record<string, unknown> } &
911
- { type: "action" } &
912
- { name?: string } &
913
- { description?: string } &
914
- { icon?: ElementIcon } &
915
- { noAuth?: boolean } &
916
- { slots?: ISlotDefinition } &
917
- { methods?: Record<string, (...args: any[]) => any> } &
918
- { hasNew?: boolean } &
919
- { initValue?: any }>(action: T & ThisType<DeriveActionInstance<T>>): T {
1061
+ export type ActionRunFn = (params: ActionRunOptions) => void | Promise<unknown>;
1062
+
1063
+ /** Canonical action entrypoint — implement `run` here (see process-internal loop, etc.). */
1064
+ export type ActionMethodsRun = {
1065
+ methods: Record<string, unknown> & { run: ActionRunFn };
1066
+ };
1067
+
1068
+ /**
1069
+ * @deprecated Use `methods: { run }` instead of a top-level `run` property.
1070
+ * Runtime still accepts this shape via `restructureElement`.
1071
+ */
1072
+ export type ActionMethodsLegacyTopLevelRun = {
1073
+ /** @deprecated Use `methods.run`. */
1074
+ run?: ActionRunFn;
1075
+ };
1076
+
1077
+ /** Minimum shape for {@link defineAction}. Prefer {@link ActionMethodsRun}. */
1078
+ export type ActionMethods = ActionMethodsRun | ActionMethodsLegacyTopLevelRun;
1079
+
1080
+ /** Structural requirements for an action module (tooling; prefer {@link defineAction}). */
1081
+ export type ActionDefinitionShape<T> = {
1082
+ methods: Record<string, unknown> & {
1083
+ run: (this: DeriveActionInstance<T>, params: ActionRunOptions) => Promise<unknown>;
1084
+ };
1085
+ /**
1086
+ * @deprecated Use `methods.run`.
1087
+ */
1088
+ run?: (this: DeriveActionInstance<T>, params: ActionRunOptions) => Promise<unknown>;
1089
+ };
1090
+
1091
+ /** Contextual `this` for top-level and `methods.*` action functions. */
1092
+ export type ActionMethodsWithThis<T> = T &
1093
+ ThisType<DeriveActionInstance<T>> &
1094
+ (T extends { methods?: infer M extends Record<string, unknown> }
1095
+ ? { methods: M & ThisType<DeriveActionInstance<T>> }
1096
+ : {});
1097
+
1098
+ export function defineAction<
1099
+ const T extends ActionMethods & { type: 'action' } & Record<string, unknown>,
1100
+ >(action: ActionMethodsWithThis<T>): T {
920
1101
  return action;
921
1102
  }
922
1103
 
923
- export function defineSignal<const T extends { run: (params: SignalRunOptions) => Promise<unknown> }>(signal: T & ThisType<DeriveSignalInstance<T>>): T {
1104
+ /** `params.$` for `hooks.activate` / `hooks.deactivate`. */
1105
+ export type SignalHostHookParameters = {
1106
+ $: SignalLifecycleHookHostServices;
1107
+ };
1108
+
1109
+ /** `params.$` for `hooks.save` — publish-only; not {@link SignalRunHostServices}. */
1110
+ export type SignalSaveHostParameters = {
1111
+ $: SignalSaveHookHostServices;
1112
+ };
1113
+
1114
+ export type SignalHostHookMethod<T> = (
1115
+ this: DeriveSignalHookInstance<T>,
1116
+ params: SignalHostHookParameters,
1117
+ ) => void | Promise<void>;
1118
+
1119
+ export type SignalSaveHookMethod<T> = (
1120
+ this: DeriveSignalHookInstance<T>,
1121
+ params: SignalSaveHostParameters,
1122
+ ) => void | Promise<void>;
1123
+
1124
+ export type SignalHooksDefinition<T> = {
1125
+ deactivate?: SignalHostHookMethod<T>;
1126
+ activate?: SignalHostHookMethod<T>;
1127
+ save?: SignalSaveHookMethod<T>;
1128
+ onDeactivate?: SignalHostHookMethod<T>;
1129
+ onActivate?: SignalHostHookMethod<T>;
1130
+ onSave?: SignalSaveHookMethod<T>;
1131
+ };
1132
+
1133
+ /**
1134
+ * Hook implementations for {@link defineSignal}: `params.$` is typed here; `this` comes from
1135
+ * {@link ThisType}<{@link DeriveSignalHookInstance}<T>> (avoids circular `T` and empty `this`).
1136
+ */
1137
+ export type SignalHooksContextualDefinition = {
1138
+ deactivate?: (params: SignalHostHookParameters) => void | Promise<void>;
1139
+ activate?: (params: SignalHostHookParameters) => void | Promise<void>;
1140
+ save?: (params: SignalSaveHostParameters) => void | Promise<void>;
1141
+ onDeactivate?: (params: SignalHostHookParameters) => void | Promise<void>;
1142
+ onActivate?: (params: SignalHostHookParameters) => void | Promise<void>;
1143
+ onSave?: (params: SignalSaveHostParameters) => void | Promise<void>;
1144
+ };
1145
+
1146
+ /** Hook bag for {@link defineSignal}. */
1147
+ export type SignalHooksWithThis<T> = SignalHooksContextualDefinition &
1148
+ ThisType<DeriveSignalHookInstance<T>>;
1149
+
1150
+ /** Structural requirements for a signal module (used by tooling; prefer {@link defineSignal}). */
1151
+ export type SignalDefinitionShape<T> = {
1152
+ run: (this: DeriveSignalInstance<T>, params: SignalRunOptions) => Promise<unknown>;
1153
+ hooks?: SignalHooksDefinition<T>;
1154
+ };
1155
+
1156
+ /** @deprecated Use {@link defineSignal} — kept for generated lib compatibility. */
1157
+ export type SignalMethods = {
1158
+ run: (params: SignalRunOptions) => Promise<unknown>;
1159
+ };
1160
+
1161
+ export function defineSignal<const T extends SignalMethods & Record<string, unknown>>(
1162
+ signal: T &
1163
+ ThisType<DeriveSignalInstance<T>> & {
1164
+ hooks?: SignalHooksWithThis<T>;
1165
+ },
1166
+ ): T {
924
1167
  return signal;
925
1168
  }
926
1169
 
@@ -0,0 +1,13 @@
1
+ import { resolveSignalHookIsDraft } from './index';
2
+
3
+ describe('resolveSignalHookIsDraft', () => {
4
+ it('uses explicit isDraft when set', () => {
5
+ expect(resolveSignalHookIsDraft({ isDraft: true, executionContext: 'production' })).toBe(true);
6
+ expect(resolveSignalHookIsDraft({ isDraft: false, executionContext: 'editor' })).toBe(false);
7
+ });
8
+
9
+ it('falls back to executionContext editor', () => {
10
+ expect(resolveSignalHookIsDraft({ executionContext: 'editor' })).toBe(true);
11
+ expect(resolveSignalHookIsDraft({ executionContext: 'production' })).toBe(false);
12
+ });
13
+ });