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