@epic-web/workshop-presence 6.47.6 → 6.47.7
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/dist/{esm/presence.d.ts → presence.d.ts} +0 -1
- package/dist/{esm/presence.js → presence.js} +0 -1
- package/dist/{esm/presence.server.d.ts → presence.server.d.ts} +1 -2
- package/dist/{esm/presence.server.js → presence.server.js} +1 -2
- package/package.json +10 -17
- package/dist/esm/package.json +0 -3
- package/dist/esm/presence.d.ts.map +0 -1
- package/dist/esm/presence.js.map +0 -1
- package/dist/esm/presence.server.d.ts.map +0 -1
- package/dist/esm/presence.server.js.map +0 -1
- package/dist/esm/presence.test.d.ts +0 -2
- package/dist/esm/presence.test.d.ts.map +0 -1
- package/dist/esm/presence.test.js +0 -140
- package/dist/esm/presence.test.js.map +0 -1
- package/dist/esm/server.d.ts +0 -53
- package/dist/esm/server.d.ts.map +0 -1
- package/dist/esm/server.js +0 -211
- package/dist/esm/server.js.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Timings } from '@epic-web/workshop-utils/timing.server';
|
|
2
|
-
import { type User } from
|
|
2
|
+
import { type User } from "./presence.js";
|
|
3
3
|
export declare const presenceCache: {
|
|
4
4
|
name: string;
|
|
5
5
|
set: (key: string, value: import("@epic-web/cachified").CacheEntry<{
|
|
@@ -58,4 +58,3 @@ export declare function getPresentUsers({ timings, request, }?: {
|
|
|
58
58
|
timings?: Timings;
|
|
59
59
|
request?: Request;
|
|
60
60
|
}): Promise<Array<User>>;
|
|
61
|
-
//# sourceMappingURL=presence.server.d.ts.map
|
|
@@ -3,7 +3,7 @@ import { getWorkshopConfig } from '@epic-web/workshop-utils/config.server';
|
|
|
3
3
|
import { getAuthInfo, getPreferences } from '@epic-web/workshop-utils/db.server';
|
|
4
4
|
import { getUserInfo, userHasAccessToWorkshop, } from '@epic-web/workshop-utils/epic-api.server';
|
|
5
5
|
import { getUserId } from '@epic-web/workshop-utils/user.server';
|
|
6
|
-
import { PresenceSchema, partykitBaseUrl } from
|
|
6
|
+
import { PresenceSchema, partykitBaseUrl } from "./presence.js";
|
|
7
7
|
export const presenceCache = makeSingletonCache('PresenceCache');
|
|
8
8
|
export async function getPresentUsers({ timings, request, } = {}) {
|
|
9
9
|
const presence = await cachified({
|
|
@@ -102,4 +102,3 @@ function uniqueUsers(users) {
|
|
|
102
102
|
return true;
|
|
103
103
|
});
|
|
104
104
|
}
|
|
105
|
-
//# sourceMappingURL=presence.server.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@epic-web/workshop-presence",
|
|
3
|
-
"version": "6.47.
|
|
3
|
+
"version": "6.47.7",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -9,28 +9,25 @@
|
|
|
9
9
|
"test": "vitest run",
|
|
10
10
|
"test:watch": "vitest",
|
|
11
11
|
"typecheck": "tsc --noEmit",
|
|
12
|
-
"build": "
|
|
12
|
+
"build": "zshy",
|
|
13
13
|
"dev": "partykit dev",
|
|
14
14
|
"deploy": "partykit deploy"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@epic-web/workshop-utils": "6.47.
|
|
17
|
+
"@epic-web/workshop-utils": "6.47.7",
|
|
18
18
|
"zod": "^3.25.76"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"partykit": "0.0.115",
|
|
22
22
|
"typescript": "^5.9.3",
|
|
23
|
-
"
|
|
23
|
+
"zshy": "^0.3.0",
|
|
24
24
|
"vitest": "^4.0.8"
|
|
25
25
|
},
|
|
26
26
|
"files": [
|
|
27
27
|
"dist"
|
|
28
28
|
],
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"dialects": [
|
|
32
|
-
"esm"
|
|
33
|
-
],
|
|
29
|
+
"zshy": {
|
|
30
|
+
"cjs": false,
|
|
34
31
|
"exports": {
|
|
35
32
|
"./package.json": "./package.json",
|
|
36
33
|
"./presence.server": "./src/presence.server.ts",
|
|
@@ -45,16 +42,12 @@
|
|
|
45
42
|
"exports": {
|
|
46
43
|
"./package.json": "./package.json",
|
|
47
44
|
"./presence.server": {
|
|
48
|
-
"
|
|
49
|
-
|
|
50
|
-
"default": "./dist/esm/presence.server.js"
|
|
51
|
-
}
|
|
45
|
+
"types": "./dist/presence.server.d.ts",
|
|
46
|
+
"import": "./dist/presence.server.js"
|
|
52
47
|
},
|
|
53
48
|
"./presence": {
|
|
54
|
-
"
|
|
55
|
-
|
|
56
|
-
"default": "./dist/esm/presence.js"
|
|
57
|
-
}
|
|
49
|
+
"types": "./dist/presence.d.ts",
|
|
50
|
+
"import": "./dist/presence.js"
|
|
58
51
|
}
|
|
59
52
|
}
|
|
60
53
|
}
|
package/dist/esm/package.json
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"presence.d.ts","sourceRoot":"","sources":["../../src/presence.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAEvB,eAAO,MAAM,YAAY,sBAAsB,CAAA;AAE/C,eAAO,MAAM,eAAe,qFAAmF,CAAA;AAE/G,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA0BrB,CAAA;AAEF,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAWxB,CAAA;AAEF,MAAM,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAA;AAEnD,MAAM,MAAM,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAA;AAE7C,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAA2C,CAAA"}
|
package/dist/esm/presence.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"presence.js","sourceRoot":"","sources":["../../src/presence.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAEvB,MAAM,CAAC,MAAM,YAAY,GAAG,mBAAmB,CAAA;AAC/C,sFAAsF;AACtF,MAAM,CAAC,MAAM,eAAe,GAAG,kEAAkE,YAAY,EAAE,CAAA;AAE/G,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,CAAC,MAAM,CAAC;IAClC,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE;IACd,SAAS,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;IAC5C,4EAA4E;IAC5E,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;IAC3C,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;IAC/C,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;IAC/C,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;IACtC,QAAQ,EAAE,CAAC;SACT,MAAM,CAAC;QACP,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;QAC/C,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;QACxC,QAAQ,EAAE,CAAC;aACT,MAAM,CAAC;YACP,IAAI,EAAE,CAAC;iBACL,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC;iBACpD,QAAQ,EAAE;iBACV,QAAQ,EAAE;YACZ,cAAc,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;YAChD,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;SAC5C,CAAC;aACD,QAAQ,EAAE;aACV,QAAQ,EAAE;KACZ,CAAC;SACD,QAAQ,EAAE;SACV,QAAQ,EAAE;CACZ,CAAC,CAAA;AAEF,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC;KAC5B,MAAM,CAAC;IACP,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,aAAa,CAAC;IAC9B,OAAO,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC;CACrC,CAAC;KACD,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,OAAO,EAAE,UAAU,EAAE,CAAC,CAAC;KAClE,EAAE,CACF,CAAC,CAAC,MAAM,CAAC;IACR,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC;IAC3B,OAAO,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,UAAU,CAAC,EAAE,CAAC;CACjD,CAAC,CACF,CAAA;AAMF,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,UAAU,CAAC,EAAE,CAAC,CAAA","sourcesContent":["import { z } from 'zod'\n\nexport const partykitRoom = 'epic-web-presence'\n// export const partykitBaseUrl = `http://127.0.0.1:1999/parties/main/${partykitRoom}`\nexport const partykitBaseUrl = `https://epic-web-presence.kentcdodds.partykit.dev/parties/main/${partykitRoom}`\n\nexport const UserSchema = z.object({\n\tid: z.string(),\n\thasAccess: z.boolean().nullable().optional(),\n\t// TODO: remove the avatarUrl field once people have updated their workshops\n\tavatarUrl: z.string().nullable().optional(),\n\timageUrlSmall: z.string().nullable().optional(),\n\timageUrlLarge: z.string().nullable().optional(),\n\tname: z.string().nullable().optional(),\n\tlocation: z\n\t\t.object({\n\t\t\tworkshopTitle: z.string().nullable().optional(),\n\t\t\torigin: z.string().nullable().optional(),\n\t\t\texercise: z\n\t\t\t\t.object({\n\t\t\t\t\ttype: z\n\t\t\t\t\t\t.union([z.literal('problem'), z.literal('solution')])\n\t\t\t\t\t\t.nullable()\n\t\t\t\t\t\t.optional(),\n\t\t\t\t\texerciseNumber: z.number().nullable().optional(),\n\t\t\t\t\tstepNumber: z.number().nullable().optional(),\n\t\t\t\t})\n\t\t\t\t.nullable()\n\t\t\t\t.optional(),\n\t\t})\n\t\t.nullable()\n\t\t.optional(),\n})\n\nexport const MessageSchema = z\n\t.object({\n\t\ttype: z.literal('remove-user'),\n\t\tpayload: z.object({ id: z.string() }),\n\t})\n\t.or(z.object({ type: z.literal('add-user'), payload: UserSchema }))\n\t.or(\n\t\tz.object({\n\t\t\ttype: z.literal('presence'),\n\t\t\tpayload: z.object({ users: z.array(UserSchema) }),\n\t\t}),\n\t)\n\nexport type Message = z.infer<typeof MessageSchema>\n\nexport type User = z.infer<typeof UserSchema>\n\nexport const PresenceSchema = z.object({ users: z.array(UserSchema) })\n"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"presence.server.d.ts","sourceRoot":"","sources":["../../src/presence.server.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,KAAK,OAAO,EAAE,MAAM,wCAAwC,CAAA;AAErE,OAAO,EAAmC,KAAK,IAAI,EAAE,MAAM,eAAe,CAAA;AAE1E,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAAmD,CAAA;AAE7E,wBAAsB,eAAe,CAAC,EACrC,OAAO,EACP,OAAO,GACP,GAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CA6FtE"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"presence.server.js","sourceRoot":"","sources":["../../src/presence.server.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n\tcachified,\n\tmakeSingletonCache,\n} from '@epic-web/workshop-utils/cache.server'\nimport { getWorkshopConfig } from '@epic-web/workshop-utils/config.server'\nimport { getAuthInfo, getPreferences } from '@epic-web/workshop-utils/db.server'\nimport {\n\tgetUserInfo,\n\tuserHasAccessToWorkshop,\n} from '@epic-web/workshop-utils/epic-api.server'\nimport { type Timings } from '@epic-web/workshop-utils/timing.server'\nimport { getUserId } from '@epic-web/workshop-utils/user.server'\nimport { PresenceSchema, partykitBaseUrl, type User } from './presence.js'\n\nexport const presenceCache = makeSingletonCache<Array<User>>('PresenceCache')\n\nexport async function getPresentUsers({\n\ttimings,\n\trequest,\n}: { timings?: Timings; request?: Request } = {}): Promise<Array<User>> {\n\tconst presence = await cachified({\n\t\tkey: 'presence',\n\t\tcache: presenceCache,\n\t\ttimings,\n\t\trequest,\n\t\tttl: 1000 * 2,\n\t\tswr: 1000 * 60 * 60 * 24,\n\t\tofflineFallbackValue: { users: [] },\n\t\tcheckValue: PresenceSchema,\n\t\tasync getFreshValue(context) {\n\t\t\ttry {\n\t\t\t\tconst response = await Promise.race([\n\t\t\t\t\tfetch(`${partykitBaseUrl}/presence`),\n\t\t\t\t\tnew Promise<Response>((resolve) =>\n\t\t\t\t\t\tsetTimeout(() => {\n\t\t\t\t\t\t\tresolve(new Response('Timeout', { status: 500 }))\n\t\t\t\t\t\t}, 500),\n\t\t\t\t\t),\n\t\t\t\t] as const)\n\t\t\t\tif (response.statusText === 'Timeout') {\n\t\t\t\t\tthrow new Error(`Timeout fetching partykit presence`)\n\t\t\t\t}\n\t\t\t\tif (!response.ok) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`Unexpected response from partykit: ${response.status} ${response.statusText}`,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\tconst presence = PresenceSchema.parse(await response.json())\n\t\t\t\treturn presence\n\t\t\t} catch {\n\t\t\t\t// console.error(err)\n\t\t\t\tcontext.metadata.ttl = 300\n\t\t\t\treturn { users: [] }\n\t\t\t}\n\t\t},\n\t})\n\tconst { users } = presence\n\n\tconst authInfo = await getAuthInfo()\n\tconst userId = request\n\t\t? (await getUserId({ request })).id\n\t\t: (authInfo?.id ?? null)\n\n\tconst preferences = await getPreferences()\n\n\tif (preferences?.presence.optOut || !userId) {\n\t\treturn uniqueUsers(users.filter((u) => u.id !== userId))\n\t} else {\n\t\tconst user: User = { id: userId }\n\t\tconst config = getWorkshopConfig()\n\t\tconst url = request ? new URL(request.url) : undefined\n\t\tuser.location = {\n\t\t\tworkshopTitle: config.title,\n\t\t\torigin: url ? url.origin : undefined,\n\t\t}\n\t\tif (url) {\n\t\t\tif (url.pathname.startsWith('/exercise/')) {\n\t\t\t\tconst [exerciseNumber, stepNumber, type] = url.pathname\n\t\t\t\t\t.split('/')\n\t\t\t\t\t.slice(2)\n\t\t\t\tuser.location.exercise = {\n\t\t\t\t\texerciseNumber: isNaN(Number(exerciseNumber))\n\t\t\t\t\t\t? null\n\t\t\t\t\t\t: Number(exerciseNumber),\n\t\t\t\t\tstepNumber: isNaN(Number(stepNumber)) ? null : Number(stepNumber),\n\t\t\t\t\ttype:\n\t\t\t\t\t\ttype === 'problem'\n\t\t\t\t\t\t\t? 'problem'\n\t\t\t\t\t\t\t: type === 'solution'\n\t\t\t\t\t\t\t\t? 'solution'\n\t\t\t\t\t\t\t\t: null,\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (authInfo) {\n\t\t\tconst [userInfo, hasAccess] = await Promise.all([\n\t\t\t\tgetUserInfo({ request, timings }),\n\t\t\t\tuserHasAccessToWorkshop({ request, timings }),\n\t\t\t])\n\n\t\t\tObject.assign(user, {\n\t\t\t\tname: userInfo?.name,\n\t\t\t\tavatarUrl: userInfo?.imageUrlLarge,\n\t\t\t\timageUrlSmall: userInfo?.imageUrlSmall,\n\t\t\t\timageUrlLarge: userInfo?.imageUrlLarge,\n\t\t\t\thasAccess,\n\t\t\t})\n\t\t}\n\n\t\treturn uniqueUsers([...users, user])\n\t}\n}\n\n// A user maybe on the same page in multiple tabs\n// so let's make sure we only show them once\nfunction uniqueUsers(users: Array<User>) {\n\tconst seen = new Set()\n\treturn users.filter(Boolean).filter((user) => {\n\t\tif (seen.has(user.id)) {\n\t\t\treturn false\n\t\t}\n\t\tseen.add(user.id)\n\t\treturn true\n\t})\n}\n"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"presence.test.d.ts","sourceRoot":"","sources":["../../src/presence.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
import { test, expect } from 'vitest';
|
|
2
|
-
import { UserSchema, MessageSchema, PresenceSchema, } from './presence.js';
|
|
3
|
-
test('UserSchema should validate a valid user object', () => {
|
|
4
|
-
const validUser = {
|
|
5
|
-
id: 'user123',
|
|
6
|
-
hasAccess: true,
|
|
7
|
-
name: 'John Doe',
|
|
8
|
-
imageUrlSmall: 'https://example.com/small.jpg',
|
|
9
|
-
imageUrlLarge: 'https://example.com/large.jpg',
|
|
10
|
-
location: {
|
|
11
|
-
workshopTitle: 'React Workshop',
|
|
12
|
-
origin: 'https://workshop.example.com',
|
|
13
|
-
exercise: {
|
|
14
|
-
type: 'problem',
|
|
15
|
-
exerciseNumber: 1,
|
|
16
|
-
stepNumber: 2,
|
|
17
|
-
},
|
|
18
|
-
},
|
|
19
|
-
};
|
|
20
|
-
const result = UserSchema.safeParse(validUser);
|
|
21
|
-
expect(result.success).toBe(true);
|
|
22
|
-
if (result.success) {
|
|
23
|
-
expect(result.data).toEqual(validUser);
|
|
24
|
-
}
|
|
25
|
-
});
|
|
26
|
-
test('UserSchema should validate minimal user object', () => {
|
|
27
|
-
const minimalUser = {
|
|
28
|
-
id: 'user123',
|
|
29
|
-
};
|
|
30
|
-
const result = UserSchema.safeParse(minimalUser);
|
|
31
|
-
expect(result.success).toBe(true);
|
|
32
|
-
if (result.success) {
|
|
33
|
-
expect(result.data).toEqual(minimalUser);
|
|
34
|
-
}
|
|
35
|
-
});
|
|
36
|
-
test('UserSchema should reject user without id', () => {
|
|
37
|
-
const invalidUser = {
|
|
38
|
-
name: 'John Doe',
|
|
39
|
-
hasAccess: true,
|
|
40
|
-
};
|
|
41
|
-
const result = UserSchema.safeParse(invalidUser);
|
|
42
|
-
expect(result.success).toBe(false);
|
|
43
|
-
});
|
|
44
|
-
test('MessageSchema should validate add-user message', () => {
|
|
45
|
-
const addUserMessage = {
|
|
46
|
-
type: 'add-user',
|
|
47
|
-
payload: {
|
|
48
|
-
id: 'user123',
|
|
49
|
-
name: 'John Doe',
|
|
50
|
-
hasAccess: true,
|
|
51
|
-
},
|
|
52
|
-
};
|
|
53
|
-
const result = MessageSchema.safeParse(addUserMessage);
|
|
54
|
-
expect(result.success).toBe(true);
|
|
55
|
-
if (result.success) {
|
|
56
|
-
expect(result.data).toEqual(addUserMessage);
|
|
57
|
-
}
|
|
58
|
-
});
|
|
59
|
-
test('MessageSchema should validate remove-user message', () => {
|
|
60
|
-
const removeUserMessage = {
|
|
61
|
-
type: 'remove-user',
|
|
62
|
-
payload: {
|
|
63
|
-
id: 'user123',
|
|
64
|
-
},
|
|
65
|
-
};
|
|
66
|
-
const result = MessageSchema.safeParse(removeUserMessage);
|
|
67
|
-
expect(result.success).toBe(true);
|
|
68
|
-
if (result.success) {
|
|
69
|
-
expect(result.data).toEqual(removeUserMessage);
|
|
70
|
-
}
|
|
71
|
-
});
|
|
72
|
-
test('MessageSchema should validate presence message', () => {
|
|
73
|
-
const presenceMessage = {
|
|
74
|
-
type: 'presence',
|
|
75
|
-
payload: {
|
|
76
|
-
users: [
|
|
77
|
-
{ id: 'user1', name: 'User 1' },
|
|
78
|
-
{ id: 'user2', name: 'User 2' },
|
|
79
|
-
],
|
|
80
|
-
},
|
|
81
|
-
};
|
|
82
|
-
const result = MessageSchema.safeParse(presenceMessage);
|
|
83
|
-
expect(result.success).toBe(true);
|
|
84
|
-
if (result.success) {
|
|
85
|
-
expect(result.data).toEqual(presenceMessage);
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
test('MessageSchema should reject invalid message type', () => {
|
|
89
|
-
const invalidMessage = {
|
|
90
|
-
type: 'invalid-type',
|
|
91
|
-
payload: { id: 'user123' },
|
|
92
|
-
};
|
|
93
|
-
const result = MessageSchema.safeParse(invalidMessage);
|
|
94
|
-
expect(result.success).toBe(false);
|
|
95
|
-
});
|
|
96
|
-
test('PresenceSchema should validate presence object', () => {
|
|
97
|
-
const presence = {
|
|
98
|
-
users: [
|
|
99
|
-
{ id: 'user1', name: 'User 1' },
|
|
100
|
-
{ id: 'user2', name: 'User 2' },
|
|
101
|
-
],
|
|
102
|
-
};
|
|
103
|
-
const result = PresenceSchema.safeParse(presence);
|
|
104
|
-
expect(result.success).toBe(true);
|
|
105
|
-
if (result.success) {
|
|
106
|
-
expect(result.data).toEqual(presence);
|
|
107
|
-
}
|
|
108
|
-
});
|
|
109
|
-
test('PresenceSchema should validate empty users array', () => {
|
|
110
|
-
const presence = {
|
|
111
|
-
users: [],
|
|
112
|
-
};
|
|
113
|
-
const result = PresenceSchema.safeParse(presence);
|
|
114
|
-
expect(result.success).toBe(true);
|
|
115
|
-
if (result.success) {
|
|
116
|
-
expect(result.data).toEqual(presence);
|
|
117
|
-
}
|
|
118
|
-
});
|
|
119
|
-
test('User type should be correctly inferred', () => {
|
|
120
|
-
const user = {
|
|
121
|
-
id: 'user123',
|
|
122
|
-
name: 'Test User',
|
|
123
|
-
hasAccess: true,
|
|
124
|
-
};
|
|
125
|
-
expect(user.id).toBe('user123');
|
|
126
|
-
expect(user.name).toBe('Test User');
|
|
127
|
-
expect(user.hasAccess).toBe(true);
|
|
128
|
-
});
|
|
129
|
-
test('Message type should be correctly inferred', () => {
|
|
130
|
-
const message = {
|
|
131
|
-
type: 'add-user',
|
|
132
|
-
payload: {
|
|
133
|
-
id: 'user123',
|
|
134
|
-
name: 'Test User',
|
|
135
|
-
},
|
|
136
|
-
};
|
|
137
|
-
expect(message.type).toBe('add-user');
|
|
138
|
-
expect(message.payload.id).toBe('user123');
|
|
139
|
-
});
|
|
140
|
-
//# sourceMappingURL=presence.test.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"presence.test.js","sourceRoot":"","sources":["../../src/presence.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { test, expect } from 'vitest'\nimport {\n\tUserSchema,\n\tMessageSchema,\n\tPresenceSchema,\n\ttype User,\n\ttype Message,\n} from './presence.js'\n\ntest('UserSchema should validate a valid user object', () => {\n\tconst validUser = {\n\t\tid: 'user123',\n\t\thasAccess: true,\n\t\tname: 'John Doe',\n\t\timageUrlSmall: 'https://example.com/small.jpg',\n\t\timageUrlLarge: 'https://example.com/large.jpg',\n\t\tlocation: {\n\t\t\tworkshopTitle: 'React Workshop',\n\t\t\torigin: 'https://workshop.example.com',\n\t\t\texercise: {\n\t\t\t\ttype: 'problem' as const,\n\t\t\t\texerciseNumber: 1,\n\t\t\t\tstepNumber: 2,\n\t\t\t},\n\t\t},\n\t}\n\n\tconst result = UserSchema.safeParse(validUser)\n\texpect(result.success).toBe(true)\n\tif (result.success) {\n\t\texpect(result.data).toEqual(validUser)\n\t}\n})\n\ntest('UserSchema should validate minimal user object', () => {\n\tconst minimalUser = {\n\t\tid: 'user123',\n\t}\n\n\tconst result = UserSchema.safeParse(minimalUser)\n\texpect(result.success).toBe(true)\n\tif (result.success) {\n\t\texpect(result.data).toEqual(minimalUser)\n\t}\n})\n\ntest('UserSchema should reject user without id', () => {\n\tconst invalidUser = {\n\t\tname: 'John Doe',\n\t\thasAccess: true,\n\t}\n\n\tconst result = UserSchema.safeParse(invalidUser)\n\texpect(result.success).toBe(false)\n})\n\ntest('MessageSchema should validate add-user message', () => {\n\tconst addUserMessage = {\n\t\ttype: 'add-user' as const,\n\t\tpayload: {\n\t\t\tid: 'user123',\n\t\t\tname: 'John Doe',\n\t\t\thasAccess: true,\n\t\t},\n\t}\n\n\tconst result = MessageSchema.safeParse(addUserMessage)\n\texpect(result.success).toBe(true)\n\tif (result.success) {\n\t\texpect(result.data).toEqual(addUserMessage)\n\t}\n})\n\ntest('MessageSchema should validate remove-user message', () => {\n\tconst removeUserMessage = {\n\t\ttype: 'remove-user' as const,\n\t\tpayload: {\n\t\t\tid: 'user123',\n\t\t},\n\t}\n\n\tconst result = MessageSchema.safeParse(removeUserMessage)\n\texpect(result.success).toBe(true)\n\tif (result.success) {\n\t\texpect(result.data).toEqual(removeUserMessage)\n\t}\n})\n\ntest('MessageSchema should validate presence message', () => {\n\tconst presenceMessage = {\n\t\ttype: 'presence' as const,\n\t\tpayload: {\n\t\t\tusers: [\n\t\t\t\t{ id: 'user1', name: 'User 1' },\n\t\t\t\t{ id: 'user2', name: 'User 2' },\n\t\t\t],\n\t\t},\n\t}\n\n\tconst result = MessageSchema.safeParse(presenceMessage)\n\texpect(result.success).toBe(true)\n\tif (result.success) {\n\t\texpect(result.data).toEqual(presenceMessage)\n\t}\n})\n\ntest('MessageSchema should reject invalid message type', () => {\n\tconst invalidMessage = {\n\t\ttype: 'invalid-type',\n\t\tpayload: { id: 'user123' },\n\t}\n\n\tconst result = MessageSchema.safeParse(invalidMessage)\n\texpect(result.success).toBe(false)\n})\n\ntest('PresenceSchema should validate presence object', () => {\n\tconst presence = {\n\t\tusers: [\n\t\t\t{ id: 'user1', name: 'User 1' },\n\t\t\t{ id: 'user2', name: 'User 2' },\n\t\t],\n\t}\n\n\tconst result = PresenceSchema.safeParse(presence)\n\texpect(result.success).toBe(true)\n\tif (result.success) {\n\t\texpect(result.data).toEqual(presence)\n\t}\n})\n\ntest('PresenceSchema should validate empty users array', () => {\n\tconst presence = {\n\t\tusers: [],\n\t}\n\n\tconst result = PresenceSchema.safeParse(presence)\n\texpect(result.success).toBe(true)\n\tif (result.success) {\n\t\texpect(result.data).toEqual(presence)\n\t}\n})\n\ntest('User type should be correctly inferred', () => {\n\tconst user: User = {\n\t\tid: 'user123',\n\t\tname: 'Test User',\n\t\thasAccess: true,\n\t}\n\n\texpect(user.id).toBe('user123')\n\texpect(user.name).toBe('Test User')\n\texpect(user.hasAccess).toBe(true)\n})\n\ntest('Message type should be correctly inferred', () => {\n\tconst message: Message = {\n\t\ttype: 'add-user',\n\t\tpayload: {\n\t\t\tid: 'user123',\n\t\t\tname: 'Test User',\n\t\t},\n\t}\n\n\texpect(message.type).toBe('add-user')\n\texpect(message.payload.id).toBe('user123')\n})\n"]}
|
package/dist/esm/server.d.ts
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import type * as Party from 'partykit/server';
|
|
2
|
-
declare const _default: {
|
|
3
|
-
new (party: Party.Room): {
|
|
4
|
-
options: Party.ServerOptions;
|
|
5
|
-
readonly party: Party.Room;
|
|
6
|
-
onClose(): void;
|
|
7
|
-
onError(): void;
|
|
8
|
-
updateUsers(): void;
|
|
9
|
-
getPresenceMessage(): {
|
|
10
|
-
type: "presence";
|
|
11
|
-
payload: {
|
|
12
|
-
users: {
|
|
13
|
-
id: string;
|
|
14
|
-
hasAccess?: boolean | null | undefined;
|
|
15
|
-
avatarUrl?: string | null | undefined;
|
|
16
|
-
imageUrlSmall?: string | null | undefined;
|
|
17
|
-
imageUrlLarge?: string | null | undefined;
|
|
18
|
-
name?: string | null | undefined;
|
|
19
|
-
location?: {
|
|
20
|
-
workshopTitle?: string | null | undefined;
|
|
21
|
-
origin?: string | null | undefined;
|
|
22
|
-
exercise?: {
|
|
23
|
-
type?: "problem" | "solution" | null | undefined;
|
|
24
|
-
exerciseNumber?: number | null | undefined;
|
|
25
|
-
stepNumber?: number | null | undefined;
|
|
26
|
-
} | null | undefined;
|
|
27
|
-
} | null | undefined;
|
|
28
|
-
}[];
|
|
29
|
-
};
|
|
30
|
-
};
|
|
31
|
-
getUsers(): {
|
|
32
|
-
id: string;
|
|
33
|
-
hasAccess?: boolean | null | undefined;
|
|
34
|
-
avatarUrl?: string | null | undefined;
|
|
35
|
-
imageUrlSmall?: string | null | undefined;
|
|
36
|
-
imageUrlLarge?: string | null | undefined;
|
|
37
|
-
name?: string | null | undefined;
|
|
38
|
-
location?: {
|
|
39
|
-
workshopTitle?: string | null | undefined;
|
|
40
|
-
origin?: string | null | undefined;
|
|
41
|
-
exercise?: {
|
|
42
|
-
type?: "problem" | "solution" | null | undefined;
|
|
43
|
-
exerciseNumber?: number | null | undefined;
|
|
44
|
-
stepNumber?: number | null | undefined;
|
|
45
|
-
} | null | undefined;
|
|
46
|
-
} | null | undefined;
|
|
47
|
-
}[];
|
|
48
|
-
onMessage(message: string, sender: Party.Connection): void;
|
|
49
|
-
onRequest(req: Party.Request): Response | Promise<Response>;
|
|
50
|
-
};
|
|
51
|
-
};
|
|
52
|
-
export default _default;
|
|
53
|
-
//# sourceMappingURL=server.d.ts.map
|
package/dist/esm/server.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,KAAK,MAAM,iBAAiB,CAAA;;gBAwCzB,KAAK,CAAC,IAAI;iBANpB,KAAK,CAAC,aAAa;wBAIZ,KAAK,CAAC,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;2BAyCP,MAAM,UAAU,KAAK,CAAC,UAAU;uBAepC,KAAK,CAAC,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;;;AA7D5D,wBA+GyB"}
|
package/dist/esm/server.js
DELETED
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
import { UserSchema } from './presence.js';
|
|
3
|
-
const ConnectionStateSchema = z
|
|
4
|
-
.object({
|
|
5
|
-
user: UserSchema.nullable().optional(),
|
|
6
|
-
})
|
|
7
|
-
.nullable();
|
|
8
|
-
const MessageSchema = z
|
|
9
|
-
.object({
|
|
10
|
-
type: z.literal('remove-user'),
|
|
11
|
-
payload: z.object({ id: z.string() }),
|
|
12
|
-
})
|
|
13
|
-
.or(z.object({ type: z.literal('add-user'), payload: UserSchema }))
|
|
14
|
-
.or(z.object({
|
|
15
|
-
type: z.literal('add-anonymous-user'),
|
|
16
|
-
payload: z.object({ id: z.string() }),
|
|
17
|
-
}))
|
|
18
|
-
.or(z.object({
|
|
19
|
-
type: z.literal('presence'),
|
|
20
|
-
payload: z.object({ users: z.array(UserSchema) }),
|
|
21
|
-
}));
|
|
22
|
-
export default (class Server {
|
|
23
|
-
options = {
|
|
24
|
-
hibernate: true,
|
|
25
|
-
};
|
|
26
|
-
party;
|
|
27
|
-
constructor(party) {
|
|
28
|
-
this.party = party;
|
|
29
|
-
}
|
|
30
|
-
onClose() {
|
|
31
|
-
this.updateUsers();
|
|
32
|
-
}
|
|
33
|
-
onError() {
|
|
34
|
-
this.updateUsers();
|
|
35
|
-
}
|
|
36
|
-
updateUsers() {
|
|
37
|
-
const presenceMessage = JSON.stringify(this.getPresenceMessage());
|
|
38
|
-
for (const connection of this.party.getConnections()) {
|
|
39
|
-
connection.send(presenceMessage);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
getPresenceMessage() {
|
|
43
|
-
return {
|
|
44
|
-
type: 'presence',
|
|
45
|
-
payload: { users: this.getUsers() },
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
getUsers() {
|
|
49
|
-
const users = new Map();
|
|
50
|
-
for (const connection of this.party.getConnections()) {
|
|
51
|
-
const state = getConnectionState(connection);
|
|
52
|
-
if (state?.user) {
|
|
53
|
-
users.set(state.user.id, state.user);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
return sortUsers(Array.from(users.values()));
|
|
57
|
-
}
|
|
58
|
-
onMessage(message, sender) {
|
|
59
|
-
const result = MessageSchema.safeParse(JSON.parse(message));
|
|
60
|
-
if (!result.success)
|
|
61
|
-
return;
|
|
62
|
-
if (result.data.type === 'add-user') {
|
|
63
|
-
shallowMergeConnectionState(sender, { user: result.data.payload });
|
|
64
|
-
this.updateUsers();
|
|
65
|
-
}
|
|
66
|
-
else if (result.data.type === 'remove-user') {
|
|
67
|
-
setConnectionState(sender, null);
|
|
68
|
-
this.updateUsers();
|
|
69
|
-
}
|
|
70
|
-
else if (result.data.type === 'add-anonymous-user') {
|
|
71
|
-
setConnectionState(sender, { user: result.data.payload });
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
onRequest(req) {
|
|
75
|
-
const url = new URL(req.url);
|
|
76
|
-
if (url.pathname.endsWith('/presence')) {
|
|
77
|
-
return Response.json(this.getPresenceMessage().payload);
|
|
78
|
-
}
|
|
79
|
-
if (url.pathname.endsWith('/show')) {
|
|
80
|
-
const users = this.getUsers();
|
|
81
|
-
const workshopUsers = organizeUsersByWorkshop(users);
|
|
82
|
-
return new Response(`
|
|
83
|
-
<!DOCTYPE html>
|
|
84
|
-
<html lang="en">
|
|
85
|
-
<head>
|
|
86
|
-
<meta charset="UTF-8">
|
|
87
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
88
|
-
<meta http-equiv="refresh" content="5">
|
|
89
|
-
<title>Epic Web Presence</title>
|
|
90
|
-
<style>
|
|
91
|
-
body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
|
|
92
|
-
h1, h2 { color: #333; }
|
|
93
|
-
ul { padding: 0; }
|
|
94
|
-
li { list-style: none; margin-bottom: 10px; }
|
|
95
|
-
.user-avatar { width: 64px; height: 64px; border-radius: 50%; vertical-align: middle; margin-right: 10px; }
|
|
96
|
-
</style>
|
|
97
|
-
</head>
|
|
98
|
-
<body>
|
|
99
|
-
<h1>Epic Web Presence</h1>
|
|
100
|
-
<p>Total Users: ${users.length}</p>
|
|
101
|
-
${Object.entries(workshopUsers)
|
|
102
|
-
.map(([workshop, workshopUsers]) => `
|
|
103
|
-
<h2>${workshop} (${workshopUsers.length})</h2>
|
|
104
|
-
<ul>
|
|
105
|
-
${workshopUsers.map(generateUserListItem).join('')}
|
|
106
|
-
</ul>
|
|
107
|
-
`)
|
|
108
|
-
.join('')}
|
|
109
|
-
</body>
|
|
110
|
-
</html>
|
|
111
|
-
`, {
|
|
112
|
-
headers: {
|
|
113
|
-
'Content-Type': 'text/html',
|
|
114
|
-
},
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
|
-
return new Response('not found', { status: 404 });
|
|
118
|
-
}
|
|
119
|
-
});
|
|
120
|
-
function shallowMergeConnectionState(connection, state) {
|
|
121
|
-
setConnectionState(connection, (prev) => ({ ...prev, ...state }));
|
|
122
|
-
}
|
|
123
|
-
function setConnectionState(connection, state) {
|
|
124
|
-
if (typeof state !== 'function') {
|
|
125
|
-
return connection.setState(state);
|
|
126
|
-
}
|
|
127
|
-
connection.setState((prev) => {
|
|
128
|
-
const prevParseResult = ConnectionStateSchema.safeParse(prev);
|
|
129
|
-
if (prevParseResult.success) {
|
|
130
|
-
return state(prevParseResult.data);
|
|
131
|
-
}
|
|
132
|
-
else {
|
|
133
|
-
return state(null);
|
|
134
|
-
}
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
function getConnectionState(connection) {
|
|
138
|
-
const result = ConnectionStateSchema.safeParse(connection.state);
|
|
139
|
-
if (result.success) {
|
|
140
|
-
return result.data;
|
|
141
|
-
}
|
|
142
|
-
else {
|
|
143
|
-
setConnectionState(connection, null);
|
|
144
|
-
return null;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
function sortUsers(users) {
|
|
148
|
-
return [...users].sort((a, b) => {
|
|
149
|
-
const aScore = getScore(a);
|
|
150
|
-
const bScore = getScore(b);
|
|
151
|
-
if (aScore === bScore)
|
|
152
|
-
return 0;
|
|
153
|
-
return aScore > bScore ? -1 : 1;
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
function getScore(user) {
|
|
157
|
-
let score = 0;
|
|
158
|
-
if (user.imageUrlSmall)
|
|
159
|
-
score += 1;
|
|
160
|
-
if (user.imageUrlSmall?.includes('discordapp'))
|
|
161
|
-
score += 0.5;
|
|
162
|
-
if (user.name)
|
|
163
|
-
score += 1;
|
|
164
|
-
return score;
|
|
165
|
-
}
|
|
166
|
-
function organizeUsersByWorkshop(users) {
|
|
167
|
-
const workshopUsers = {};
|
|
168
|
-
for (const user of users) {
|
|
169
|
-
const workshop = user.location?.workshopTitle ?? 'Unknown Workshop';
|
|
170
|
-
if (!workshopUsers[workshop]) {
|
|
171
|
-
workshopUsers[workshop] = [];
|
|
172
|
-
}
|
|
173
|
-
workshopUsers[workshop]?.push(user);
|
|
174
|
-
}
|
|
175
|
-
// Sort users within each workshop by exercise and step number
|
|
176
|
-
for (const workshop in workshopUsers) {
|
|
177
|
-
workshopUsers[workshop]?.sort((a, b) => {
|
|
178
|
-
const aExercise = a.location?.exercise?.exerciseNumber ?? 0;
|
|
179
|
-
const bExercise = b.location?.exercise?.exerciseNumber ?? 0;
|
|
180
|
-
if (aExercise !== bExercise)
|
|
181
|
-
return aExercise - bExercise;
|
|
182
|
-
const aStep = a.location?.exercise?.stepNumber ?? 0;
|
|
183
|
-
const bStep = b.location?.exercise?.stepNumber ?? 0;
|
|
184
|
-
return aStep - bStep;
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
return workshopUsers;
|
|
188
|
-
}
|
|
189
|
-
function generateUserListItem(user) {
|
|
190
|
-
const imageUrl = user.imageUrlLarge ?? user.avatarUrl ?? '/avatar.png';
|
|
191
|
-
const name = user.name ?? 'Anonymous';
|
|
192
|
-
const location = user.location?.exercise
|
|
193
|
-
? [
|
|
194
|
-
`Exercise ${user.location.exercise.exerciseNumber}`,
|
|
195
|
-
user.location.exercise.stepNumber &&
|
|
196
|
-
`Step ${user.location.exercise.stepNumber}`,
|
|
197
|
-
]
|
|
198
|
-
.filter(Boolean)
|
|
199
|
-
.join(', ')
|
|
200
|
-
: user.location?.origin
|
|
201
|
-
? user.location.origin
|
|
202
|
-
: 'Unknown location';
|
|
203
|
-
const accessLabel = typeof user.hasAccess === 'boolean' ? (user.hasAccess ? '🔑' : '🆓') : '';
|
|
204
|
-
return `
|
|
205
|
-
<li>
|
|
206
|
-
<img class="user-avatar" src="${imageUrl}" alt="${name}" />
|
|
207
|
-
<strong>${name}</strong> - ${location} ${accessLabel}
|
|
208
|
-
</li>
|
|
209
|
-
`;
|
|
210
|
-
}
|
|
211
|
-
//# sourceMappingURL=server.js.map
|
package/dist/esm/server.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/server.ts"],"names":[],"mappings":"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","sourcesContent":["import type * as Party from 'partykit/server'\nimport { z } from 'zod'\nimport { UserSchema } from './presence.js'\n\ntype User = z.infer<typeof UserSchema>\nconst ConnectionStateSchema = z\n\t.object({\n\t\tuser: UserSchema.nullable().optional(),\n\t})\n\t.nullable()\n\ntype ConnectionState = z.infer<typeof ConnectionStateSchema>\n\nconst MessageSchema = z\n\t.object({\n\t\ttype: z.literal('remove-user'),\n\t\tpayload: z.object({ id: z.string() }),\n\t})\n\t.or(z.object({ type: z.literal('add-user'), payload: UserSchema }))\n\t.or(\n\t\tz.object({\n\t\t\ttype: z.literal('add-anonymous-user'),\n\t\t\tpayload: z.object({ id: z.string() }),\n\t\t}),\n\t)\n\t.or(\n\t\tz.object({\n\t\t\ttype: z.literal('presence'),\n\t\t\tpayload: z.object({ users: z.array(UserSchema) }),\n\t\t}),\n\t)\ntype Message = z.infer<typeof MessageSchema>\n\nexport default (class Server implements Party.Server {\n\toptions: Party.ServerOptions = {\n\t\thibernate: true,\n\t}\n\n\treadonly party: Party.Room\n\n\tconstructor(party: Party.Room) {\n\t\tthis.party = party\n\t}\n\n\tonClose() {\n\t\tthis.updateUsers()\n\t}\n\n\tonError() {\n\t\tthis.updateUsers()\n\t}\n\n\tupdateUsers() {\n\t\tconst presenceMessage = JSON.stringify(this.getPresenceMessage())\n\t\tfor (const connection of this.party.getConnections()) {\n\t\t\tconnection.send(presenceMessage)\n\t\t}\n\t}\n\n\tgetPresenceMessage() {\n\t\treturn {\n\t\t\ttype: 'presence',\n\t\t\tpayload: { users: this.getUsers() },\n\t\t} satisfies Message\n\t}\n\n\tgetUsers() {\n\t\tconst users = new Map<string, z.infer<typeof UserSchema>>()\n\n\t\tfor (const connection of this.party.getConnections()) {\n\t\t\tconst state = getConnectionState(connection)\n\t\t\tif (state?.user) {\n\t\t\t\tusers.set(state.user.id, state.user)\n\t\t\t}\n\t\t}\n\n\t\treturn sortUsers(Array.from(users.values()))\n\t}\n\n\tonMessage(message: string, sender: Party.Connection) {\n\t\tconst result = MessageSchema.safeParse(JSON.parse(message))\n\t\tif (!result.success) return\n\n\t\tif (result.data.type === 'add-user') {\n\t\t\tshallowMergeConnectionState(sender, { user: result.data.payload })\n\t\t\tthis.updateUsers()\n\t\t} else if (result.data.type === 'remove-user') {\n\t\t\tsetConnectionState(sender, null)\n\t\t\tthis.updateUsers()\n\t\t} else if (result.data.type === 'add-anonymous-user') {\n\t\t\tsetConnectionState(sender, { user: result.data.payload })\n\t\t}\n\t}\n\n\tonRequest(req: Party.Request): Response | Promise<Response> {\n\t\tconst url = new URL(req.url)\n\t\tif (url.pathname.endsWith('/presence')) {\n\t\t\treturn Response.json(this.getPresenceMessage().payload)\n\t\t}\n\t\tif (url.pathname.endsWith('/show')) {\n\t\t\tconst users = this.getUsers()\n\t\t\tconst workshopUsers = organizeUsersByWorkshop(users)\n\t\t\treturn new Response(\n\t\t\t\t`\n\t\t\t\t<!DOCTYPE html>\n\t\t\t\t<html lang=\"en\">\n\t\t\t\t<head>\n\t\t\t\t\t<meta charset=\"UTF-8\">\n\t\t\t\t\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n\t\t\t\t\t<meta http-equiv=\"refresh\" content=\"5\">\n\t\t\t\t\t<title>Epic Web Presence</title>\n\t\t\t\t\t<style>\n\t\t\t\t\t\tbody { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }\n\t\t\t\t\t\th1, h2 { color: #333; }\n\t\t\t\t\t\tul { padding: 0; }\n\t\t\t\t\t\tli { list-style: none; margin-bottom: 10px; }\n\t\t\t\t\t\t.user-avatar { width: 64px; height: 64px; border-radius: 50%; vertical-align: middle; margin-right: 10px; }\n\t\t\t\t\t</style>\n\t\t\t\t</head>\n\t\t\t\t<body>\n\t\t\t\t\t<h1>Epic Web Presence</h1>\n\t\t\t\t\t<p>Total Users: ${users.length}</p>\n\t\t\t\t\t${Object.entries(workshopUsers)\n\t\t\t\t\t\t.map(\n\t\t\t\t\t\t\t([workshop, workshopUsers]) => `\n\t\t\t\t\t\t\t<h2>${workshop} (${workshopUsers.length})</h2>\n\t\t\t\t\t\t\t<ul>\n\t\t\t\t\t\t\t\t${workshopUsers.map(generateUserListItem).join('')}\n\t\t\t\t\t\t\t</ul>\n\t\t\t\t\t\t`,\n\t\t\t\t\t\t)\n\t\t\t\t\t\t.join('')}\n\t\t\t\t</body>\n\t\t\t\t</html>\n\t\t\t\t`,\n\t\t\t\t{\n\t\t\t\t\theaders: {\n\t\t\t\t\t\t'Content-Type': 'text/html',\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t)\n\t\t}\n\t\treturn new Response('not found', { status: 404 })\n\t}\n} satisfies Party.Worker)\n\nfunction shallowMergeConnectionState(\n\tconnection: Party.Connection,\n\tstate: ConnectionState,\n) {\n\tsetConnectionState(connection, (prev) => ({ ...prev, ...state }))\n}\n\nfunction setConnectionState(\n\tconnection: Party.Connection,\n\tstate:\n\t\t| ConnectionState\n\t\t| ((prev: ConnectionState | null) => ConnectionState | null),\n) {\n\tif (typeof state !== 'function') {\n\t\treturn connection.setState(state)\n\t}\n\tconnection.setState((prev: unknown) => {\n\t\tconst prevParseResult = ConnectionStateSchema.safeParse(prev)\n\t\tif (prevParseResult.success) {\n\t\t\treturn state(prevParseResult.data)\n\t\t} else {\n\t\t\treturn state(null)\n\t\t}\n\t})\n}\n\nfunction getConnectionState(connection: Party.Connection) {\n\tconst result = ConnectionStateSchema.safeParse(connection.state)\n\tif (result.success) {\n\t\treturn result.data\n\t} else {\n\t\tsetConnectionState(connection, null)\n\t\treturn null\n\t}\n}\n\nfunction sortUsers(users: Array<User>) {\n\treturn [...users].sort((a, b) => {\n\t\tconst aScore = getScore(a)\n\t\tconst bScore = getScore(b)\n\t\tif (aScore === bScore) return 0\n\t\treturn aScore > bScore ? -1 : 1\n\t})\n}\n\nfunction getScore(user: User) {\n\tlet score = 0\n\tif (user.imageUrlSmall) score += 1\n\tif (user.imageUrlSmall?.includes('discordapp')) score += 0.5\n\tif (user.name) score += 1\n\treturn score\n}\n\nfunction organizeUsersByWorkshop(users: Array<User>) {\n\tconst workshopUsers: Record<string, Array<User>> = {}\n\n\tfor (const user of users) {\n\t\tconst workshop = user.location?.workshopTitle ?? 'Unknown Workshop'\n\t\tif (!workshopUsers[workshop]) {\n\t\t\tworkshopUsers[workshop] = []\n\t\t}\n\t\tworkshopUsers[workshop]?.push(user)\n\t}\n\n\t// Sort users within each workshop by exercise and step number\n\tfor (const workshop in workshopUsers) {\n\t\tworkshopUsers[workshop]?.sort((a, b) => {\n\t\t\tconst aExercise = a.location?.exercise?.exerciseNumber ?? 0\n\t\t\tconst bExercise = b.location?.exercise?.exerciseNumber ?? 0\n\t\t\tif (aExercise !== bExercise) return aExercise - bExercise\n\n\t\t\tconst aStep = a.location?.exercise?.stepNumber ?? 0\n\t\t\tconst bStep = b.location?.exercise?.stepNumber ?? 0\n\t\t\treturn aStep - bStep\n\t\t})\n\t}\n\n\treturn workshopUsers\n}\n\nfunction generateUserListItem(user: User) {\n\tconst imageUrl = user.imageUrlLarge ?? user.avatarUrl ?? '/avatar.png'\n\tconst name = user.name ?? 'Anonymous'\n\tconst location = user.location?.exercise\n\t\t? [\n\t\t\t\t`Exercise ${user.location.exercise.exerciseNumber}`,\n\t\t\t\tuser.location.exercise.stepNumber &&\n\t\t\t\t\t`Step ${user.location.exercise.stepNumber}`,\n\t\t\t]\n\t\t\t\t.filter(Boolean)\n\t\t\t\t.join(', ')\n\t\t: user.location?.origin\n\t\t\t? user.location.origin\n\t\t\t: 'Unknown location'\n\n\tconst accessLabel =\n\t\ttypeof user.hasAccess === 'boolean' ? (user.hasAccess ? '🔑' : '🆓') : ''\n\n\treturn `\n\t\t<li>\n\t\t\t<img class=\"user-avatar\" src=\"${imageUrl}\" alt=\"${name}\" />\n\t\t\t<strong>${name}</strong> - ${location} ${accessLabel}\n\t\t</li>\n\t`\n}\n"]}
|