Skip to content

Commit 6ac280c

Browse files
committed
wip
1 parent b310d2f commit 6ac280c

File tree

6 files changed

+255
-384
lines changed

6 files changed

+255
-384
lines changed

app/course/[course_id]/manage/assignments/[assignment_id]/repositories/page.tsx

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import {
2121
Skeleton,
2222
Table,
2323
Text,
24-
Tooltip,
2524
VStack
2625
} from "@chakra-ui/react";
2726
import { UnstableGetResult as GetResult } from "@supabase/postgrest-js";
@@ -170,12 +169,23 @@ function SyncStatusBadge({ row, latestTemplateSha }: { row: RepositoryRow; lates
170169

171170
if (syncData?.last_sync_error) {
172171
return (
173-
<Tooltip.Root>
174-
<Tooltip.Trigger asChild>
175-
<Badge colorPalette="red">Error</Badge>
176-
</Tooltip.Trigger>
177-
<Tooltip.Content>{syncData.last_sync_error}</Tooltip.Content>
178-
</Tooltip.Root>
172+
<VStack gap={2} alignItems="flex-start" width="full">
173+
<Badge colorPalette="red">Sync Error</Badge>
174+
<Box
175+
borderWidth="1px"
176+
borderColor="red.500"
177+
bg="red.50"
178+
_dark={{ bg: "red.950", borderColor: "red.800" }}
179+
px={3}
180+
py={2}
181+
borderRadius="md"
182+
width="full"
183+
>
184+
<Text fontSize="sm" color="red.700" _dark={{ color: "red.300" }} wordBreak="break-word">
185+
{syncData.last_sync_error}
186+
</Text>
187+
</Box>
188+
</VStack>
179189
);
180190
}
181191

supabase/functions/_shared/GitHubWrapper.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,11 @@ const installations: {
184184
}[] = [];
185185
const MyOctokit = Octokit.plugin(throttling);
186186

187+
export async function getOctoKitAndInstallationID(repoOrOrgName: string, scope?: Sentry.Scope) {
188+
const org = repoOrOrgName.includes("/") ? repoOrOrgName.split("/")[0] : repoOrOrgName;
189+
const installationId = installations.find((i) => i.orgName === org)?.id;
190+
return { octokit: await getOctoKit(repoOrOrgName, scope), installationId };
191+
}
187192
export async function getOctoKit(repoOrOrgName: string, scope?: Sentry.Scope) {
188193
const org = repoOrOrgName.includes("/") ? repoOrOrgName.split("/")[0] : repoOrOrgName;
189194
scope?.addBreadcrumb({

supabase/functions/_shared/Redis.ts

Lines changed: 87 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ const channelBus: Map<string, Set<Redis>> = new Map();
88
/**
99
* IORedis-compatible adapter backed by Upstash Redis REST client.
1010
* Implements the subset of the API used by Bottleneck's IORedis store.
11+
*
12+
* Uses Proxy to forward all unknown methods to the underlying Upstash client.
1113
*/
1214
export class Redis {
1315
private client: UpstashRedis;
@@ -39,6 +41,29 @@ export class Redis {
3941

4042
// Emit ready asynchronously to mimic ioredis behavior
4143
queueMicrotask(() => this.emit("ready"));
44+
45+
// Return a Proxy that forwards unknown method calls to the underlying Upstash client
46+
// This allows Redis commands like get, set, incr, etc. to work transparently
47+
return new Proxy(this, {
48+
get(target, prop, receiver) {
49+
// First check if the property exists on the Redis wrapper class
50+
const targetValue = Reflect.get(target, prop, receiver);
51+
if (targetValue !== undefined) {
52+
return targetValue;
53+
}
54+
55+
// Then check the underlying Upstash client
56+
const clientAsAny = target.client as unknown as Record<string, unknown>;
57+
const clientMethod = clientAsAny[String(prop)];
58+
59+
if (typeof clientMethod === "function") {
60+
// Bind the method to the client so 'this' works correctly
61+
return clientMethod.bind(target.client);
62+
}
63+
64+
return undefined;
65+
}
66+
});
4267
}
4368

4469
// EventEmitter-like API expected by bottleneck
@@ -74,7 +99,12 @@ export class Redis {
7499

75100
// ioredis API: duplicate returns a new connection with same options
76101
duplicate() {
77-
return new Redis(this.initOptions);
102+
const newRedis = new Redis(this.initOptions);
103+
// Copy defined scripts to the new instance
104+
for (const [name, script] of this.scripts.entries()) {
105+
newRedis.defineCommand(name, { lua: script });
106+
}
107+
return newRedis;
78108
}
79109

80110
// ioredis API: defineCommand(name, { lua }) registers a script callable as client[name](...)
@@ -93,13 +123,33 @@ export class Redis {
93123

94124
try {
95125
if (Deno.env.get("REDIS_DEBUG") === "true") {
96-
console.log("eval script", { name, numKeys, keysCount: keys.length, argvCount: argv.length });
126+
console.log("eval script", {
127+
name,
128+
numKeys,
129+
keysCount: keys.length,
130+
argvCount: argv.length,
131+
rawArgsCount: args.length,
132+
firstKey: keys[0],
133+
firstArg: argv[0],
134+
allKeys: keys,
135+
scriptLength: script.length
136+
});
137+
}
138+
139+
// Check if Upstash client has eval method
140+
const clientAsAny = this.client as unknown as Record<string, unknown>;
141+
if (typeof clientAsAny.eval !== "function") {
142+
throw new Error("Upstash Redis client does not support EVAL command");
97143
}
144+
98145
const result = await (
99146
this.client as unknown as {
100147
eval: (script: string, keys: string[], args: (string | number)[]) => Promise<unknown>;
101148
}
102149
).eval(script, keys, argv);
150+
if (Deno.env.get("REDIS_DEBUG") === "true") {
151+
console.log("eval script result", { name, result });
152+
}
103153
if (cb) cb(null, result);
104154
return result;
105155
} catch (error) {
@@ -139,6 +189,8 @@ export class Redis {
139189
// ioredis API: pipeline([...]).exec() => [[err, result], ...]
140190
pipeline(commands: Array<[string, ...unknown[]]>) {
141191
const emitError = this.emit.bind(this);
192+
const executeCommand = this.executeCommand.bind(this);
193+
const scripts = this.scripts; // Capture for closure
142194

143195
// Capture credentials at pipeline creation time
144196
const url = String(
@@ -158,15 +210,42 @@ export class Redis {
158210
return out;
159211
}
160212

161-
try {
162-
// Build 2D JSON array of commands for Upstash pipeline
163-
const pipelineCommands = commands.map((cmd) => cmd);
213+
if (Deno.env.get("REDIS_DEBUG") === "true") {
214+
console.log("pipeline exec", {
215+
commandCount: commands.length,
216+
commands: commands.map((cmd) => cmd[0])
217+
});
218+
}
164219

165-
if (Deno.env.get("REDIS_DEBUG") === "true") {
166-
console.log("pipeline exec", { commandCount: commands.length, commands: commands.map((cmd) => cmd[0]) });
220+
// Check if ANY commands are custom scripts (defined via defineCommand)
221+
const hasCustomScripts = commands.some(cmd => {
222+
const commandName = String(cmd[0]);
223+
return scripts.has(commandName);
224+
});
225+
226+
if (Deno.env.get("REDIS_DEBUG") === "true") {
227+
console.log(` pipeline hasCustomScripts: ${hasCustomScripts}, executing ${hasCustomScripts ? 'SEQUENTIALLY' : 'BATCHED'}`);
228+
}
229+
230+
// If there are custom scripts, we must execute sequentially because
231+
// Upstash pipeline doesn't support our defineCommand Lua scripts
232+
if (hasCustomScripts) {
233+
for (const cmd of commands) {
234+
try {
235+
const result = await executeCommand(cmd);
236+
out.push([null, result]);
237+
} catch (err) {
238+
out.push([err, null]);
239+
emitError("error", err);
240+
}
167241
}
242+
return out;
243+
}
244+
245+
// No custom scripts - use Upstash's atomic pipeline endpoint
246+
try {
247+
const pipelineCommands = commands.map((cmd) => cmd);
168248

169-
// Make single REST POST to Upstash pipeline endpoint
170249
const pipelineUrl = `${url}/pipeline`;
171250
const response = await fetch(pipelineUrl, {
172251
method: "POST",
@@ -183,25 +262,19 @@ export class Redis {
183262

184263
const results = (await response.json()) as Array<{ result?: unknown; error?: string }>;
185264

186-
// Map pipeline results to expected ioredis format: [err, result] or [null, result]
187-
// Upstash returns: { result: ... } on success or { error: ... } on error
188265
for (let i = 0; i < commands.length; i++) {
189266
const resp = results[i];
190267

191268
if (resp && typeof resp === "object" && "error" in resp) {
192-
// Error case: { error: "..." }
193269
out.push([resp.error, null]);
194270
emitError("error", resp.error);
195271
} else if (resp && typeof resp === "object" && "result" in resp) {
196-
// Success case: { result: ... }
197272
out.push([null, resp.result]);
198273
} else {
199-
// Unexpected format - treat as success with raw value for backwards compatibility
200274
out.push([null, resp]);
201275
}
202276
}
203277
} catch (err) {
204-
// If pipeline request fails entirely, treat all commands as failed
205278
for (let i = 0; i < commands.length; i++) {
206279
out.push([err, null]);
207280
emitError("error", err);

supabase/functions/deno.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)