gencow 0.1.240 → 0.1.241

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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) => {
@@ -1439,239 +1098,843 @@ function parseArgs(schema, args) {
1439
1098
  return args;
1440
1099
  }
1441
1100
 
1442
- // ../core/src/procedure.ts
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
+ }
1443
1160
  function hasStandardSchema2(schema) {
1444
- return !!schema && typeof schema === "object" && "~standard" in schema && !!schema["~standard"] && typeof schema["~standard"].validate === "function";
1161
+ return !!schema && typeof schema === "object" && typeof schema["~standard"]?.validate === "function";
1445
1162
  }
1446
- async function validateWithSchema(schema, value, options) {
1163
+ async function parseStandardSchema(schema, value) {
1447
1164
  if (!schema) return value;
1448
- const valueToValidate = options?.normalizeMissingOptionalObject ? normalizeMissingOptionalObjectInput(schema, value) : value;
1449
- if (hasStandardSchema2(schema)) {
1450
- const result = await schema["~standard"].validate(valueToValidate);
1451
- if ("issues" in result && result.issues.length > 0) {
1452
- throw new Error(result.issues[0]?.message ?? "Validation failed");
1453
- }
1454
- if ("value" in result) {
1455
- return result.value;
1456
- }
1457
- throw new Error("Validation failed");
1165
+ if (!hasStandardSchema2(schema)) {
1166
+ throw new Error("Realtime channels require a Standard Schema v1 validator");
1458
1167
  }
1459
- throw new Error("procedure.query / procedure.mutation / procedure.internal only supports Standard Schema v1 validators");
1460
- }
1461
- function composeMiddlewares(middlewares, handler, inputSchema, outputSchema, inputValidationIndex, outputValidationIndex) {
1462
- return async (initialContext, initialInput) => {
1463
- let idx = -1;
1464
- const boundedInputValidationIndex = Math.min(Math.max(0, inputValidationIndex), middlewares.length);
1465
- const boundedOutputValidationIndex = Math.min(Math.max(0, outputValidationIndex), middlewares.length);
1466
- const run = async (currentIdx, context, input) => {
1467
- if (currentIdx <= idx) {
1468
- throw new Error("next() called multiple times in the same middleware");
1469
- }
1470
- idx = currentIdx;
1471
- let currentInput = input;
1472
- if (currentIdx === boundedInputValidationIndex) {
1473
- currentInput = await validateWithSchema(inputSchema, currentInput, {
1474
- normalizeMissingOptionalObject: true
1475
- });
1476
- }
1477
- if (currentIdx === middlewares.length) {
1478
- const output2 = await handler({ context, input: currentInput });
1479
- if (currentIdx === boundedOutputValidationIndex) {
1480
- return await validateWithSchema(outputSchema, output2);
1481
- }
1482
- return output2;
1483
- }
1484
- const middleware = middlewares[currentIdx];
1485
- const output = await middleware({
1486
- context,
1487
- input: currentInput,
1488
- next: async (overrides) => {
1489
- return await run(
1490
- currentIdx + 1,
1491
- overrides?.context ?? context,
1492
- overrides?.input ?? currentInput
1493
- );
1494
- }
1495
- });
1496
- if (currentIdx === boundedOutputValidationIndex) {
1497
- return await validateWithSchema(outputSchema, output);
1498
- }
1499
- return output;
1500
- };
1501
- return await run(0, initialContext, initialInput);
1502
- };
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");
1503
1175
  }
1504
- var GencowProcedureBuilderImpl = class _GencowProcedureBuilderImpl {
1505
- constructor(kind, procedureName, middlewares = [], inputSchema, outputSchema, inputValidationIndex = -1, outputValidationIndex = -1, anonymousAllowed = false) {
1506
- this.kind = kind;
1507
- this.procedureName = procedureName;
1508
- this.middlewares = middlewares;
1509
- this.inputSchema = inputSchema;
1510
- this.outputSchema = outputSchema;
1511
- this.inputValidationIndex = inputValidationIndex;
1512
- this.outputValidationIndex = outputValidationIndex;
1513
- this.anonymousAllowed = anonymousAllowed;
1176
+ var RealtimeChannelEventValidationError = class extends GencowValidationError {
1177
+ code = "REALTIME_CHANNEL_EVENT_INVALID";
1178
+ constructor(message) {
1179
+ super(message);
1180
+ this.name = "RealtimeChannelEventValidationError";
1514
1181
  }
1515
- kind;
1516
- procedureName;
1517
- middlewares;
1518
- inputSchema;
1519
- outputSchema;
1520
- inputValidationIndex;
1521
- outputValidationIndex;
1522
- anonymousAllowed;
1523
- name(name) {
1524
- return new _GencowProcedureBuilderImpl(
1525
- this.kind,
1526
- name,
1527
- this.middlewares,
1528
- this.inputSchema,
1529
- this.outputSchema,
1530
- this.inputValidationIndex,
1531
- this.outputValidationIndex,
1532
- this.anonymousAllowed
1533
- );
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;
1534
1191
  }
1535
- allowAnonymous() {
1536
- return new _GencowProcedureBuilderImpl(
1537
- this.kind,
1538
- this.procedureName,
1539
- this.middlewares,
1540
- this.inputSchema,
1541
- this.outputSchema,
1542
- this.inputValidationIndex,
1543
- this.outputValidationIndex,
1544
- true
1545
- );
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");
1546
1198
  }
1547
- use(middleware) {
1548
- return new _GencowProcedureBuilderImpl(
1549
- this.kind,
1550
- this.procedureName,
1551
- [...this.middlewares, middleware],
1552
- this.inputSchema,
1553
- this.outputSchema,
1554
- this.inputValidationIndex,
1555
- this.outputValidationIndex,
1556
- this.anonymousAllowed
1199
+ if (typeof value !== "object") {
1200
+ throw new RealtimeChannelEventValidationError(
1201
+ "Realtime channel events must be JSON-serializable wire values"
1557
1202
  );
1558
1203
  }
1559
- input(schema) {
1560
- const nextInputValidationIndex = this.middlewares.length < 0 ? 0 : this.middlewares.length;
1561
- return new _GencowProcedureBuilderImpl(
1562
- this.kind,
1563
- this.procedureName,
1564
- this.middlewares,
1565
- schema,
1566
- this.outputSchema,
1567
- nextInputValidationIndex,
1568
- this.outputValidationIndex,
1569
- this.anonymousAllowed
1570
- );
1204
+ if (seen.has(value)) {
1205
+ throw new RealtimeChannelEventValidationError("Realtime channel events must not contain circular values");
1571
1206
  }
1572
- output(schema) {
1573
- const nextOutputValidationIndex = this.middlewares.length < 0 ? 0 : this.middlewares.length;
1574
- return new _GencowProcedureBuilderImpl(
1575
- this.kind,
1576
- this.procedureName,
1577
- this.middlewares,
1578
- this.inputSchema,
1579
- schema,
1580
- this.inputValidationIndex,
1581
- nextOutputValidationIndex,
1582
- this.anonymousAllowed
1583
- );
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);
1584
1216
  }
1585
- handler(handler) {
1586
- if (!this.procedureName) {
1587
- throw new Error("Procedure name is required. Call .name(...) before .handler(...)");
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
+ );
1588
1227
  }
1589
- const run = composeMiddlewares(
1590
- this.middlewares,
1591
- handler,
1592
- this.inputSchema,
1593
- this.outputSchema,
1594
- this.inputValidationIndex,
1595
- this.outputValidationIndex
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"
1596
1233
  );
1597
- return {
1598
- kind: this.kind,
1599
- name: this.procedureName,
1600
- allowAnonymous: this.kind === "internal" ? false : this.anonymousAllowed,
1601
- middlewares: [...this.middlewares],
1602
- inputSchema: this.inputSchema,
1603
- outputSchema: this.outputSchema,
1604
- inputValidationIndex: this.inputValidationIndex,
1605
- outputValidationIndex: this.outputValidationIndex,
1606
- handler: run
1607
- };
1608
1234
  }
1609
- };
1610
- function createGencowProcedureBuilders() {
1611
- return {
1612
- query: new GencowProcedureBuilderImpl("query"),
1613
- mutation: new GencowProcedureBuilderImpl("mutation"),
1614
- internal: new GencowProcedureBuilderImpl("internal")
1615
- };
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;
1616
1242
  }
1617
1243
 
1618
- // ../core/src/http-action.ts
1619
- if (!globalThis.__gencow_httpActionRegistry) globalThis.__gencow_httpActionRegistry = [];
1620
- var httpActionRegistry = globalThis.__gencow_httpActionRegistry;
1621
- function httpAction(def) {
1622
- const actionDef = {
1623
- method: def.method,
1624
- path: def.path,
1625
- isPublic: def.public === true,
1626
- handler: def.handler
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
+ }
1627
1280
  };
1628
- httpActionRegistry.push(actionDef);
1629
- return actionDef;
1630
- }
1631
- function getRegisteredHttpActions() {
1632
- return [...httpActionRegistry];
1633
1281
  }
1634
1282
 
1635
- // ../core/src/realtime-ticket.ts
1636
- import { createHmac as createHmac3, randomBytes as randomBytes3, timingSafeEqual as timingSafeEqual3 } from "crypto";
1637
- var TICKET_PREFIX = "gencow_ws1";
1638
- var DEFAULT_TTL_SECONDS3 = 60;
1639
- var MAX_TTL_SECONDS3 = 120;
1640
- var MAX_TICKET_LENGTH = 4096;
1641
- function encodeBase64Url3(value) {
1642
- return Buffer.from(value, "utf8").toString("base64url");
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)}`;
1643
1289
  }
1644
- function decodeBase64Url3(value) {
1645
- try {
1646
- return Buffer.from(value, "base64url").toString("utf8");
1647
- } catch {
1648
- return null;
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;
1649
1303
  }
1304
+ return value;
1650
1305
  }
1651
- function signSegment3(segment, secret) {
1652
- return createHmac3("sha256", secret).update(segment).digest("base64url");
1653
- }
1654
- function safeEqual3(a, b) {
1655
- const left = Buffer.from(a);
1656
- const right = Buffer.from(b);
1657
- return left.length === right.length && timingSafeEqual3(left, right);
1658
- }
1659
- function validateRequiredString3(value, label) {
1660
- if (typeof value !== "string" || value.length === 0) {
1661
- throw new Error(`${label} required`);
1662
- }
1306
+ function isEmptyPlainObject3(value) {
1307
+ return !!value && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === 0;
1663
1308
  }
1664
- function resolveTtlMs3(ttlSeconds) {
1665
- const ttl = Number.isFinite(ttlSeconds) && ttlSeconds > 0 ? ttlSeconds : DEFAULT_TTL_SECONDS3;
1666
- return Math.floor(Math.min(ttl, MAX_TTL_SECONDS3) * 1e3);
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)}`;
1667
1313
  }
1668
- function randomNonce3() {
1669
- return randomBytes3(16).toString("base64url");
1314
+ function subscriptionKeyMatchesQueryKey(subscriptionKey, queryKey) {
1315
+ return subscriptionKey === queryKey || subscriptionKey.startsWith(`${queryKey}${SUBSCRIPTION_KEY_SEPARATOR}`);
1670
1316
  }
1671
- function resolveRealtimeCapabilityNegotiation(value) {
1672
- const record = value && typeof value === "object" ? value : null;
1673
- return {
1674
- crudRealtimeMode: record?.crudRealtimeMode === "scoped" ? "scoped" : "legacy"
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;
1573
+ }
1574
+ try {
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 });
1583
+ } catch (e) {
1584
+ console.warn(`[gencow] refresh("${key}") failed:`, e instanceof Error ? e.message : e);
1585
+ }
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);
1593
+ }
1594
+ }
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}`;
1621
+ }
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];
1638
+ }
1639
+
1640
+ // ../core/src/procedure.ts
1641
+ function hasStandardSchema3(schema) {
1642
+ return !!schema && typeof schema === "object" && "~standard" in schema && !!schema["~standard"] && typeof schema["~standard"].validate === "function";
1643
+ }
1644
+ async function validateWithSchema(schema, value, options) {
1645
+ if (!schema) return value;
1646
+ const valueToValidate = options?.normalizeMissingOptionalObject ? normalizeMissingOptionalObjectInput(schema, value) : value;
1647
+ if (hasStandardSchema3(schema)) {
1648
+ const result = await schema["~standard"].validate(valueToValidate);
1649
+ if ("issues" in result && result.issues.length > 0) {
1650
+ throw new GencowValidationError(result.issues[0]?.message ?? "Validation failed");
1651
+ }
1652
+ if ("value" in result) {
1653
+ return result.value;
1654
+ }
1655
+ throw new GencowValidationError("Validation failed");
1656
+ }
1657
+ throw new Error("procedure.query / procedure.mutation / procedure.internal only supports Standard Schema v1 validators");
1658
+ }
1659
+ function composeMiddlewares(middlewares, handler, inputSchema, outputSchema, inputValidationIndex, outputValidationIndex) {
1660
+ return async (initialContext, initialInput) => {
1661
+ let idx = -1;
1662
+ const boundedInputValidationIndex = Math.min(Math.max(0, inputValidationIndex), middlewares.length);
1663
+ const boundedOutputValidationIndex = Math.min(Math.max(0, outputValidationIndex), middlewares.length);
1664
+ const run = async (currentIdx, context, input) => {
1665
+ if (currentIdx <= idx) {
1666
+ throw new Error("next() called multiple times in the same middleware");
1667
+ }
1668
+ idx = currentIdx;
1669
+ let currentInput = input;
1670
+ if (currentIdx === boundedInputValidationIndex) {
1671
+ currentInput = await validateWithSchema(inputSchema, currentInput, {
1672
+ normalizeMissingOptionalObject: true
1673
+ });
1674
+ }
1675
+ if (currentIdx === middlewares.length) {
1676
+ const output2 = await handler({ context, input: currentInput });
1677
+ if (currentIdx === boundedOutputValidationIndex) {
1678
+ return await validateWithSchema(outputSchema, output2);
1679
+ }
1680
+ return output2;
1681
+ }
1682
+ const middleware = middlewares[currentIdx];
1683
+ const output = await middleware({
1684
+ context,
1685
+ input: currentInput,
1686
+ next: async (overrides) => {
1687
+ return await run(
1688
+ currentIdx + 1,
1689
+ overrides?.context ?? context,
1690
+ overrides?.input ?? currentInput
1691
+ );
1692
+ }
1693
+ });
1694
+ if (currentIdx === boundedOutputValidationIndex) {
1695
+ return await validateWithSchema(outputSchema, output);
1696
+ }
1697
+ return output;
1698
+ };
1699
+ return await run(0, initialContext, initialInput);
1700
+ };
1701
+ }
1702
+ var GencowProcedureBuilderImpl = class _GencowProcedureBuilderImpl {
1703
+ constructor(kind, procedureName, middlewares = [], inputSchema, outputSchema, inputValidationIndex = -1, outputValidationIndex = -1, anonymousAllowed = false) {
1704
+ this.kind = kind;
1705
+ this.procedureName = procedureName;
1706
+ this.middlewares = middlewares;
1707
+ this.inputSchema = inputSchema;
1708
+ this.outputSchema = outputSchema;
1709
+ this.inputValidationIndex = inputValidationIndex;
1710
+ this.outputValidationIndex = outputValidationIndex;
1711
+ this.anonymousAllowed = anonymousAllowed;
1712
+ }
1713
+ kind;
1714
+ procedureName;
1715
+ middlewares;
1716
+ inputSchema;
1717
+ outputSchema;
1718
+ inputValidationIndex;
1719
+ outputValidationIndex;
1720
+ anonymousAllowed;
1721
+ name(name) {
1722
+ return new _GencowProcedureBuilderImpl(
1723
+ this.kind,
1724
+ name,
1725
+ this.middlewares,
1726
+ this.inputSchema,
1727
+ this.outputSchema,
1728
+ this.inputValidationIndex,
1729
+ this.outputValidationIndex,
1730
+ this.anonymousAllowed
1731
+ );
1732
+ }
1733
+ allowAnonymous() {
1734
+ return new _GencowProcedureBuilderImpl(
1735
+ this.kind,
1736
+ this.procedureName,
1737
+ this.middlewares,
1738
+ this.inputSchema,
1739
+ this.outputSchema,
1740
+ this.inputValidationIndex,
1741
+ this.outputValidationIndex,
1742
+ true
1743
+ );
1744
+ }
1745
+ use(middleware) {
1746
+ return new _GencowProcedureBuilderImpl(
1747
+ this.kind,
1748
+ this.procedureName,
1749
+ [...this.middlewares, middleware],
1750
+ this.inputSchema,
1751
+ this.outputSchema,
1752
+ this.inputValidationIndex,
1753
+ this.outputValidationIndex,
1754
+ this.anonymousAllowed
1755
+ );
1756
+ }
1757
+ input(schema) {
1758
+ const nextInputValidationIndex = this.middlewares.length < 0 ? 0 : this.middlewares.length;
1759
+ return new _GencowProcedureBuilderImpl(
1760
+ this.kind,
1761
+ this.procedureName,
1762
+ this.middlewares,
1763
+ schema,
1764
+ this.outputSchema,
1765
+ nextInputValidationIndex,
1766
+ this.outputValidationIndex,
1767
+ this.anonymousAllowed
1768
+ );
1769
+ }
1770
+ output(schema) {
1771
+ const nextOutputValidationIndex = this.middlewares.length < 0 ? 0 : this.middlewares.length;
1772
+ return new _GencowProcedureBuilderImpl(
1773
+ this.kind,
1774
+ this.procedureName,
1775
+ this.middlewares,
1776
+ this.inputSchema,
1777
+ schema,
1778
+ this.inputValidationIndex,
1779
+ nextOutputValidationIndex,
1780
+ this.anonymousAllowed
1781
+ );
1782
+ }
1783
+ handler(handler) {
1784
+ if (!this.procedureName) {
1785
+ throw new Error("Procedure name is required. Call .name(...) before .handler(...)");
1786
+ }
1787
+ const run = composeMiddlewares(
1788
+ this.middlewares,
1789
+ handler,
1790
+ this.inputSchema,
1791
+ this.outputSchema,
1792
+ this.inputValidationIndex,
1793
+ this.outputValidationIndex
1794
+ );
1795
+ return {
1796
+ kind: this.kind,
1797
+ name: this.procedureName,
1798
+ allowAnonymous: this.kind === "internal" ? false : this.anonymousAllowed,
1799
+ middlewares: [...this.middlewares],
1800
+ inputSchema: this.inputSchema,
1801
+ outputSchema: this.outputSchema,
1802
+ inputValidationIndex: this.inputValidationIndex,
1803
+ outputValidationIndex: this.outputValidationIndex,
1804
+ handler: run
1805
+ };
1806
+ }
1807
+ };
1808
+ function createGencowProcedureBuilders() {
1809
+ return {
1810
+ query: new GencowProcedureBuilderImpl("query"),
1811
+ mutation: new GencowProcedureBuilderImpl("mutation"),
1812
+ internal: new GencowProcedureBuilderImpl("internal")
1813
+ };
1814
+ }
1815
+
1816
+ // ../core/src/http-action.ts
1817
+ if (!globalThis.__gencow_httpActionRegistry) globalThis.__gencow_httpActionRegistry = [];
1818
+ var httpActionRegistry = globalThis.__gencow_httpActionRegistry;
1819
+ function httpAction(def) {
1820
+ const actionDef = {
1821
+ method: def.method,
1822
+ path: def.path,
1823
+ isPublic: def.public === true,
1824
+ handler: def.handler
1825
+ };
1826
+ httpActionRegistry.push(actionDef);
1827
+ return actionDef;
1828
+ }
1829
+ function getRegisteredHttpActions() {
1830
+ return [...httpActionRegistry];
1831
+ }
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
+
1898
+ // ../core/src/realtime-ticket.ts
1899
+ import { createHmac as createHmac3, randomBytes as randomBytes3, timingSafeEqual as timingSafeEqual3 } from "crypto";
1900
+ var TICKET_PREFIX = "gencow_ws1";
1901
+ var DEFAULT_TTL_SECONDS3 = 60;
1902
+ var MAX_TTL_SECONDS3 = 120;
1903
+ var MAX_TICKET_LENGTH = 4096;
1904
+ function encodeBase64Url3(value) {
1905
+ return Buffer.from(value, "utf8").toString("base64url");
1906
+ }
1907
+ function decodeBase64Url3(value) {
1908
+ try {
1909
+ return Buffer.from(value, "base64url").toString("utf8");
1910
+ } catch {
1911
+ return null;
1912
+ }
1913
+ }
1914
+ function signSegment3(segment, secret) {
1915
+ return createHmac3("sha256", secret).update(segment).digest("base64url");
1916
+ }
1917
+ function safeEqual3(a, b) {
1918
+ const left = Buffer.from(a);
1919
+ const right = Buffer.from(b);
1920
+ return left.length === right.length && timingSafeEqual3(left, right);
1921
+ }
1922
+ function validateRequiredString3(value, label) {
1923
+ if (typeof value !== "string" || value.length === 0) {
1924
+ throw new Error(`${label} required`);
1925
+ }
1926
+ }
1927
+ function resolveTtlMs3(ttlSeconds) {
1928
+ const ttl = Number.isFinite(ttlSeconds) && ttlSeconds > 0 ? ttlSeconds : DEFAULT_TTL_SECONDS3;
1929
+ return Math.floor(Math.min(ttl, MAX_TTL_SECONDS3) * 1e3);
1930
+ }
1931
+ function randomNonce3() {
1932
+ return randomBytes3(16).toString("base64url");
1933
+ }
1934
+ function resolveRealtimeCapabilityNegotiation(value) {
1935
+ const record = value && typeof value === "object" ? value : null;
1936
+ return {
1937
+ crudRealtimeMode: record?.crudRealtimeMode === "scoped" ? "scoped" : "legacy"
1675
1938
  };
1676
1939
  }
1677
1940
  function createRealtimeTicket(input) {
@@ -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,538 @@ 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
- }
2430
- }
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
2453
- `);
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;
2459
- }
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
- }
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
2628
+ }
2629
+ }
2630
+ async function tryDeleteWorkflowV2Run(db, workflowId) {
2631
+ try {
2632
+ await db.execute(sql3`
2633
+ DELETE FROM _gencow_workflow_runs_v2
2634
+ WHERE id = ${workflowId}
2635
+ `);
2636
+ } catch (error) {
2637
+ if (!isMissingWorkflowV2SchemaError2(error)) throw error;
2638
+ }
2639
+ }
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();
2636
2910
  }
2637
2911
 
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);
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";
2646
2924
  }
2647
- function serializeWorkflowValue(value) {
2648
- const payload = value === void 0 ? { __gencowUndefined: true } : { value };
2925
+
2926
+ // ../core/src/workflows-api.ts
2927
+ import { sql as sql4 } from "drizzle-orm";
2928
+ var WORKFLOW_STATUSES = /* @__PURE__ */ new Set(["pending", "running", "completed", "failed"]);
2929
+ var WORKFLOW_DERIVED_PENDING_STATUSES = /* @__PURE__ */ new Set(["queued", "waiting", "sleeping"]);
2930
+ function rowsFromResult(result) {
2931
+ if (Array.isArray(result)) return result;
2932
+ if (result && typeof result === "object" && Array.isArray(result.rows)) {
2933
+ return result.rows;
2934
+ }
2935
+ return [];
2936
+ }
2937
+ function parseJsonField(value) {
2938
+ if (typeof value !== "string") return value;
2649
2939
  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
- );
2940
+ return JSON.parse(value);
2941
+ } catch {
2942
+ return value;
2656
2943
  }
2657
2944
  }
2658
- function deserializeWorkflowValue(value) {
2659
- if (!isSerializedWorkflowValue(value)) return value;
2660
- if ("__gencowUndefined" in value) return void 0;
2661
- return value.value;
2945
+ function toIsoString(value) {
2946
+ if (value instanceof Date) return value.toISOString();
2947
+ const parsed = new Date(value);
2948
+ return Number.isFinite(parsed.getTime()) ? parsed.toISOString() : String(value);
2949
+ }
2950
+ function toOptionalIsoString(value) {
2951
+ return value ? toIsoString(value) : null;
2952
+ }
2953
+ function mapWorkflowSummary(row) {
2954
+ return {
2955
+ id: row.id,
2956
+ name: row.name,
2957
+ status: row.status,
2958
+ derivedStatus: deriveWorkflowStatus(row.status, row.current_step),
2959
+ currentStep: row.current_step,
2960
+ error: row.error,
2961
+ errorCode: row.error_code,
2962
+ retryCount: row.retry_count,
2963
+ maxRetries: row.max_retries,
2964
+ maxDurationMs: Number(row.max_duration_ms),
2965
+ startedAt: toIsoString(row.started_at),
2966
+ updatedAt: toIsoString(row.updated_at),
2967
+ completedAt: toOptionalIsoString(row.completed_at)
2968
+ };
2969
+ }
2970
+ function mapWorkflowStep(row) {
2971
+ return {
2972
+ name: row.step_name,
2973
+ status: row.status,
2974
+ output: deserializeWorkflowValue(parseJsonField(row.output)),
2975
+ error: row.error,
2976
+ startedAt: toOptionalIsoString(row.started_at),
2977
+ updatedAt: toIsoString(row.updated_at),
2978
+ completedAt: toOptionalIsoString(row.completed_at)
2979
+ };
2662
2980
  }
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}"`);
2981
+ function normalizeListLimit(limit) {
2982
+ if (limit == null) return 20;
2983
+ if (!Number.isFinite(limit)) {
2984
+ throw new GencowValidationError(`Argument "limit": expected a finite number, got ${limit}`);
2667
2985
  }
2668
- return Math.floor(retries);
2986
+ return Math.max(1, Math.min(100, Math.floor(limit)));
2669
2987
  }
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}"`);
2988
+ function normalizeStatus(status) {
2989
+ if (status == null) return void 0;
2990
+ if (!WORKFLOW_STATUSES.has(status)) {
2991
+ throw new GencowValidationError(`Argument "status": expected one of pending, running, completed, failed`);
2675
2992
  }
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;
2993
+ return status;
2680
2994
  }
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);
2995
+ function normalizeDerivedStatus(status) {
2996
+ if (status == null) return void 0;
2997
+ if (WORKFLOW_DERIVED_PENDING_STATUSES.has(status)) {
2998
+ return status;
2687
2999
  }
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
- );
3000
+ return normalizeStatus(status);
3001
+ }
3002
+ function toWorkflowStatusFilter(status) {
3003
+ if (status == null) return void 0;
3004
+ if (WORKFLOW_DERIVED_PENDING_STATUSES.has(status)) {
3005
+ return "pending";
2692
3006
  }
2693
- return parseDurationString(raw, label);
3007
+ return status;
2694
3008
  }
2695
- function normalizeMaxDurationMs(maxDuration, label = "workflow() maxDuration") {
2696
- if (maxDuration == null) return DEFAULT_WORKFLOW_MAX_DURATION_MS;
2697
- return parseWorkflowDurationMs(maxDuration, label);
3009
+ async function ensureWorkflowRealtimeToken(db, workflowId, currentToken) {
3010
+ if (currentToken && currentToken.trim() !== "") return currentToken;
3011
+ const nextToken = createWorkflowRealtimeToken();
3012
+ const updateResult = await db.execute(sql4`
3013
+ UPDATE _gencow_workflows
3014
+ SET realtime_token = ${nextToken}
3015
+ WHERE id = ${workflowId}
3016
+ AND (realtime_token IS NULL OR realtime_token = '')
3017
+ RETURNING realtime_token
3018
+ `);
3019
+ const updatedToken = rowsFromResult(updateResult)[0]?.realtime_token ?? null;
3020
+ if (updatedToken && updatedToken.trim() !== "") return updatedToken;
3021
+ const rereadResult = await db.execute(sql4`
3022
+ SELECT realtime_token
3023
+ FROM _gencow_workflows
3024
+ WHERE id = ${workflowId}
3025
+ LIMIT 1
3026
+ `);
3027
+ const rereadToken = rowsFromResult(rereadResult)[0]?.realtime_token ?? null;
3028
+ return rereadToken && rereadToken.trim() !== "" ? rereadToken : null;
2698
3029
  }
2699
- function normalizeOptionalDurationMs(duration, label) {
2700
- return duration == null ? null : parseWorkflowDurationMs(duration, label);
3030
+ async function loadWorkflowSignalTarget(db, workflowId) {
3031
+ const result = await db.execute(sql4`
3032
+ SELECT
3033
+ id,
3034
+ name,
3035
+ status,
3036
+ current_step,
3037
+ user_id
3038
+ FROM _gencow_workflows
3039
+ WHERE id = ${workflowId}
3040
+ LIMIT 1
3041
+ `);
3042
+ return rowsFromResult(result)[0] ?? null;
2701
3043
  }
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}"`);
2706
- }
2707
- return Math.floor(concurrency);
3044
+ function isMissingWorkflowV2SignalSchemaError(error) {
3045
+ const code = error && typeof error === "object" && "code" in error ? String(error.code) : "";
3046
+ const cause = error && typeof error === "object" && "cause" in error ? error.cause : null;
3047
+ const causeCode = cause && typeof cause === "object" && "code" in cause ? String(cause.code) : "";
3048
+ const message = error instanceof Error ? error.message : String(error);
3049
+ 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");
2708
3050
  }
2709
- function isMissingWorkflowV2SchemaError2(error) {
3051
+ function isMissingWorkflowV2PayloadSchemaError(error) {
2710
3052
  const code = error && typeof error === "object" && "code" in error ? String(error.code) : "";
2711
3053
  const cause = error && typeof error === "object" && "cause" in error ? error.cause : null;
2712
3054
  const causeCode = cause && typeof cause === "object" && "code" in cause ? String(cause.code) : "";
2713
3055
  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"');
3056
+ 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");
2715
3057
  }
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;
3058
+ async function loadWorkflowV2PayloadSnapshot(db, workflowId) {
3059
+ try {
3060
+ const runResult = await db.execute(sql4`
3061
+ SELECT args_json, result_json
3062
+ FROM _gencow_workflow_runs_v2
3063
+ WHERE id = ${workflowId}
3064
+ LIMIT 1
3065
+ `);
3066
+ const run = rowsFromResult(runResult)[0];
3067
+ if (!run) return null;
3068
+ const stepResult = await db.execute(sql4`
3069
+ SELECT step_key, output_json
3070
+ FROM _gencow_workflow_steps_v2
3071
+ WHERE run_id = ${workflowId}
3072
+ `);
3073
+ return {
3074
+ run,
3075
+ steps: new Map(
3076
+ rowsFromResult(stepResult).map((step) => [
3077
+ step.step_key,
3078
+ parseJsonField(step.output_json)
3079
+ ])
3080
+ )
3081
+ };
3082
+ } catch (error) {
3083
+ if (isMissingWorkflowV2PayloadSchemaError(error)) return null;
3084
+ throw error;
2720
3085
  }
2721
- return [];
2722
- }
2723
- function numberFrom2(value) {
2724
- const parsed = Number(value ?? 0);
2725
- return Number.isFinite(parsed) ? parsed : 0;
2726
- }
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`, `
2748
- );
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
3086
  }
2769
- async function tryInsertWorkflowV2Run(options) {
3087
+ async function tryRecordWorkflowV2Signal(options) {
2770
3088
  try {
2771
- if (!await hasWorkflowV2StartSchema(options.db)) return false;
2772
3089
  await options.db.execute(sql4`
2773
- WITH inserted_run AS (
2774
- INSERT INTO _gencow_workflow_runs_v2 (
3090
+ WITH inserted AS (
3091
+ INSERT INTO _gencow_workflow_signals_v2 (
2775
3092
  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
3093
+ run_id,
3094
+ event_name,
3095
+ payload_json,
3096
+ idempotency_key
2785
3097
  )
2786
3098
  VALUES (
3099
+ ${crypto.randomUUID()},
2787
3100
  ${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}
3101
+ ${options.event},
3102
+ ${workflowJsonb(options.payload)},
3103
+ ${crypto.randomUUID()}
3104
+ )
3105
+ RETURNING id, run_id
3106
+ ),
3107
+ inserted_legacy AS (
3108
+ INSERT INTO _gencow_workflow_events (
3109
+ id,
3110
+ workflow_id,
3111
+ event_name,
3112
+ payload
3113
+ )
3114
+ VALUES (
3115
+ ${options.eventId},
3116
+ ${options.workflowId},
3117
+ ${options.event},
3118
+ NULL
2800
3119
  )
2801
3120
  RETURNING id
2802
3121
  ),
2803
- inserted_outbox AS (
3122
+ queued_run AS (
3123
+ UPDATE _gencow_workflow_runs_v2 run
3124
+ SET
3125
+ status = 'queued',
3126
+ runnable_at = NOW(),
3127
+ lease_owner = NULL,
3128
+ lease_expires_at = NULL,
3129
+ heartbeat_at = NULL,
3130
+ updated_at = NOW()
3131
+ FROM inserted
3132
+ WHERE run.id = inserted.run_id
3133
+ AND run.status = 'waiting'
3134
+ AND run.completed_at IS NULL
3135
+ AND run.cancel_requested_at IS NULL
3136
+ RETURNING run.id
3137
+ ),
3138
+ wake_outbox AS (
2804
3139
  INSERT INTO _gencow_workflow_outbox_v2 (
2805
3140
  id,
2806
3141
  run_id,
@@ -2809,156 +3144,257 @@ async function tryInsertWorkflowV2Run(options) {
2809
3144
  status
2810
3145
  )
2811
3146
  SELECT
2812
- 'start:' || inserted_run.id,
2813
- inserted_run.id,
2814
- 'wake_run',
3147
+ 'signal:' || inserted.id,
3148
+ inserted.run_id,
3149
+ 'wake_signal',
2815
3150
  NOW(),
2816
3151
  'pending'
2817
- FROM inserted_run
3152
+ FROM inserted
3153
+ JOIN queued_run ON queued_run.id = inserted.run_id
2818
3154
  ON CONFLICT (id) DO NOTHING
2819
3155
  RETURNING id
2820
3156
  ),
2821
3157
  wake_notified AS (
2822
3158
  SELECT pg_notify(${WORKFLOW_WAKE_SIGNAL_CHANNEL}, ${WORKFLOW_WAKE_SIGNAL_PAYLOAD})
2823
- FROM inserted_outbox
3159
+ FROM wake_outbox
2824
3160
  )
2825
- SELECT inserted_run.id
2826
- FROM inserted_run
3161
+ SELECT 1
3162
+ FROM inserted
3163
+ LEFT JOIN inserted_legacy ON TRUE
2827
3164
  LEFT JOIN wake_notified ON TRUE
2828
3165
  `);
2829
3166
  return true;
2830
3167
  } catch (error) {
2831
- if (isMissingWorkflowV2SchemaError2(error)) return false;
3168
+ if (isMissingWorkflowV2SignalSchemaError(error)) return false;
2832
3169
  throw error;
2833
3170
  }
2834
3171
  }
2835
- async function tryDeleteWorkflowV2Run(db, workflowId) {
2836
- try {
2837
- await db.execute(sql4`
2838
- DELETE FROM _gencow_workflow_runs_v2
2839
- WHERE id = ${workflowId}
3172
+ async function loadWorkflowSnapshot(db, workflowId, options) {
3173
+ const workflowResult = await db.execute(sql4`
3174
+ SELECT
3175
+ id,
3176
+ name,
3177
+ args,
3178
+ status,
3179
+ current_step,
3180
+ result,
3181
+ error,
3182
+ error_code,
3183
+ retry_count,
3184
+ max_retries,
3185
+ max_duration_ms,
3186
+ started_at,
3187
+ updated_at,
3188
+ completed_at,
3189
+ realtime_token,
3190
+ user_id
3191
+ FROM _gencow_workflows
3192
+ WHERE id = ${workflowId}
3193
+ LIMIT 1
2840
3194
  `);
2841
- } catch (error) {
2842
- if (!isMissingWorkflowV2SchemaError2(error)) throw error;
3195
+ const row = rowsFromResult(workflowResult)[0] ?? null;
3196
+ if (!row) return null;
3197
+ const viewerUserId = options?.viewerUserId ?? null;
3198
+ if (options?.requireViewerMatch && row.user_id && row.user_id !== viewerUserId) {
3199
+ return null;
2843
3200
  }
3201
+ const realtimeToken = await ensureWorkflowRealtimeToken(db, workflowId, row.realtime_token);
3202
+ if (!realtimeToken) return null;
3203
+ const v2Payload = await loadWorkflowV2PayloadSnapshot(db, workflowId);
3204
+ const stepsResult = await db.execute(sql4`
3205
+ SELECT
3206
+ step_name,
3207
+ status,
3208
+ output,
3209
+ error,
3210
+ started_at,
3211
+ updated_at,
3212
+ completed_at
3213
+ FROM _gencow_workflow_steps
3214
+ WHERE workflow_id = ${workflowId}
3215
+ ORDER BY COALESCE(started_at, updated_at) ASC, step_name ASC
3216
+ `);
3217
+ const steps = rowsFromResult(stepsResult).map((stepRow) => {
3218
+ const mapped = mapWorkflowStep(stepRow);
3219
+ return v2Payload?.steps.has(stepRow.step_name) ? { ...mapped, output: v2Payload.steps.get(stepRow.step_name) } : mapped;
3220
+ });
3221
+ return {
3222
+ ...mapWorkflowSummary(row),
3223
+ args: v2Payload ? deserializeWorkflowValue(parseJsonField(v2Payload.run.args_json)) : deserializeWorkflowValue(parseJsonField(row.args)),
3224
+ result: v2Payload ? parseJsonField(v2Payload.run.result_json) : deserializeWorkflowValue(parseJsonField(row.result)),
3225
+ steps,
3226
+ realtimeKey: getWorkflowRealtimeKey(row.id, realtimeToken)
3227
+ };
2844
3228
  }
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());
2859
- }
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
3229
+ function buildWorkflowBuiltinDefinitions() {
3230
+ const get = {
3231
+ key: "workflows.get",
3232
+ argsSchema: { id: v.string() },
3233
+ isPublic: true,
3234
+ handler: async (ctx, args) => {
3235
+ return loadWorkflowSnapshot(ctx.unsafeDb, args.id, {
3236
+ viewerUserId: ctx.auth.getUserIdentity()?.id ?? null,
3237
+ requireViewerMatch: true
3238
+ });
3239
+ }
2882
3240
  };
2883
- workflowRegistry.set(name, def);
2884
- return mutation(name, {
2885
- args: options.args,
2886
- public: options.public,
3241
+ const signal = {
3242
+ name: "workflows.signal",
3243
+ argsSchema: {
3244
+ id: v.string(),
3245
+ event: v.string(),
3246
+ payload: v.optional(v.any())
3247
+ },
3248
+ isPublic: true,
2887
3249
  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 ?? {});
3250
+ const normalizedEvent = args.event.trim();
3251
+ if (!normalizedEvent) {
3252
+ throw new GencowValidationError(`Argument "event": expected a non-empty string`);
3253
+ }
3254
+ const workflow2 = await loadWorkflowSignalTarget(ctx.unsafeDb, args.id);
3255
+ const viewerUserId = ctx.auth.getUserIdentity()?.id ?? null;
3256
+ if (!workflow2 || workflow2.user_id && workflow2.user_id !== viewerUserId) {
3257
+ return {
3258
+ ok: false,
3259
+ workflowId: args.id,
3260
+ event: normalizedEvent,
3261
+ scheduledJobId: null
3262
+ };
3263
+ }
3264
+ if (workflow2.status === "completed" || workflow2.status === "failed") {
3265
+ return {
3266
+ ok: false,
3267
+ workflowId: workflow2.id,
3268
+ event: normalizedEvent,
3269
+ scheduledJobId: null
3270
+ };
3271
+ }
3272
+ const persistedPayload = serializeWorkflowValue(args.payload);
2892
3273
  assertWorkflowPayloadWithinLimit({
2893
- workflowName: name,
2894
- kind: "args",
2895
- serializedValue: persistedArgs
3274
+ workflowName: workflow2.name,
3275
+ kind: "signal",
3276
+ serializedValue: persistedPayload
2896
3277
  });
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,
2908
- workflowId,
2909
- workflowName: name,
2910
- workflowVersion: options.version ?? null,
2911
- args: persistedArgs,
2912
- userId: ownerId,
2913
- maxActiveDurationMs,
2914
- lifecycleTimeoutMs,
2915
- maxRetries
3278
+ const eventId = crypto.randomUUID();
3279
+ const recordedV2 = await tryRecordWorkflowV2Signal({
3280
+ db: ctx.unsafeDb,
3281
+ workflowId: workflow2.id,
3282
+ event: normalizedEvent,
3283
+ eventId,
3284
+ payload: persistedPayload
3285
+ });
3286
+ if (!recordedV2) {
3287
+ await ctx.unsafeDb.execute(sql4`
3288
+ INSERT INTO _gencow_workflow_events (
3289
+ id,
3290
+ workflow_id,
3291
+ event_name,
3292
+ payload
3293
+ )
3294
+ VALUES (
3295
+ ${eventId},
3296
+ ${workflow2.id},
3297
+ ${normalizedEvent},
3298
+ ${workflowJsonb(persistedPayload)}
3299
+ )
3300
+ `);
3301
+ }
3302
+ let scheduledJobId = null;
3303
+ if (workflow2.status === "pending" && workflow2.current_step?.startsWith("wait:")) {
3304
+ try {
3305
+ scheduledJobId = ctx.scheduler.runAfter(0, getWorkflowResumeActionName(workflow2.name), {
3306
+ workflowId: workflow2.id
2916
3307
  });
2917
- const legacyArgs = insertedWorkflowV2 ? serializeWorkflowValue(void 0) : persistedArgs;
2918
- await db.execute(sql4`
2919
- INSERT INTO _gencow_workflows (
3308
+ } catch {
3309
+ scheduledJobId = null;
3310
+ }
3311
+ }
3312
+ return {
3313
+ ok: true,
3314
+ workflowId: workflow2.id,
3315
+ event: normalizedEvent,
3316
+ scheduledJobId
3317
+ };
3318
+ }
3319
+ };
3320
+ const list = {
3321
+ key: "workflows.list",
3322
+ argsSchema: {
3323
+ limit: v.optional(v.number()),
3324
+ status: v.optional(v.string())
3325
+ },
3326
+ isPublic: false,
3327
+ handler: async (ctx, args) => {
3328
+ const userId = ctx.auth.requireAuth().id;
3329
+ const limit = normalizeListLimit(args.limit);
3330
+ const requestedStatus = normalizeDerivedStatus(args.status);
3331
+ const status = toWorkflowStatusFilter(requestedStatus);
3332
+ const result = status == null ? await ctx.unsafeDb.execute(sql4`
3333
+ SELECT
2920
3334
  id,
2921
3335
  name,
2922
3336
  args,
2923
- realtime_token,
2924
3337
  status,
3338
+ current_step,
3339
+ result,
3340
+ error,
3341
+ error_code,
2925
3342
  retry_count,
2926
3343
  max_retries,
2927
3344
  max_duration_ms,
3345
+ started_at,
3346
+ updated_at,
3347
+ completed_at,
2928
3348
  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) {
2951
- await ctx.unsafeDb.execute(sql4`
2952
- DELETE FROM _gencow_workflows
2953
- WHERE id = ${workflowId}
3349
+ FROM _gencow_workflows
3350
+ WHERE user_id = ${userId}
3351
+ ORDER BY started_at DESC
3352
+ LIMIT ${limit}
3353
+ `) : await ctx.unsafeDb.execute(sql4`
3354
+ SELECT
3355
+ id,
3356
+ name,
3357
+ args,
3358
+ status,
3359
+ current_step,
3360
+ result,
3361
+ error,
3362
+ error_code,
3363
+ retry_count,
3364
+ max_retries,
3365
+ max_duration_ms,
3366
+ started_at,
3367
+ updated_at,
3368
+ completed_at,
3369
+ user_id
3370
+ FROM _gencow_workflows
3371
+ WHERE user_id = ${userId}
3372
+ AND status = ${status}
3373
+ ORDER BY started_at DESC
3374
+ LIMIT ${limit}
2954
3375
  `);
2955
- if (insertedWorkflowV2) {
2956
- await tryDeleteWorkflowV2Run(ctx.unsafeDb, workflowId);
2957
- }
2958
- throw error;
2959
- }
3376
+ return rowsFromResult(result).map(mapWorkflowSummary).filter((row) => requestedStatus == null || row.derivedStatus === requestedStatus);
2960
3377
  }
3378
+ };
3379
+ const definitions = Object.freeze({
3380
+ get: Object.freeze(get),
3381
+ list: Object.freeze(list),
3382
+ signal: Object.freeze(signal)
2961
3383
  });
3384
+ return definitions;
3385
+ }
3386
+ function createWorkflowBuiltinDefinitions() {
3387
+ return buildWorkflowBuiltinDefinitions();
3388
+ }
3389
+ function registerWorkflowsApi() {
3390
+ const registeredDefinitions = globalThis.__gencow_workflowsApiRegistered;
3391
+ if (registeredDefinitions) return registeredDefinitions;
3392
+ const definitions = buildWorkflowBuiltinDefinitions();
3393
+ queryRegistry.set(definitions.get.key, definitions.get);
3394
+ queryRegistry.set(definitions.list.key, definitions.list);
3395
+ mutationRegistry.push(definitions.signal);
3396
+ globalThis.__gencow_workflowsApiRegistered = definitions;
3397
+ return definitions;
2962
3398
  }
2963
3399
 
2964
3400
  // ../core/src/retry.ts
@@ -3530,6 +3966,35 @@ var pgBaseTable = pgTable;
3530
3966
  // ../core/src/rls-db.ts
3531
3967
  import { AsyncLocalStorage } from "node:async_hooks";
3532
3968
  import { sql as sql7 } from "drizzle-orm";
3969
+ var TRANSACTION_ISOLATION_LEVELS = /* @__PURE__ */ new Set([
3970
+ "read uncommitted",
3971
+ "read committed",
3972
+ "repeatable read",
3973
+ "serializable"
3974
+ ]);
3975
+ var TRANSACTION_ACCESS_MODES = /* @__PURE__ */ new Set(["read only", "read write"]);
3976
+ function transactionConfigSql(config) {
3977
+ if (!config || typeof config !== "object") return null;
3978
+ const value = config;
3979
+ const chunks = [];
3980
+ if (value.isolationLevel !== void 0) {
3981
+ if (!TRANSACTION_ISOLATION_LEVELS.has(value.isolationLevel)) return null;
3982
+ chunks.push(`isolation level ${value.isolationLevel}`);
3983
+ }
3984
+ if (value.accessMode !== void 0) {
3985
+ if (!TRANSACTION_ACCESS_MODES.has(value.accessMode)) return null;
3986
+ chunks.push(value.accessMode);
3987
+ }
3988
+ if (value.deferrable !== void 0) {
3989
+ if (typeof value.deferrable !== "boolean") return null;
3990
+ chunks.push(value.deferrable ? "deferrable" : "not deferrable");
3991
+ }
3992
+ return chunks.length > 0 ? sql7.raw(`SET TRANSACTION ${chunks.join(", ")}`) : null;
3993
+ }
3994
+ async function applyTransactionConfig(tx, config) {
3995
+ const statement = transactionConfigSql(config);
3996
+ if (statement) await tx.execute(statement);
3997
+ }
3533
3998
  function rlsSetConfigPairs(rls) {
3534
3999
  const pairs = [];
3535
4000
  for (const [sessionKey, value] of Object.entries(rls)) {
@@ -3569,8 +4034,16 @@ async function execSetConfig(client, name, value) {
3569
4034
  async function applyRlsSessionVars(client, rls) {
3570
4035
  await forEachSetConfig(rls, (name, value) => execSetConfig(client, name, value));
3571
4036
  }
3572
- async function injectRlsVarsOnTx(tx, rls) {
3573
- await forEachSetConfig(rls, (name, value) => tx.execute(sql7`SELECT set_config(${name}, ${value}, true)`));
4037
+ async function injectRlsVarsOnTx(tx, rls, onTransactionContextFailure) {
4038
+ try {
4039
+ await forEachSetConfig(rls, (name, value) => tx.execute(sql7`SELECT set_config(${name}, ${value}, true)`));
4040
+ } catch (error) {
4041
+ try {
4042
+ onTransactionContextFailure?.(error);
4043
+ } catch {
4044
+ }
4045
+ throw error;
4046
+ }
3574
4047
  }
3575
4048
  async function withRlsLeasedConnection(leased, rls, fn) {
3576
4049
  try {
@@ -3667,7 +4140,11 @@ function createBunSqlPreparedQueryClientExecutors(baseSession, prepareQuery, pre
3667
4140
  }
3668
4141
  function createPreparedQueryClientExecutors(params) {
3669
4142
  if (isBunSqlSession(params.baseSession)) {
3670
- return createBunSqlPreparedQueryClientExecutors(params.baseSession, params.prepareQuery, params.prepareArgs);
4143
+ return createBunSqlPreparedQueryClientExecutors(
4144
+ params.baseSession,
4145
+ params.prepareQuery,
4146
+ params.prepareArgs
4147
+ );
3671
4148
  }
3672
4149
  return createGenericPreparedQueryClientExecutors(params.pq);
3673
4150
  }
@@ -3676,7 +4153,12 @@ async function runPreparedQueryWithRls(session, rls, reuseOuterConnection, runOn
3676
4153
  if (active) {
3677
4154
  return runOnClient(active, placeholderValues);
3678
4155
  }
3679
- return withRlsConnection(session, rls, reuseOuterConnection, (client) => runOnClient(client, placeholderValues));
4156
+ return withRlsConnection(
4157
+ session,
4158
+ rls,
4159
+ reuseOuterConnection,
4160
+ (client) => runOnClient(client, placeholderValues)
4161
+ );
3680
4162
  }
3681
4163
  function wrapPreparedQuery(pq, session, baseSession, prepareQuery, prepareArgs, rls, reuseOuterConnection) {
3682
4164
  const clientExecutors = createPreparedQueryClientExecutors({
@@ -3709,7 +4191,7 @@ function wrapSession(session, rls, reuseOuterConnection) {
3709
4191
  }
3710
4192
  });
3711
4193
  }
3712
- function createRlsDb(db, rls) {
4194
+ function createRlsDb(db, rls, options = {}) {
3713
4195
  const reuseOuterConnection = isDrizzleTransactionDb(db);
3714
4196
  const baseSession = db.session;
3715
4197
  const wrappedSession = wrapSession(baseSession, rls, reuseOuterConnection);
@@ -3729,13 +4211,41 @@ function createRlsDb(db, rls) {
3729
4211
  }
3730
4212
  if (prop === "transaction") {
3731
4213
  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
- );
4214
+ let callbackStarted = false;
4215
+ const configStatement = transactionConfigSql(rest[0]);
4216
+ const transactionArgs = configStatement ? [] : rest;
4217
+ try {
4218
+ return await target.transaction(
4219
+ async (tx) => {
4220
+ if (configStatement) await tx.execute(configStatement);
4221
+ await injectRlsVarsOnTx(tx, rls, options.onTransactionContextFailure);
4222
+ callbackStarted = true;
4223
+ return await callback(tx);
4224
+ },
4225
+ ...transactionArgs
4226
+ );
4227
+ } catch (error) {
4228
+ const markedRlsContextFailure = error && typeof error === "object" && error.gencowRlsTransactionContextFailure === true;
4229
+ if (!callbackStarted && !markedRlsContextFailure && error && typeof error === "object") {
4230
+ try {
4231
+ Object.defineProperty(error, "gencowTransactionSetupFailure", { value: true });
4232
+ } catch {
4233
+ }
4234
+ }
4235
+ throw error;
4236
+ }
4237
+ };
4238
+ }
4239
+ if (prop === "transactionWithCharacteristics") {
4240
+ return async (characteristics, callback) => {
4241
+ return await target.transaction(async (tx) => {
4242
+ await applyTransactionConfig(tx, {
4243
+ ...characteristics?.isolationLevel ? { isolationLevel: characteristics.isolationLevel } : {},
4244
+ ...characteristics?.readOnly ? { accessMode: "read only" } : {}
4245
+ });
4246
+ await injectRlsVarsOnTx(tx, rls, options.onTransactionContextFailure);
4247
+ return callback(tx);
4248
+ });
3739
4249
  };
3740
4250
  }
3741
4251
  const value = Reflect.get(target, prop, receiver);
@@ -5137,6 +5647,7 @@ export {
5137
5647
  PLATFORM_CAPACITY_PRESETS,
5138
5648
  PLATFORM_CAPACITY_PROFILE_ENV,
5139
5649
  RLS_SESSION_KEY_RE,
5650
+ RealtimeChannelEventValidationError,
5140
5651
  STORAGE_LIST_DEFAULT_LIMIT,
5141
5652
  STORAGE_LIST_MAX_LIMIT,
5142
5653
  StorageListUnsupportedError,
@@ -5157,16 +5668,20 @@ export {
5157
5668
  buildSearchIndexNames,
5158
5669
  buildWakeAppBootFailedResult,
5159
5670
  buildWakeAppSuccessResult,
5671
+ captureLegacyRealtimeChannels,
5160
5672
  compareUnreadCursors,
5161
5673
  computeCronActionToken,
5162
5674
  countUnreadAfterCursor,
5163
5675
  createCrudFactory,
5164
5676
  createGencowHttpRouteBuilders,
5165
5677
  createGencowProcedureBuilders,
5678
+ createGencowRealtimeChannelBuilder,
5679
+ createGencowWorkflowBuilders,
5166
5680
  createRealtimeChannelToken,
5167
5681
  createRealtimeQuerySubscriptionToken,
5168
5682
  createRealtimeTicket,
5169
5683
  createRlsDb,
5684
+ createWorkflowBuiltinDefinitions,
5170
5685
  createWorkflowRealtimeToken,
5171
5686
  cron,
5172
5687
  cronJobs,
@@ -5221,6 +5736,8 @@ export {
5221
5736
  ownerRls,
5222
5737
  parseArgs,
5223
5738
  parseFilterNode,
5739
+ parseRealtimeChannelEvent,
5740
+ parseStandardSchema,
5224
5741
  parseWorkflowDurationMs,
5225
5742
  pgBaseTable,
5226
5743
  query,
@@ -5228,8 +5745,10 @@ export {
5228
5745
  readNonNegativeIntegerEnv,
5229
5746
  readPositiveIntegerEnv,
5230
5747
  registerOwnerRls,
5748
+ registerWorkflowsApi,
5231
5749
  resetRealtimeChannelRegistryForTests,
5232
5750
  resetRealtimeSubscriptionRegistryForTests,
5751
+ resolveExplicitRealtimeChannelKey,
5233
5752
  resolvePlatformCapacityPreset,
5234
5753
  resolveRealtimeChannelKey,
5235
5754
  resolveRealtimeQueryScopeHash,
@@ -5237,6 +5756,7 @@ export {
5237
5756
  resolveWorkflowPayloadPolicyConfig,
5238
5757
  rlsContextKey,
5239
5758
  rlsContextKeys,
5759
+ selectLegacyRealtimeChannelSnapshot,
5240
5760
  serializeWorkflowValue,
5241
5761
  sessionKeyToGucName,
5242
5762
  stringifySessionValue,