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 +1828 -1308
- package/lib/add-agent.mjs +498 -0
- package/lib/app-command.mjs +44 -2
- package/lib/component-readme.mjs +1 -1
- package/lib/deploy-auditor.mjs +4 -2
- package/lib/deploy-command.mjs +62 -0
- package/lib/deploy-runtime.mjs +1 -0
- package/lib/env-command.mjs +155 -16
- package/lib/install-features.mjs +17 -5
- package/lib/readme-codegen.mjs +2 -2
- package/package.json +3 -3
- package/runtime/server.mjs +16710 -10899
- package/runtime/tooling.mjs +3836 -2002
- package/scripts/tooling-codegen-runtime.ts +26 -23
- package/templateFeature/agent/agent.ts +29 -14
- package/templateFeature/agent/manifest.json +2 -2
- package/templates/fullstack/files.ts +2 -0
- package/templates/fullstack/index.ts +5 -1
- package/templates/runtime.ts +5 -1
package/core/index.js
CHANGED
|
@@ -813,449 +813,98 @@ function verifyRealtimeChannelToken(token, input) {
|
|
|
813
813
|
};
|
|
814
814
|
}
|
|
815
815
|
|
|
816
|
-
// ../core/src/
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
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
|
-
|
|
831
|
-
|
|
832
|
-
function
|
|
833
|
-
return
|
|
823
|
+
};
|
|
824
|
+
var GENCOW_OPTIONAL_INNER = /* @__PURE__ */ Symbol.for("gencow.v.optionalInner");
|
|
825
|
+
function assertStandardResult(value) {
|
|
826
|
+
return { value };
|
|
834
827
|
}
|
|
835
|
-
function
|
|
836
|
-
return [
|
|
828
|
+
function failStandardResult(message) {
|
|
829
|
+
return { issues: [{ message }] };
|
|
837
830
|
}
|
|
838
|
-
function
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
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
|
|
851
|
-
|
|
838
|
+
function isOptionalValidator(validator) {
|
|
839
|
+
return !!validator && Object.prototype.hasOwnProperty.call(validator, GENCOW_OPTIONAL_INNER);
|
|
852
840
|
}
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
const
|
|
858
|
-
if (
|
|
859
|
-
return
|
|
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
|
|
862
|
-
|
|
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
|
|
878
|
-
|
|
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
|
|
881
|
-
|
|
882
|
-
|
|
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
|
|
886
|
-
|
|
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
|
|
889
|
-
const
|
|
890
|
-
const
|
|
891
|
-
const
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
if (
|
|
895
|
-
|
|
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
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
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
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
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/
|
|
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" &&
|
|
1161
|
+
return !!schema && typeof schema === "object" && typeof schema["~standard"]?.validate === "function";
|
|
1445
1162
|
}
|
|
1446
|
-
async function
|
|
1163
|
+
async function parseStandardSchema(schema, value) {
|
|
1447
1164
|
if (!schema) return value;
|
|
1448
|
-
|
|
1449
|
-
|
|
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
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
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
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
this.
|
|
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
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
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
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
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
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
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
|
-
|
|
1560
|
-
|
|
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
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
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
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
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
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
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
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
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/
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
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
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
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
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
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
|
|
1652
|
-
return
|
|
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
|
|
1665
|
-
const
|
|
1666
|
-
|
|
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
|
|
1669
|
-
return
|
|
1314
|
+
function subscriptionKeyMatchesQueryKey(subscriptionKey, queryKey) {
|
|
1315
|
+
return subscriptionKey === queryKey || subscriptionKey.startsWith(`${queryKey}${SUBSCRIPTION_KEY_SEPARATOR}`);
|
|
1670
1316
|
}
|
|
1671
|
-
function
|
|
1672
|
-
const
|
|
1673
|
-
|
|
1674
|
-
|
|
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
|
|
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
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
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
|
|
2197
|
-
|
|
2442
|
+
function serializeWorkflowValue(value) {
|
|
2443
|
+
const payload = value === void 0 ? { __gencowUndefined: true } : { value };
|
|
2198
2444
|
try {
|
|
2199
|
-
return JSON.parse(
|
|
2200
|
-
} catch {
|
|
2201
|
-
|
|
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
|
|
2205
|
-
if (value
|
|
2206
|
-
|
|
2207
|
-
return
|
|
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
|
|
2241
|
-
if (
|
|
2242
|
-
if (!Number.isFinite(
|
|
2243
|
-
throw new
|
|
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.
|
|
2463
|
+
return Math.floor(retries);
|
|
2246
2464
|
}
|
|
2247
|
-
function
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
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
|
-
|
|
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
|
|
2255
|
-
if (
|
|
2256
|
-
|
|
2257
|
-
|
|
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
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
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
|
|
2488
|
+
return parseDurationString(raw, label);
|
|
2267
2489
|
}
|
|
2268
|
-
|
|
2269
|
-
if (
|
|
2270
|
-
|
|
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
|
-
|
|
2290
|
-
|
|
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
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
return
|
|
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
|
|
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 "
|
|
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
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
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
|
-
|
|
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
|
|
2350
|
-
INSERT INTO
|
|
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
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
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.
|
|
2377
|
-
|
|
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
|
-
|
|
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
|
-
'
|
|
2407
|
-
|
|
2408
|
-
'
|
|
2607
|
+
'start:' || inserted_run.id,
|
|
2608
|
+
inserted_run.id,
|
|
2609
|
+
'wake_run',
|
|
2409
2610
|
NOW(),
|
|
2410
2611
|
'pending'
|
|
2411
|
-
FROM
|
|
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
|
|
2618
|
+
FROM inserted_outbox
|
|
2419
2619
|
)
|
|
2420
|
-
SELECT
|
|
2421
|
-
FROM
|
|
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 (
|
|
2626
|
+
if (isMissingWorkflowV2SchemaError2(error)) return false;
|
|
2428
2627
|
throw error;
|
|
2429
|
-
}
|
|
2430
|
-
}
|
|
2431
|
-
async function
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
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
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
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
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
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
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
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
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
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
|
-
|
|
2648
|
-
|
|
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(
|
|
2651
|
-
} catch
|
|
2652
|
-
|
|
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
|
|
2659
|
-
if (
|
|
2660
|
-
|
|
2661
|
-
return
|
|
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
|
|
2664
|
-
if (
|
|
2665
|
-
if (!Number.isFinite(
|
|
2666
|
-
throw new
|
|
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(
|
|
2986
|
+
return Math.max(1, Math.min(100, Math.floor(limit)));
|
|
2669
2987
|
}
|
|
2670
|
-
function
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
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
|
-
|
|
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
|
|
2682
|
-
if (
|
|
2683
|
-
|
|
2684
|
-
|
|
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
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
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
|
|
3007
|
+
return status;
|
|
2694
3008
|
}
|
|
2695
|
-
function
|
|
2696
|
-
if (
|
|
2697
|
-
|
|
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
|
|
2700
|
-
|
|
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
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
return
|
|
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
|
|
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" ||
|
|
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
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
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
|
|
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
|
|
2774
|
-
INSERT INTO
|
|
3090
|
+
WITH inserted AS (
|
|
3091
|
+
INSERT INTO _gencow_workflow_signals_v2 (
|
|
2775
3092
|
id,
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
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.
|
|
2789
|
-
${options.
|
|
2790
|
-
${
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
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
|
-
|
|
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
|
-
'
|
|
2813
|
-
|
|
2814
|
-
'
|
|
3147
|
+
'signal:' || inserted.id,
|
|
3148
|
+
inserted.run_id,
|
|
3149
|
+
'wake_signal',
|
|
2815
3150
|
NOW(),
|
|
2816
3151
|
'pending'
|
|
2817
|
-
FROM
|
|
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
|
|
3159
|
+
FROM wake_outbox
|
|
2824
3160
|
)
|
|
2825
|
-
SELECT
|
|
2826
|
-
FROM
|
|
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 (
|
|
3168
|
+
if (isMissingWorkflowV2SignalSchemaError(error)) return false;
|
|
2832
3169
|
throw error;
|
|
2833
3170
|
}
|
|
2834
3171
|
}
|
|
2835
|
-
async function
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
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
|
-
|
|
2842
|
-
|
|
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
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
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
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
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
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
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: "
|
|
2895
|
-
serializedValue:
|
|
3274
|
+
workflowName: workflow2.name,
|
|
3275
|
+
kind: "signal",
|
|
3276
|
+
serializedValue: persistedPayload
|
|
2896
3277
|
});
|
|
2897
|
-
const
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
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
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
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
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
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,
|