@terminals-tech/sdk 1.0.0-rc.1 → 1.0.0
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/README.md +15 -19
- package/dist/WebContainerManager-4LIOGRVM.js +22 -0
- package/dist/browser-http-client-ZQLDWZMU.js +317 -0
- package/dist/cache-VKYSQRXX.js +45 -0
- package/dist/capabilities-MIPUMBLL.js +96 -0
- package/dist/chunk-2ESYSVXG.js +48 -0
- package/dist/chunk-2WTYE4SW.js +190 -0
- package/dist/chunk-3LFMIVJM.js +40 -0
- package/dist/chunk-ABCK4FWN.js +136 -0
- package/dist/chunk-AFDUOYHD.js +2060 -0
- package/dist/chunk-BCOQMFKT.js +265 -0
- package/dist/chunk-BYXBJQAS.js +0 -0
- package/dist/chunk-DKFJIILR.js +9798 -0
- package/dist/chunk-EXI3LJVJ.js +51 -0
- package/dist/chunk-FOXUEYWK.js +42 -0
- package/dist/chunk-GJWAJAX3.js +173 -0
- package/dist/chunk-H3POJCFA.js +333 -0
- package/dist/chunk-KASHT6C5.js +784 -0
- package/dist/chunk-KHR7ZYCX.js +4034 -0
- package/dist/chunk-L45BSQDJ.js +296 -0
- package/dist/chunk-LLGZTP3G.js +5521 -0
- package/dist/chunk-NTMBOESX.js +152 -0
- package/dist/chunk-OCLSAUCD.js +474 -0
- package/dist/chunk-OSSRZOGC.js +190 -0
- package/dist/chunk-PPFTKJDB.js +497 -0
- package/dist/chunk-PWAHFID5.js +381 -0
- package/dist/chunk-Q2VI6ICE.js +188 -0
- package/dist/chunk-QJFKEQHF.js +6460 -0
- package/dist/chunk-QWXPVB2L.js +320 -0
- package/dist/chunk-QWZRZKLZ.js +896 -0
- package/dist/chunk-STMI72WH.js +1005 -0
- package/dist/chunk-TSQ3BGLA.js +11945 -0
- package/dist/chunk-UJDUQNE2.js +79 -0
- package/dist/chunk-VZA2NUH3.js +118 -0
- package/dist/chunk-WGBCRNMB.js +1817 -0
- package/dist/chunk-WU4OTGJE.js +752 -0
- package/dist/chunk-XPJ63Y6T.js +70 -0
- package/dist/chunk-Y2EULKA2.js +172 -0
- package/dist/chunk-YJEZWCYV.js +94 -0
- package/dist/chunk-ZVO47SQV.js +150 -0
- package/dist/container-lite-QD3CRLS4.js +327 -0
- package/dist/core-H2UUDATO.js +146 -0
- package/dist/crypto-D4LMI2RN.js +45 -0
- package/dist/db-BWC2GGBN.js +50 -0
- package/dist/demo-T655Z5S4.js +87 -0
- package/dist/diagnostics-6RQTBR6I.js +113 -0
- package/dist/dist-OPDCWARF.js +727 -0
- package/dist/dist-VXJEKX3T.js +2441 -0
- package/dist/dist-VYGJXGUS.js +1008 -0
- package/dist/embeddings-7QXTXUMC.js +15 -0
- package/dist/embeddings-MAEWWUHW.js +9 -0
- package/dist/graph-RKMNE2X5.js +36 -0
- package/dist/hvm-CBEP3M4F.js +126 -0
- package/dist/index.cjs +49874 -8001
- package/dist/index.d.cts +1629 -1363
- package/dist/index.d.ts +1629 -1363
- package/dist/index.js +2462 -8130
- package/dist/mcp-NK34ZNM5.js +101 -0
- package/dist/mcp-client-service-browser-SGB2K3VZ.js +14 -0
- package/dist/neuro-state-XHRGIRVO.js +498 -0
- package/dist/nodes-K6GKI2FM.js +364 -0
- package/dist/package-EXUIU2RL.js +93 -0
- package/dist/package-VGL7HYTO.js +106 -0
- package/dist/package-XHMLOAQ4.js +98 -0
- package/dist/pg-events-QJAM2HIP.js +15 -0
- package/dist/pglite-adapter-43IOUBMV.js +50 -0
- package/dist/pgliteService-IUGNNOVU.js +258 -0
- package/dist/policy-IRJCM6FS.js +13 -0
- package/dist/registry-5WTDYQVQ.js +26 -0
- package/dist/registry-FW63E7FE.js +16 -0
- package/dist/registry-ZQ2IBLF6.js +9 -0
- package/dist/resolver-ALOJSOK5.js +24 -0
- package/dist/scheduler-B5CEYKWT.js +127 -0
- package/dist/secret-store-H7273UIT.js +18 -0
- package/dist/server-VW6DYDLH.js +18 -0
- package/dist/skills-VN7IN7SJ.js +6375 -0
- package/dist/stack-4KWCQQP7.js +103 -0
- package/dist/storage-L7MWNSPG.js +13 -0
- package/dist/supabaseService-6AYP2VY3.js +476 -0
- package/dist/topology-CIWWNVAN.js +13 -0
- package/dist/webcontainer-XWCE56F3.js +281 -0
- package/package.json +9 -3
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import {
|
|
2
|
+
OrchestratorV0,
|
|
3
|
+
loadStack
|
|
4
|
+
} from "./chunk-STMI72WH.js";
|
|
5
|
+
import {
|
|
6
|
+
runners
|
|
7
|
+
} from "./chunk-PPFTKJDB.js";
|
|
8
|
+
import "./chunk-KASHT6C5.js";
|
|
9
|
+
import "./chunk-GJWAJAX3.js";
|
|
10
|
+
import "./chunk-VZA2NUH3.js";
|
|
11
|
+
import "./chunk-H3POJCFA.js";
|
|
12
|
+
import "./chunk-KHR7ZYCX.js";
|
|
13
|
+
import "./chunk-QWXPVB2L.js";
|
|
14
|
+
import "./chunk-TSQ3BGLA.js";
|
|
15
|
+
import "./chunk-Q2VI6ICE.js";
|
|
16
|
+
import "./chunk-PWAHFID5.js";
|
|
17
|
+
import "./chunk-Y2EULKA2.js";
|
|
18
|
+
import "./chunk-WU4OTGJE.js";
|
|
19
|
+
import {
|
|
20
|
+
resolveNode
|
|
21
|
+
} from "./chunk-OCLSAUCD.js";
|
|
22
|
+
import "./chunk-BYXBJQAS.js";
|
|
23
|
+
import "./chunk-BCOQMFKT.js";
|
|
24
|
+
import "./chunk-2WTYE4SW.js";
|
|
25
|
+
import "./chunk-FOXUEYWK.js";
|
|
26
|
+
import "./chunk-EXI3LJVJ.js";
|
|
27
|
+
import "./chunk-QJFKEQHF.js";
|
|
28
|
+
import "./chunk-ZVO47SQV.js";
|
|
29
|
+
import "./chunk-XPJ63Y6T.js";
|
|
30
|
+
import "./chunk-WGBCRNMB.js";
|
|
31
|
+
import "./chunk-3LFMIVJM.js";
|
|
32
|
+
import "./chunk-AFDUOYHD.js";
|
|
33
|
+
import "./chunk-NTMBOESX.js";
|
|
34
|
+
import "./chunk-2ESYSVXG.js";
|
|
35
|
+
|
|
36
|
+
// ../../lib/terminals-tech/machines/core/terminal-mapper.ts
|
|
37
|
+
function mapTerminalInputs(terminal, externalInput) {
|
|
38
|
+
const overrides = {};
|
|
39
|
+
if (!terminal.terminalInterface?.inputMap) {
|
|
40
|
+
return overrides;
|
|
41
|
+
}
|
|
42
|
+
for (const [externalKey, internalTarget] of Object.entries(terminal.terminalInterface.inputMap)) {
|
|
43
|
+
if (externalInput[externalKey] !== void 0) {
|
|
44
|
+
const [nodeId, ...fieldParts] = internalTarget.split(".");
|
|
45
|
+
const field = fieldParts.join(".");
|
|
46
|
+
if (!overrides[nodeId]) {
|
|
47
|
+
overrides[nodeId] = {};
|
|
48
|
+
}
|
|
49
|
+
if (field) {
|
|
50
|
+
overrides[nodeId][field] = externalInput[externalKey];
|
|
51
|
+
} else {
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return overrides;
|
|
56
|
+
}
|
|
57
|
+
function mapTerminalOutputs(terminal, nodeOutputs) {
|
|
58
|
+
const result = {};
|
|
59
|
+
if (!terminal.terminalInterface?.outputMap) {
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
for (const [internalSource, externalKey] of Object.entries(
|
|
63
|
+
terminal.terminalInterface.outputMap
|
|
64
|
+
)) {
|
|
65
|
+
const [nodeId, ...fieldParts] = internalSource.split(".");
|
|
66
|
+
const field = fieldParts.join(".");
|
|
67
|
+
const output = nodeOutputs.get(nodeId);
|
|
68
|
+
if (output && field && output[field] !== void 0) {
|
|
69
|
+
result[externalKey] = output[field];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ../../lib/terminals-tech/machines/core/runners/stack.ts
|
|
76
|
+
async function runInStack(manifest, input, ctx) {
|
|
77
|
+
const stackRef = manifest.id;
|
|
78
|
+
const targetStack = await loadStack(stackRef);
|
|
79
|
+
if (!targetStack) {
|
|
80
|
+
throw new Error(`StackRunner: target stack not found: ${stackRef}`);
|
|
81
|
+
}
|
|
82
|
+
const orchestrator = new OrchestratorV0(resolveNode);
|
|
83
|
+
try {
|
|
84
|
+
const configOverrides = mapTerminalInputs(targetStack, input);
|
|
85
|
+
const subRun = await orchestrator.run(targetStack, {
|
|
86
|
+
configOverrides,
|
|
87
|
+
userId: ctx.userId,
|
|
88
|
+
appletId: ctx.appletId,
|
|
89
|
+
worldId: ctx.worldId,
|
|
90
|
+
terminalId: ctx.terminalId
|
|
91
|
+
});
|
|
92
|
+
if (!subRun.succeeded || !subRun.outputs) {
|
|
93
|
+
throw subRun.error || new Error(`Recursive stack execution failed: ${subRun.runId}`);
|
|
94
|
+
}
|
|
95
|
+
return mapTerminalOutputs(targetStack, subRun.outputs);
|
|
96
|
+
} finally {
|
|
97
|
+
orchestrator.dispose();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
runners.registerRunner("stack", runInStack);
|
|
101
|
+
export {
|
|
102
|
+
runInStack
|
|
103
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import {
|
|
2
|
+
deleteDatabaseByDataDir,
|
|
3
|
+
getStorageEstimate,
|
|
4
|
+
isStoragePersisted,
|
|
5
|
+
requestPersistentStorage
|
|
6
|
+
} from "./chunk-3LFMIVJM.js";
|
|
7
|
+
import "./chunk-2ESYSVXG.js";
|
|
8
|
+
export {
|
|
9
|
+
deleteDatabaseByDataDir,
|
|
10
|
+
getStorageEstimate,
|
|
11
|
+
isStoragePersisted,
|
|
12
|
+
requestPersistentStorage
|
|
13
|
+
};
|
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createSupabaseClientComponentClient
|
|
3
|
+
} from "./chunk-QWZRZKLZ.js";
|
|
4
|
+
import {
|
|
5
|
+
ErrorCodes,
|
|
6
|
+
SyncError
|
|
7
|
+
} from "./chunk-OSSRZOGC.js";
|
|
8
|
+
import "./chunk-TSQ3BGLA.js";
|
|
9
|
+
import {
|
|
10
|
+
secureValidate
|
|
11
|
+
} from "./chunk-FOXUEYWK.js";
|
|
12
|
+
import "./chunk-EXI3LJVJ.js";
|
|
13
|
+
import "./chunk-QJFKEQHF.js";
|
|
14
|
+
import "./chunk-2ESYSVXG.js";
|
|
15
|
+
|
|
16
|
+
// ../../lib/utils/network.ts
|
|
17
|
+
function isNavigatorOnline() {
|
|
18
|
+
try {
|
|
19
|
+
if (typeof navigator === "undefined") return true;
|
|
20
|
+
return navigator.onLine;
|
|
21
|
+
} catch {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function withTimeout(p, ms) {
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
const t = setTimeout(() => reject(new Error("timeout")), ms);
|
|
28
|
+
p.then((v) => {
|
|
29
|
+
clearTimeout(t);
|
|
30
|
+
resolve(v);
|
|
31
|
+
}).catch((e) => {
|
|
32
|
+
clearTimeout(t);
|
|
33
|
+
reject(e);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
var lastSupabaseCheckAt = 0;
|
|
38
|
+
var lastSupabaseReachable = false;
|
|
39
|
+
var MIN_CHECK_INTERVAL_MS = 1e4;
|
|
40
|
+
async function supabaseReachable(ttlMs = 3e4, timeoutMs = 2500) {
|
|
41
|
+
const now = Date.now();
|
|
42
|
+
const effectiveTtl = Math.max(ttlMs, MIN_CHECK_INTERVAL_MS);
|
|
43
|
+
if (now - lastSupabaseCheckAt < effectiveTtl) return lastSupabaseReachable;
|
|
44
|
+
const base = process.env.NEXT_PUBLIC_SUPABASE_URL || "";
|
|
45
|
+
if (!base) {
|
|
46
|
+
lastSupabaseCheckAt = now;
|
|
47
|
+
lastSupabaseReachable = false;
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
if (!isNavigatorOnline()) {
|
|
51
|
+
lastSupabaseCheckAt = now;
|
|
52
|
+
lastSupabaseReachable = false;
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
const url = base.replace(/\/$/, "") + "/auth/v1/health";
|
|
57
|
+
const headers = {};
|
|
58
|
+
const anonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
|
59
|
+
if (anonKey) {
|
|
60
|
+
headers["apikey"] = anonKey;
|
|
61
|
+
headers["Authorization"] = `Bearer ${anonKey}`;
|
|
62
|
+
}
|
|
63
|
+
const resp = await withTimeout(
|
|
64
|
+
fetch(url, {
|
|
65
|
+
method: "GET",
|
|
66
|
+
cache: "no-store",
|
|
67
|
+
mode: "cors",
|
|
68
|
+
headers
|
|
69
|
+
}),
|
|
70
|
+
timeoutMs
|
|
71
|
+
);
|
|
72
|
+
lastSupabaseCheckAt = now;
|
|
73
|
+
lastSupabaseReachable = resp.ok;
|
|
74
|
+
return lastSupabaseReachable;
|
|
75
|
+
} catch {
|
|
76
|
+
lastSupabaseCheckAt = now;
|
|
77
|
+
lastSupabaseReachable = false;
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ../../lib/supabaseService.ts
|
|
83
|
+
var USER_PROFILES_TABLE = "user_profiles";
|
|
84
|
+
var USER_PROGRESS_TABLE = "user_progress";
|
|
85
|
+
var MESH_EVENTS_TABLE = "mesh_events";
|
|
86
|
+
var SUPABASE_MESH_EVENT_SCHEMA = {
|
|
87
|
+
type: "object",
|
|
88
|
+
required: ["id", "user_id", "trace_path", "type", "payload", "created_at"],
|
|
89
|
+
properties: {
|
|
90
|
+
id: { type: "string" },
|
|
91
|
+
user_id: { type: "string" },
|
|
92
|
+
interaction_id: { type: ["string", "null"] },
|
|
93
|
+
run_id: { type: ["string", "null"] },
|
|
94
|
+
stack_id: { type: ["string", "null"] },
|
|
95
|
+
prev_hash: { type: ["string", "null"] },
|
|
96
|
+
trace_path: { type: "string" },
|
|
97
|
+
type: { type: "string" },
|
|
98
|
+
payload: { type: ["object", "array", "string", "number", "boolean", "null"] },
|
|
99
|
+
metadata: { type: ["object", "null"] },
|
|
100
|
+
embedding: { type: ["array", "null"], items: { type: "number" } },
|
|
101
|
+
span: { type: ["array", "null"], items: { type: "number" }, minItems: 2, maxItems: 2 },
|
|
102
|
+
created_at: { type: "string" }
|
|
103
|
+
},
|
|
104
|
+
additionalProperties: true
|
|
105
|
+
};
|
|
106
|
+
function getSupabaseClient() {
|
|
107
|
+
return createSupabaseClientComponentClient();
|
|
108
|
+
}
|
|
109
|
+
async function ensureReachableOrThrow() {
|
|
110
|
+
const ok = await supabaseReachable(4e3, 1500);
|
|
111
|
+
if (!ok) {
|
|
112
|
+
throw new SyncError("Supabase is unreachable", ErrorCodes.SYNC_OFFLINE, {
|
|
113
|
+
operation: "ensureReachable"
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
var singleflight = /* @__PURE__ */ new Map();
|
|
118
|
+
function sf(key, fn) {
|
|
119
|
+
const existing = singleflight.get(key);
|
|
120
|
+
if (existing) return existing;
|
|
121
|
+
const p = fn().finally(() => singleflight.delete(key));
|
|
122
|
+
singleflight.set(key, p);
|
|
123
|
+
return p;
|
|
124
|
+
}
|
|
125
|
+
async function getSupabaseUserProgress(userId) {
|
|
126
|
+
if (!userId) {
|
|
127
|
+
throw new SyncError(
|
|
128
|
+
"User ID is required to fetch Supabase user progress",
|
|
129
|
+
ErrorCodes.SYNC_FAILED,
|
|
130
|
+
{ operation: "getSupabaseUserProgress" }
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
return sf(`progress:${userId}`, async () => {
|
|
134
|
+
try {
|
|
135
|
+
const supabase = getSupabaseClient();
|
|
136
|
+
let quizResults = [];
|
|
137
|
+
let unlockedRewards = [];
|
|
138
|
+
let claimedRewards = [];
|
|
139
|
+
await ensureReachableOrThrow();
|
|
140
|
+
const { data: quizData, error: quizError } = await supabase.from(USER_PROGRESS_TABLE).select("quiz_id, highest_score, completed_at").eq("user_id", userId);
|
|
141
|
+
if (quizError) {
|
|
142
|
+
throw new SyncError(
|
|
143
|
+
`Failed to fetch quiz progress: ${quizError.message}`,
|
|
144
|
+
ErrorCodes.SYNC_FAILED,
|
|
145
|
+
{ operation: "getQuizProgress", userId, postgrestCode: quizError.code }
|
|
146
|
+
);
|
|
147
|
+
} else if (quizData) {
|
|
148
|
+
quizResults = quizData.map((item) => ({
|
|
149
|
+
quizId: item.quiz_id,
|
|
150
|
+
score: item.highest_score || 0,
|
|
151
|
+
completed: !!item.completed_at
|
|
152
|
+
}));
|
|
153
|
+
}
|
|
154
|
+
const { data: profileData, error: profileError } = await supabase.from(USER_PROFILES_TABLE).select("*").eq("user_id", userId).single();
|
|
155
|
+
if (profileError && profileError.code !== "PGRST116") {
|
|
156
|
+
throw new SyncError(
|
|
157
|
+
`Failed to fetch user profile: ${profileError.message}`,
|
|
158
|
+
ErrorCodes.SYNC_FAILED,
|
|
159
|
+
{ operation: "getUserProfile", userId, postgrestCode: profileError.code }
|
|
160
|
+
);
|
|
161
|
+
} else if (profileError && profileError.code === "PGRST116") {
|
|
162
|
+
try {
|
|
163
|
+
const { error: insertErr } = await supabase.from(USER_PROFILES_TABLE).insert({ user_id: userId, preferences: {} }).single();
|
|
164
|
+
if (insertErr && insertErr.code !== "23505") {
|
|
165
|
+
throw new SyncError(
|
|
166
|
+
`Failed to create user profile: ${insertErr.message}`,
|
|
167
|
+
ErrorCodes.SYNC_FAILED,
|
|
168
|
+
{ operation: "createUserProfile", userId, postgrestCode: insertErr.code }
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
} catch (error) {
|
|
172
|
+
if (error instanceof SyncError) throw error;
|
|
173
|
+
throw SyncError.fromUnknown(error, "sync");
|
|
174
|
+
}
|
|
175
|
+
} else if (profileData && profileData.preferences) {
|
|
176
|
+
const prefs = profileData.preferences;
|
|
177
|
+
unlockedRewards = prefs.unlocked_reward_ids || [];
|
|
178
|
+
claimedRewards = prefs.claimed_reward_ids || [];
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
quizResults,
|
|
182
|
+
unlockedRewards,
|
|
183
|
+
claimedRewards,
|
|
184
|
+
persistenceEnabled: true,
|
|
185
|
+
preferences: profileData?.preferences || {}
|
|
186
|
+
};
|
|
187
|
+
} catch (error) {
|
|
188
|
+
if (error instanceof SyncError) throw error;
|
|
189
|
+
throw SyncError.fromUnknown(error, "sync");
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
async function upsertUserProgressSimple(userId, quizId, score, completedAt) {
|
|
194
|
+
if (!userId || !quizId) return;
|
|
195
|
+
try {
|
|
196
|
+
const supabase = getSupabaseClient();
|
|
197
|
+
await ensureReachableOrThrow();
|
|
198
|
+
const ts = completedAt || (/* @__PURE__ */ new Date()).toISOString();
|
|
199
|
+
const { error } = await supabase.from(USER_PROGRESS_TABLE).upsert({
|
|
200
|
+
user_id: userId,
|
|
201
|
+
quiz_id: quizId,
|
|
202
|
+
highest_score: score,
|
|
203
|
+
completed_at: ts,
|
|
204
|
+
last_attempted_at: ts
|
|
205
|
+
});
|
|
206
|
+
if (error) {
|
|
207
|
+
throw new SyncError(
|
|
208
|
+
`Failed to upsert user progress: ${error.message}`,
|
|
209
|
+
ErrorCodes.SYNC_FAILED,
|
|
210
|
+
{ operation: "upsertUserProgressSimple", userId, quizId, postgrestCode: error.code }
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
} catch (error) {
|
|
214
|
+
if (error instanceof SyncError) throw error;
|
|
215
|
+
throw SyncError.fromUnknown(error, "sync");
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
async function saveSupabaseQuizAttempt(userId, quizId, score, allRewardsData) {
|
|
219
|
+
if (!userId || !quizId) {
|
|
220
|
+
throw new SyncError("User ID and Quiz ID are required", ErrorCodes.SYNC_FAILED, {
|
|
221
|
+
operation: "saveSupabaseQuizAttempt"
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
const supabase = getSupabaseClient();
|
|
226
|
+
const { data: existingAttempt, error: fetchError } = await supabase.from(USER_PROGRESS_TABLE).select("highest_score").eq("user_id", userId).eq("quiz_id", quizId).single();
|
|
227
|
+
if (fetchError && fetchError.code !== "PGRST116") {
|
|
228
|
+
throw new SyncError(
|
|
229
|
+
`Failed to fetch existing quiz attempt: ${fetchError.message}`,
|
|
230
|
+
ErrorCodes.SYNC_FAILED,
|
|
231
|
+
{ operation: "fetchExistingAttempt", userId, quizId, postgrestCode: fetchError.code }
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
const existingScore = existingAttempt?.highest_score || 0;
|
|
235
|
+
if (score > existingScore) {
|
|
236
|
+
const { error: upsertError } = await supabase.from(USER_PROGRESS_TABLE).upsert({
|
|
237
|
+
user_id: userId,
|
|
238
|
+
quiz_id: quizId,
|
|
239
|
+
highest_score: score,
|
|
240
|
+
completed_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
241
|
+
last_attempted_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
242
|
+
// Add last_attempted_at
|
|
243
|
+
});
|
|
244
|
+
if (upsertError) {
|
|
245
|
+
throw new SyncError(
|
|
246
|
+
`Failed to upsert quiz attempt: ${upsertError.message}`,
|
|
247
|
+
ErrorCodes.SYNC_FAILED,
|
|
248
|
+
{ operation: "upsertQuizAttempt", userId, quizId, postgrestCode: upsertError.code }
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
} else {
|
|
252
|
+
const { error: updateError } = await supabase.from(USER_PROGRESS_TABLE).update({ last_attempted_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("user_id", userId).eq("quiz_id", quizId);
|
|
253
|
+
if (updateError) {
|
|
254
|
+
throw new SyncError(
|
|
255
|
+
`Failed to update last_attempted_at: ${updateError.message}`,
|
|
256
|
+
ErrorCodes.SYNC_FAILED,
|
|
257
|
+
{ operation: "updateLastAttempted", userId, quizId, postgrestCode: updateError.code }
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
const newlyUnlockedRewardIds = [];
|
|
262
|
+
const { data: currentProfile, error: profileError } = await supabase.from(USER_PROFILES_TABLE).select("preferences, user_id").eq("user_id", userId).single();
|
|
263
|
+
if (profileError && profileError.code !== "PGRST116") {
|
|
264
|
+
throw new SyncError(
|
|
265
|
+
`Failed to fetch user profile for reward processing: ${profileError.message}`,
|
|
266
|
+
ErrorCodes.SYNC_FAILED,
|
|
267
|
+
{ operation: "fetchProfileForRewards", userId, postgrestCode: profileError.code }
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
const currentPreferences = currentProfile?.preferences || {};
|
|
271
|
+
const currentUnlockedIds = currentPreferences.unlocked_reward_ids || [];
|
|
272
|
+
const { data: allCompletedQuizData, error: allQuizError } = await supabase.from(USER_PROGRESS_TABLE).select("quiz_id").eq("user_id", userId).not("completed_at", "is", null);
|
|
273
|
+
if (allQuizError) {
|
|
274
|
+
throw new SyncError(
|
|
275
|
+
`Failed to fetch completed quizzes for reward processing: ${allQuizError.message}`,
|
|
276
|
+
ErrorCodes.SYNC_FAILED,
|
|
277
|
+
{ operation: "fetchCompletedQuizzes", userId, postgrestCode: allQuizError.code }
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
const completedQuizIds = new Set((allCompletedQuizData || []).map((q) => q.quiz_id));
|
|
281
|
+
if (!completedQuizIds.has(quizId) && score > 0) {
|
|
282
|
+
completedQuizIds.add(quizId);
|
|
283
|
+
}
|
|
284
|
+
for (const reward of allRewardsData) {
|
|
285
|
+
if (reward.unlockRequirement?.type === "quiz" && reward.unlockRequirement.quizId) {
|
|
286
|
+
if (reward.unlockRequirement.quizId === quizId && !currentUnlockedIds.includes(reward.id)) {
|
|
287
|
+
newlyUnlockedRewardIds.push(reward.id);
|
|
288
|
+
}
|
|
289
|
+
} else if (reward.unlockRequirement?.type === "completion" && reward.unlockRequirement.required_quizzes) {
|
|
290
|
+
const required = reward.unlockRequirement.required_quizzes;
|
|
291
|
+
if (required.every((reqQuizId) => completedQuizIds.has(reqQuizId)) && !currentUnlockedIds.includes(reward.id)) {
|
|
292
|
+
newlyUnlockedRewardIds.push(reward.id);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (newlyUnlockedRewardIds.length > 0) {
|
|
297
|
+
const updatedUnlockedIds = Array.from(
|
|
298
|
+
/* @__PURE__ */ new Set([...currentUnlockedIds, ...newlyUnlockedRewardIds])
|
|
299
|
+
);
|
|
300
|
+
const newPreferences = {
|
|
301
|
+
...currentPreferences,
|
|
302
|
+
unlocked_reward_ids: updatedUnlockedIds
|
|
303
|
+
};
|
|
304
|
+
const { error: updatePrefsError } = await supabase.from(USER_PROFILES_TABLE).update({ preferences: newPreferences }).eq("user_id", userId);
|
|
305
|
+
if (updatePrefsError) {
|
|
306
|
+
throw new SyncError(
|
|
307
|
+
`Failed to update user preferences with new rewards: ${updatePrefsError.message}`,
|
|
308
|
+
ErrorCodes.SYNC_FAILED,
|
|
309
|
+
{
|
|
310
|
+
operation: "updateRewards",
|
|
311
|
+
userId,
|
|
312
|
+
rewardIds: newlyUnlockedRewardIds,
|
|
313
|
+
postgrestCode: updatePrefsError.code
|
|
314
|
+
}
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return { newlyUnlockedRewardIds };
|
|
319
|
+
} catch (error) {
|
|
320
|
+
if (error instanceof SyncError) throw error;
|
|
321
|
+
throw SyncError.fromUnknown(error, "sync");
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
async function claimSupabaseReward(userId, rewardId) {
|
|
325
|
+
if (!userId || !rewardId) {
|
|
326
|
+
throw new SyncError("User ID and Reward ID are required", ErrorCodes.SYNC_FAILED, {
|
|
327
|
+
operation: "claimSupabaseReward"
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
try {
|
|
331
|
+
const supabase = getSupabaseClient();
|
|
332
|
+
const { data: profileData, error: profileError } = await supabase.from(USER_PROFILES_TABLE).select("preferences").eq("user_id", userId).single();
|
|
333
|
+
if (profileError) {
|
|
334
|
+
if (profileError.code !== "PGRST116") {
|
|
335
|
+
throw new SyncError(
|
|
336
|
+
`Failed to fetch user profile for claiming reward: ${profileError.message}`,
|
|
337
|
+
ErrorCodes.SYNC_FAILED,
|
|
338
|
+
{ operation: "fetchProfileForClaim", userId, rewardId, postgrestCode: profileError.code }
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
try {
|
|
342
|
+
await supabase.from(USER_PROFILES_TABLE).insert({ user_id: userId, preferences: {} }).single();
|
|
343
|
+
} catch (error) {
|
|
344
|
+
throw SyncError.fromUnknown(error, "sync");
|
|
345
|
+
}
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
const currentPreferences = profileData?.preferences || {};
|
|
349
|
+
const currentClaimedIds = currentPreferences.claimed_reward_ids || [];
|
|
350
|
+
const currentUnlockedIds = currentPreferences.unlocked_reward_ids || [];
|
|
351
|
+
if (currentUnlockedIds.includes(rewardId) && !currentClaimedIds.includes(rewardId)) {
|
|
352
|
+
const updatedClaimedIds = Array.from(/* @__PURE__ */ new Set([...currentClaimedIds, rewardId]));
|
|
353
|
+
const newPreferences = {
|
|
354
|
+
...currentPreferences,
|
|
355
|
+
claimed_reward_ids: updatedClaimedIds
|
|
356
|
+
};
|
|
357
|
+
const { error: updatePrefsError } = await supabase.from(USER_PROFILES_TABLE).update({ preferences: newPreferences }).eq("user_id", userId);
|
|
358
|
+
if (updatePrefsError) {
|
|
359
|
+
throw new SyncError(
|
|
360
|
+
`Failed to update user preferences with claimed reward: ${updatePrefsError.message}`,
|
|
361
|
+
ErrorCodes.SYNC_FAILED,
|
|
362
|
+
{
|
|
363
|
+
operation: "updateClaimedReward",
|
|
364
|
+
userId,
|
|
365
|
+
rewardId,
|
|
366
|
+
postgrestCode: updatePrefsError.code
|
|
367
|
+
}
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
} else if (!currentUnlockedIds.includes(rewardId)) {
|
|
371
|
+
throw new SyncError(
|
|
372
|
+
`Attempted to claim reward that is not unlocked`,
|
|
373
|
+
ErrorCodes.SYNC_FAILED,
|
|
374
|
+
{ operation: "claimLockedReward", userId, rewardId }
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
} catch (error) {
|
|
378
|
+
if (error instanceof SyncError) throw error;
|
|
379
|
+
throw SyncError.fromUnknown(error, "sync");
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
async function updateUserPreferences(userId, preferences) {
|
|
383
|
+
if (!userId) {
|
|
384
|
+
throw new SyncError("User ID is required to update preferences", ErrorCodes.SYNC_FAILED, {
|
|
385
|
+
operation: "updateUserPreferences"
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
try {
|
|
389
|
+
const supabase = getSupabaseClient();
|
|
390
|
+
const { error } = await supabase.from(USER_PROFILES_TABLE).update({ preferences }).eq("user_id", userId);
|
|
391
|
+
if (error) {
|
|
392
|
+
throw new SyncError(
|
|
393
|
+
`Failed to update user preferences: ${error.message}`,
|
|
394
|
+
ErrorCodes.SYNC_FAILED,
|
|
395
|
+
{ operation: "updateUserPreferences", userId, postgrestCode: error.code }
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
} catch (error) {
|
|
399
|
+
if (error instanceof SyncError) throw error;
|
|
400
|
+
throw SyncError.fromUnknown(error, "sync");
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
async function appendSupabaseMeshEvent(event) {
|
|
404
|
+
await ensureReachableOrThrow();
|
|
405
|
+
const supabase = getSupabaseClient();
|
|
406
|
+
const { data, error } = await supabase.from(MESH_EVENTS_TABLE).insert(event).select("*").single();
|
|
407
|
+
if (error || !data) {
|
|
408
|
+
throw new SyncError(
|
|
409
|
+
`Failed to insert mesh event: ${error?.message || "Unknown error"}`,
|
|
410
|
+
ErrorCodes.SYNC_FAILED,
|
|
411
|
+
{ operation: "appendSupabaseMeshEvent", postgrestCode: error?.code }
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
const validated = secureValidate(data, SUPABASE_MESH_EVENT_SCHEMA);
|
|
415
|
+
if (!validated) {
|
|
416
|
+
throw new SyncError("Supabase mesh event validation failed", ErrorCodes.SYNC_FAILED, {
|
|
417
|
+
operation: "appendSupabaseMeshEvent"
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
return validated;
|
|
421
|
+
}
|
|
422
|
+
async function querySupabaseMeshEvents(userId, interactionId, limit = 200) {
|
|
423
|
+
await ensureReachableOrThrow();
|
|
424
|
+
const supabase = getSupabaseClient();
|
|
425
|
+
let query = supabase.from(MESH_EVENTS_TABLE).select("*").eq("user_id", userId);
|
|
426
|
+
if (interactionId) {
|
|
427
|
+
query = query.eq("interaction_id", interactionId);
|
|
428
|
+
}
|
|
429
|
+
const { data, error } = await query.order("created_at", { ascending: false }).limit(limit);
|
|
430
|
+
if (error) {
|
|
431
|
+
throw new SyncError(`Failed to query mesh events: ${error.message}`, ErrorCodes.SYNC_FAILED, {
|
|
432
|
+
operation: "querySupabaseMeshEvents",
|
|
433
|
+
postgrestCode: error.code
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
const output = [];
|
|
437
|
+
for (const row of data || []) {
|
|
438
|
+
const validated = secureValidate(row, SUPABASE_MESH_EVENT_SCHEMA);
|
|
439
|
+
if (validated) {
|
|
440
|
+
output.push(validated);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
return output;
|
|
444
|
+
}
|
|
445
|
+
async function updateSupabaseUserProfileAlias(userId, alias) {
|
|
446
|
+
if (!userId) {
|
|
447
|
+
throw new SyncError("User ID is required to update alias", ErrorCodes.SYNC_FAILED, {
|
|
448
|
+
operation: "updateSupabaseUserProfileAlias"
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
try {
|
|
452
|
+
const supabase = getSupabaseClient();
|
|
453
|
+
const { error } = await supabase.from(USER_PROFILES_TABLE).update({ username: alias }).eq("user_id", userId);
|
|
454
|
+
if (error) {
|
|
455
|
+
throw new SyncError(`Failed to update user alias: ${error.message}`, ErrorCodes.SYNC_FAILED, {
|
|
456
|
+
operation: "updateUserAlias",
|
|
457
|
+
userId,
|
|
458
|
+
alias,
|
|
459
|
+
postgrestCode: error.code
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
} catch (error) {
|
|
463
|
+
if (error instanceof SyncError) throw error;
|
|
464
|
+
throw SyncError.fromUnknown(error, "sync");
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
export {
|
|
468
|
+
appendSupabaseMeshEvent,
|
|
469
|
+
claimSupabaseReward,
|
|
470
|
+
getSupabaseUserProgress,
|
|
471
|
+
querySupabaseMeshEvents,
|
|
472
|
+
saveSupabaseQuizAttempt,
|
|
473
|
+
updateSupabaseUserProfileAlias,
|
|
474
|
+
updateUserPreferences,
|
|
475
|
+
upsertUserProgressSimple
|
|
476
|
+
};
|