gencow 0.1.240 → 0.1.242

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 (47) hide show
  1. package/core/index.js +1824 -1185
  2. package/core/package.json +1 -1
  3. package/lib/add-agent.mjs +498 -0
  4. package/lib/app-command.mjs +44 -2
  5. package/lib/component-readme.mjs +25 -56
  6. package/lib/deploy-auditor.mjs +4 -2
  7. package/lib/deploy-command.mjs +66 -0
  8. package/lib/deploy-failure-diagnostic.mjs +86 -2
  9. package/lib/deploy-runtime.mjs +1 -0
  10. package/lib/deployment-operation-poll.mjs +17 -33
  11. package/lib/env-command.mjs +155 -16
  12. package/lib/init-command.mjs +39 -2
  13. package/lib/install-features.mjs +17 -5
  14. package/lib/jobs-command.mjs +17 -1
  15. package/lib/readme-codegen.mjs +15 -22
  16. package/lib/static-deploy-command.mjs +39 -5
  17. package/package.json +3 -3
  18. package/runtime/server.mjs +24547 -15958
  19. package/runtime/tooling.mjs +3943 -1992
  20. package/scripts/bundle-core.mjs +3 -2
  21. package/scripts/tooling-codegen-runtime.ts +26 -23
  22. package/templateFeature/agent/agent.ts +30 -15
  23. package/templateFeature/agent/manifest.json +3 -3
  24. package/templateFeature/ai/ai-facade.ts +16 -10
  25. package/templateFeature/ai/ai-provider.ts +16 -3
  26. package/templateFeature/ai/manifest.json +5 -5
  27. package/templateFeature/guardrails/guardrails.ts +22 -13
  28. package/templateFeature/guardrails/manifest.json +1 -1
  29. package/templateFeature/memory/manifest.json +1 -1
  30. package/templateFeature/memory/memory.ts +2 -1
  31. package/templateFeature/rag/manifest.json +1 -1
  32. package/templateFeature/rag/rag.ts +1 -1
  33. package/templateFeature/reranker/manifest.json +4 -9
  34. package/templateFeature/reranker/reranker.ts +3 -6
  35. package/templateFeature/search/manifest.json +1 -0
  36. package/templateFeature/search/schema-search.ts +1 -1
  37. package/templateFeature/search/search-index-contract.ts +20 -0
  38. package/templateFeature/tools/manifest.json +1 -1
  39. package/templates/SECURITY.md +18 -19
  40. package/templates/ai-chat/README.md +18 -28
  41. package/templates/ai-chat/chat.ts +11 -7
  42. package/templates/ai-chat/prompt.md +3 -3
  43. package/templates/fullstack/README.md +8 -33
  44. package/templates/fullstack/files.ts +2 -0
  45. package/templates/fullstack/index.ts +5 -1
  46. package/templates/fullstack/prompt.md +1 -1
  47. package/templates/runtime.ts +5 -1
package/core/index.js CHANGED
@@ -813,449 +813,98 @@ function verifyRealtimeChannelToken(token, input) {
813
813
  };
814
814
  }
815
815
 
816
- // ../core/src/realtime-channel-registry.ts
817
- if (!globalThis.__gencow_realtimeChannelRegistry) {
818
- globalThis.__gencow_realtimeChannelRegistry = /* @__PURE__ */ new Map();
819
- }
820
- var realtimeChannelRegistry = globalThis.__gencow_realtimeChannelRegistry;
821
- function defineRealtimeChannels(channels) {
822
- for (const [name, def] of Object.entries(channels)) {
823
- realtimeChannelRegistry.set(name, {
824
- name,
825
- argsSchema: def.args,
826
- channelKey: def.channelKey,
827
- authorize: def.authorize
828
- });
816
+ // ../core/src/v.ts
817
+ var GencowValidationError = class extends Error {
818
+ statusCode = 400;
819
+ constructor(message) {
820
+ super(message);
821
+ this.name = "GencowValidationError";
829
822
  }
830
- return channels;
831
- }
832
- function getRealtimeChannelDef(name) {
833
- return realtimeChannelRegistry.get(name);
823
+ };
824
+ var GENCOW_OPTIONAL_INNER = /* @__PURE__ */ Symbol.for("gencow.v.optionalInner");
825
+ function assertStandardResult(value) {
826
+ return { value };
834
827
  }
835
- function getRegisteredRealtimeChannels() {
836
- return [...realtimeChannelRegistry.keys()];
828
+ function failStandardResult(message) {
829
+ return { issues: [{ message }] };
837
830
  }
838
- function resolveRealtimeChannelKey(params) {
839
- if (params.result === false || params.result === null) return null;
840
- if (typeof params.result === "string") return params.result.length > 0 ? params.result : null;
841
- if (params.result && typeof params.result === "object" && "channelKey" in params.result) {
842
- const channelKey2 = params.result.channelKey;
843
- if (channelKey2 !== void 0) return typeof channelKey2 === "string" && channelKey2.length > 0 ? channelKey2 : null;
844
- }
845
- const def = getRealtimeChannelDef(params.channel);
846
- const channelKey = def?.channelKey?.(params.args);
847
- if (channelKey !== void 0) return typeof channelKey === "string" && channelKey.length > 0 ? channelKey : null;
848
- return buildRealtimeChannelKey(params.channel, params.args);
831
+ function readJsonSchemaFragment(validator) {
832
+ const opt = validator[GENCOW_OPTIONAL_INNER];
833
+ const target = opt ?? validator;
834
+ const raw = target["~standard"]?.jsonSchema;
835
+ if (raw === void 0) return {};
836
+ return raw.output({ target: "draft-2020-12" });
849
837
  }
850
- function resetRealtimeChannelRegistryForTests() {
851
- realtimeChannelRegistry.clear();
838
+ function isOptionalValidator(validator) {
839
+ return !!validator && Object.prototype.hasOwnProperty.call(validator, GENCOW_OPTIONAL_INNER);
852
840
  }
853
-
854
- // ../core/src/reactive-realtime.ts
855
- var SUBSCRIPTION_KEY_SEPARATOR = "::";
856
- function createRealtimeEventId() {
857
- const randomUUID = globalThis.crypto?.randomUUID;
858
- if (typeof randomUUID === "function") return `evt_${randomUUID.call(globalThis.crypto)}`;
859
- return `evt_${Date.now().toString(36)}_${Math.random().toString(36).slice(2)}`;
841
+ function readGencowStandardJsonSchema(schema) {
842
+ if (!schema || typeof schema !== "object" || !("~standard" in schema)) return void 0;
843
+ const props = schema["~standard"];
844
+ if (!props || props.vendor !== "gencow") return void 0;
845
+ const raw = props.jsonSchema;
846
+ if (raw === void 0) return void 0;
847
+ return raw.output({ target: "draft-2020-12" });
860
848
  }
861
- function normalizeForStableJson3(value) {
862
- if (value === void 0) return void 0;
863
- if (value === null) return null;
864
- if (value instanceof Date) return value.toISOString();
865
- if (Array.isArray(value)) return value.map((item) => normalizeForStableJson3(item));
866
- if (typeof value === "object") {
867
- const source = value;
868
- const sorted = {};
869
- for (const key of Object.keys(source).sort()) {
870
- const normalized = normalizeForStableJson3(source[key]);
871
- if (normalized !== void 0) sorted[key] = normalized;
872
- }
873
- return sorted;
874
- }
875
- return value;
849
+ function isRecord(value) {
850
+ return typeof value === "object" && value !== null && !Array.isArray(value);
876
851
  }
877
- function isEmptyPlainObject3(value) {
878
- return !!value && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === 0;
852
+ function isAllOptionalObjectSchema(schema) {
853
+ const jsonSchema = readGencowStandardJsonSchema(schema);
854
+ if (!isRecord(jsonSchema) || jsonSchema.type !== "object") return false;
855
+ const required = jsonSchema.required;
856
+ return required === void 0 || Array.isArray(required) && required.length === 0;
879
857
  }
880
- function buildQuerySubscriptionKey(queryKey, args) {
881
- const normalized = normalizeForStableJson3(args);
882
- if (normalized === void 0 || isEmptyPlainObject3(normalized)) return queryKey;
883
- return `${queryKey}${SUBSCRIPTION_KEY_SEPARATOR}${JSON.stringify(normalized)}`;
858
+ function normalizeMissingOptionalObjectInput(schema, value) {
859
+ if (value !== void 0) return value;
860
+ return isAllOptionalObjectSchema(schema) ? {} : value;
884
861
  }
885
- function subscriptionKeyMatchesQueryKey(subscriptionKey, queryKey) {
886
- return subscriptionKey === queryKey || subscriptionKey.startsWith(`${queryKey}${SUBSCRIPTION_KEY_SEPARATOR}`);
862
+ function matchesMimeType(fileType, pattern) {
863
+ const normalizedFileType = fileType.split(";")[0].trim().toLowerCase();
864
+ const normalizedPattern = pattern.trim().toLowerCase();
865
+ if (normalizedPattern === "*/*") return true;
866
+ if (normalizedPattern.endsWith("/*")) {
867
+ const prefix = normalizedPattern.slice(0, -1);
868
+ return normalizedFileType.startsWith(prefix);
869
+ }
870
+ return normalizedFileType === normalizedPattern;
887
871
  }
888
- function buildRealtimeCtx(options) {
889
- const pendingEmits = /* @__PURE__ */ new Map();
890
- const pendingEvents = [];
891
- const _pendingRefresh = [];
892
- let _hasEmitted = false;
893
- function dispatchEvent(event) {
894
- if (options.deferFanout) {
895
- pendingEvents.push(event);
896
- return;
872
+ function buildObjectJsonSchema(fields) {
873
+ const properties = {};
874
+ const required = [];
875
+ for (const key of Object.keys(fields)) {
876
+ const field = fields[key];
877
+ properties[key] = readJsonSchemaFragment(field);
878
+ if (!isOptionalValidator(field)) {
879
+ required.push(key);
897
880
  }
898
- void options.fanout.notify(event);
899
881
  }
900
882
  return {
901
- emit(queryKey, data) {
902
- _hasEmitted = true;
903
- if (options.deferFanout) {
904
- pendingEvents.push({ type: "emit", queryKey, data });
905
- return;
883
+ type: "object",
884
+ properties,
885
+ ...required.length > 0 ? { required } : {}
886
+ };
887
+ }
888
+ function createValidator(config) {
889
+ const { parse, _type, jsonSchema } = config;
890
+ const standard = {
891
+ version: 1,
892
+ vendor: "gencow",
893
+ types: {},
894
+ validate: async (value) => {
895
+ try {
896
+ return assertStandardResult(parse(value));
897
+ } catch (e) {
898
+ const message = e instanceof Error ? e.message : "Validation failed";
899
+ return failStandardResult(message);
906
900
  }
907
- const existing = pendingEmits.get(queryKey);
908
- if (existing) clearTimeout(existing.timer);
909
- const timer = setTimeout(() => {
910
- pendingEmits.delete(queryKey);
911
- dispatchEvent({ type: "emit", queryKey, data });
912
- }, 50);
913
- pendingEmits.set(queryKey, { data, timer });
914
- },
915
- invalidate(queryKey) {
916
- _hasEmitted = true;
917
- const queryKeys = Array.isArray(queryKey) ? [...new Set(queryKey)] : [queryKey];
918
- dispatchEvent({ type: "invalidate", queryKeys });
919
901
  },
920
- refresh(queryKey) {
921
- _hasEmitted = true;
922
- if (!_pendingRefresh.includes(queryKey)) {
923
- _pendingRefresh.push(queryKey);
902
+ ...jsonSchema !== void 0 ? {
903
+ jsonSchema: {
904
+ input: () => jsonSchema,
905
+ output: () => jsonSchema
924
906
  }
925
- },
926
- channel(channel, args) {
927
- const channelKey = resolveRealtimeChannelKey({ channel, args });
928
- if (!channelKey) throw new Error(`Realtime channel "${channel}" resolved to an invalid channelKey`);
929
- return {
930
- authorize(authOptions) {
931
- if (!options.appName || !options.channelTokenSecret || !options.auth) {
932
- throw new Error("Authorized realtime channel token signer is not configured");
933
- }
934
- const user = options.auth.requireAuth();
935
- const nowMs = authOptions?.nowMs ?? Date.now();
936
- const rawTtl = authOptions?.ttlSeconds;
937
- const ttlSeconds = Number.isFinite(rawTtl) && rawTtl > 0 ? Math.min(rawTtl, 3600) : 300;
938
- const epoch = normalizeRealtimeChannelEpoch(authOptions?.epoch);
939
- return {
940
- token: createRealtimeChannelToken({
941
- appName: options.appName,
942
- channel,
943
- channelKey,
944
- args,
945
- userId: user.id,
946
- secret: options.channelTokenSecret,
947
- ttlSeconds,
948
- nowMs,
949
- nonce: authOptions?.nonce,
950
- epoch
951
- }),
952
- channel,
953
- channelKey,
954
- userId: user.id,
955
- expiresAt: nowMs + Math.floor(ttlSeconds * 1e3),
956
- ...epoch ? { epoch } : {}
957
- };
958
- },
959
- publish(data, publishOptions) {
960
- _hasEmitted = true;
961
- dispatchEvent({
962
- type: "channel_publish",
963
- channelKey,
964
- data,
965
- eventId: publishOptions?.eventId ?? createRealtimeEventId()
966
- });
967
- },
968
- invalidate() {
969
- _hasEmitted = true;
970
- dispatchEvent({ type: "channel_invalidate", channelKey });
971
- },
972
- revoke(revokeOptions) {
973
- _hasEmitted = true;
974
- const epoch = normalizeRealtimeChannelEpoch(revokeOptions?.epoch);
975
- const reason = typeof revokeOptions?.reason === "string" && revokeOptions.reason.length > 0 ? revokeOptions.reason : void 0;
976
- dispatchEvent({
977
- type: "channel_revoke",
978
- channelKey,
979
- ...epoch ? { epoch } : {},
980
- ...reason ? { reason } : {}
981
- });
982
- }
983
- };
984
- },
985
- query(query2, args) {
986
- const subscriptionKey = resolveRealtimeQuerySubscriptionKey({ query: query2, args });
987
- if (!subscriptionKey)
988
- throw new Error(`Realtime query "${query2}" resolved to an invalid subscriptionKey`);
989
- const scopeHash = resolveRealtimeQueryScopeHash({ query: query2, args, subscriptionKey });
990
- return {
991
- authorize(authOptions) {
992
- if (!options.appName || !options.channelTokenSecret || !options.auth) {
993
- throw new Error("Authorized realtime query token signer is not configured");
994
- }
995
- const user = options.auth.requireAuth();
996
- const nowMs = authOptions?.nowMs ?? Date.now();
997
- const rawTtl = authOptions?.ttlSeconds;
998
- const ttlSeconds = Number.isFinite(rawTtl) && rawTtl > 0 ? Math.min(rawTtl, 3600) : 300;
999
- const epoch = normalizeRealtimeQuerySubscriptionEpoch(authOptions?.epoch);
1000
- return {
1001
- token: createRealtimeQuerySubscriptionToken({
1002
- appName: options.appName,
1003
- query: query2,
1004
- subscriptionKey,
1005
- args,
1006
- scopeHash,
1007
- policy: "authorized",
1008
- userId: user.id,
1009
- secret: options.channelTokenSecret,
1010
- ttlSeconds,
1011
- nowMs,
1012
- nonce: authOptions?.nonce,
1013
- epoch
1014
- }),
1015
- query: query2,
1016
- subscriptionKey,
1017
- policy: "authorized",
1018
- userId: user.id,
1019
- expiresAt: nowMs + Math.floor(ttlSeconds * 1e3),
1020
- ...epoch ? { epoch } : {}
1021
- };
1022
- },
1023
- invalidate(invalidateOptions) {
1024
- _hasEmitted = true;
1025
- dispatchEvent({
1026
- type: "query_invalidate",
1027
- query: query2,
1028
- subscriptionKey,
1029
- scopeHash,
1030
- eventId: invalidateOptions?.eventId ?? createRealtimeEventId()
1031
- });
1032
- },
1033
- revoke(revokeOptions) {
1034
- _hasEmitted = true;
1035
- const epoch = normalizeRealtimeQuerySubscriptionEpoch(revokeOptions?.epoch);
1036
- const reason = typeof revokeOptions?.reason === "string" && revokeOptions.reason.length > 0 ? revokeOptions.reason : void 0;
1037
- dispatchEvent({
1038
- type: "query_revoke",
1039
- query: query2,
1040
- subscriptionKey,
1041
- scopeHash,
1042
- ...epoch ? { epoch } : {},
1043
- ...reason ? { reason } : {}
1044
- });
1045
- }
1046
- };
1047
- },
1048
- invalidateScoped(input) {
1049
- _hasEmitted = true;
1050
- const subscriptionKey = input.subscriptionKey ?? resolveRealtimeQuerySubscriptionKey({ query: input.query, args: input.args });
1051
- if (!subscriptionKey)
1052
- throw new Error(`Realtime query "${input.query}" resolved to an invalid subscriptionKey`);
1053
- const scopeHash = input.scopeHash ?? resolveRealtimeQueryScopeHash({
1054
- query: input.query,
1055
- args: input.args,
1056
- subscriptionKey
1057
- });
1058
- dispatchEvent({
1059
- type: "query_invalidate",
1060
- query: input.query,
1061
- subscriptionKey,
1062
- scopeHash,
1063
- eventId: input.eventId ?? createRealtimeEventId()
1064
- });
1065
- },
1066
- invalidateCrudQuery(input) {
1067
- _hasEmitted = true;
1068
- const subscriptionKey = input.subscriptionKey ?? resolveRealtimeQuerySubscriptionKey({ query: input.query, args: input.args });
1069
- if (!subscriptionKey)
1070
- throw new Error(`Realtime query "${input.query}" resolved to an invalid subscriptionKey`);
1071
- const scopeHash = input.scopeHash ?? resolveRealtimeQueryScopeHash({
1072
- query: input.query,
1073
- args: input.args,
1074
- subscriptionKey
1075
- });
1076
- dispatchEvent({
1077
- type: "query_invalidate",
1078
- query: input.query,
1079
- subscriptionKey,
1080
- scopeHash,
1081
- eventId: input.eventId ?? createRealtimeEventId(),
1082
- legacyQueryKey: input.legacyQueryKey,
1083
- crudRealtimeEmitMode: input.crudRealtimeEmitMode ?? options.generatedCrudRealtimeEmitMode ?? "dual"
1084
- });
1085
- },
1086
- get _hasEmitted() {
1087
- return _hasEmitted;
1088
- },
1089
- get _pendingRefresh() {
1090
- return [..._pendingRefresh];
1091
- },
1092
- async _flushRefresh() {
1093
- if (_pendingRefresh.length === 0) return;
1094
- const qMap = options.queryMap ?? queryRegistry;
1095
- for (const key of _pendingRefresh) {
1096
- const legacyDef = qMap.get(key);
1097
- const procedureDef = legacyDef ? void 0 : options.getProcedureQuery?.(key);
1098
- if (!legacyDef && !procedureDef) {
1099
- console.warn(`[gencow] refresh("${key}"): query not found in registry. Skipping.`);
1100
- continue;
1101
- }
1102
- if (procedureDef?.inputSchema) {
1103
- options.fanout.notify({ type: "invalidate", queryKeys: [key] });
1104
- continue;
1105
- }
1106
- try {
1107
- if (!options.buildCtxForRefresh) {
1108
- console.warn(
1109
- `[gencow] \u26A0\uFE0F refresh("${key}"): buildCtxForRefresh not provided. Query handler will receive an empty ctx \u2014 ctx.db will be undefined. This is a framework configuration error. \u{1F4A1} Ensure buildRealtimeCtx() receives a buildCtxForRefresh callback.`
1110
- );
1111
- }
1112
- const refreshCtx = options.buildCtxForRefresh?.() ?? {};
1113
- const result = legacyDef ? await legacyDef.handler(refreshCtx, {}) : await procedureDef.handler(refreshCtx, {});
1114
- dispatchEvent({ type: "emit", queryKey: key, data: result });
1115
- } catch (e) {
1116
- console.warn(`[gencow] refresh("${key}") failed:`, e instanceof Error ? e.message : e);
1117
- }
1118
- }
1119
- _pendingRefresh.length = 0;
1120
- },
1121
- async _flushRealtimeEvents() {
1122
- while (pendingEvents.length > 0) {
1123
- const event = pendingEvents.shift();
1124
- await options.fanout.notify(event);
1125
- }
1126
- }
1127
- };
1128
- }
1129
-
1130
- // ../core/src/reactive-mutation.ts
1131
- if (!globalThis.__gencow_mutationRegistry) globalThis.__gencow_mutationRegistry = [];
1132
- var mutationRegistry = globalThis.__gencow_mutationRegistry;
1133
- var mutationCounter = 0;
1134
- function mutation(nameOrInvalidatesOrDef, handlerOrDef, name) {
1135
- let argsSchema;
1136
- let actualHandler;
1137
- let mutName;
1138
- let isPublic = false;
1139
- if (typeof nameOrInvalidatesOrDef === "string") {
1140
- mutName = nameOrInvalidatesOrDef;
1141
- const def2 = handlerOrDef;
1142
- actualHandler = def2.handler;
1143
- argsSchema = def2.args;
1144
- isPublic = def2.public === true;
1145
- } else if (Array.isArray(nameOrInvalidatesOrDef)) {
1146
- actualHandler = handlerOrDef;
1147
- mutName = name || `mutation_${++mutationCounter}`;
1148
- } else {
1149
- actualHandler = nameOrInvalidatesOrDef.handler;
1150
- argsSchema = nameOrInvalidatesOrDef.args;
1151
- isPublic = nameOrInvalidatesOrDef.public === true;
1152
- mutName = nameOrInvalidatesOrDef.name || (typeof name === "string" ? name : "") || `mutation_${++mutationCounter}`;
1153
- }
1154
- if (mutName.startsWith("mutation_")) {
1155
- console.warn(
1156
- `[gencow] mutation registered without explicit name \u2192 "${mutName}". Use mutation("myMutation", { handler }) for better debugging.`
1157
- );
1158
- }
1159
- const def = {
1160
- name: mutName,
1161
- handler: actualHandler,
1162
- argsSchema,
1163
- isPublic
1164
- };
1165
- mutationRegistry.push(def);
1166
- return def;
1167
- }
1168
- function getRegisteredMutations() {
1169
- return [...mutationRegistry];
1170
- }
1171
-
1172
- // ../core/src/v.ts
1173
- var GencowValidationError = class extends Error {
1174
- statusCode = 400;
1175
- constructor(message) {
1176
- super(message);
1177
- this.name = "GencowValidationError";
1178
- }
1179
- };
1180
- var GENCOW_OPTIONAL_INNER = /* @__PURE__ */ Symbol.for("gencow.v.optionalInner");
1181
- function assertStandardResult(value) {
1182
- return { value };
1183
- }
1184
- function failStandardResult(message) {
1185
- return { issues: [{ message }] };
1186
- }
1187
- function readJsonSchemaFragment(validator) {
1188
- const opt = validator[GENCOW_OPTIONAL_INNER];
1189
- const target = opt ?? validator;
1190
- const raw = target["~standard"]?.jsonSchema;
1191
- if (raw === void 0) return {};
1192
- return typeof raw === "function" ? raw() : raw;
1193
- }
1194
- function isOptionalValidator(validator) {
1195
- return !!validator && Object.prototype.hasOwnProperty.call(validator, GENCOW_OPTIONAL_INNER);
1196
- }
1197
- function readGencowStandardJsonSchema(schema) {
1198
- if (!schema || typeof schema !== "object" || !("~standard" in schema)) return void 0;
1199
- const props = schema["~standard"];
1200
- if (!props || props.vendor !== "gencow") return void 0;
1201
- const raw = props.jsonSchema;
1202
- if (raw === void 0) return void 0;
1203
- return typeof raw === "function" ? raw() : raw;
1204
- }
1205
- function isRecord(value) {
1206
- return typeof value === "object" && value !== null && !Array.isArray(value);
1207
- }
1208
- function isAllOptionalObjectSchema(schema) {
1209
- const jsonSchema = readGencowStandardJsonSchema(schema);
1210
- if (!isRecord(jsonSchema) || jsonSchema.type !== "object") return false;
1211
- const required = jsonSchema.required;
1212
- return required === void 0 || Array.isArray(required) && required.length === 0;
1213
- }
1214
- function normalizeMissingOptionalObjectInput(schema, value) {
1215
- if (value !== void 0) return value;
1216
- return isAllOptionalObjectSchema(schema) ? {} : value;
1217
- }
1218
- function matchesMimeType(fileType, pattern) {
1219
- const normalizedFileType = fileType.split(";")[0].trim().toLowerCase();
1220
- const normalizedPattern = pattern.trim().toLowerCase();
1221
- if (normalizedPattern === "*/*") return true;
1222
- if (normalizedPattern.endsWith("/*")) {
1223
- const prefix = normalizedPattern.slice(0, -1);
1224
- return normalizedFileType.startsWith(prefix);
1225
- }
1226
- return normalizedFileType === normalizedPattern;
1227
- }
1228
- function buildObjectJsonSchema(fields) {
1229
- const properties = {};
1230
- const required = [];
1231
- for (const key of Object.keys(fields)) {
1232
- const field = fields[key];
1233
- properties[key] = readJsonSchemaFragment(field);
1234
- if (!isOptionalValidator(field)) {
1235
- required.push(key);
1236
- }
1237
- }
1238
- return {
1239
- type: "object",
1240
- properties,
1241
- ...required.length > 0 ? { required } : {}
1242
- };
1243
- }
1244
- function createValidator(config) {
1245
- const { parse, _type, jsonSchema } = config;
1246
- const standard = {
1247
- version: 1,
1248
- vendor: "gencow",
1249
- types: {},
1250
- validate: async (value) => {
1251
- try {
1252
- return assertStandardResult(parse(value));
1253
- } catch (e) {
1254
- const message = e instanceof Error ? e.message : "Validation failed";
1255
- return failStandardResult(message);
1256
- }
1257
- },
1258
- ...jsonSchema !== void 0 ? { jsonSchema } : {}
907
+ } : {}
1259
908
  };
1260
909
  return {
1261
910
  parse,
@@ -1264,6 +913,16 @@ function createValidator(config) {
1264
913
  };
1265
914
  }
1266
915
  var v = {
916
+ literal(expected) {
917
+ return createValidator({
918
+ parse: (value) => {
919
+ if (value !== expected) throw new Error(`Expected literal ${JSON.stringify(expected)}`);
920
+ return expected;
921
+ },
922
+ _type: expected,
923
+ jsonSchema: { const: expected, enum: [expected] }
924
+ });
925
+ },
1267
926
  string() {
1268
927
  return createValidator({
1269
928
  parse: (val) => {
@@ -1353,108 +1012,647 @@ var v = {
1353
1012
  for (const key in fields) {
1354
1013
  result[key] = fields[key].parse(val[key]);
1355
1014
  }
1356
- return result;
1357
- },
1358
- _type: {},
1359
- jsonSchema: buildObjectJsonSchema(fields)
1360
- });
1361
- },
1362
- array(inner) {
1363
- const items = readJsonSchemaFragment(inner);
1364
- return createValidator({
1365
- parse: (val) => {
1366
- if (!Array.isArray(val)) throw new Error("Expected array");
1367
- return val.map((item) => inner.parse(item));
1368
- },
1369
- _type: [],
1370
- jsonSchema: { type: "array", items }
1371
- });
1372
- },
1373
- /** Browser `File` / `Blob` for procedure multipart uploads. */
1374
- file() {
1375
- const base = createValidator({
1376
- parse: (val) => {
1377
- if (!(val instanceof Blob)) {
1378
- throw new Error(`Expected file, got ${typeof val}`);
1015
+ return result;
1016
+ },
1017
+ _type: {},
1018
+ jsonSchema: buildObjectJsonSchema(fields)
1019
+ });
1020
+ },
1021
+ array(inner) {
1022
+ const items = readJsonSchemaFragment(inner);
1023
+ return createValidator({
1024
+ parse: (val) => {
1025
+ if (!Array.isArray(val)) throw new Error("Expected array");
1026
+ return val.map((item) => inner.parse(item));
1027
+ },
1028
+ _type: [],
1029
+ jsonSchema: { type: "array", items }
1030
+ });
1031
+ },
1032
+ /** Browser `File` / `Blob` for procedure multipart uploads. */
1033
+ file() {
1034
+ const base = createValidator({
1035
+ parse: (val) => {
1036
+ if (!(val instanceof Blob)) {
1037
+ throw new Error(`Expected file, got ${typeof val}`);
1038
+ }
1039
+ return val;
1040
+ },
1041
+ _type: null,
1042
+ jsonSchema: { type: "string", format: "binary", "x-gencow-type": "file" }
1043
+ });
1044
+ return Object.assign(base, {
1045
+ mime(mimeType) {
1046
+ return createValidator({
1047
+ parse: (val) => {
1048
+ const blob = base.parse(val);
1049
+ if (!matchesMimeType(blob.type, mimeType)) {
1050
+ throw new Error(
1051
+ `Expected a file of type ${mimeType} but got ${blob.type || "unknown"}`
1052
+ );
1053
+ }
1054
+ return blob;
1055
+ },
1056
+ _type: null,
1057
+ jsonSchema: {
1058
+ type: "string",
1059
+ format: "binary",
1060
+ "x-gencow-type": "file",
1061
+ "x-gencow-mime": mimeType
1062
+ }
1063
+ });
1064
+ }
1065
+ });
1066
+ }
1067
+ };
1068
+ function parseArgs(schema, args) {
1069
+ if (!schema) return args;
1070
+ if (typeof schema.parse === "function") {
1071
+ try {
1072
+ return schema.parse(normalizeMissingOptionalObjectInput(schema, args));
1073
+ } catch (e) {
1074
+ throw new GencowValidationError(e.message);
1075
+ }
1076
+ }
1077
+ if (typeof schema === "object" && schema !== null) {
1078
+ const schemaKeys = Object.keys(schema);
1079
+ if (schemaKeys.length === 0) return args;
1080
+ if (typeof args !== "object" || args === null) {
1081
+ throw new GencowValidationError("Expected an object for arguments");
1082
+ }
1083
+ const result = {};
1084
+ for (const key of schemaKeys) {
1085
+ const validator = schema[key];
1086
+ if (validator && typeof validator.parse === "function") {
1087
+ try {
1088
+ result[key] = validator.parse(args[key]);
1089
+ } catch (e) {
1090
+ throw new GencowValidationError(`Argument "${key}": ${e.message}`);
1091
+ }
1092
+ } else {
1093
+ result[key] = args[key];
1094
+ }
1095
+ }
1096
+ return result;
1097
+ }
1098
+ return args;
1099
+ }
1100
+
1101
+ // ../core/src/realtime-channel-definition.ts
1102
+ var RealtimeChannelBuilderImpl = class _RealtimeChannelBuilderImpl {
1103
+ constructor(channelName, inputValidator, eventValidator, keyBuilder) {
1104
+ this.channelName = channelName;
1105
+ this.inputValidator = inputValidator;
1106
+ this.eventValidator = eventValidator;
1107
+ this.keyBuilder = keyBuilder;
1108
+ }
1109
+ channelName;
1110
+ inputValidator;
1111
+ eventValidator;
1112
+ keyBuilder;
1113
+ name(name) {
1114
+ return new _RealtimeChannelBuilderImpl(
1115
+ name,
1116
+ this.inputValidator,
1117
+ this.eventValidator,
1118
+ this.keyBuilder
1119
+ );
1120
+ }
1121
+ input(schema) {
1122
+ return new _RealtimeChannelBuilderImpl(
1123
+ this.channelName,
1124
+ schema,
1125
+ this.eventValidator,
1126
+ this.keyBuilder
1127
+ );
1128
+ }
1129
+ event(schema) {
1130
+ return new _RealtimeChannelBuilderImpl(
1131
+ this.channelName,
1132
+ this.inputValidator,
1133
+ schema,
1134
+ this.keyBuilder
1135
+ );
1136
+ }
1137
+ channelKey(build) {
1138
+ return new _RealtimeChannelBuilderImpl(
1139
+ this.channelName,
1140
+ this.inputValidator,
1141
+ this.eventValidator,
1142
+ build
1143
+ );
1144
+ }
1145
+ authorize(handler) {
1146
+ if (!this.channelName?.trim()) throw new Error("Realtime channel name is required before authorize()");
1147
+ return Object.freeze({
1148
+ kind: "realtime_channel",
1149
+ name: this.channelName,
1150
+ ...this.inputValidator ? { inputSchema: this.inputValidator } : {},
1151
+ ...this.eventValidator ? { eventSchema: this.eventValidator } : {},
1152
+ ...this.keyBuilder ? { channelKey: this.keyBuilder } : {},
1153
+ authorize: handler
1154
+ });
1155
+ }
1156
+ };
1157
+ function createGencowRealtimeChannelBuilder() {
1158
+ return new RealtimeChannelBuilderImpl();
1159
+ }
1160
+ function hasStandardSchema2(schema) {
1161
+ return !!schema && typeof schema === "object" && typeof schema["~standard"]?.validate === "function";
1162
+ }
1163
+ async function parseStandardSchema(schema, value) {
1164
+ if (!schema) return value;
1165
+ if (!hasStandardSchema2(schema)) {
1166
+ throw new Error("Realtime channels require a Standard Schema v1 validator");
1167
+ }
1168
+ const normalized = normalizeMissingOptionalObjectInput(schema, value);
1169
+ const result = await schema["~standard"].validate(normalized);
1170
+ if ("issues" in result && result.issues?.length) {
1171
+ throw new GencowValidationError(result.issues[0]?.message ?? "Validation failed");
1172
+ }
1173
+ if ("value" in result) return result.value;
1174
+ throw new GencowValidationError("Validation failed");
1175
+ }
1176
+ var RealtimeChannelEventValidationError = class extends GencowValidationError {
1177
+ code = "REALTIME_CHANNEL_EVENT_INVALID";
1178
+ constructor(message) {
1179
+ super(message);
1180
+ this.name = "RealtimeChannelEventValidationError";
1181
+ }
1182
+ };
1183
+ async function parseRealtimeChannelEvent(schema, value) {
1184
+ try {
1185
+ return await parseStandardSchema(schema, value);
1186
+ } catch (error) {
1187
+ if (error instanceof GencowValidationError) {
1188
+ throw new RealtimeChannelEventValidationError(error.message);
1189
+ }
1190
+ throw error;
1191
+ }
1192
+ }
1193
+ function assertJsonWireValue(value, seen) {
1194
+ if (value === null || typeof value === "string" || typeof value === "boolean") return;
1195
+ if (typeof value === "number") {
1196
+ if (Number.isFinite(value)) return;
1197
+ throw new RealtimeChannelEventValidationError("Realtime channel events must use finite JSON numbers");
1198
+ }
1199
+ if (typeof value !== "object") {
1200
+ throw new RealtimeChannelEventValidationError(
1201
+ "Realtime channel events must be JSON-serializable wire values"
1202
+ );
1203
+ }
1204
+ if (seen.has(value)) {
1205
+ throw new RealtimeChannelEventValidationError("Realtime channel events must not contain circular values");
1206
+ }
1207
+ seen.add(value);
1208
+ if (Array.isArray(value)) {
1209
+ for (const item of value) assertJsonWireValue(item, seen);
1210
+ } else {
1211
+ const prototype = Object.getPrototypeOf(value);
1212
+ if (prototype !== Object.prototype && prototype !== null) {
1213
+ throw new RealtimeChannelEventValidationError("Realtime channel events must use plain JSON objects");
1214
+ }
1215
+ for (const child of Object.values(value)) assertJsonWireValue(child, seen);
1216
+ }
1217
+ seen.delete(value);
1218
+ }
1219
+ function normalizeRealtimeChannelEventWireValue(value) {
1220
+ assertJsonWireValue(value, /* @__PURE__ */ new WeakSet());
1221
+ try {
1222
+ const serialized = JSON.stringify(value);
1223
+ if (serialized === void 0) {
1224
+ throw new RealtimeChannelEventValidationError(
1225
+ "Realtime channel events must be JSON-serializable wire values"
1226
+ );
1227
+ }
1228
+ return JSON.parse(serialized);
1229
+ } catch (error) {
1230
+ if (error instanceof RealtimeChannelEventValidationError) throw error;
1231
+ throw new RealtimeChannelEventValidationError(
1232
+ "Realtime channel events must be JSON-serializable wire values"
1233
+ );
1234
+ }
1235
+ }
1236
+ function resolveExplicitRealtimeChannelKey(definition, input) {
1237
+ const key = definition.channelKey?.(input) ?? buildRealtimeChannelKey(definition.name, input);
1238
+ if (typeof key !== "string" || key.length === 0) {
1239
+ throw new Error(`Realtime channel "${definition.name}" resolved to an invalid channelKey`);
1240
+ }
1241
+ return key;
1242
+ }
1243
+
1244
+ // ../core/src/realtime-channel-publisher.ts
1245
+ function createExplicitRealtimeChannelPublisher(definition, args, runtime) {
1246
+ async function resolveKey() {
1247
+ const parsedArgs = await parseStandardSchema(definition.inputSchema, args);
1248
+ return resolveExplicitRealtimeChannelKey(definition, parsedArgs);
1249
+ }
1250
+ return {
1251
+ async publish(data, options) {
1252
+ const channelKey = await resolveKey();
1253
+ const parsedEvent = await parseRealtimeChannelEvent(definition.eventSchema, data);
1254
+ const wireEvent = normalizeRealtimeChannelEventWireValue(parsedEvent);
1255
+ runtime.markEmitted();
1256
+ runtime.dispatch({
1257
+ type: "channel_publish",
1258
+ channelKey,
1259
+ data: wireEvent,
1260
+ eventId: options?.eventId ?? runtime.createEventId()
1261
+ });
1262
+ },
1263
+ async invalidate() {
1264
+ const channelKey = await resolveKey();
1265
+ runtime.markEmitted();
1266
+ runtime.dispatch({ type: "channel_invalidate", channelKey });
1267
+ },
1268
+ async revoke(options) {
1269
+ const channelKey = await resolveKey();
1270
+ const epoch = normalizeRealtimeChannelEpoch(options?.epoch);
1271
+ const reason = typeof options?.reason === "string" && options.reason.length > 0 ? options.reason : void 0;
1272
+ runtime.markEmitted();
1273
+ runtime.dispatch({
1274
+ type: "channel_revoke",
1275
+ channelKey,
1276
+ ...epoch ? { epoch } : {},
1277
+ ...reason ? { reason } : {}
1278
+ });
1279
+ }
1280
+ };
1281
+ }
1282
+
1283
+ // ../core/src/reactive-realtime.ts
1284
+ var SUBSCRIPTION_KEY_SEPARATOR = "::";
1285
+ function createRealtimeEventId() {
1286
+ const randomUUID = globalThis.crypto?.randomUUID;
1287
+ if (typeof randomUUID === "function") return `evt_${randomUUID.call(globalThis.crypto)}`;
1288
+ return `evt_${Date.now().toString(36)}_${Math.random().toString(36).slice(2)}`;
1289
+ }
1290
+ function normalizeForStableJson3(value) {
1291
+ if (value === void 0) return void 0;
1292
+ if (value === null) return null;
1293
+ if (value instanceof Date) return value.toISOString();
1294
+ if (Array.isArray(value)) return value.map((item) => normalizeForStableJson3(item));
1295
+ if (typeof value === "object") {
1296
+ const source = value;
1297
+ const sorted = {};
1298
+ for (const key of Object.keys(source).sort()) {
1299
+ const normalized = normalizeForStableJson3(source[key]);
1300
+ if (normalized !== void 0) sorted[key] = normalized;
1301
+ }
1302
+ return sorted;
1303
+ }
1304
+ return value;
1305
+ }
1306
+ function isEmptyPlainObject3(value) {
1307
+ return !!value && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === 0;
1308
+ }
1309
+ function buildQuerySubscriptionKey(queryKey, args) {
1310
+ const normalized = normalizeForStableJson3(args);
1311
+ if (normalized === void 0 || isEmptyPlainObject3(normalized)) return queryKey;
1312
+ return `${queryKey}${SUBSCRIPTION_KEY_SEPARATOR}${JSON.stringify(normalized)}`;
1313
+ }
1314
+ function subscriptionKeyMatchesQueryKey(subscriptionKey, queryKey) {
1315
+ return subscriptionKey === queryKey || subscriptionKey.startsWith(`${queryKey}${SUBSCRIPTION_KEY_SEPARATOR}`);
1316
+ }
1317
+ function buildRealtimeCtx(options) {
1318
+ const pendingEmits = /* @__PURE__ */ new Map();
1319
+ const pendingEvents = [];
1320
+ const _pendingRefresh = [];
1321
+ let _hasEmitted = false;
1322
+ function dispatchEvent(event) {
1323
+ if (options.deferFanout) {
1324
+ pendingEvents.push(event);
1325
+ return;
1326
+ }
1327
+ void options.fanout.notify(event);
1328
+ }
1329
+ return {
1330
+ emit(queryKey, data) {
1331
+ _hasEmitted = true;
1332
+ if (options.deferFanout) {
1333
+ pendingEvents.push({ type: "emit", queryKey, data });
1334
+ return;
1335
+ }
1336
+ const existing = pendingEmits.get(queryKey);
1337
+ if (existing) clearTimeout(existing.timer);
1338
+ const timer = setTimeout(() => {
1339
+ pendingEmits.delete(queryKey);
1340
+ dispatchEvent({ type: "emit", queryKey, data });
1341
+ }, 50);
1342
+ pendingEmits.set(queryKey, { data, timer });
1343
+ },
1344
+ invalidate(queryKey) {
1345
+ _hasEmitted = true;
1346
+ const queryKeys = Array.isArray(queryKey) ? [...new Set(queryKey)] : [queryKey];
1347
+ dispatchEvent({ type: "invalidate", queryKeys });
1348
+ },
1349
+ refresh(queryKey) {
1350
+ _hasEmitted = true;
1351
+ if (!_pendingRefresh.includes(queryKey)) {
1352
+ _pendingRefresh.push(queryKey);
1353
+ }
1354
+ },
1355
+ channel: function(channel, args) {
1356
+ if (typeof channel !== "string") {
1357
+ const selected2 = options.realtimeChannelResolver?.get(channel.name);
1358
+ if (options.realtimeChannelResolver && (!selected2 || selected2.source !== "explicit" || selected2.definition !== channel)) {
1359
+ throw new Error(`REALTIME_CHANNEL_NOT_DECLARED: ${channel.name}`);
1360
+ }
1361
+ return createExplicitRealtimeChannelPublisher(channel, args, {
1362
+ markEmitted: () => {
1363
+ _hasEmitted = true;
1364
+ },
1365
+ dispatch: dispatchEvent,
1366
+ createEventId: createRealtimeEventId
1367
+ });
1368
+ }
1369
+ const selected = options.realtimeChannelResolver?.get(channel);
1370
+ if (!options.realtimeChannelResolver) {
1371
+ throw new Error(
1372
+ "REALTIME_CHANNEL_CATALOG_REQUIRED: string realtime channels require an injected selected catalog"
1373
+ );
1374
+ }
1375
+ if (selected?.source === "explicit") {
1376
+ const publisher = createExplicitRealtimeChannelPublisher(selected.definition, args, {
1377
+ markEmitted: () => {
1378
+ _hasEmitted = true;
1379
+ },
1380
+ dispatch: dispatchEvent,
1381
+ createEventId: createRealtimeEventId
1382
+ });
1383
+ return {
1384
+ ...publisher,
1385
+ authorize() {
1386
+ const error = new Error(
1387
+ "REALTIME_CHANNEL_EXPLICIT_SIGNER_UNSUPPORTED: explicit channels must be authorized by the subscription route"
1388
+ );
1389
+ error.code = "REALTIME_CHANNEL_EXPLICIT_SIGNER_UNSUPPORTED";
1390
+ throw error;
1391
+ }
1392
+ };
1393
+ }
1394
+ if (!selected) throw new Error(`REALTIME_CHANNEL_NOT_DECLARED: ${channel}`);
1395
+ const channelKey = selected.definition.channelKey?.(args) ?? buildRealtimeChannelKey(channel, args);
1396
+ if (!channelKey) throw new Error(`Realtime channel "${channel}" resolved to an invalid channelKey`);
1397
+ return {
1398
+ authorize(authOptions) {
1399
+ if (!options.appName || !options.channelTokenSecret || !options.auth) {
1400
+ throw new Error("Authorized realtime channel token signer is not configured");
1401
+ }
1402
+ const user = options.auth.requireAuth();
1403
+ const nowMs = authOptions?.nowMs ?? Date.now();
1404
+ const rawTtl = authOptions?.ttlSeconds;
1405
+ const ttlSeconds = Number.isFinite(rawTtl) && rawTtl > 0 ? Math.min(rawTtl, 3600) : 300;
1406
+ const epoch = normalizeRealtimeChannelEpoch(authOptions?.epoch);
1407
+ return {
1408
+ token: createRealtimeChannelToken({
1409
+ appName: options.appName,
1410
+ channel,
1411
+ channelKey,
1412
+ args,
1413
+ userId: user.id,
1414
+ secret: options.channelTokenSecret,
1415
+ ttlSeconds,
1416
+ nowMs,
1417
+ nonce: authOptions?.nonce,
1418
+ epoch
1419
+ }),
1420
+ channel,
1421
+ channelKey,
1422
+ userId: user.id,
1423
+ expiresAt: nowMs + Math.floor(ttlSeconds * 1e3),
1424
+ ...epoch ? { epoch } : {}
1425
+ };
1426
+ },
1427
+ publish(data, publishOptions) {
1428
+ _hasEmitted = true;
1429
+ dispatchEvent({
1430
+ type: "channel_publish",
1431
+ channelKey,
1432
+ data,
1433
+ eventId: publishOptions?.eventId ?? createRealtimeEventId()
1434
+ });
1435
+ },
1436
+ invalidate() {
1437
+ _hasEmitted = true;
1438
+ dispatchEvent({ type: "channel_invalidate", channelKey });
1439
+ },
1440
+ revoke(revokeOptions) {
1441
+ _hasEmitted = true;
1442
+ const epoch = normalizeRealtimeChannelEpoch(revokeOptions?.epoch);
1443
+ const reason = typeof revokeOptions?.reason === "string" && revokeOptions.reason.length > 0 ? revokeOptions.reason : void 0;
1444
+ dispatchEvent({
1445
+ type: "channel_revoke",
1446
+ channelKey,
1447
+ ...epoch ? { epoch } : {},
1448
+ ...reason ? { reason } : {}
1449
+ });
1450
+ }
1451
+ };
1452
+ },
1453
+ query(query2, args) {
1454
+ const subscriptionKey = resolveRealtimeQuerySubscriptionKey({ query: query2, args });
1455
+ if (!subscriptionKey)
1456
+ throw new Error(`Realtime query "${query2}" resolved to an invalid subscriptionKey`);
1457
+ const scopeHash = resolveRealtimeQueryScopeHash({ query: query2, args, subscriptionKey });
1458
+ return {
1459
+ authorize(authOptions) {
1460
+ if (!options.appName || !options.channelTokenSecret || !options.auth) {
1461
+ throw new Error("Authorized realtime query token signer is not configured");
1462
+ }
1463
+ const user = options.auth.requireAuth();
1464
+ const nowMs = authOptions?.nowMs ?? Date.now();
1465
+ const rawTtl = authOptions?.ttlSeconds;
1466
+ const ttlSeconds = Number.isFinite(rawTtl) && rawTtl > 0 ? Math.min(rawTtl, 3600) : 300;
1467
+ const epoch = normalizeRealtimeQuerySubscriptionEpoch(authOptions?.epoch);
1468
+ return {
1469
+ token: createRealtimeQuerySubscriptionToken({
1470
+ appName: options.appName,
1471
+ query: query2,
1472
+ subscriptionKey,
1473
+ args,
1474
+ scopeHash,
1475
+ policy: "authorized",
1476
+ userId: user.id,
1477
+ secret: options.channelTokenSecret,
1478
+ ttlSeconds,
1479
+ nowMs,
1480
+ nonce: authOptions?.nonce,
1481
+ epoch
1482
+ }),
1483
+ query: query2,
1484
+ subscriptionKey,
1485
+ policy: "authorized",
1486
+ userId: user.id,
1487
+ expiresAt: nowMs + Math.floor(ttlSeconds * 1e3),
1488
+ ...epoch ? { epoch } : {}
1489
+ };
1490
+ },
1491
+ invalidate(invalidateOptions) {
1492
+ _hasEmitted = true;
1493
+ dispatchEvent({
1494
+ type: "query_invalidate",
1495
+ query: query2,
1496
+ subscriptionKey,
1497
+ scopeHash,
1498
+ eventId: invalidateOptions?.eventId ?? createRealtimeEventId()
1499
+ });
1500
+ },
1501
+ revoke(revokeOptions) {
1502
+ _hasEmitted = true;
1503
+ const epoch = normalizeRealtimeQuerySubscriptionEpoch(revokeOptions?.epoch);
1504
+ const reason = typeof revokeOptions?.reason === "string" && revokeOptions.reason.length > 0 ? revokeOptions.reason : void 0;
1505
+ dispatchEvent({
1506
+ type: "query_revoke",
1507
+ query: query2,
1508
+ subscriptionKey,
1509
+ scopeHash,
1510
+ ...epoch ? { epoch } : {},
1511
+ ...reason ? { reason } : {}
1512
+ });
1513
+ }
1514
+ };
1515
+ },
1516
+ invalidateScoped(input) {
1517
+ _hasEmitted = true;
1518
+ const subscriptionKey = input.subscriptionKey ?? resolveRealtimeQuerySubscriptionKey({ query: input.query, args: input.args });
1519
+ if (!subscriptionKey)
1520
+ throw new Error(`Realtime query "${input.query}" resolved to an invalid subscriptionKey`);
1521
+ const scopeHash = input.scopeHash ?? resolveRealtimeQueryScopeHash({
1522
+ query: input.query,
1523
+ args: input.args,
1524
+ subscriptionKey
1525
+ });
1526
+ dispatchEvent({
1527
+ type: "query_invalidate",
1528
+ query: input.query,
1529
+ subscriptionKey,
1530
+ scopeHash,
1531
+ eventId: input.eventId ?? createRealtimeEventId()
1532
+ });
1533
+ },
1534
+ invalidateCrudQuery(input) {
1535
+ _hasEmitted = true;
1536
+ const subscriptionKey = input.subscriptionKey ?? resolveRealtimeQuerySubscriptionKey({ query: input.query, args: input.args });
1537
+ if (!subscriptionKey)
1538
+ throw new Error(`Realtime query "${input.query}" resolved to an invalid subscriptionKey`);
1539
+ const scopeHash = input.scopeHash ?? resolveRealtimeQueryScopeHash({
1540
+ query: input.query,
1541
+ args: input.args,
1542
+ subscriptionKey
1543
+ });
1544
+ dispatchEvent({
1545
+ type: "query_invalidate",
1546
+ query: input.query,
1547
+ subscriptionKey,
1548
+ scopeHash,
1549
+ eventId: input.eventId ?? createRealtimeEventId(),
1550
+ legacyQueryKey: input.legacyQueryKey,
1551
+ crudRealtimeEmitMode: input.crudRealtimeEmitMode ?? options.generatedCrudRealtimeEmitMode ?? "dual"
1552
+ });
1553
+ },
1554
+ get _hasEmitted() {
1555
+ return _hasEmitted;
1556
+ },
1557
+ get _pendingRefresh() {
1558
+ return [..._pendingRefresh];
1559
+ },
1560
+ async _flushRefresh() {
1561
+ if (_pendingRefresh.length === 0) return;
1562
+ const qMap = options.queryMap ?? queryRegistry;
1563
+ for (const key of _pendingRefresh) {
1564
+ const legacyDef = qMap.get(key);
1565
+ const procedureDef = legacyDef ? void 0 : options.getProcedureQuery?.(key);
1566
+ if (!legacyDef && !procedureDef) {
1567
+ console.warn(`[gencow] refresh("${key}"): query not found in registry. Skipping.`);
1568
+ continue;
1569
+ }
1570
+ if (procedureDef?.inputSchema) {
1571
+ options.fanout.notify({ type: "invalidate", queryKeys: [key] });
1572
+ continue;
1379
1573
  }
1380
- return val;
1381
- },
1382
- _type: null,
1383
- jsonSchema: { type: "string", format: "binary", "x-gencow-type": "file" }
1384
- });
1385
- return Object.assign(base, {
1386
- mime(mimeType) {
1387
- return createValidator({
1388
- parse: (val) => {
1389
- const blob = base.parse(val);
1390
- if (!matchesMimeType(blob.type, mimeType)) {
1391
- throw new Error(
1392
- `Expected a file of type ${mimeType} but got ${blob.type || "unknown"}`
1393
- );
1394
- }
1395
- return blob;
1396
- },
1397
- _type: null,
1398
- jsonSchema: {
1399
- type: "string",
1400
- format: "binary",
1401
- "x-gencow-type": "file",
1402
- "x-gencow-mime": mimeType
1403
- }
1404
- });
1405
- }
1406
- });
1407
- }
1408
- };
1409
- function parseArgs(schema, args) {
1410
- if (!schema) return args;
1411
- if (typeof schema.parse === "function") {
1412
- try {
1413
- return schema.parse(normalizeMissingOptionalObjectInput(schema, args));
1414
- } catch (e) {
1415
- throw new GencowValidationError(e.message);
1416
- }
1417
- }
1418
- if (typeof schema === "object" && schema !== null) {
1419
- const schemaKeys = Object.keys(schema);
1420
- if (schemaKeys.length === 0) return args;
1421
- if (typeof args !== "object" || args === null) {
1422
- throw new GencowValidationError("Expected an object for arguments");
1423
- }
1424
- const result = {};
1425
- for (const key of schemaKeys) {
1426
- const validator = schema[key];
1427
- if (validator && typeof validator.parse === "function") {
1428
1574
  try {
1429
- result[key] = validator.parse(args[key]);
1575
+ if (!options.buildCtxForRefresh) {
1576
+ console.warn(
1577
+ `[gencow] \u26A0\uFE0F refresh("${key}"): buildCtxForRefresh not provided. Query handler will receive an empty ctx \u2014 ctx.db will be undefined. This is a framework configuration error. \u{1F4A1} Ensure buildRealtimeCtx() receives a buildCtxForRefresh callback.`
1578
+ );
1579
+ }
1580
+ const refreshCtx = options.buildCtxForRefresh?.() ?? {};
1581
+ const result = legacyDef ? await legacyDef.handler(refreshCtx, {}) : await procedureDef.handler(refreshCtx, {});
1582
+ dispatchEvent({ type: "emit", queryKey: key, data: result });
1430
1583
  } catch (e) {
1431
- throw new GencowValidationError(`Argument "${key}": ${e.message}`);
1584
+ console.warn(`[gencow] refresh("${key}") failed:`, e instanceof Error ? e.message : e);
1432
1585
  }
1433
- } else {
1434
- result[key] = args[key];
1586
+ }
1587
+ _pendingRefresh.length = 0;
1588
+ },
1589
+ async _flushRealtimeEvents() {
1590
+ while (pendingEvents.length > 0) {
1591
+ const event = pendingEvents.shift();
1592
+ await options.fanout.notify(event);
1435
1593
  }
1436
1594
  }
1437
- return result;
1595
+ };
1596
+ }
1597
+
1598
+ // ../core/src/reactive-mutation.ts
1599
+ if (!globalThis.__gencow_mutationRegistry) globalThis.__gencow_mutationRegistry = [];
1600
+ var mutationRegistry = globalThis.__gencow_mutationRegistry;
1601
+ var mutationCounter = 0;
1602
+ function mutation(nameOrInvalidatesOrDef, handlerOrDef, name) {
1603
+ let argsSchema;
1604
+ let actualHandler;
1605
+ let mutName;
1606
+ let isPublic = false;
1607
+ if (typeof nameOrInvalidatesOrDef === "string") {
1608
+ mutName = nameOrInvalidatesOrDef;
1609
+ const def2 = handlerOrDef;
1610
+ actualHandler = def2.handler;
1611
+ argsSchema = def2.args;
1612
+ isPublic = def2.public === true;
1613
+ } else if (Array.isArray(nameOrInvalidatesOrDef)) {
1614
+ actualHandler = handlerOrDef;
1615
+ mutName = name || `mutation_${++mutationCounter}`;
1616
+ } else {
1617
+ actualHandler = nameOrInvalidatesOrDef.handler;
1618
+ argsSchema = nameOrInvalidatesOrDef.args;
1619
+ isPublic = nameOrInvalidatesOrDef.public === true;
1620
+ mutName = nameOrInvalidatesOrDef.name || (typeof name === "string" ? name : "") || `mutation_${++mutationCounter}`;
1438
1621
  }
1439
- return args;
1622
+ if (mutName.startsWith("mutation_")) {
1623
+ console.warn(
1624
+ `[gencow] mutation registered without explicit name \u2192 "${mutName}". Use mutation("myMutation", { handler }) for better debugging.`
1625
+ );
1626
+ }
1627
+ const def = {
1628
+ name: mutName,
1629
+ handler: actualHandler,
1630
+ argsSchema,
1631
+ isPublic
1632
+ };
1633
+ mutationRegistry.push(def);
1634
+ return def;
1635
+ }
1636
+ function getRegisteredMutations() {
1637
+ return [...mutationRegistry];
1440
1638
  }
1441
1639
 
1442
1640
  // ../core/src/procedure.ts
1443
- function hasStandardSchema2(schema) {
1641
+ function hasStandardSchema3(schema) {
1444
1642
  return !!schema && typeof schema === "object" && "~standard" in schema && !!schema["~standard"] && typeof schema["~standard"].validate === "function";
1445
1643
  }
1446
1644
  async function validateWithSchema(schema, value, options) {
1447
1645
  if (!schema) return value;
1448
1646
  const valueToValidate = options?.normalizeMissingOptionalObject ? normalizeMissingOptionalObjectInput(schema, value) : value;
1449
- if (hasStandardSchema2(schema)) {
1647
+ if (hasStandardSchema3(schema)) {
1450
1648
  const result = await schema["~standard"].validate(valueToValidate);
1451
1649
  if ("issues" in result && result.issues.length > 0) {
1452
- throw new Error(result.issues[0]?.message ?? "Validation failed");
1650
+ throw new GencowValidationError(result.issues[0]?.message ?? "Validation failed");
1453
1651
  }
1454
1652
  if ("value" in result) {
1455
1653
  return result.value;
1456
1654
  }
1457
- throw new Error("Validation failed");
1655
+ throw new GencowValidationError("Validation failed");
1458
1656
  }
1459
1657
  throw new Error("procedure.query / procedure.mutation / procedure.internal only supports Standard Schema v1 validators");
1460
1658
  }
@@ -1632,6 +1830,71 @@ function getRegisteredHttpActions() {
1632
1830
  return [...httpActionRegistry];
1633
1831
  }
1634
1832
 
1833
+ // ../core/src/realtime-channel-registry.ts
1834
+ if (!globalThis.__gencow_realtimeChannelRegistry) {
1835
+ globalThis.__gencow_realtimeChannelRegistry = /* @__PURE__ */ new Map();
1836
+ }
1837
+ var realtimeChannelRegistry = globalThis.__gencow_realtimeChannelRegistry;
1838
+ var LEGACY_REGISTRATION_VERSION = /* @__PURE__ */ Symbol("gencow.realtimeChannelRegistrationVersion");
1839
+ var legacyRegistrationVersion = 0;
1840
+ function registrationVersion(definition) {
1841
+ return definition[LEGACY_REGISTRATION_VERSION];
1842
+ }
1843
+ function copyLegacyDefinition(definition) {
1844
+ const copy = { ...definition };
1845
+ const version = registrationVersion(definition);
1846
+ if (version !== void 0) {
1847
+ Object.defineProperty(copy, LEGACY_REGISTRATION_VERSION, { value: version });
1848
+ }
1849
+ return copy;
1850
+ }
1851
+ function defineRealtimeChannels(channels) {
1852
+ for (const [name, def] of Object.entries(channels)) {
1853
+ const registered = {
1854
+ name,
1855
+ argsSchema: def.args,
1856
+ channelKey: def.channelKey,
1857
+ authorize: def.authorize
1858
+ };
1859
+ Object.defineProperty(registered, LEGACY_REGISTRATION_VERSION, { value: ++legacyRegistrationVersion });
1860
+ realtimeChannelRegistry.set(name, registered);
1861
+ }
1862
+ return channels;
1863
+ }
1864
+ function getRealtimeChannelDef(name) {
1865
+ return realtimeChannelRegistry.get(name);
1866
+ }
1867
+ function getRegisteredRealtimeChannels() {
1868
+ return [...realtimeChannelRegistry.keys()];
1869
+ }
1870
+ function captureLegacyRealtimeChannels() {
1871
+ return [...realtimeChannelRegistry.values()].map(copyLegacyDefinition);
1872
+ }
1873
+ function selectLegacyRealtimeChannelSnapshot(before, after) {
1874
+ const prior = new Map(before.map((definition) => [definition.name, definition]));
1875
+ return after.filter((definition) => {
1876
+ const existing = prior.get(definition.name);
1877
+ return !existing || registrationVersion(existing) !== registrationVersion(definition) || existing.authorize !== definition.authorize || existing.channelKey !== definition.channelKey;
1878
+ });
1879
+ }
1880
+ function resolveRealtimeChannelKey(params) {
1881
+ if (params.result === false || params.result === null) return null;
1882
+ if (typeof params.result === "string") return params.result.length > 0 ? params.result : null;
1883
+ if (params.result && typeof params.result === "object" && "channelKey" in params.result) {
1884
+ const channelKey2 = params.result.channelKey;
1885
+ if (channelKey2 !== void 0)
1886
+ return typeof channelKey2 === "string" && channelKey2.length > 0 ? channelKey2 : null;
1887
+ }
1888
+ const def = getRealtimeChannelDef(params.channel);
1889
+ const channelKey = def?.channelKey?.(params.args);
1890
+ if (channelKey !== void 0)
1891
+ return typeof channelKey === "string" && channelKey.length > 0 ? channelKey : null;
1892
+ return buildRealtimeChannelKey(params.channel, params.args);
1893
+ }
1894
+ function resetRealtimeChannelRegistryForTests() {
1895
+ realtimeChannelRegistry.clear();
1896
+ }
1897
+
1635
1898
  // ../core/src/realtime-ticket.ts
1636
1899
  import { createHmac as createHmac3, randomBytes as randomBytes3, timingSafeEqual as timingSafeEqual3 } from "crypto";
1637
1900
  var TICKET_PREFIX = "gencow_ws1";
@@ -1886,7 +2149,7 @@ function isJobAdmissionKillSwitchActive(env, scope) {
1886
2149
  }
1887
2150
 
1888
2151
  // ../core/src/workflow.ts
1889
- import { sql as sql4 } from "drizzle-orm";
2152
+ import { sql as sql3 } from "drizzle-orm";
1890
2153
 
1891
2154
  // ../core/src/workflow-admission.ts
1892
2155
  import { sql } from "drizzle-orm";
@@ -2093,9 +2356,6 @@ function workflowJsonb(value) {
2093
2356
  var WORKFLOW_WAKE_SIGNAL_CHANNEL = "gencow_app_wake_v1";
2094
2357
  var WORKFLOW_WAKE_SIGNAL_PAYLOAD = JSON.stringify({ kind: "workflow" });
2095
2358
 
2096
- // ../core/src/workflows-api.ts
2097
- import { sql as sql3 } from "drizzle-orm";
2098
-
2099
2359
  // ../core/src/workflow-payload-policy.ts
2100
2360
  var CHECKPOINT_MODE_ENV = "GENCOW_WORKFLOW_CHECKPOINT_LIMIT_MODE";
2101
2361
  var CHECKPOINT_SOFT_LIMIT_ENV = "GENCOW_WORKFLOW_CHECKPOINT_SOFT_LIMIT_BYTES";
@@ -2169,232 +2429,173 @@ function assertWorkflowPayloadWithinLimit(params) {
2169
2429
  return sizeBytes;
2170
2430
  }
2171
2431
 
2172
- // ../core/src/workflow-types.ts
2173
- function deriveWorkflowStatus(status, currentStep) {
2174
- if (status !== "pending") {
2175
- return status;
2176
- }
2177
- if (currentStep?.startsWith("sleep#")) {
2178
- return "sleeping";
2179
- }
2180
- if (currentStep?.startsWith("wait:")) {
2181
- return "waiting";
2182
- }
2183
- return "queued";
2184
- }
2185
-
2186
- // ../core/src/workflows-api.ts
2187
- var WORKFLOW_STATUSES = /* @__PURE__ */ new Set(["pending", "running", "completed", "failed"]);
2188
- var WORKFLOW_DERIVED_PENDING_STATUSES = /* @__PURE__ */ new Set(["queued", "waiting", "sleeping"]);
2189
- function rowsFromResult(result) {
2190
- if (Array.isArray(result)) return result;
2191
- if (result && typeof result === "object" && Array.isArray(result.rows)) {
2192
- return result.rows;
2193
- }
2194
- return [];
2432
+ // ../core/src/workflow.ts
2433
+ var workflowRegistry = globalThis.__gencow_workflowRegistry ??= /* @__PURE__ */ new Map();
2434
+ var WORKFLOW_DEFINITION_METADATA = /* @__PURE__ */ Symbol.for("@gencow/core/workflow-definition");
2435
+ var DEFAULT_WORKFLOW_MAX_DURATION_MS = 30 * 60 * 1e3;
2436
+ var DEFAULT_WORKFLOW_MAX_RETRIES = 3;
2437
+ var WORKFLOW_RESUME_ACTION_PREFIX = "__gencow.workflow.resume";
2438
+ var WORKFLOW_REALTIME_KEY_PREFIX = "__gencow.workflow.state";
2439
+ function isSerializedWorkflowValue(value) {
2440
+ return !!value && typeof value === "object" && ("__gencowUndefined" in value || "value" in value);
2195
2441
  }
2196
- function parseJsonField(value) {
2197
- if (typeof value !== "string") return value;
2442
+ function serializeWorkflowValue(value) {
2443
+ const payload = value === void 0 ? { __gencowUndefined: true } : { value };
2198
2444
  try {
2199
- return JSON.parse(value);
2200
- } catch {
2201
- return value;
2445
+ return JSON.parse(JSON.stringify(payload));
2446
+ } catch (error) {
2447
+ const reason = error instanceof Error ? error.message : String(error);
2448
+ throw new Error(
2449
+ `workflow() only persists JSON-serializable values. Failed to serialize workflow payload: ${reason}`
2450
+ );
2202
2451
  }
2203
2452
  }
2204
- function toIsoString(value) {
2205
- if (value instanceof Date) return value.toISOString();
2206
- const parsed = new Date(value);
2207
- return Number.isFinite(parsed.getTime()) ? parsed.toISOString() : String(value);
2208
- }
2209
- function toOptionalIsoString(value) {
2210
- return value ? toIsoString(value) : null;
2211
- }
2212
- function mapWorkflowSummary(row) {
2213
- return {
2214
- id: row.id,
2215
- name: row.name,
2216
- status: row.status,
2217
- derivedStatus: deriveWorkflowStatus(row.status, row.current_step),
2218
- currentStep: row.current_step,
2219
- error: row.error,
2220
- errorCode: row.error_code,
2221
- retryCount: row.retry_count,
2222
- maxRetries: row.max_retries,
2223
- maxDurationMs: Number(row.max_duration_ms),
2224
- startedAt: toIsoString(row.started_at),
2225
- updatedAt: toIsoString(row.updated_at),
2226
- completedAt: toOptionalIsoString(row.completed_at)
2227
- };
2228
- }
2229
- function mapWorkflowStep(row) {
2230
- return {
2231
- name: row.step_name,
2232
- status: row.status,
2233
- output: deserializeWorkflowValue(parseJsonField(row.output)),
2234
- error: row.error,
2235
- startedAt: toOptionalIsoString(row.started_at),
2236
- updatedAt: toIsoString(row.updated_at),
2237
- completedAt: toOptionalIsoString(row.completed_at)
2238
- };
2453
+ function deserializeWorkflowValue(value) {
2454
+ if (!isSerializedWorkflowValue(value)) return value;
2455
+ if ("__gencowUndefined" in value) return void 0;
2456
+ return value.value;
2239
2457
  }
2240
- function normalizeListLimit(limit) {
2241
- if (limit == null) return 20;
2242
- if (!Number.isFinite(limit)) {
2243
- throw new GencowValidationError(`Argument "limit": expected a finite number, got ${limit}`);
2458
+ function clampRetries(retries) {
2459
+ if (retries == null) return DEFAULT_WORKFLOW_MAX_RETRIES;
2460
+ if (!Number.isFinite(retries) || retries < 0) {
2461
+ throw new Error(`workflow() retries must be a non-negative finite number, got "${retries}"`);
2244
2462
  }
2245
- return Math.max(1, Math.min(100, Math.floor(limit)));
2463
+ return Math.floor(retries);
2246
2464
  }
2247
- function normalizeStatus(status) {
2248
- if (status == null) return void 0;
2249
- if (!WORKFLOW_STATUSES.has(status)) {
2250
- throw new GencowValidationError(`Argument "status": expected one of pending, running, completed, failed`);
2465
+ function parseDurationString(raw, label) {
2466
+ const normalized = raw.trim().toLowerCase();
2467
+ const match = normalized.match(/^(\d+)(ms|s|m|h|d)$/);
2468
+ if (!match) {
2469
+ throw new Error(`${label} must be a number of ms or a string like "30m", "90s", "1h" \u2014 got "${raw}"`);
2251
2470
  }
2252
- return status;
2471
+ const value = Number(match[1]);
2472
+ const unit = match[2];
2473
+ const unitMs = unit === "ms" ? 1 : unit === "s" ? 1e3 : unit === "m" ? 6e4 : unit === "h" ? 36e5 : 864e5;
2474
+ return value * unitMs;
2253
2475
  }
2254
- function normalizeDerivedStatus(status) {
2255
- if (status == null) return void 0;
2256
- if (WORKFLOW_DERIVED_PENDING_STATUSES.has(status)) {
2257
- return status;
2476
+ function parseWorkflowDurationMs(raw, label = "workflow duration") {
2477
+ if (typeof raw === "number") {
2478
+ if (!Number.isFinite(raw) || raw <= 0) {
2479
+ throw new Error(`${label} must be a positive finite number, got "${raw}"`);
2480
+ }
2481
+ return Math.floor(raw);
2258
2482
  }
2259
- return normalizeStatus(status);
2260
- }
2261
- function toWorkflowStatusFilter(status) {
2262
- if (status == null) return void 0;
2263
- if (WORKFLOW_DERIVED_PENDING_STATUSES.has(status)) {
2264
- return "pending";
2483
+ if (typeof raw !== "string") {
2484
+ throw new Error(
2485
+ `${label} must be a positive finite number or a string like "30m", "90s", "1h" \u2014 got "${String(raw)}"`
2486
+ );
2265
2487
  }
2266
- return status;
2488
+ return parseDurationString(raw, label);
2267
2489
  }
2268
- async function ensureWorkflowRealtimeToken(db, workflowId, currentToken) {
2269
- if (currentToken && currentToken.trim() !== "") return currentToken;
2270
- const nextToken = createWorkflowRealtimeToken();
2271
- const updateResult = await db.execute(sql3`
2272
- UPDATE _gencow_workflows
2273
- SET realtime_token = ${nextToken}
2274
- WHERE id = ${workflowId}
2275
- AND (realtime_token IS NULL OR realtime_token = '')
2276
- RETURNING realtime_token
2277
- `);
2278
- const updatedToken = rowsFromResult(updateResult)[0]?.realtime_token ?? null;
2279
- if (updatedToken && updatedToken.trim() !== "") return updatedToken;
2280
- const rereadResult = await db.execute(sql3`
2281
- SELECT realtime_token
2282
- FROM _gencow_workflows
2283
- WHERE id = ${workflowId}
2284
- LIMIT 1
2285
- `);
2286
- const rereadToken = rowsFromResult(rereadResult)[0]?.realtime_token ?? null;
2287
- return rereadToken && rereadToken.trim() !== "" ? rereadToken : null;
2490
+ function normalizeMaxDurationMs(maxDuration, label = "workflow() maxDuration") {
2491
+ if (maxDuration == null) return DEFAULT_WORKFLOW_MAX_DURATION_MS;
2492
+ return parseWorkflowDurationMs(maxDuration, label);
2288
2493
  }
2289
- async function loadWorkflowSignalTarget(db, workflowId) {
2290
- const result = await db.execute(sql3`
2291
- SELECT
2292
- id,
2293
- name,
2294
- status,
2295
- current_step,
2296
- user_id
2297
- FROM _gencow_workflows
2298
- WHERE id = ${workflowId}
2299
- LIMIT 1
2300
- `);
2301
- return rowsFromResult(result)[0] ?? null;
2494
+ function normalizeOptionalDurationMs(duration, label) {
2495
+ return duration == null ? null : parseWorkflowDurationMs(duration, label);
2302
2496
  }
2303
- function isMissingWorkflowV2SignalSchemaError(error) {
2304
- const code = error && typeof error === "object" && "code" in error ? String(error.code) : "";
2305
- const cause = error && typeof error === "object" && "cause" in error ? error.cause : null;
2306
- const causeCode = cause && typeof cause === "object" && "code" in cause ? String(cause.code) : "";
2307
- const message = error instanceof Error ? error.message : String(error);
2308
- return code === "42P01" || code === "42703" || causeCode === "42P01" || causeCode === "42703" || (code === "23503" || causeCode === "23503") && message.includes("_gencow_workflow_signals_v2") || message.includes('relation "_gencow_workflow_signals_v2" does not exist') || message.includes("relation _gencow_workflow_signals_v2 does not exist") || message.includes('relation "_gencow_workflow_runs_v2" does not exist') || message.includes("relation _gencow_workflow_runs_v2 does not exist") || message.includes('relation "_gencow_workflow_outbox_v2" does not exist') || message.includes("relation _gencow_workflow_outbox_v2 does not exist") || message.includes('relation "_gencow_workflow_steps_v2" does not exist') || message.includes("relation _gencow_workflow_steps_v2 does not exist");
2497
+ function normalizeConcurrency(concurrency) {
2498
+ if (concurrency == null) return null;
2499
+ if (!Number.isFinite(concurrency) || concurrency <= 0) {
2500
+ throw new Error(`workflow() concurrency must be a positive finite number, got "${concurrency}"`);
2501
+ }
2502
+ return Math.floor(concurrency);
2309
2503
  }
2310
- function isMissingWorkflowV2PayloadSchemaError(error) {
2504
+ function isMissingWorkflowV2SchemaError2(error) {
2311
2505
  const code = error && typeof error === "object" && "code" in error ? String(error.code) : "";
2312
2506
  const cause = error && typeof error === "object" && "cause" in error ? error.cause : null;
2313
2507
  const causeCode = cause && typeof cause === "object" && "code" in cause ? String(cause.code) : "";
2314
2508
  const message = error instanceof Error ? error.message : String(error);
2315
- return code === "42P01" || causeCode === "42P01" || message.includes('relation "_gencow_workflow_runs_v2" does not exist') || message.includes("relation _gencow_workflow_runs_v2 does not exist") || message.includes('relation "_gencow_workflow_steps_v2" does not exist') || message.includes("relation _gencow_workflow_steps_v2 does not exist");
2509
+ return code === "42P01" || code === "42703" || causeCode === "42P01" || causeCode === "42703" || message.includes('relation "_gencow_workflow_runs_v2" does not exist') || message.includes("relation _gencow_workflow_runs_v2 does not exist") || message.includes('relation "_gencow_workflow_outbox_v2" does not exist') || message.includes("relation _gencow_workflow_outbox_v2 does not exist") || message.includes('column "max_active_duration_ms"') || message.includes('column "retry_count"') || message.includes('column "user_id"');
2316
2510
  }
2317
- async function loadWorkflowV2PayloadSnapshot(db, workflowId) {
2318
- try {
2319
- const runResult = await db.execute(sql3`
2320
- SELECT args_json, result_json
2321
- FROM _gencow_workflow_runs_v2
2322
- WHERE id = ${workflowId}
2323
- LIMIT 1
2324
- `);
2325
- const run = rowsFromResult(runResult)[0];
2326
- if (!run) return null;
2327
- const stepResult = await db.execute(sql3`
2328
- SELECT step_key, output_json
2329
- FROM _gencow_workflow_steps_v2
2330
- WHERE run_id = ${workflowId}
2331
- `);
2332
- return {
2333
- run,
2334
- steps: new Map(
2335
- rowsFromResult(stepResult).map((step) => [
2336
- step.step_key,
2337
- parseJsonField(step.output_json)
2338
- ])
2339
- )
2340
- };
2341
- } catch (error) {
2342
- if (isMissingWorkflowV2PayloadSchemaError(error)) return null;
2343
- throw error;
2511
+ function rows2(result) {
2512
+ if (Array.isArray(result)) return result;
2513
+ if (result && typeof result === "object" && Array.isArray(result.rows)) {
2514
+ return result.rows;
2344
2515
  }
2516
+ return [];
2345
2517
  }
2346
- async function tryRecordWorkflowV2Signal(options) {
2518
+ function numberFrom2(value) {
2519
+ const parsed = Number(value ?? 0);
2520
+ return Number.isFinite(parsed) ? parsed : 0;
2521
+ }
2522
+ async function hasWorkflowV2StartSchema(db) {
2523
+ const runColumns = [
2524
+ "id",
2525
+ "workflow_name",
2526
+ "workflow_version",
2527
+ "args_json",
2528
+ "user_id",
2529
+ "max_active_duration_ms",
2530
+ "lifecycle_deadline_at",
2531
+ "retry_count",
2532
+ "max_retries",
2533
+ "max_attempts"
2534
+ ];
2535
+ const outboxColumns = ["id", "run_id", "kind", "available_at", "status"];
2536
+ const runColumnList = sql3.join(
2537
+ runColumns.map((column) => sql3`${column}`),
2538
+ sql3`, `
2539
+ );
2540
+ const outboxColumnList = sql3.join(
2541
+ outboxColumns.map((column) => sql3`${column}`),
2542
+ sql3`, `
2543
+ );
2544
+ const result = await db.execute(sql3`
2545
+ SELECT
2546
+ (
2547
+ SELECT COUNT(*)::int
2548
+ FROM information_schema.columns
2549
+ WHERE table_schema = 'public'
2550
+ AND table_name = '_gencow_workflow_runs_v2'
2551
+ AND column_name IN (${runColumnList})
2552
+ ) AS run_column_count,
2553
+ (
2554
+ SELECT COUNT(*)::int
2555
+ FROM information_schema.columns
2556
+ WHERE table_schema = 'public'
2557
+ AND table_name = '_gencow_workflow_outbox_v2'
2558
+ AND column_name IN (${outboxColumnList})
2559
+ ) AS outbox_column_count
2560
+ `);
2561
+ const row = rows2(result)[0];
2562
+ return numberFrom2(row?.run_column_count) === runColumns.length && numberFrom2(row?.outbox_column_count) === outboxColumns.length;
2563
+ }
2564
+ async function tryInsertWorkflowV2Run(options) {
2347
2565
  try {
2566
+ if (!await hasWorkflowV2StartSchema(options.db)) return false;
2348
2567
  await options.db.execute(sql3`
2349
- WITH inserted AS (
2350
- INSERT INTO _gencow_workflow_signals_v2 (
2351
- id,
2352
- run_id,
2353
- event_name,
2354
- payload_json,
2355
- idempotency_key
2356
- )
2357
- VALUES (
2358
- ${crypto.randomUUID()},
2359
- ${options.workflowId},
2360
- ${options.event},
2361
- ${workflowJsonb(options.payload)},
2362
- ${crypto.randomUUID()}
2363
- )
2364
- RETURNING id, run_id
2365
- ),
2366
- inserted_legacy AS (
2367
- INSERT INTO _gencow_workflow_events (
2568
+ WITH inserted_run AS (
2569
+ INSERT INTO _gencow_workflow_runs_v2 (
2368
2570
  id,
2369
- workflow_id,
2370
- event_name,
2371
- payload
2571
+ workflow_name,
2572
+ workflow_version,
2573
+ args_json,
2574
+ user_id,
2575
+ max_active_duration_ms,
2576
+ lifecycle_deadline_at,
2577
+ retry_count,
2578
+ max_retries,
2579
+ max_attempts
2372
2580
  )
2373
2581
  VALUES (
2374
- ${options.eventId},
2375
2582
  ${options.workflowId},
2376
- ${options.event},
2377
- NULL
2583
+ ${options.workflowName},
2584
+ ${options.workflowVersion ?? null},
2585
+ ${workflowJsonb(options.args)},
2586
+ ${options.userId},
2587
+ ${options.maxActiveDurationMs},
2588
+ CASE
2589
+ WHEN ${options.lifecycleTimeoutMs}::bigint IS NULL THEN NULL
2590
+ ELSE NOW() + (${options.lifecycleTimeoutMs}::bigint * INTERVAL '1 millisecond')
2591
+ END,
2592
+ 0,
2593
+ ${options.maxRetries},
2594
+ ${options.maxRetries + 1}
2378
2595
  )
2379
2596
  RETURNING id
2380
2597
  ),
2381
- queued_run AS (
2382
- UPDATE _gencow_workflow_runs_v2 run
2383
- SET
2384
- status = 'queued',
2385
- runnable_at = NOW(),
2386
- lease_owner = NULL,
2387
- lease_expires_at = NULL,
2388
- heartbeat_at = NULL,
2389
- updated_at = NOW()
2390
- FROM inserted
2391
- WHERE run.id = inserted.run_id
2392
- AND run.status = 'waiting'
2393
- AND run.completed_at IS NULL
2394
- AND run.cancel_requested_at IS NULL
2395
- RETURNING run.id
2396
- ),
2397
- wake_outbox AS (
2598
+ inserted_outbox AS (
2398
2599
  INSERT INTO _gencow_workflow_outbox_v2 (
2399
2600
  id,
2400
2601
  run_id,
@@ -2403,404 +2604,601 @@ async function tryRecordWorkflowV2Signal(options) {
2403
2604
  status
2404
2605
  )
2405
2606
  SELECT
2406
- 'signal:' || inserted.id,
2407
- inserted.run_id,
2408
- 'wake_signal',
2607
+ 'start:' || inserted_run.id,
2608
+ inserted_run.id,
2609
+ 'wake_run',
2409
2610
  NOW(),
2410
2611
  'pending'
2411
- FROM inserted
2412
- JOIN queued_run ON queued_run.id = inserted.run_id
2612
+ FROM inserted_run
2413
2613
  ON CONFLICT (id) DO NOTHING
2414
2614
  RETURNING id
2415
2615
  ),
2416
2616
  wake_notified AS (
2417
2617
  SELECT pg_notify(${WORKFLOW_WAKE_SIGNAL_CHANNEL}, ${WORKFLOW_WAKE_SIGNAL_PAYLOAD})
2418
- FROM wake_outbox
2618
+ FROM inserted_outbox
2419
2619
  )
2420
- SELECT 1
2421
- FROM inserted
2422
- LEFT JOIN inserted_legacy ON TRUE
2620
+ SELECT inserted_run.id
2621
+ FROM inserted_run
2423
2622
  LEFT JOIN wake_notified ON TRUE
2424
2623
  `);
2425
2624
  return true;
2426
2625
  } catch (error) {
2427
- if (isMissingWorkflowV2SignalSchemaError(error)) return false;
2626
+ if (isMissingWorkflowV2SchemaError2(error)) return false;
2428
2627
  throw error;
2429
2628
  }
2430
2629
  }
2431
- async function loadWorkflowSnapshot(db, workflowId, options) {
2432
- const workflowResult = await db.execute(sql3`
2433
- SELECT
2434
- id,
2435
- name,
2436
- args,
2437
- status,
2438
- current_step,
2439
- result,
2440
- error,
2441
- error_code,
2442
- retry_count,
2443
- max_retries,
2444
- max_duration_ms,
2445
- started_at,
2446
- updated_at,
2447
- completed_at,
2448
- realtime_token,
2449
- user_id
2450
- FROM _gencow_workflows
2451
- WHERE id = ${workflowId}
2452
- LIMIT 1
2630
+ async function tryDeleteWorkflowV2Run(db, workflowId) {
2631
+ try {
2632
+ await db.execute(sql3`
2633
+ DELETE FROM _gencow_workflow_runs_v2
2634
+ WHERE id = ${workflowId}
2453
2635
  `);
2454
- const row = rowsFromResult(workflowResult)[0] ?? null;
2455
- if (!row) return null;
2456
- const viewerUserId = options?.viewerUserId ?? null;
2457
- if (options?.requireViewerMatch && row.user_id && row.user_id !== viewerUserId) {
2458
- return null;
2636
+ } catch (error) {
2637
+ if (!isMissingWorkflowV2SchemaError2(error)) throw error;
2459
2638
  }
2460
- const realtimeToken = await ensureWorkflowRealtimeToken(db, workflowId, row.realtime_token);
2461
- if (!realtimeToken) return null;
2462
- const v2Payload = await loadWorkflowV2PayloadSnapshot(db, workflowId);
2463
- const stepsResult = await db.execute(sql3`
2464
- SELECT
2465
- step_name,
2466
- status,
2467
- output,
2468
- error,
2469
- started_at,
2470
- updated_at,
2471
- completed_at
2472
- FROM _gencow_workflow_steps
2473
- WHERE workflow_id = ${workflowId}
2474
- ORDER BY COALESCE(started_at, updated_at) ASC, step_name ASC
2475
- `);
2476
- const steps = rowsFromResult(stepsResult).map((stepRow) => {
2477
- const mapped = mapWorkflowStep(stepRow);
2478
- return v2Payload?.steps.has(stepRow.step_name) ? { ...mapped, output: v2Payload.steps.get(stepRow.step_name) } : mapped;
2479
- });
2480
- return {
2481
- ...mapWorkflowSummary(row),
2482
- args: v2Payload ? deserializeWorkflowValue(parseJsonField(v2Payload.run.args_json)) : deserializeWorkflowValue(parseJsonField(row.args)),
2483
- result: v2Payload ? parseJsonField(v2Payload.run.result_json) : deserializeWorkflowValue(parseJsonField(row.result)),
2484
- steps,
2485
- realtimeKey: getWorkflowRealtimeKey(row.id, realtimeToken)
2486
- };
2487
2639
  }
2488
- function registerWorkflowsApi() {
2489
- if (globalThis.__gencow_workflowsApiRegistered) return;
2490
- globalThis.__gencow_workflowsApiRegistered = true;
2491
- query("workflows.get", {
2492
- args: { id: v.string() },
2493
- public: true,
2494
- handler: async (ctx, args) => {
2495
- return loadWorkflowSnapshot(ctx.unsafeDb, args.id, {
2496
- viewerUserId: ctx.auth.getUserIdentity()?.id ?? null,
2497
- requireViewerMatch: true
2498
- });
2499
- }
2500
- });
2501
- mutation("workflows.signal", {
2502
- args: {
2503
- id: v.string(),
2504
- event: v.string(),
2505
- payload: v.optional(v.any())
2506
- },
2507
- public: true,
2508
- handler: async (ctx, args) => {
2509
- const normalizedEvent = args.event.trim();
2510
- if (!normalizedEvent) {
2511
- throw new GencowValidationError(`Argument "event": expected a non-empty string`);
2512
- }
2513
- const workflow2 = await loadWorkflowSignalTarget(ctx.unsafeDb, args.id);
2514
- const viewerUserId = ctx.auth.getUserIdentity()?.id ?? null;
2515
- if (!workflow2 || workflow2.user_id && workflow2.user_id !== viewerUserId) {
2516
- return {
2517
- ok: false,
2518
- workflowId: args.id,
2519
- event: normalizedEvent,
2520
- scheduledJobId: null
2521
- };
2522
- }
2523
- if (workflow2.status === "completed" || workflow2.status === "failed") {
2524
- return {
2525
- ok: false,
2526
- workflowId: workflow2.id,
2527
- event: normalizedEvent,
2528
- scheduledJobId: null
2529
- };
2530
- }
2531
- const persistedPayload = serializeWorkflowValue(args.payload);
2532
- assertWorkflowPayloadWithinLimit({
2533
- workflowName: workflow2.name,
2534
- kind: "signal",
2535
- serializedValue: persistedPayload
2640
+ function getWorkflowResumeActionName(name) {
2641
+ return `${WORKFLOW_RESUME_ACTION_PREFIX}.${name}`;
2642
+ }
2643
+ function createWorkflowRealtimeToken() {
2644
+ return crypto.randomUUID().replace(/-/g, "");
2645
+ }
2646
+ function getWorkflowRealtimeKey(workflowId, realtimeToken) {
2647
+ return `${WORKFLOW_REALTIME_KEY_PREFIX}.${workflowId}.${realtimeToken}`;
2648
+ }
2649
+ function getWorkflowDef(name) {
2650
+ return workflowRegistry.get(name);
2651
+ }
2652
+ function getRegisteredWorkflows() {
2653
+ return Array.from(workflowRegistry.values());
2654
+ }
2655
+ function hasStandardSchema4(schema) {
2656
+ return !!schema && typeof schema === "object" && "~standard" in schema && !!schema["~standard"] && typeof schema["~standard"].validate === "function";
2657
+ }
2658
+ async function validateWorkflowStartInput(schema, value) {
2659
+ if (!schema) return value;
2660
+ const result = await schema["~standard"].validate(normalizeMissingOptionalObjectInput(schema, value));
2661
+ if ("issues" in result && result.issues.length > 0) {
2662
+ throw new GencowValidationError(result.issues[0]?.message ?? "Validation failed");
2663
+ }
2664
+ if ("value" in result) return result.value;
2665
+ throw new GencowValidationError("Validation failed");
2666
+ }
2667
+ function composeWorkflowStartMiddleware(options) {
2668
+ return async (initialContext, initialInput) => {
2669
+ let lastIndex = -1;
2670
+ const validationIndex = Math.min(Math.max(0, options.inputValidationIndex), options.middlewares.length);
2671
+ const run = async (index, context, input) => {
2672
+ if (index <= lastIndex) throw new Error("next() called multiple times in the same middleware");
2673
+ lastIndex = index;
2674
+ const validatedInput = index === validationIndex ? await validateWorkflowStartInput(options.inputSchema, input) : input;
2675
+ if (index === options.middlewares.length) return await options.handler(context, validatedInput);
2676
+ return await options.middlewares[index]({
2677
+ context,
2678
+ input: validatedInput,
2679
+ next: async (overrides) => await run(
2680
+ index + 1,
2681
+ overrides?.context ?? context,
2682
+ overrides?.input ?? validatedInput
2683
+ )
2536
2684
  });
2537
- const eventId = crypto.randomUUID();
2538
- const recordedV2 = await tryRecordWorkflowV2Signal({
2539
- db: ctx.unsafeDb,
2540
- workflowId: workflow2.id,
2541
- event: normalizedEvent,
2542
- eventId,
2543
- payload: persistedPayload
2685
+ };
2686
+ return await run(0, initialContext, initialInput);
2687
+ };
2688
+ }
2689
+ async function startWorkflowRun(ctx, args, def) {
2690
+ const workflowId = crypto.randomUUID();
2691
+ const resumeAction = getWorkflowResumeActionName(def.name);
2692
+ const ownerId = ctx.auth.getUserIdentity()?.id ?? null;
2693
+ const persistedArgs = serializeWorkflowValue(args ?? {});
2694
+ assertWorkflowPayloadWithinLimit({ workflowName: def.name, kind: "args", serializedValue: persistedArgs });
2695
+ const realtimeToken = createWorkflowRealtimeToken();
2696
+ let insertedWorkflowV2 = false;
2697
+ try {
2698
+ await runWorkflowAdmissionCriticalSection(ctx.unsafeDb, async (db) => {
2699
+ await applyWorkflowStartAdmission({ db, workflowName: def.name, args: persistedArgs });
2700
+ insertedWorkflowV2 = await tryInsertWorkflowV2Run({
2701
+ db,
2702
+ workflowId,
2703
+ workflowName: def.name,
2704
+ workflowVersion: def.version ?? null,
2705
+ args: persistedArgs,
2706
+ userId: ownerId,
2707
+ maxActiveDurationMs: def.maxActiveDurationMs,
2708
+ lifecycleTimeoutMs: def.lifecycleTimeoutMs,
2709
+ maxRetries: def.maxRetries
2544
2710
  });
2545
- if (!recordedV2) {
2546
- await ctx.unsafeDb.execute(sql3`
2547
- INSERT INTO _gencow_workflow_events (
2548
- id,
2549
- workflow_id,
2550
- event_name,
2551
- payload
2552
- )
2553
- VALUES (
2554
- ${eventId},
2555
- ${workflow2.id},
2556
- ${normalizedEvent},
2557
- ${workflowJsonb(persistedPayload)}
2558
- )
2559
- `);
2560
- }
2561
- let scheduledJobId = null;
2562
- if (workflow2.status === "pending" && workflow2.current_step?.startsWith("wait:")) {
2563
- try {
2564
- scheduledJobId = ctx.scheduler.runAfter(0, getWorkflowResumeActionName(workflow2.name), {
2565
- workflowId: workflow2.id
2566
- });
2567
- } catch {
2568
- scheduledJobId = null;
2569
- }
2570
- }
2571
- return {
2572
- ok: true,
2573
- workflowId: workflow2.id,
2574
- event: normalizedEvent,
2575
- scheduledJobId
2576
- };
2577
- }
2711
+ const legacyArgs = insertedWorkflowV2 ? serializeWorkflowValue(void 0) : persistedArgs;
2712
+ await db.execute(sql3`
2713
+ INSERT INTO _gencow_workflows (
2714
+ id, name, args, realtime_token, status, retry_count, max_retries, max_duration_ms, user_id
2715
+ ) VALUES (
2716
+ ${workflowId}, ${def.name}, ${workflowJsonb(legacyArgs)}, ${realtimeToken}, 'pending', 0,
2717
+ ${def.maxRetries}, ${def.maxActiveDurationMs}, ${ownerId}
2718
+ )
2719
+ `);
2720
+ });
2721
+ return {
2722
+ id: workflowId,
2723
+ name: def.name,
2724
+ status: "pending",
2725
+ scheduledJobId: insertedWorkflowV2 ? `start:${workflowId}` : ctx.scheduler.runAfter(0, resumeAction, { workflowId })
2726
+ };
2727
+ } catch (error) {
2728
+ await ctx.unsafeDb.execute(sql3`DELETE FROM _gencow_workflows WHERE id = ${workflowId}`);
2729
+ if (insertedWorkflowV2) await tryDeleteWorkflowV2Run(ctx.unsafeDb, workflowId);
2730
+ throw error;
2731
+ }
2732
+ }
2733
+ function createWorkflowMetadata(options) {
2734
+ const { definition } = options;
2735
+ const legacyStart = options.legacy ? {
2736
+ name: definition.name,
2737
+ argsSchema: definition.argsSchema,
2738
+ isPublic: definition.isPublic,
2739
+ handler: async (ctx, args) => await startWorkflowRun(ctx, args, definition)
2740
+ } : void 0;
2741
+ const middlewares = options.middlewares ?? [];
2742
+ const explicitHandler = composeWorkflowStartMiddleware({
2743
+ middlewares,
2744
+ inputSchema: hasStandardSchema4(definition.argsSchema) ? definition.argsSchema : void 0,
2745
+ inputValidationIndex: options.inputValidationIndex ?? 0,
2746
+ handler: async (ctx, args) => await startWorkflowRun(ctx, args, definition)
2578
2747
  });
2579
- query("workflows.list", {
2580
- args: {
2581
- limit: v.optional(v.number()),
2582
- status: v.optional(v.string())
2583
- },
2584
- handler: async (ctx, args) => {
2585
- const userId = ctx.auth.requireAuth().id;
2586
- const limit = normalizeListLimit(args.limit);
2587
- const requestedStatus = normalizeDerivedStatus(args.status);
2588
- const status = toWorkflowStatusFilter(requestedStatus);
2589
- const result = status == null ? await ctx.unsafeDb.execute(sql3`
2590
- SELECT
2591
- id,
2592
- name,
2593
- args,
2594
- status,
2595
- current_step,
2596
- result,
2597
- error,
2598
- error_code,
2599
- retry_count,
2600
- max_retries,
2601
- max_duration_ms,
2602
- started_at,
2603
- updated_at,
2604
- completed_at,
2605
- user_id
2606
- FROM _gencow_workflows
2607
- WHERE user_id = ${userId}
2608
- ORDER BY started_at DESC
2609
- LIMIT ${limit}
2610
- `) : await ctx.unsafeDb.execute(sql3`
2611
- SELECT
2612
- id,
2613
- name,
2614
- args,
2615
- status,
2616
- current_step,
2617
- result,
2618
- error,
2619
- error_code,
2620
- retry_count,
2621
- max_retries,
2622
- max_duration_ms,
2623
- started_at,
2624
- updated_at,
2625
- completed_at,
2626
- user_id
2627
- FROM _gencow_workflows
2628
- WHERE user_id = ${userId}
2629
- AND status = ${status}
2630
- ORDER BY started_at DESC
2631
- LIMIT ${limit}
2632
- `);
2633
- return rowsFromResult(result).map(mapWorkflowSummary).filter((row) => requestedStatus == null || row.derivedStatus === requestedStatus);
2748
+ const explicitStart = {
2749
+ kind: "mutation",
2750
+ name: definition.name,
2751
+ allowAnonymous: definition.isPublic,
2752
+ middlewares: [...middlewares],
2753
+ inputSchema: hasStandardSchema4(definition.argsSchema) ? definition.argsSchema : void 0,
2754
+ outputSchema: void 0,
2755
+ inputValidationIndex: options.inputValidationIndex ?? 0,
2756
+ outputValidationIndex: 0,
2757
+ handler: legacyStart ? async (ctx, args) => await legacyStart.handler(ctx, args) : async (ctx, args) => await explicitHandler(ctx, args)
2758
+ };
2759
+ const internalResume = {
2760
+ kind: "internal",
2761
+ name: getWorkflowResumeActionName(definition.name),
2762
+ allowAnonymous: false,
2763
+ middlewares: [],
2764
+ inputValidationIndex: 0,
2765
+ outputValidationIndex: 0,
2766
+ handler: async () => {
2767
+ throw new Error("Workflow resume is scheduler-owned and cannot be invoked directly.");
2634
2768
  }
2769
+ };
2770
+ const metadata = {
2771
+ definition,
2772
+ legacyStart,
2773
+ explicitStart,
2774
+ internalResume
2775
+ };
2776
+ for (const carrier of [definition, legacyStart].filter(Boolean)) {
2777
+ Object.defineProperty(carrier, WORKFLOW_DEFINITION_METADATA, {
2778
+ value: metadata,
2779
+ enumerable: false,
2780
+ configurable: false,
2781
+ writable: false
2782
+ });
2783
+ }
2784
+ if (legacyStart) return { metadata };
2785
+ const pure = {};
2786
+ Object.defineProperty(pure, WORKFLOW_DEFINITION_METADATA, {
2787
+ value: metadata,
2788
+ enumerable: false,
2789
+ configurable: false,
2790
+ writable: false
2635
2791
  });
2792
+ return { metadata, pure };
2793
+ }
2794
+ function workflow(name, options) {
2795
+ const maxActiveDurationMs = normalizeMaxDurationMs(
2796
+ options.maxActiveDuration ?? options.maxDuration,
2797
+ options.maxActiveDuration == null ? "workflow() maxDuration" : "workflow() maxActiveDuration"
2798
+ );
2799
+ const definition = {
2800
+ name,
2801
+ argsSchema: options.args,
2802
+ isPublic: options.public === true,
2803
+ version: options.version,
2804
+ maxDurationMs: maxActiveDurationMs,
2805
+ maxActiveDurationMs,
2806
+ lifecycleTimeoutMs: normalizeOptionalDurationMs(options.lifecycleTimeout, "workflow() lifecycleTimeout"),
2807
+ concurrency: normalizeConcurrency(options.concurrency),
2808
+ maxRetries: clampRetries(options.retries),
2809
+ handler: options.handler
2810
+ };
2811
+ workflowRegistry.set(name, definition);
2812
+ const { metadata } = createWorkflowMetadata({ definition, legacy: true });
2813
+ return metadata.legacyStart;
2814
+ }
2815
+ var GencowWorkflowBuilderImpl = class _GencowWorkflowBuilderImpl {
2816
+ constructor(workflowName, middlewares = [], inputSchema, inputValidationIndex = -1, anonymousAllowed = false, workflowVersion, maxDurationValue, lifecycleTimeoutValue, concurrencyValue, retriesValue) {
2817
+ this.workflowName = workflowName;
2818
+ this.middlewares = middlewares;
2819
+ this.inputSchema = inputSchema;
2820
+ this.inputValidationIndex = inputValidationIndex;
2821
+ this.anonymousAllowed = anonymousAllowed;
2822
+ this.workflowVersion = workflowVersion;
2823
+ this.maxDurationValue = maxDurationValue;
2824
+ this.lifecycleTimeoutValue = lifecycleTimeoutValue;
2825
+ this.concurrencyValue = concurrencyValue;
2826
+ this.retriesValue = retriesValue;
2827
+ }
2828
+ workflowName;
2829
+ middlewares;
2830
+ inputSchema;
2831
+ inputValidationIndex;
2832
+ anonymousAllowed;
2833
+ workflowVersion;
2834
+ maxDurationValue;
2835
+ lifecycleTimeoutValue;
2836
+ concurrencyValue;
2837
+ retriesValue;
2838
+ copy(changes = {}) {
2839
+ return new _GencowWorkflowBuilderImpl(
2840
+ changes.workflowName ?? this.workflowName,
2841
+ changes.middlewares ?? this.middlewares,
2842
+ changes.inputSchema ?? this.inputSchema,
2843
+ changes.inputValidationIndex ?? this.inputValidationIndex,
2844
+ changes.anonymousAllowed ?? this.anonymousAllowed,
2845
+ changes.workflowVersion ?? this.workflowVersion,
2846
+ changes.maxDurationValue ?? this.maxDurationValue,
2847
+ changes.lifecycleTimeoutValue ?? this.lifecycleTimeoutValue,
2848
+ changes.concurrencyValue ?? this.concurrencyValue,
2849
+ changes.retriesValue ?? this.retriesValue
2850
+ );
2851
+ }
2852
+ name(name) {
2853
+ return this.copy({ workflowName: name });
2854
+ }
2855
+ use(middleware) {
2856
+ return this.copy({ middlewares: [...this.middlewares, middleware] });
2857
+ }
2858
+ input(schema) {
2859
+ return this.copy({
2860
+ inputSchema: schema,
2861
+ inputValidationIndex: this.middlewares.length
2862
+ });
2863
+ }
2864
+ allowAnonymous() {
2865
+ return this.copy({ anonymousAllowed: true });
2866
+ }
2867
+ version(version) {
2868
+ return this.copy({ workflowVersion: version });
2869
+ }
2870
+ maxDuration(duration) {
2871
+ return this.copy({ maxDurationValue: duration });
2872
+ }
2873
+ lifecycleTimeout(duration) {
2874
+ return this.copy({ lifecycleTimeoutValue: duration });
2875
+ }
2876
+ concurrency(limit) {
2877
+ return this.copy({ concurrencyValue: limit });
2878
+ }
2879
+ retries(count) {
2880
+ return this.copy({ retriesValue: count });
2881
+ }
2882
+ handler(handler) {
2883
+ if (!this.workflowName)
2884
+ throw new Error("Workflow name is required. Call .name(...) before .handler(...)");
2885
+ const maxActiveDurationMs = normalizeMaxDurationMs(this.maxDurationValue, "workflow.maxDuration()");
2886
+ const definition = {
2887
+ name: this.workflowName,
2888
+ argsSchema: this.inputSchema,
2889
+ isPublic: this.anonymousAllowed,
2890
+ version: this.workflowVersion,
2891
+ maxDurationMs: maxActiveDurationMs,
2892
+ maxActiveDurationMs,
2893
+ lifecycleTimeoutMs: normalizeOptionalDurationMs(
2894
+ this.lifecycleTimeoutValue,
2895
+ "workflow.lifecycleTimeout()"
2896
+ ),
2897
+ concurrency: normalizeConcurrency(this.concurrencyValue),
2898
+ maxRetries: clampRetries(this.retriesValue),
2899
+ handler
2900
+ };
2901
+ return createWorkflowMetadata({
2902
+ definition,
2903
+ middlewares: this.middlewares,
2904
+ inputValidationIndex: this.inputValidationIndex < 0 ? 0 : this.inputValidationIndex
2905
+ }).pure;
2906
+ }
2907
+ };
2908
+ function createGencowWorkflowBuilders() {
2909
+ return new GencowWorkflowBuilderImpl();
2910
+ }
2911
+
2912
+ // ../core/src/workflow-types.ts
2913
+ function deriveWorkflowStatus(status, currentStep) {
2914
+ if (status !== "pending") {
2915
+ return status;
2916
+ }
2917
+ if (currentStep?.startsWith("sleep#")) {
2918
+ return "sleeping";
2919
+ }
2920
+ if (currentStep?.startsWith("wait:")) {
2921
+ return "waiting";
2922
+ }
2923
+ return "queued";
2636
2924
  }
2637
2925
 
2638
- // ../core/src/workflow.ts
2639
- var workflowRegistry = globalThis.__gencow_workflowRegistry ??= /* @__PURE__ */ new Map();
2640
- var DEFAULT_WORKFLOW_MAX_DURATION_MS = 30 * 60 * 1e3;
2641
- var DEFAULT_WORKFLOW_MAX_RETRIES = 3;
2642
- var WORKFLOW_RESUME_ACTION_PREFIX = "__gencow.workflow.resume";
2643
- var WORKFLOW_REALTIME_KEY_PREFIX = "__gencow.workflow.state";
2644
- function isSerializedWorkflowValue(value) {
2645
- return !!value && typeof value === "object" && ("__gencowUndefined" in value || "value" in value);
2646
- }
2647
- function serializeWorkflowValue(value) {
2648
- const payload = value === void 0 ? { __gencowUndefined: true } : { value };
2926
+ // ../core/src/workflows-api.ts
2927
+ import { sql as sql4 } from "drizzle-orm";
2928
+
2929
+ // ../core/src/workflow-public-read-model.ts
2930
+ function parseJsonField(value) {
2931
+ if (typeof value !== "string") return value;
2649
2932
  try {
2650
- return JSON.parse(JSON.stringify(payload));
2651
- } catch (error) {
2652
- const reason = error instanceof Error ? error.message : String(error);
2653
- throw new Error(
2654
- `workflow() only persists JSON-serializable values. Failed to serialize workflow payload: ${reason}`
2655
- );
2933
+ return JSON.parse(value);
2934
+ } catch {
2935
+ return value;
2656
2936
  }
2657
2937
  }
2658
- function deserializeWorkflowValue(value) {
2659
- if (!isSerializedWorkflowValue(value)) return value;
2660
- if ("__gencowUndefined" in value) return void 0;
2661
- return value.value;
2938
+ function toIsoString(value) {
2939
+ if (value instanceof Date) return value.toISOString();
2940
+ const parsed = new Date(value);
2941
+ return Number.isFinite(parsed.getTime()) ? parsed.toISOString() : String(value);
2662
2942
  }
2663
- function clampRetries(retries) {
2664
- if (retries == null) return DEFAULT_WORKFLOW_MAX_RETRIES;
2665
- if (!Number.isFinite(retries) || retries < 0) {
2666
- throw new Error(`workflow() retries must be a non-negative finite number, got "${retries}"`);
2667
- }
2668
- return Math.floor(retries);
2943
+ function toOptionalIsoString(value) {
2944
+ return value ? toIsoString(value) : null;
2669
2945
  }
2670
- function parseDurationString(raw, label) {
2671
- const normalized = raw.trim().toLowerCase();
2672
- const match = normalized.match(/^(\d+)(ms|s|m|h|d)$/);
2673
- if (!match) {
2674
- throw new Error(`${label} must be a number of ms or a string like "30m", "90s", "1h" \u2014 got "${raw}"`);
2946
+ function publicV2Status(status) {
2947
+ if (status === "completed" || status === "failed") return status;
2948
+ if (status === "cancelled") return "failed";
2949
+ if (status === "running") return "running";
2950
+ return "pending";
2951
+ }
2952
+ function publicV2DerivedStatus(status) {
2953
+ if (status === "queued" || status === "waiting" || status === "sleeping") return status;
2954
+ return publicV2Status(status);
2955
+ }
2956
+ function mapV2WorkflowSummary(row) {
2957
+ const error = parseJsonField(row.error_json);
2958
+ return {
2959
+ id: row.id,
2960
+ name: row.workflow_name,
2961
+ status: publicV2Status(row.status),
2962
+ derivedStatus: publicV2DerivedStatus(row.status),
2963
+ currentStep: row.current_step_key,
2964
+ error: typeof error === "string" ? error : error && typeof error === "object" && "message" in error ? String(error.message) : null,
2965
+ errorCode: row.status === "cancelled" ? "cancelled" : row.error_code,
2966
+ retryCount: row.retry_count,
2967
+ maxRetries: row.max_retries,
2968
+ maxDurationMs: Number(row.max_active_duration_ms),
2969
+ startedAt: toIsoString(row.created_at),
2970
+ updatedAt: toIsoString(row.updated_at),
2971
+ completedAt: toOptionalIsoString(row.completed_at)
2972
+ };
2973
+ }
2974
+ function mapV2WorkflowStep(row) {
2975
+ const error = parseJsonField(row.error_json);
2976
+ return {
2977
+ name: row.step_key,
2978
+ status: row.status === "running" || row.status === "failed" || row.status === "completed" ? row.status : row.status === "skipped" ? "completed" : "pending",
2979
+ output: parseJsonField(row.output_json),
2980
+ error: typeof error === "string" ? error : error && typeof error === "object" && "message" in error ? String(error.message) : null,
2981
+ startedAt: toOptionalIsoString(row.started_at),
2982
+ updatedAt: toIsoString(row.updated_at),
2983
+ completedAt: toOptionalIsoString(row.completed_at)
2984
+ };
2985
+ }
2986
+
2987
+ // ../core/src/workflows-api.ts
2988
+ var WORKFLOW_STATUSES = /* @__PURE__ */ new Set(["pending", "running", "completed", "failed"]);
2989
+ var WORKFLOW_DERIVED_PENDING_STATUSES = /* @__PURE__ */ new Set(["queued", "waiting", "sleeping"]);
2990
+ function rowsFromResult(result) {
2991
+ if (Array.isArray(result)) return result;
2992
+ if (result && typeof result === "object" && Array.isArray(result.rows)) {
2993
+ return result.rows;
2675
2994
  }
2676
- const value = Number(match[1]);
2677
- const unit = match[2];
2678
- const unitMs = unit === "ms" ? 1 : unit === "s" ? 1e3 : unit === "m" ? 6e4 : unit === "h" ? 36e5 : 864e5;
2679
- return value * unitMs;
2995
+ return [];
2680
2996
  }
2681
- function parseWorkflowDurationMs(raw, label = "workflow duration") {
2682
- if (typeof raw === "number") {
2683
- if (!Number.isFinite(raw) || raw <= 0) {
2684
- throw new Error(`${label} must be a positive finite number, got "${raw}"`);
2685
- }
2686
- return Math.floor(raw);
2997
+ function mapWorkflowSummary(row) {
2998
+ return {
2999
+ id: row.id,
3000
+ name: row.name,
3001
+ status: row.status,
3002
+ derivedStatus: deriveWorkflowStatus(row.status, row.current_step),
3003
+ currentStep: row.current_step,
3004
+ error: row.error,
3005
+ errorCode: row.error_code,
3006
+ retryCount: row.retry_count,
3007
+ maxRetries: row.max_retries,
3008
+ maxDurationMs: Number(row.max_duration_ms),
3009
+ startedAt: toIsoString(row.started_at),
3010
+ updatedAt: toIsoString(row.updated_at),
3011
+ completedAt: toOptionalIsoString(row.completed_at)
3012
+ };
3013
+ }
3014
+ function mapJoinedWorkflowSummary(row) {
3015
+ if (!row.v2_id) return mapWorkflowSummary(row);
3016
+ return mapV2WorkflowSummary({
3017
+ id: row.v2_id,
3018
+ workflow_name: row.v2_name,
3019
+ status: row.v2_status,
3020
+ args_json: null,
3021
+ result_json: null,
3022
+ error_json: row.v2_error_json,
3023
+ error_code: row.v2_error_code,
3024
+ current_step_key: row.v2_current_step,
3025
+ retry_count: row.v2_retry_count,
3026
+ max_retries: row.v2_max_retries,
3027
+ max_active_duration_ms: row.v2_max_duration_ms,
3028
+ created_at: row.v2_created_at,
3029
+ updated_at: row.v2_updated_at,
3030
+ completed_at: row.v2_completed_at,
3031
+ user_id: null
3032
+ });
3033
+ }
3034
+ function mapWorkflowStep(row) {
3035
+ return {
3036
+ name: row.step_name,
3037
+ status: row.status,
3038
+ output: deserializeWorkflowValue(parseJsonField(row.output)),
3039
+ error: row.error,
3040
+ startedAt: toOptionalIsoString(row.started_at),
3041
+ updatedAt: toIsoString(row.updated_at),
3042
+ completedAt: toOptionalIsoString(row.completed_at)
3043
+ };
3044
+ }
3045
+ function normalizeListLimit(limit) {
3046
+ if (limit == null) return 20;
3047
+ if (!Number.isFinite(limit)) {
3048
+ throw new GencowValidationError(`Argument "limit": expected a finite number, got ${limit}`);
2687
3049
  }
2688
- if (typeof raw !== "string") {
2689
- throw new Error(
2690
- `${label} must be a positive finite number or a string like "30m", "90s", "1h" \u2014 got "${String(raw)}"`
2691
- );
3050
+ return Math.max(1, Math.min(100, Math.floor(limit)));
3051
+ }
3052
+ function normalizeStatus(status) {
3053
+ if (status == null) return void 0;
3054
+ if (!WORKFLOW_STATUSES.has(status)) {
3055
+ throw new GencowValidationError(`Argument "status": expected one of pending, running, completed, failed`);
2692
3056
  }
2693
- return parseDurationString(raw, label);
3057
+ return status;
2694
3058
  }
2695
- function normalizeMaxDurationMs(maxDuration, label = "workflow() maxDuration") {
2696
- if (maxDuration == null) return DEFAULT_WORKFLOW_MAX_DURATION_MS;
2697
- return parseWorkflowDurationMs(maxDuration, label);
3059
+ function normalizeDerivedStatus(status) {
3060
+ if (status == null) return void 0;
3061
+ if (WORKFLOW_DERIVED_PENDING_STATUSES.has(status)) {
3062
+ return status;
3063
+ }
3064
+ return normalizeStatus(status);
2698
3065
  }
2699
- function normalizeOptionalDurationMs(duration, label) {
2700
- return duration == null ? null : parseWorkflowDurationMs(duration, label);
3066
+ async function ensureWorkflowRealtimeToken(db, workflowId, currentToken) {
3067
+ if (currentToken && currentToken.trim() !== "") return currentToken;
3068
+ const nextToken = createWorkflowRealtimeToken();
3069
+ const updateResult = await db.execute(sql4`
3070
+ UPDATE _gencow_workflows
3071
+ SET realtime_token = ${nextToken}
3072
+ WHERE id = ${workflowId}
3073
+ AND (realtime_token IS NULL OR realtime_token = '')
3074
+ RETURNING realtime_token
3075
+ `);
3076
+ const updatedToken = rowsFromResult(updateResult)[0]?.realtime_token ?? null;
3077
+ if (updatedToken && updatedToken.trim() !== "") return updatedToken;
3078
+ const rereadResult = await db.execute(sql4`
3079
+ SELECT realtime_token
3080
+ FROM _gencow_workflows
3081
+ WHERE id = ${workflowId}
3082
+ LIMIT 1
3083
+ `);
3084
+ const rereadToken = rowsFromResult(rereadResult)[0]?.realtime_token ?? null;
3085
+ return rereadToken && rereadToken.trim() !== "" ? rereadToken : null;
2701
3086
  }
2702
- function normalizeConcurrency(concurrency) {
2703
- if (concurrency == null) return null;
2704
- if (!Number.isFinite(concurrency) || concurrency <= 0) {
2705
- throw new Error(`workflow() concurrency must be a positive finite number, got "${concurrency}"`);
3087
+ async function loadWorkflowSignalTarget(db, workflowId) {
3088
+ try {
3089
+ const result = await db.execute(sql4`
3090
+ SELECT
3091
+ id,
3092
+ name,
3093
+ status,
3094
+ current_step,
3095
+ user_id
3096
+ FROM _gencow_workflows
3097
+ WHERE id = ${workflowId}
3098
+ LIMIT 1
3099
+ `);
3100
+ return rowsFromResult(result)[0] ?? null;
3101
+ } catch (error) {
3102
+ if (isMissingWorkflowV1Table(error)) return null;
3103
+ throw error;
2706
3104
  }
2707
- return Math.floor(concurrency);
2708
3105
  }
2709
- function isMissingWorkflowV2SchemaError2(error) {
3106
+ function isMissingWorkflowV2SignalSchemaError(error) {
2710
3107
  const code = error && typeof error === "object" && "code" in error ? String(error.code) : "";
2711
3108
  const cause = error && typeof error === "object" && "cause" in error ? error.cause : null;
2712
3109
  const causeCode = cause && typeof cause === "object" && "code" in cause ? String(cause.code) : "";
2713
3110
  const message = error instanceof Error ? error.message : String(error);
2714
- return code === "42P01" || code === "42703" || causeCode === "42P01" || causeCode === "42703" || message.includes('relation "_gencow_workflow_runs_v2" does not exist') || message.includes("relation _gencow_workflow_runs_v2 does not exist") || message.includes('relation "_gencow_workflow_outbox_v2" does not exist') || message.includes("relation _gencow_workflow_outbox_v2 does not exist") || message.includes('column "max_active_duration_ms"') || message.includes('column "retry_count"') || message.includes('column "user_id"');
2715
- }
2716
- function rows2(result) {
2717
- if (Array.isArray(result)) return result;
2718
- if (result && typeof result === "object" && Array.isArray(result.rows)) {
2719
- return result.rows;
2720
- }
2721
- return [];
3111
+ return code === "42P01" || code === "42703" || causeCode === "42P01" || causeCode === "42703" || (code === "23503" || causeCode === "23503") && message.includes("_gencow_workflow_signals_v2") || message.includes('relation "_gencow_workflow_signals_v2" does not exist') || message.includes("relation _gencow_workflow_signals_v2 does not exist") || message.includes('relation "_gencow_workflow_runs_v2" does not exist') || message.includes("relation _gencow_workflow_runs_v2 does not exist") || message.includes('relation "_gencow_workflow_outbox_v2" does not exist') || message.includes("relation _gencow_workflow_outbox_v2 does not exist") || message.includes('relation "_gencow_workflow_steps_v2" does not exist') || message.includes("relation _gencow_workflow_steps_v2 does not exist");
2722
3112
  }
2723
- function numberFrom2(value) {
2724
- const parsed = Number(value ?? 0);
2725
- return Number.isFinite(parsed) ? parsed : 0;
3113
+ function isMissingWorkflowV2PayloadSchemaError(error) {
3114
+ const code = error && typeof error === "object" && "code" in error ? String(error.code) : "";
3115
+ const cause = error && typeof error === "object" && "cause" in error ? error.cause : null;
3116
+ const causeCode = cause && typeof cause === "object" && "code" in cause ? String(cause.code) : "";
3117
+ const message = error instanceof Error ? error.message : String(error);
3118
+ return code === "42P01" || causeCode === "42P01" || message.includes('relation "_gencow_workflow_runs_v2" does not exist') || message.includes("relation _gencow_workflow_runs_v2 does not exist") || message.includes('relation "_gencow_workflow_steps_v2" does not exist') || message.includes("relation _gencow_workflow_steps_v2 does not exist");
2726
3119
  }
2727
- async function hasWorkflowV2StartSchema(db) {
2728
- const runColumns = [
2729
- "id",
2730
- "workflow_name",
2731
- "workflow_version",
2732
- "args_json",
2733
- "user_id",
2734
- "max_active_duration_ms",
2735
- "lifecycle_deadline_at",
2736
- "retry_count",
2737
- "max_retries",
2738
- "max_attempts"
2739
- ];
2740
- const outboxColumns = ["id", "run_id", "kind", "available_at", "status"];
2741
- const runColumnList = sql4.join(
2742
- runColumns.map((column) => sql4`${column}`),
2743
- sql4`, `
2744
- );
2745
- const outboxColumnList = sql4.join(
2746
- outboxColumns.map((column) => sql4`${column}`),
2747
- sql4`, `
3120
+ function isMissingWorkflowV1Table(error) {
3121
+ const cause = error && typeof error === "object" && "cause" in error ? error.cause : null;
3122
+ const message = `${error instanceof Error ? error.message : String(error)} ${cause instanceof Error ? cause.message : String(cause ?? "")}`;
3123
+ return /relation "?_gencow_workflows"? does not exist|relation "?_gencow_workflow_steps"? does not exist|relation "?_gencow_workflow_events"? does not exist/.test(
3124
+ message
2748
3125
  );
2749
- const result = await db.execute(sql4`
2750
- SELECT
2751
- (
2752
- SELECT COUNT(*)::int
2753
- FROM information_schema.columns
2754
- WHERE table_schema = 'public'
2755
- AND table_name = '_gencow_workflow_runs_v2'
2756
- AND column_name IN (${runColumnList})
2757
- ) AS run_column_count,
2758
- (
2759
- SELECT COUNT(*)::int
2760
- FROM information_schema.columns
2761
- WHERE table_schema = 'public'
2762
- AND table_name = '_gencow_workflow_outbox_v2'
2763
- AND column_name IN (${outboxColumnList})
2764
- ) AS outbox_column_count
2765
- `);
2766
- const row = rows2(result)[0];
2767
- return numberFrom2(row?.run_column_count) === runColumns.length && numberFrom2(row?.outbox_column_count) === outboxColumns.length;
2768
3126
  }
2769
- async function tryInsertWorkflowV2Run(options) {
3127
+ async function loadWorkflowV2Run(db, workflowId) {
3128
+ try {
3129
+ const result = await db.execute(sql4`
3130
+ SELECT id, workflow_name, status, args_json, result_json, error_json, error_code,
3131
+ current_step_key, retry_count, max_retries, max_active_duration_ms,
3132
+ created_at, updated_at, completed_at, user_id
3133
+ FROM _gencow_workflow_runs_v2
3134
+ WHERE id = ${workflowId}
3135
+ LIMIT 1
3136
+ `);
3137
+ return rowsFromResult(result)[0] ?? null;
3138
+ } catch (error) {
3139
+ if (isMissingWorkflowV2PayloadSchemaError(error)) return null;
3140
+ throw error;
3141
+ }
3142
+ }
3143
+ async function loadWorkflowV2Steps(db, workflowId) {
3144
+ try {
3145
+ const stepResult = await db.execute(sql4`
3146
+ SELECT step_key, status, output_json, error_json, started_at, updated_at, completed_at
3147
+ FROM _gencow_workflow_steps_v2
3148
+ WHERE run_id = ${workflowId}
3149
+ ORDER BY step_index ASC, COALESCE(started_at, updated_at) ASC, step_key ASC
3150
+ `);
3151
+ return rowsFromResult(stepResult);
3152
+ } catch (error) {
3153
+ if (isMissingWorkflowV2PayloadSchemaError(error)) return [];
3154
+ throw error;
3155
+ }
3156
+ }
3157
+ async function tryRecordWorkflowV2Signal(options) {
2770
3158
  try {
2771
- if (!await hasWorkflowV2StartSchema(options.db)) return false;
2772
- await options.db.execute(sql4`
2773
- WITH inserted_run AS (
2774
- INSERT INTO _gencow_workflow_runs_v2 (
3159
+ const result = await options.db.execute(sql4`
3160
+ WITH eligible_run AS (
3161
+ SELECT id
3162
+ FROM _gencow_workflow_runs_v2
3163
+ WHERE id = ${options.workflowId}
3164
+ AND status IN ('queued', 'running', 'sleeping', 'waiting')
3165
+ AND completed_at IS NULL
3166
+ AND cancel_requested_at IS NULL
3167
+ FOR UPDATE
3168
+ ), inserted AS (
3169
+ INSERT INTO _gencow_workflow_signals_v2 (
2775
3170
  id,
2776
- workflow_name,
2777
- workflow_version,
2778
- args_json,
2779
- user_id,
2780
- max_active_duration_ms,
2781
- lifecycle_deadline_at,
2782
- retry_count,
2783
- max_retries,
2784
- max_attempts
2785
- )
2786
- VALUES (
2787
- ${options.workflowId},
2788
- ${options.workflowName},
2789
- ${options.workflowVersion ?? null},
2790
- ${workflowJsonb(options.args)},
2791
- ${options.userId},
2792
- ${options.maxActiveDurationMs},
2793
- CASE
2794
- WHEN ${options.lifecycleTimeoutMs}::bigint IS NULL THEN NULL
2795
- ELSE NOW() + (${options.lifecycleTimeoutMs}::bigint * INTERVAL '1 millisecond')
2796
- END,
2797
- 0,
2798
- ${options.maxRetries},
2799
- ${options.maxRetries + 1}
3171
+ run_id,
3172
+ event_name,
3173
+ payload_json,
3174
+ idempotency_key
2800
3175
  )
2801
- RETURNING id
3176
+ SELECT
3177
+ ${options.signalId},
3178
+ eligible_run.id,
3179
+ ${options.event},
3180
+ ${workflowJsonb(options.payload)},
3181
+ ${options.signalId}
3182
+ FROM eligible_run
3183
+ RETURNING id, run_id
2802
3184
  ),
2803
- inserted_outbox AS (
3185
+ queued_run AS (
3186
+ UPDATE _gencow_workflow_runs_v2 run
3187
+ SET
3188
+ status = 'queued',
3189
+ runnable_at = NOW(),
3190
+ lease_owner = NULL,
3191
+ lease_expires_at = NULL,
3192
+ heartbeat_at = NULL,
3193
+ updated_at = NOW()
3194
+ FROM inserted
3195
+ WHERE run.id = inserted.run_id
3196
+ AND run.status = 'waiting'
3197
+ AND run.completed_at IS NULL
3198
+ AND run.cancel_requested_at IS NULL
3199
+ RETURNING run.id
3200
+ ),
3201
+ wake_outbox AS (
2804
3202
  INSERT INTO _gencow_workflow_outbox_v2 (
2805
3203
  id,
2806
3204
  run_id,
@@ -2809,156 +3207,313 @@ async function tryInsertWorkflowV2Run(options) {
2809
3207
  status
2810
3208
  )
2811
3209
  SELECT
2812
- 'start:' || inserted_run.id,
2813
- inserted_run.id,
2814
- 'wake_run',
3210
+ 'signal:' || inserted.id,
3211
+ inserted.run_id,
3212
+ 'wake_signal',
2815
3213
  NOW(),
2816
3214
  'pending'
2817
- FROM inserted_run
3215
+ FROM inserted
3216
+ JOIN queued_run ON queued_run.id = inserted.run_id
2818
3217
  ON CONFLICT (id) DO NOTHING
2819
3218
  RETURNING id
2820
3219
  ),
2821
3220
  wake_notified AS (
2822
3221
  SELECT pg_notify(${WORKFLOW_WAKE_SIGNAL_CHANNEL}, ${WORKFLOW_WAKE_SIGNAL_PAYLOAD})
2823
- FROM inserted_outbox
3222
+ FROM wake_outbox
2824
3223
  )
2825
- SELECT inserted_run.id
2826
- FROM inserted_run
3224
+ SELECT 1
3225
+ FROM inserted
2827
3226
  LEFT JOIN wake_notified ON TRUE
2828
3227
  `);
2829
- return true;
3228
+ return rowsFromResult(result).length > 0 ? "accepted" : "rejected";
2830
3229
  } catch (error) {
2831
- if (isMissingWorkflowV2SchemaError2(error)) return false;
3230
+ if (isMissingWorkflowV2SignalSchemaError(error)) return "absent";
2832
3231
  throw error;
2833
3232
  }
2834
3233
  }
2835
- async function tryDeleteWorkflowV2Run(db, workflowId) {
3234
+ async function mirrorWorkflowV2SignalToV1(options) {
2836
3235
  try {
2837
- await db.execute(sql4`
2838
- DELETE FROM _gencow_workflow_runs_v2
2839
- WHERE id = ${workflowId}
3236
+ await options.db.execute(sql4`
3237
+ INSERT INTO _gencow_workflow_events (id, workflow_id, event_name, payload)
3238
+ SELECT ${options.signalId}, workflow.id, ${options.event}, NULL
3239
+ FROM _gencow_workflows workflow
3240
+ WHERE workflow.id = ${options.workflowId}
3241
+ ON CONFLICT (id) DO NOTHING
2840
3242
  `);
2841
3243
  } catch (error) {
2842
- if (!isMissingWorkflowV2SchemaError2(error)) throw error;
3244
+ if (!isMissingWorkflowV1Table(error)) {
3245
+ console.warn("Workflow V1 signal mirror failed", { signalId: options.signalId });
3246
+ }
2843
3247
  }
2844
3248
  }
2845
- function getWorkflowResumeActionName(name) {
2846
- return `${WORKFLOW_RESUME_ACTION_PREFIX}.${name}`;
2847
- }
2848
- function createWorkflowRealtimeToken() {
2849
- return crypto.randomUUID().replace(/-/g, "");
2850
- }
2851
- function getWorkflowRealtimeKey(workflowId, realtimeToken) {
2852
- return `${WORKFLOW_REALTIME_KEY_PREFIX}.${workflowId}.${realtimeToken}`;
2853
- }
2854
- function getWorkflowDef(name) {
2855
- return workflowRegistry.get(name);
2856
- }
2857
- function getRegisteredWorkflows() {
2858
- return Array.from(workflowRegistry.values());
3249
+ async function loadWorkflowSnapshot(db, workflowId, options) {
3250
+ const v2Run = await loadWorkflowV2Run(db, workflowId);
3251
+ let workflowResult;
3252
+ try {
3253
+ workflowResult = await db.execute(sql4`
3254
+ SELECT
3255
+ id,
3256
+ name,
3257
+ args,
3258
+ status,
3259
+ current_step,
3260
+ result,
3261
+ error,
3262
+ error_code,
3263
+ retry_count,
3264
+ max_retries,
3265
+ max_duration_ms,
3266
+ started_at,
3267
+ updated_at,
3268
+ completed_at,
3269
+ realtime_token,
3270
+ user_id
3271
+ FROM _gencow_workflows
3272
+ WHERE id = ${workflowId}
3273
+ LIMIT 1
3274
+ `);
3275
+ } catch (error) {
3276
+ if (!v2Run || !isMissingWorkflowV1Table(error)) throw error;
3277
+ workflowResult = [];
3278
+ }
3279
+ const row = rowsFromResult(workflowResult)[0] ?? null;
3280
+ if (!v2Run && !row) return null;
3281
+ const viewerUserId = options?.viewerUserId ?? null;
3282
+ const ownerId = v2Run ? v2Run.user_id : row?.user_id;
3283
+ if (options?.requireViewerMatch && ownerId && ownerId !== viewerUserId) {
3284
+ return null;
3285
+ }
3286
+ const realtimeToken = row ? await ensureWorkflowRealtimeToken(db, workflowId, row.realtime_token) : v2Run?.id ?? null;
3287
+ if (!realtimeToken) return null;
3288
+ const v2Steps = v2Run ? await loadWorkflowV2Steps(db, workflowId) : [];
3289
+ let steps = v2Steps.map(mapV2WorkflowStep);
3290
+ if (steps.length === 0) {
3291
+ let stepsResult;
3292
+ try {
3293
+ stepsResult = await db.execute(sql4`
3294
+ SELECT
3295
+ step_name,
3296
+ status,
3297
+ output,
3298
+ error,
3299
+ started_at,
3300
+ updated_at,
3301
+ completed_at
3302
+ FROM _gencow_workflow_steps
3303
+ WHERE workflow_id = ${workflowId}
3304
+ ORDER BY COALESCE(started_at, updated_at) ASC, step_name ASC
3305
+ `);
3306
+ } catch (error) {
3307
+ if (!v2Run || !isMissingWorkflowV1Table(error)) throw error;
3308
+ stepsResult = [];
3309
+ }
3310
+ steps = rowsFromResult(stepsResult).map(mapWorkflowStep);
3311
+ }
3312
+ return {
3313
+ ...v2Run ? mapV2WorkflowSummary(v2Run) : mapWorkflowSummary(row),
3314
+ args: deserializeWorkflowValue(parseJsonField(v2Run ? v2Run.args_json : row.args)),
3315
+ result: v2Run ? parseJsonField(v2Run.result_json) : deserializeWorkflowValue(parseJsonField(row.result)),
3316
+ steps,
3317
+ realtimeKey: getWorkflowRealtimeKey(workflowId, realtimeToken)
3318
+ };
2859
3319
  }
2860
- function workflow(name, options) {
2861
- registerWorkflowsApi();
2862
- const maxActiveDurationMs = normalizeMaxDurationMs(
2863
- options.maxActiveDuration ?? options.maxDuration,
2864
- options.maxActiveDuration == null ? "workflow() maxDuration" : "workflow() maxActiveDuration"
2865
- );
2866
- const lifecycleTimeoutMs = normalizeOptionalDurationMs(
2867
- options.lifecycleTimeout,
2868
- "workflow() lifecycleTimeout"
2869
- );
2870
- const maxRetries = clampRetries(options.retries);
2871
- const def = {
2872
- name,
2873
- argsSchema: options.args,
2874
- isPublic: options.public === true,
2875
- version: options.version,
2876
- maxDurationMs: maxActiveDurationMs,
2877
- maxActiveDurationMs,
2878
- lifecycleTimeoutMs,
2879
- concurrency: normalizeConcurrency(options.concurrency),
2880
- maxRetries,
2881
- handler: options.handler
3320
+ function buildWorkflowBuiltinDefinitions() {
3321
+ const get = {
3322
+ key: "workflows.get",
3323
+ argsSchema: { id: v.string() },
3324
+ isPublic: true,
3325
+ handler: async (ctx, args) => {
3326
+ return loadWorkflowSnapshot(ctx.unsafeDb, args.id, {
3327
+ viewerUserId: ctx.auth.getUserIdentity()?.id ?? null,
3328
+ requireViewerMatch: true
3329
+ });
3330
+ }
2882
3331
  };
2883
- workflowRegistry.set(name, def);
2884
- return mutation(name, {
2885
- args: options.args,
2886
- public: options.public,
3332
+ const signal = {
3333
+ name: "workflows.signal",
3334
+ argsSchema: {
3335
+ id: v.string(),
3336
+ event: v.string(),
3337
+ payload: v.optional(v.any())
3338
+ },
3339
+ isPublic: true,
2887
3340
  handler: async (ctx, args) => {
2888
- const workflowId = crypto.randomUUID();
2889
- const resumeAction = getWorkflowResumeActionName(name);
2890
- const ownerId = ctx.auth.getUserIdentity()?.id ?? null;
2891
- const persistedArgs = serializeWorkflowValue(args ?? {});
3341
+ const normalizedEvent = args.event.trim();
3342
+ if (!normalizedEvent) {
3343
+ throw new GencowValidationError(`Argument "event": expected a non-empty string`);
3344
+ }
3345
+ const v2Run = await loadWorkflowV2Run(ctx.unsafeDb, args.id);
3346
+ const workflow2 = await loadWorkflowSignalTarget(ctx.unsafeDb, args.id);
3347
+ const viewerUserId = ctx.auth.getUserIdentity()?.id ?? null;
3348
+ const ownerId = v2Run ? v2Run.user_id : workflow2?.user_id;
3349
+ if (!v2Run && !workflow2 || ownerId && ownerId !== viewerUserId) {
3350
+ return {
3351
+ ok: false,
3352
+ workflowId: args.id,
3353
+ event: normalizedEvent,
3354
+ scheduledJobId: null
3355
+ };
3356
+ }
3357
+ const workflowId = v2Run?.id ?? workflow2.id;
3358
+ const workflowName = v2Run?.workflow_name ?? workflow2.name;
3359
+ if (v2Run ? v2Run.status === "completed" || v2Run.status === "failed" || v2Run.status === "cancelled" || v2Run.completed_at !== null : workflow2.status === "completed" || workflow2.status === "failed") {
3360
+ return {
3361
+ ok: false,
3362
+ workflowId,
3363
+ event: normalizedEvent,
3364
+ scheduledJobId: null
3365
+ };
3366
+ }
3367
+ const persistedPayload = serializeWorkflowValue(args.payload);
2892
3368
  assertWorkflowPayloadWithinLimit({
2893
- workflowName: name,
2894
- kind: "args",
2895
- serializedValue: persistedArgs
3369
+ workflowName,
3370
+ kind: "signal",
3371
+ serializedValue: persistedPayload
2896
3372
  });
2897
- const realtimeToken = createWorkflowRealtimeToken();
2898
- let insertedWorkflowV2 = false;
2899
- try {
2900
- await runWorkflowAdmissionCriticalSection(ctx.unsafeDb, async (db) => {
2901
- await applyWorkflowStartAdmission({
2902
- db,
2903
- workflowName: name,
2904
- args: persistedArgs
2905
- });
2906
- insertedWorkflowV2 = await tryInsertWorkflowV2Run({
2907
- db,
3373
+ const signalId = crypto.randomUUID();
3374
+ if (v2Run) {
3375
+ const recordedV2 = await tryRecordWorkflowV2Signal({
3376
+ db: ctx.unsafeDb,
3377
+ workflowId,
3378
+ event: normalizedEvent,
3379
+ signalId,
3380
+ payload: persistedPayload
3381
+ });
3382
+ if (recordedV2 !== "accepted") {
3383
+ return { ok: false, workflowId, event: normalizedEvent, scheduledJobId: null };
3384
+ }
3385
+ if (workflow2) {
3386
+ await mirrorWorkflowV2SignalToV1({
3387
+ db: ctx.unsafeDb,
2908
3388
  workflowId,
2909
- workflowName: name,
2910
- workflowVersion: options.version ?? null,
2911
- args: persistedArgs,
2912
- userId: ownerId,
2913
- maxActiveDurationMs,
2914
- lifecycleTimeoutMs,
2915
- maxRetries
3389
+ event: normalizedEvent,
3390
+ signalId
2916
3391
  });
2917
- const legacyArgs = insertedWorkflowV2 ? serializeWorkflowValue(void 0) : persistedArgs;
2918
- await db.execute(sql4`
2919
- INSERT INTO _gencow_workflows (
2920
- id,
2921
- name,
2922
- args,
2923
- realtime_token,
2924
- status,
2925
- retry_count,
2926
- max_retries,
2927
- max_duration_ms,
2928
- user_id
2929
- )
2930
- VALUES (
2931
- ${workflowId},
2932
- ${name},
2933
- ${workflowJsonb(legacyArgs)},
2934
- ${realtimeToken},
2935
- 'pending',
2936
- 0,
2937
- ${maxRetries},
2938
- ${maxActiveDurationMs},
2939
- ${ownerId}
2940
- )
2941
- `);
2942
- });
2943
- const scheduledJobId = insertedWorkflowV2 ? `start:${workflowId}` : ctx.scheduler.runAfter(0, resumeAction, { workflowId });
2944
- return {
2945
- id: workflowId,
2946
- name,
2947
- status: "pending",
2948
- scheduledJobId
2949
- };
2950
- } catch (error) {
3392
+ }
3393
+ } else {
2951
3394
  await ctx.unsafeDb.execute(sql4`
2952
- DELETE FROM _gencow_workflows
2953
- WHERE id = ${workflowId}
2954
- `);
2955
- if (insertedWorkflowV2) {
2956
- await tryDeleteWorkflowV2Run(ctx.unsafeDb, workflowId);
3395
+ INSERT INTO _gencow_workflow_events (
3396
+ id,
3397
+ workflow_id,
3398
+ event_name,
3399
+ payload
3400
+ )
3401
+ VALUES (
3402
+ ${signalId},
3403
+ ${workflowId},
3404
+ ${normalizedEvent},
3405
+ ${workflowJsonb(persistedPayload)}
3406
+ )
3407
+ `);
3408
+ }
3409
+ let scheduledJobId = null;
3410
+ if (workflow2?.status === "pending" && workflow2.current_step?.startsWith("wait:")) {
3411
+ try {
3412
+ scheduledJobId = ctx.scheduler.runAfter(0, getWorkflowResumeActionName(workflowName), {
3413
+ workflowId
3414
+ });
3415
+ } catch {
3416
+ scheduledJobId = null;
3417
+ }
3418
+ }
3419
+ return {
3420
+ ok: true,
3421
+ workflowId,
3422
+ event: normalizedEvent,
3423
+ scheduledJobId
3424
+ };
3425
+ }
3426
+ };
3427
+ const list = {
3428
+ key: "workflows.list",
3429
+ argsSchema: {
3430
+ limit: v.optional(v.number()),
3431
+ status: v.optional(v.string())
3432
+ },
3433
+ isPublic: false,
3434
+ handler: async (ctx, args) => {
3435
+ const userId = ctx.auth.requireAuth().id;
3436
+ const limit = normalizeListLimit(args.limit);
3437
+ const requestedStatus = normalizeDerivedStatus(args.status);
3438
+ const v1Derived = sql4`CASE WHEN w.status = 'pending' AND w.current_step LIKE 'sleep#%' THEN 'sleeping'
3439
+ WHEN w.status = 'pending' AND w.current_step LIKE 'wait:%' THEN 'waiting'
3440
+ WHEN w.status = 'pending' THEN 'queued' ELSE w.status END`;
3441
+ const effectiveStatus = sql4`CASE WHEN v.id IS NOT NULL THEN
3442
+ CASE WHEN v.status = 'cancelled' THEN 'failed'
3443
+ WHEN v.status IN ('queued', 'waiting', 'sleeping') THEN 'pending'
3444
+ ELSE v.status END
3445
+ ELSE w.status END`;
3446
+ const effectiveDerived = sql4`CASE WHEN v.id IS NOT NULL THEN
3447
+ CASE WHEN v.status = 'cancelled' THEN 'failed'
3448
+ WHEN v.status IN ('queued', 'waiting', 'sleeping') THEN v.status
3449
+ ELSE v.status END
3450
+ ELSE ${v1Derived} END`;
3451
+ const statusForFilter = requestedStatus === "pending" ? effectiveStatus : effectiveDerived;
3452
+ const v1StatusForFilter = requestedStatus === "pending" ? sql4`w.status` : v1Derived;
3453
+ try {
3454
+ const result = await ctx.unsafeDb.execute(sql4`
3455
+ SELECT w.*, v.id AS v2_id, v.workflow_name AS v2_name,
3456
+ v.status AS v2_status, v.error_json AS v2_error_json,
3457
+ v.error_code AS v2_error_code, v.current_step_key AS v2_current_step,
3458
+ v.retry_count AS v2_retry_count, v.max_retries AS v2_max_retries,
3459
+ v.max_active_duration_ms AS v2_max_duration_ms,
3460
+ v.created_at AS v2_created_at, v.updated_at AS v2_updated_at,
3461
+ v.completed_at AS v2_completed_at
3462
+ FROM _gencow_workflows w
3463
+ FULL OUTER JOIN _gencow_workflow_runs_v2 v ON v.id = w.id
3464
+ WHERE (CASE WHEN v.id IS NOT NULL THEN v.user_id ELSE w.user_id END) = ${userId}
3465
+ AND (${requestedStatus ?? null}::text IS NULL OR ${statusForFilter} = ${requestedStatus ?? null})
3466
+ ORDER BY COALESCE(v.created_at, w.started_at) DESC, COALESCE(v.id, w.id) DESC
3467
+ LIMIT ${limit}
3468
+ `);
3469
+ return rowsFromResult(result).map(mapJoinedWorkflowSummary);
3470
+ } catch (error) {
3471
+ if (isMissingWorkflowV1Table(error)) {
3472
+ const v2StatusForFilter = requestedStatus === "pending" ? sql4`CASE WHEN status IN ('queued', 'waiting', 'sleeping') THEN 'pending'
3473
+ WHEN status = 'cancelled' THEN 'failed' ELSE status END` : sql4`CASE WHEN status = 'cancelled' THEN 'failed' ELSE status END`;
3474
+ const result2 = await ctx.unsafeDb.execute(sql4`
3475
+ SELECT id, workflow_name, status, args_json, result_json, error_json, error_code,
3476
+ current_step_key, retry_count, max_retries, max_active_duration_ms,
3477
+ created_at, updated_at, completed_at, user_id
3478
+ FROM _gencow_workflow_runs_v2
3479
+ WHERE user_id = ${userId}
3480
+ AND (${requestedStatus ?? null}::text IS NULL OR ${v2StatusForFilter} = ${requestedStatus ?? null})
3481
+ ORDER BY created_at DESC, id DESC
3482
+ LIMIT ${limit}
3483
+ `);
3484
+ return rowsFromResult(result2).map(mapV2WorkflowSummary);
2957
3485
  }
2958
- throw error;
3486
+ if (!isMissingWorkflowV2PayloadSchemaError(error)) throw error;
3487
+ const result = await ctx.unsafeDb.execute(sql4`
3488
+ SELECT w.* FROM _gencow_workflows w
3489
+ WHERE w.user_id = ${userId}
3490
+ AND (${requestedStatus ?? null}::text IS NULL OR ${v1StatusForFilter} = ${requestedStatus ?? null})
3491
+ ORDER BY w.started_at DESC, w.id DESC
3492
+ LIMIT ${limit}
3493
+ `);
3494
+ return rowsFromResult(result).map(mapWorkflowSummary);
2959
3495
  }
2960
3496
  }
3497
+ };
3498
+ const definitions = Object.freeze({
3499
+ get: Object.freeze(get),
3500
+ list: Object.freeze(list),
3501
+ signal: Object.freeze(signal)
2961
3502
  });
3503
+ return definitions;
3504
+ }
3505
+ function createWorkflowBuiltinDefinitions() {
3506
+ return buildWorkflowBuiltinDefinitions();
3507
+ }
3508
+ function registerWorkflowsApi() {
3509
+ const registeredDefinitions = globalThis.__gencow_workflowsApiRegistered;
3510
+ if (registeredDefinitions) return registeredDefinitions;
3511
+ const definitions = buildWorkflowBuiltinDefinitions();
3512
+ queryRegistry.set(definitions.get.key, definitions.get);
3513
+ queryRegistry.set(definitions.list.key, definitions.list);
3514
+ mutationRegistry.push(definitions.signal);
3515
+ globalThis.__gencow_workflowsApiRegistered = definitions;
3516
+ return definitions;
2962
3517
  }
2963
3518
 
2964
3519
  // ../core/src/retry.ts
@@ -3530,6 +4085,35 @@ var pgBaseTable = pgTable;
3530
4085
  // ../core/src/rls-db.ts
3531
4086
  import { AsyncLocalStorage } from "node:async_hooks";
3532
4087
  import { sql as sql7 } from "drizzle-orm";
4088
+ var TRANSACTION_ISOLATION_LEVELS = /* @__PURE__ */ new Set([
4089
+ "read uncommitted",
4090
+ "read committed",
4091
+ "repeatable read",
4092
+ "serializable"
4093
+ ]);
4094
+ var TRANSACTION_ACCESS_MODES = /* @__PURE__ */ new Set(["read only", "read write"]);
4095
+ function transactionConfigSql(config) {
4096
+ if (!config || typeof config !== "object") return null;
4097
+ const value = config;
4098
+ const chunks = [];
4099
+ if (value.isolationLevel !== void 0) {
4100
+ if (!TRANSACTION_ISOLATION_LEVELS.has(value.isolationLevel)) return null;
4101
+ chunks.push(`isolation level ${value.isolationLevel}`);
4102
+ }
4103
+ if (value.accessMode !== void 0) {
4104
+ if (!TRANSACTION_ACCESS_MODES.has(value.accessMode)) return null;
4105
+ chunks.push(value.accessMode);
4106
+ }
4107
+ if (value.deferrable !== void 0) {
4108
+ if (typeof value.deferrable !== "boolean") return null;
4109
+ chunks.push(value.deferrable ? "deferrable" : "not deferrable");
4110
+ }
4111
+ return chunks.length > 0 ? sql7.raw(`SET TRANSACTION ${chunks.join(", ")}`) : null;
4112
+ }
4113
+ async function applyTransactionConfig(tx, config) {
4114
+ const statement = transactionConfigSql(config);
4115
+ if (statement) await tx.execute(statement);
4116
+ }
3533
4117
  function rlsSetConfigPairs(rls) {
3534
4118
  const pairs = [];
3535
4119
  for (const [sessionKey, value] of Object.entries(rls)) {
@@ -3569,8 +4153,16 @@ async function execSetConfig(client, name, value) {
3569
4153
  async function applyRlsSessionVars(client, rls) {
3570
4154
  await forEachSetConfig(rls, (name, value) => execSetConfig(client, name, value));
3571
4155
  }
3572
- async function injectRlsVarsOnTx(tx, rls) {
3573
- await forEachSetConfig(rls, (name, value) => tx.execute(sql7`SELECT set_config(${name}, ${value}, true)`));
4156
+ async function injectRlsVarsOnTx(tx, rls, onTransactionContextFailure) {
4157
+ try {
4158
+ await forEachSetConfig(rls, (name, value) => tx.execute(sql7`SELECT set_config(${name}, ${value}, true)`));
4159
+ } catch (error) {
4160
+ try {
4161
+ onTransactionContextFailure?.(error);
4162
+ } catch {
4163
+ }
4164
+ throw error;
4165
+ }
3574
4166
  }
3575
4167
  async function withRlsLeasedConnection(leased, rls, fn) {
3576
4168
  try {
@@ -3667,7 +4259,11 @@ function createBunSqlPreparedQueryClientExecutors(baseSession, prepareQuery, pre
3667
4259
  }
3668
4260
  function createPreparedQueryClientExecutors(params) {
3669
4261
  if (isBunSqlSession(params.baseSession)) {
3670
- return createBunSqlPreparedQueryClientExecutors(params.baseSession, params.prepareQuery, params.prepareArgs);
4262
+ return createBunSqlPreparedQueryClientExecutors(
4263
+ params.baseSession,
4264
+ params.prepareQuery,
4265
+ params.prepareArgs
4266
+ );
3671
4267
  }
3672
4268
  return createGenericPreparedQueryClientExecutors(params.pq);
3673
4269
  }
@@ -3676,7 +4272,12 @@ async function runPreparedQueryWithRls(session, rls, reuseOuterConnection, runOn
3676
4272
  if (active) {
3677
4273
  return runOnClient(active, placeholderValues);
3678
4274
  }
3679
- return withRlsConnection(session, rls, reuseOuterConnection, (client) => runOnClient(client, placeholderValues));
4275
+ return withRlsConnection(
4276
+ session,
4277
+ rls,
4278
+ reuseOuterConnection,
4279
+ (client) => runOnClient(client, placeholderValues)
4280
+ );
3680
4281
  }
3681
4282
  function wrapPreparedQuery(pq, session, baseSession, prepareQuery, prepareArgs, rls, reuseOuterConnection) {
3682
4283
  const clientExecutors = createPreparedQueryClientExecutors({
@@ -3709,7 +4310,7 @@ function wrapSession(session, rls, reuseOuterConnection) {
3709
4310
  }
3710
4311
  });
3711
4312
  }
3712
- function createRlsDb(db, rls) {
4313
+ function createRlsDb(db, rls, options = {}) {
3713
4314
  const reuseOuterConnection = isDrizzleTransactionDb(db);
3714
4315
  const baseSession = db.session;
3715
4316
  const wrappedSession = wrapSession(baseSession, rls, reuseOuterConnection);
@@ -3729,13 +4330,41 @@ function createRlsDb(db, rls) {
3729
4330
  }
3730
4331
  if (prop === "transaction") {
3731
4332
  return async (callback, ...rest) => {
3732
- return await target.transaction(
3733
- async (tx) => {
3734
- await injectRlsVarsOnTx(tx, rls);
3735
- return await callback(tx);
3736
- },
3737
- ...rest
3738
- );
4333
+ let callbackStarted = false;
4334
+ const configStatement = transactionConfigSql(rest[0]);
4335
+ const transactionArgs = configStatement ? [] : rest;
4336
+ try {
4337
+ return await target.transaction(
4338
+ async (tx) => {
4339
+ if (configStatement) await tx.execute(configStatement);
4340
+ await injectRlsVarsOnTx(tx, rls, options.onTransactionContextFailure);
4341
+ callbackStarted = true;
4342
+ return await callback(tx);
4343
+ },
4344
+ ...transactionArgs
4345
+ );
4346
+ } catch (error) {
4347
+ const markedRlsContextFailure = error && typeof error === "object" && error.gencowRlsTransactionContextFailure === true;
4348
+ if (!callbackStarted && !markedRlsContextFailure && error && typeof error === "object") {
4349
+ try {
4350
+ Object.defineProperty(error, "gencowTransactionSetupFailure", { value: true });
4351
+ } catch {
4352
+ }
4353
+ }
4354
+ throw error;
4355
+ }
4356
+ };
4357
+ }
4358
+ if (prop === "transactionWithCharacteristics") {
4359
+ return async (characteristics, callback) => {
4360
+ return await target.transaction(async (tx) => {
4361
+ await applyTransactionConfig(tx, {
4362
+ ...characteristics?.isolationLevel ? { isolationLevel: characteristics.isolationLevel } : {},
4363
+ ...characteristics?.readOnly ? { accessMode: "read only" } : {}
4364
+ });
4365
+ await injectRlsVarsOnTx(tx, rls, options.onTransactionContextFailure);
4366
+ return callback(tx);
4367
+ });
3739
4368
  };
3740
4369
  }
3741
4370
  const value = Reflect.get(target, prop, receiver);
@@ -5137,6 +5766,7 @@ export {
5137
5766
  PLATFORM_CAPACITY_PRESETS,
5138
5767
  PLATFORM_CAPACITY_PROFILE_ENV,
5139
5768
  RLS_SESSION_KEY_RE,
5769
+ RealtimeChannelEventValidationError,
5140
5770
  STORAGE_LIST_DEFAULT_LIMIT,
5141
5771
  STORAGE_LIST_MAX_LIMIT,
5142
5772
  StorageListUnsupportedError,
@@ -5157,16 +5787,20 @@ export {
5157
5787
  buildSearchIndexNames,
5158
5788
  buildWakeAppBootFailedResult,
5159
5789
  buildWakeAppSuccessResult,
5790
+ captureLegacyRealtimeChannels,
5160
5791
  compareUnreadCursors,
5161
5792
  computeCronActionToken,
5162
5793
  countUnreadAfterCursor,
5163
5794
  createCrudFactory,
5164
5795
  createGencowHttpRouteBuilders,
5165
5796
  createGencowProcedureBuilders,
5797
+ createGencowRealtimeChannelBuilder,
5798
+ createGencowWorkflowBuilders,
5166
5799
  createRealtimeChannelToken,
5167
5800
  createRealtimeQuerySubscriptionToken,
5168
5801
  createRealtimeTicket,
5169
5802
  createRlsDb,
5803
+ createWorkflowBuiltinDefinitions,
5170
5804
  createWorkflowRealtimeToken,
5171
5805
  cron,
5172
5806
  cronJobs,
@@ -5221,6 +5855,8 @@ export {
5221
5855
  ownerRls,
5222
5856
  parseArgs,
5223
5857
  parseFilterNode,
5858
+ parseRealtimeChannelEvent,
5859
+ parseStandardSchema,
5224
5860
  parseWorkflowDurationMs,
5225
5861
  pgBaseTable,
5226
5862
  query,
@@ -5228,8 +5864,10 @@ export {
5228
5864
  readNonNegativeIntegerEnv,
5229
5865
  readPositiveIntegerEnv,
5230
5866
  registerOwnerRls,
5867
+ registerWorkflowsApi,
5231
5868
  resetRealtimeChannelRegistryForTests,
5232
5869
  resetRealtimeSubscriptionRegistryForTests,
5870
+ resolveExplicitRealtimeChannelKey,
5233
5871
  resolvePlatformCapacityPreset,
5234
5872
  resolveRealtimeChannelKey,
5235
5873
  resolveRealtimeQueryScopeHash,
@@ -5237,6 +5875,7 @@ export {
5237
5875
  resolveWorkflowPayloadPolicyConfig,
5238
5876
  rlsContextKey,
5239
5877
  rlsContextKeys,
5878
+ selectLegacyRealtimeChannelSnapshot,
5240
5879
  serializeWorkflowValue,
5241
5880
  sessionKeyToGucName,
5242
5881
  stringifySessionValue,