@more-ink/irt-edge 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 ADDED
@@ -0,0 +1,77 @@
1
+ # @more-ink/irt-edge
2
+
3
+ Production HTTP API server for the streaming IRT engine, built with Hono for deployment on Aliyun Function Compute.
4
+
5
+ ## Features
6
+
7
+ - RESTful IRT API with Redis storage (Upstash)
8
+ - Aliyun FC lifecycle hooks (`/initialize`, `/pre-stop`)
9
+ - Graceful shutdown and health checks
10
+
11
+ ## Quick Start
12
+
13
+ ```bash
14
+ # From repo root
15
+ pnpm install
16
+
17
+ # Create .env file
18
+ # UPSTASH_REDIS_REST_URL=https://...
19
+ # UPSTASH_REDIS_REST_TOKEN=...
20
+
21
+ # Run dev server
22
+ pnpm --filter @more-ink/irt-edge dev
23
+
24
+ # Seed data
25
+ pnpm --filter @more-ink/irt-edge seed
26
+ ```
27
+
28
+ ## Deployment
29
+
30
+ To deploy to Aliyun FC from the repo root, run:
31
+
32
+ ```bash
33
+ pnpm --filter @more-ink/irt-edge run deploy
34
+ ```
35
+ or
36
+ ```bash
37
+ pnpm dp
38
+ ```
39
+
40
+ ### How Deployment Works
41
+
42
+ The deploy process handles pnpm's symlinked `node_modules` by:
43
+ 1. Building TypeScript to `dist/`
44
+ 2. Running `pnpm deploy --prod` to create `deploy-output/` with flat, real `node_modules` (no symlinks)
45
+ 3. Uploading from `deploy-output/` as an FC layer
46
+
47
+ This ensures all dependencies are included in the layer, not just symlinks to pnpm's store.
48
+
49
+ ## API Endpoints
50
+
51
+ ### IRT Operations
52
+ - `POST /api/irt/answer` - Record response and get next item
53
+ - `POST /api/irt/next-item` - Get next item without recording response
54
+ - `GET /api/irt/health` - Health check
55
+
56
+ ### Aliyun FC Lifecycle
57
+ - `POST /initialize` - Instance startup (verifies Redis)
58
+ - `GET /pre-stop` - Instance shutdown (closes Redis)
59
+
60
+ ### System
61
+ - `GET /` - Service info
62
+
63
+ ## Environment Variables
64
+
65
+ ```bash
66
+ UPSTASH_REDIS_URL_DK=https://...
67
+ UPSTASH_REDIS_TOKEN_DK=...
68
+ ALIYUN_ACCESS_KEY=...
69
+ ALIYUN_SECRET_ACCESS_KEY=...
70
+ FEISHU_APP_ID=...
71
+ FEISHU_APP_SECRET=...
72
+ FEISHU_CHAT_ID=...
73
+ PORT=9000 # optional
74
+ ```
75
+
76
+ See `src/index.ts` for default IRT engine parameters (learning rates, selection options).
77
+
@@ -0,0 +1 @@
1
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,116 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const node_server_1 = require("@hono/node-server");
4
+ const hono_1 = require("hono");
5
+ const irt_core_1 = require("@more-ink/irt-core");
6
+ const routes_1 = require("./routes");
7
+ const redis_1 = require("./storage/redis");
8
+ const app = new hono_1.Hono();
9
+ // Initialize Redis client and repositories
10
+ const redis = (0, redis_1.createRedisClient)();
11
+ const userRepo = (0, redis_1.createRedisUserSkillStateRepo)(redis);
12
+ const itemRepo = (0, redis_1.createRedisItemRepo)(redis);
13
+ const engine = (0, irt_core_1.createStreamingIrtEngine)({
14
+ userRepo,
15
+ itemRepo,
16
+ hooks: {
17
+ onUpdateUserAndItem: async (payload) => {
18
+ console.log('[IRT Update]', {
19
+ userId: payload.userAfter.userId,
20
+ skillId: payload.userAfter.skillId,
21
+ itemId: payload.itemAfter.id,
22
+ score: payload.score,
23
+ thetaBefore: payload.userBefore.theta.toFixed(3),
24
+ thetaAfter: payload.userAfter.theta.toFixed(3),
25
+ se: payload.se.toFixed(3),
26
+ itemA: payload.itemAfter.a.toFixed(3),
27
+ itemB: payload.itemAfter.b.toFixed(3),
28
+ infoSum: payload.userAfter.infoSum.toFixed(3),
29
+ });
30
+ },
31
+ onSelectNextItem: async (payload) => {
32
+ console.log('[IRT Selection]', {
33
+ userId: payload.user.userId,
34
+ skillId: payload.user.skillId,
35
+ theta: payload.user.theta.toFixed(3),
36
+ candidatesCount: payload.candidates.length,
37
+ chosenItemId: payload.chosen?.id ?? 'none',
38
+ chosenItemDifficulty: payload.chosen?.b.toFixed(3) ?? 'N/A',
39
+ });
40
+ },
41
+ },
42
+ config: {
43
+ updateDefaults: {
44
+ thetaLR: 0.05, // Learning rate for ability
45
+ aLR: 0.001, // Learning rate for discrimination
46
+ bLR: 0.01, // Learning rate for difficulty
47
+ minA: 0.2, // Min discrimination bound
48
+ maxA: 3.0, // Max discrimination bound
49
+ },
50
+ selectionDefaults: {
51
+ minGapMs: 10 * 60_000, // Don't repeat item within 10 minutes
52
+ difficultyPenaltyWidth: 1.0, // Gaussian width for difficulty penalty
53
+ maxTimesSeen: 100, // Cap on item exposure
54
+ topKRandomize: 5, // Randomize among top 5 items
55
+ explorationChance: 0.1, // 10% random exploration
56
+ },
57
+ },
58
+ });
59
+ // Mount IRT routes
60
+ const irtRouter = (0, routes_1.createIrtRouter)(engine);
61
+ app.route('/api/irt', irtRouter);
62
+ // Mount Redis test routes
63
+ const redisRouter = (0, routes_1.createRedisRouter)(redis);
64
+ app.route('/api/redis', redisRouter);
65
+ // Aliyun Function Compute lifecycle hooks
66
+ app.post('/initialize', async (c) => {
67
+ console.log('[Lifecycle] Function instance initializing...');
68
+ try {
69
+ await redis.ping();
70
+ console.log('[Lifecycle] Redis connection verified');
71
+ return c.body(null, 200);
72
+ }
73
+ catch (error) {
74
+ console.error('[Lifecycle] Initialization failed:', error);
75
+ return c.body(null, 500);
76
+ }
77
+ });
78
+ app.get('/pre-stop', async (c) => {
79
+ console.log('[Lifecycle] Function instance stopping...');
80
+ try {
81
+ await redis.quit();
82
+ console.log('[Lifecycle] Redis connection closed');
83
+ return c.body(null, 200);
84
+ }
85
+ catch (error) {
86
+ console.error('[Lifecycle] Pre-stop error:', error);
87
+ return c.body(null, 500);
88
+ }
89
+ });
90
+ // Health check at root
91
+ app.get('/', c => c.json({
92
+ status: 'ok',
93
+ }));
94
+ const port = Number(process.env.PORT) || 9000;
95
+ const server = (0, node_server_1.serve)({
96
+ fetch: app.fetch,
97
+ port,
98
+ });
99
+ console.log(`IRT Edge listening on http://localhost:${port}`);
100
+ // graceful shutdown
101
+ process.on('SIGINT', async () => {
102
+ server.close();
103
+ await redis.quit();
104
+ process.exit(0);
105
+ });
106
+ process.on('SIGTERM', async () => {
107
+ server.close((err) => {
108
+ if (err) {
109
+ console.error(err);
110
+ process.exit(1);
111
+ }
112
+ });
113
+ await redis.quit();
114
+ process.exit(0);
115
+ });
116
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;AAAA,mDAAyC;AACzC,+BAA2B;AAC3B,iDAA6D;AAC7D,qCAA6D;AAC7D,2CAIwB;AAExB,MAAM,GAAG,GAAG,IAAI,WAAI,EAAE,CAAA;AAEtB,2CAA2C;AAC3C,MAAM,KAAK,GAAG,IAAA,yBAAiB,GAAE,CAAA;AACjC,MAAM,QAAQ,GAAG,IAAA,qCAA6B,EAAC,KAAK,CAAC,CAAA;AACrD,MAAM,QAAQ,GAAG,IAAA,2BAAmB,EAAC,KAAK,CAAC,CAAA;AAE3C,MAAM,MAAM,GAAG,IAAA,mCAAwB,EAAC;IACtC,QAAQ;IACR,QAAQ;IACR,KAAK,EAAE;QACL,mBAAmB,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE;YACrC,OAAO,CAAC,GAAG,CAAC,cAAc,EAAE;gBAC1B,MAAM,EAAE,OAAO,CAAC,SAAS,CAAC,MAAM;gBAChC,OAAO,EAAE,OAAO,CAAC,SAAS,CAAC,OAAO;gBAClC,MAAM,EAAE,OAAO,CAAC,SAAS,CAAC,EAAE;gBAC5B,KAAK,EAAE,OAAO,CAAC,KAAK;gBACpB,WAAW,EAAE,OAAO,CAAC,UAAU,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC;gBAChD,UAAU,EAAE,OAAO,CAAC,SAAS,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC;gBAC9C,EAAE,EAAE,OAAO,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC;gBACzB,KAAK,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;gBACrC,KAAK,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;gBACrC,OAAO,EAAE,OAAO,CAAC,SAAS,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC;aAC9C,CAAC,CAAA;QACJ,CAAC;QACD,gBAAgB,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE;YAClC,OAAO,CAAC,GAAG,CAAC,iBAAiB,EAAE;gBAC7B,MAAM,EAAE,OAAO,CAAC,IAAI,CAAC,MAAM;gBAC3B,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,OAAO;gBAC7B,KAAK,EAAE,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC;gBACpC,eAAe,EAAE,OAAO,CAAC,UAAU,CAAC,MAAM;gBAC1C,YAAY,EAAE,OAAO,CAAC,MAAM,EAAE,EAAE,IAAI,MAAM;gBAC1C,oBAAoB,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,KAAK;aAC5D,CAAC,CAAA;QACJ,CAAC;KACF;IACD,MAAM,EAAE;QACN,cAAc,EAAE;YACd,OAAO,EAAE,IAAI,EAAK,4BAA4B;YAC9C,GAAG,EAAE,KAAK,EAAQ,mCAAmC;YACrD,GAAG,EAAE,IAAI,EAAS,+BAA+B;YACjD,IAAI,EAAE,GAAG,EAAS,2BAA2B;YAC7C,IAAI,EAAE,GAAG,EAAS,2BAA2B;SAC9C;QACD,iBAAiB,EAAE;YACjB,QAAQ,EAAE,EAAE,GAAG,MAAM,EAAO,sCAAsC;YAClE,sBAAsB,EAAE,GAAG,EAAE,wCAAwC;YACrE,YAAY,EAAE,GAAG,EAAY,uBAAuB;YACpD,aAAa,EAAE,CAAC,EAAa,8BAA8B;YAC3D,iBAAiB,EAAE,GAAG,EAAO,yBAAyB;SACvD;KACF;CACF,CAAC,CAAA;AAEF,mBAAmB;AACnB,MAAM,SAAS,GAAG,IAAA,wBAAe,EAAC,MAAM,CAAC,CAAA;AACzC,GAAG,CAAC,KAAK,CAAC,UAAU,EAAE,SAAS,CAAC,CAAA;AAEhC,0BAA0B;AAC1B,MAAM,WAAW,GAAG,IAAA,0BAAiB,EAAC,KAAK,CAAC,CAAA;AAC5C,GAAG,CAAC,KAAK,CAAC,YAAY,EAAE,WAAW,CAAC,CAAA;AAEpC,0CAA0C;AAC1C,GAAG,CAAC,IAAI,CAAC,aAAa,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE;IAClC,OAAO,CAAC,GAAG,CAAC,+CAA+C,CAAC,CAAA;IAC5D,IAAI,CAAC;QACH,MAAM,KAAK,CAAC,IAAI,EAAE,CAAA;QAClB,OAAO,CAAC,GAAG,CAAC,uCAAuC,CAAC,CAAA;QACpD,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,CAAA;IAC1B,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,KAAK,CAAC,oCAAoC,EAAE,KAAK,CAAC,CAAA;QAC1D,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,CAAA;IAC1B,CAAC;AACH,CAAC,CAAC,CAAA;AAEF,GAAG,CAAC,GAAG,CAAC,WAAW,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE;IAC/B,OAAO,CAAC,GAAG,CAAC,2CAA2C,CAAC,CAAA;IACxD,IAAI,CAAC;QACH,MAAM,KAAK,CAAC,IAAI,EAAE,CAAA;QAClB,OAAO,CAAC,GAAG,CAAC,qCAAqC,CAAC,CAAA;QAClD,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,CAAA;IAC1B,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,KAAK,CAAC,6BAA6B,EAAE,KAAK,CAAC,CAAA;QACnD,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,CAAA;IAC1B,CAAC;AACH,CAAC,CAAC,CAAA;AAEF,uBAAuB;AACvB,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,CACf,CAAC,CAAC,IAAI,CAAC;IACL,MAAM,EAAE,IAAI;CACb,CAAC,CACH,CAAA;AAED,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,IAAI,CAAA;AAE7C,MAAM,MAAM,GAAG,IAAA,mBAAK,EAAC;IACnB,KAAK,EAAE,GAAG,CAAC,KAAK;IAChB,IAAI;CACL,CAAC,CAAA;AAEF,OAAO,CAAC,GAAG,CAAC,0CAA0C,IAAI,EAAE,CAAC,CAAA;AAE7D,oBAAoB;AACpB,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,KAAK,IAAI,EAAE;IAC9B,MAAM,CAAC,KAAK,EAAE,CAAA;IACd,MAAM,KAAK,CAAC,IAAI,EAAE,CAAA;IAClB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;AACjB,CAAC,CAAC,CAAA;AACF,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,KAAK,IAAI,EAAE;IAC/B,MAAM,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;QACnB,IAAI,GAAG,EAAE,CAAC;YACR,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;YAClB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;QACjB,CAAC;IACH,CAAC,CAAC,CAAA;IACF,MAAM,KAAK,CAAC,IAAI,EAAE,CAAA;IAClB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;AACjB,CAAC,CAAC,CAAA"}
@@ -0,0 +1,24 @@
1
+ import { Hono } from 'hono';
2
+ import type { StreamingIrtEngine } from '@more-ink/irt-core';
3
+ /**
4
+ * Creates the IRT API router with the following endpoints:
5
+ *
6
+ * POST /api/irt/answer
7
+ * - Record a response and get next item
8
+ * - Body: { userId, skillId, itemId, score, updateOptions?, selectionOptions? }
9
+ * - Returns: { theta, se, nextItem, updatedUser, updatedItem }
10
+ *
11
+ * POST /api/irt/next-item
12
+ * - Select next item without recording response
13
+ * - Body: { userId, skillId, selectionOptions? }
14
+ * - Returns: { user, nextItem }
15
+ *
16
+ * GET /api/irt/users/:userId/skills/:skillId
17
+ * - Get current user skill state
18
+ * - Returns: { user, se }
19
+ */
20
+ export declare function createIrtRouter(engine: StreamingIrtEngine): Hono<import("hono/types").BlankEnv, import("hono/types").BlankSchema, "/">;
21
+ /**
22
+ * Creates Redis testing/info router
23
+ */
24
+ export declare function createRedisRouter(redis: any): Hono<import("hono/types").BlankEnv, import("hono/types").BlankSchema, "/">;
package/dist/routes.js ADDED
@@ -0,0 +1,212 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createIrtRouter = createIrtRouter;
4
+ exports.createRedisRouter = createRedisRouter;
5
+ const hono_1 = require("hono");
6
+ /**
7
+ * Creates the IRT API router with the following endpoints:
8
+ *
9
+ * POST /api/irt/answer
10
+ * - Record a response and get next item
11
+ * - Body: { userId, skillId, itemId, score, updateOptions?, selectionOptions? }
12
+ * - Returns: { theta, se, nextItem, updatedUser, updatedItem }
13
+ *
14
+ * POST /api/irt/next-item
15
+ * - Select next item without recording response
16
+ * - Body: { userId, skillId, selectionOptions? }
17
+ * - Returns: { user, nextItem }
18
+ *
19
+ * GET /api/irt/users/:userId/skills/:skillId
20
+ * - Get current user skill state
21
+ * - Returns: { user, se }
22
+ */
23
+ function createIrtRouter(engine) {
24
+ const router = new hono_1.Hono();
25
+ /**
26
+ * POST /api/irt/answer
27
+ * Record a response and get the next recommended item
28
+ */
29
+ router.post('/answer', async (c) => {
30
+ try {
31
+ const body = await c.req.json();
32
+ const { userId, skillId, itemId, score, timestamp, updateOptions, selectionOptions } = body;
33
+ // Validate required fields
34
+ if (!userId || !skillId || !itemId || score === undefined) {
35
+ return c.json({ error: 'Missing required fields: userId, skillId, itemId, score' }, 400);
36
+ }
37
+ // Validate score range
38
+ if (typeof score !== 'number' || score < 0 || score > 1) {
39
+ return c.json({ error: 'score must be a number between 0 and 1' }, 400);
40
+ }
41
+ const result = await engine.recordResponseAndSelectNext({
42
+ userId,
43
+ skillId,
44
+ itemId,
45
+ score,
46
+ timestamp,
47
+ updateOptions,
48
+ selectionOptions,
49
+ });
50
+ return c.json({
51
+ success: true,
52
+ data: {
53
+ theta: result.theta,
54
+ se: result.se,
55
+ nextItem: result.nextItem,
56
+ updatedUser: result.updatedUser,
57
+ updatedItem: result.updatedItem,
58
+ },
59
+ });
60
+ }
61
+ catch (error) {
62
+ console.error('Error recording response:', error);
63
+ return c.json({ error: 'Internal server error', message: error instanceof Error ? error.message : 'Unknown error' }, 500);
64
+ }
65
+ });
66
+ /**
67
+ * POST /api/irt/next-item
68
+ * Select the next item for a user/skill without recording a response
69
+ */
70
+ router.post('/next-item', async (c) => {
71
+ try {
72
+ const body = await c.req.json();
73
+ const { userId, skillId, selectionOptions } = body;
74
+ if (!userId || !skillId) {
75
+ return c.json({ error: 'Missing required fields: userId, skillId' }, 400);
76
+ }
77
+ const result = await engine.selectNextItem({
78
+ userId,
79
+ skillId,
80
+ selectionOptions,
81
+ });
82
+ return c.json({
83
+ success: true,
84
+ data: {
85
+ user: result.user,
86
+ nextItem: result.nextItem,
87
+ },
88
+ });
89
+ }
90
+ catch (error) {
91
+ console.error('Error selecting next item:', error);
92
+ return c.json({ error: 'Internal server error', message: error instanceof Error ? error.message : 'Unknown error' }, 500);
93
+ }
94
+ });
95
+ /**
96
+ * GET /api/irt/users/:userId/skills/:skillId
97
+ * Get current user skill state (read-only, no updates)
98
+ *
99
+ * Note: This requires the engine to expose a way to read user state.
100
+ * For now, we'll return a placeholder indicating this needs implementation.
101
+ */
102
+ router.get('/users/:userId/skills/:skillId', async (c) => {
103
+ try {
104
+ const userId = c.req.param('userId');
105
+ const skillId = c.req.param('skillId');
106
+ // The current engine interface doesn't expose a direct getUserState method
107
+ // Host apps would typically implement this via their userRepo directly
108
+ return c.json({
109
+ success: false,
110
+ error: 'User state retrieval should be implemented via userRepo in host app',
111
+ suggestion: 'Use selectNextItem with no selection to get user state, or extend engine interface',
112
+ }, 501);
113
+ }
114
+ catch (error) {
115
+ console.error('Error getting user state:', error);
116
+ return c.json({ error: 'Internal server error', message: error instanceof Error ? error.message : 'Unknown error' }, 500);
117
+ }
118
+ });
119
+ /**
120
+ * Health check endpoint
121
+ */
122
+ router.get('/health', (c) => {
123
+ return c.json({
124
+ success: true,
125
+ service: 'irt-edge',
126
+ status: 'healthy',
127
+ timestamp: Date.now(),
128
+ });
129
+ });
130
+ return router;
131
+ }
132
+ /**
133
+ * Creates Redis testing/info router
134
+ */
135
+ function createRedisRouter(redis) {
136
+ const router = new hono_1.Hono();
137
+ /**
138
+ * GET /api/redis/test
139
+ * Test Redis connection and return server info
140
+ */
141
+ router.get('/test', async (c) => {
142
+ const diagnostics = {
143
+ connectionState: redis.status,
144
+ redisUrl: process.env.REDIS_URL ? 'configured' : 'not configured',
145
+ };
146
+ try {
147
+ console.log('[Redis Test] Starting test, connection state:', redis.status);
148
+ // Wait for Redis to be ready if not already
149
+ if (redis.status !== 'ready') {
150
+ console.log('[Redis Test] Waiting for Redis to be ready...');
151
+ await new Promise((resolve, reject) => {
152
+ const timeout = setTimeout(() => {
153
+ reject(new Error('Redis did not become ready in time'));
154
+ }, 10000);
155
+ redis.once('ready', () => {
156
+ clearTimeout(timeout);
157
+ console.log('[Redis Test] Redis is now ready');
158
+ resolve(true);
159
+ });
160
+ redis.once('error', (err) => {
161
+ clearTimeout(timeout);
162
+ reject(err);
163
+ });
164
+ });
165
+ }
166
+ // Add timeout to prevent hanging
167
+ const timeoutPromise = new Promise((_, reject) => {
168
+ setTimeout(() => {
169
+ console.error('[Redis Test] Operation timed out after 10s');
170
+ reject(new Error('Redis operation timeout after 10s'));
171
+ }, 10000);
172
+ });
173
+ // Test Redis with SET/GET/DEL operations
174
+ const testKey = `irt:test:${Date.now()}`;
175
+ const testValue = 'ping';
176
+ const start = Date.now();
177
+ const testOperation = (async () => {
178
+ console.log('[Redis Test] Attempting SET...');
179
+ await redis.set(testKey, testValue, 'EX', 10);
180
+ console.log('[Redis Test] SET successful, attempting GET...');
181
+ const retrieved = await redis.get(testKey);
182
+ console.log('[Redis Test] GET successful, attempting DEL...');
183
+ await redis.del(testKey);
184
+ console.log('[Redis Test] All operations successful');
185
+ return retrieved;
186
+ })();
187
+ const retrieved = await Promise.race([testOperation, timeoutPromise]);
188
+ const durationMs = Date.now() - start;
189
+ const isValid = retrieved === testValue;
190
+ return c.json({
191
+ success: isValid,
192
+ status: isValid ? 'connected' : 'error',
193
+ operationMs: durationMs,
194
+ diagnostics,
195
+ timestamp: Date.now(),
196
+ });
197
+ }
198
+ catch (error) {
199
+ console.error('[Redis Test] Error:', error);
200
+ diagnostics.error = error instanceof Error ? error.message : 'Unknown error';
201
+ diagnostics.stack = error instanceof Error ? error.stack : undefined;
202
+ return c.json({
203
+ success: false,
204
+ status: 'error',
205
+ diagnostics,
206
+ timestamp: Date.now(),
207
+ }, 503);
208
+ }
209
+ });
210
+ return router;
211
+ }
212
+ //# sourceMappingURL=routes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.js","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"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"}
package/dist/seed.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Seed script to populate Redis with initial test data:
3
+ * - A few items for testing with different difficulties
4
+ * - Optional: sample user states
5
+ *
6
+ * Run with: pnpm pe tsx src/seed.ts
7
+ */
8
+ export {};
package/dist/seed.js ADDED
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ /**
3
+ * Seed script to populate Redis with initial test data:
4
+ * - A few items for testing with different difficulties
5
+ * - Optional: sample user states
6
+ *
7
+ * Run with: pnpm pe tsx src/seed.ts
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ const redis_1 = require("./storage/redis");
11
+ async function seed() {
12
+ console.log('Starting seed...');
13
+ const redis = (0, redis_1.createRedisClient)();
14
+ const itemRepo = (0, redis_1.createRedisItemRepo)(redis);
15
+ // Define test items for skill "math"
16
+ const mathItems = [
17
+ { id: 'item-001', skillId: 'math', a: 1.2, b: -1.5, timesSeen: 0 }, // Easy
18
+ { id: 'item-002', skillId: 'math', a: 1.0, b: -0.5, timesSeen: 0 },
19
+ { id: 'item-003', skillId: 'math', a: 1.5, b: 0.0, timesSeen: 0 }, // Medium
20
+ { id: 'item-004', skillId: 'math', a: 1.3, b: 0.5, timesSeen: 0 },
21
+ { id: 'item-005', skillId: 'math', a: 1.8, b: 1.0, timesSeen: 0 }, // Hard
22
+ { id: 'item-006', skillId: 'math', a: 2.0, b: 1.5, timesSeen: 0 },
23
+ ];
24
+ // Define test items for skill "vocab"
25
+ const vocabItems = [
26
+ { id: 'item-101', skillId: 'vocab', a: 1.0, b: -1.0, timesSeen: 0 },
27
+ { id: 'item-102', skillId: 'vocab', a: 1.2, b: 0.0, timesSeen: 0 },
28
+ { id: 'item-103', skillId: 'vocab', a: 1.5, b: 0.5, timesSeen: 0 },
29
+ { id: 'item-104', skillId: 'vocab', a: 1.8, b: 1.2, timesSeen: 0 },
30
+ ];
31
+ const allItems = [...mathItems, ...vocabItems];
32
+ console.log(`Seeding ${allItems.length} items...`);
33
+ for (const item of allItems) {
34
+ await itemRepo.saveItem(item);
35
+ console.log(` ✓ Saved ${item.id} (skill: ${item.skillId}, b: ${item.b})`);
36
+ }
37
+ console.log('\nSeed complete!');
38
+ console.log('\nTest the API with:');
39
+ console.log(' POST /api/irt/answer');
40
+ console.log(' Body: { "userId": "user-1", "skillId": "math", "itemId": "item-001", "score": 0.8 }');
41
+ console.log('\n POST /api/irt/next-item');
42
+ console.log(' Body: { "userId": "user-1", "skillId": "math" }');
43
+ await redis.quit();
44
+ process.exit(0);
45
+ }
46
+ seed().catch((err) => {
47
+ console.error('Seed failed:', err);
48
+ process.exit(1);
49
+ });
50
+ //# sourceMappingURL=seed.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"seed.js","sourceRoot":"","sources":["../src/seed.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ import Redis from 'ioredis';
2
+ import type { UserSkillStateRepo, ItemRepo } from '@more-ink/irt-core';
3
+ /**
4
+ * Redis-backed implementation of UserSkillStateRepo.
5
+ *
6
+ * Keys: `irt:user:{userId}:skill:{skillId}`
7
+ * Value: JSON { theta, infoSum, se }
8
+ */
9
+ export declare function createRedisUserSkillStateRepo<TSkillId = string>(redis: Redis): UserSkillStateRepo<TSkillId>;
10
+ /**
11
+ * Redis-backed implementation of ItemRepo.
12
+ *
13
+ * Keys:
14
+ * - `irt:item:{itemId}:skill:{skillId}` - individual item
15
+ * - `irt:skill:{skillId}:items` - set of itemIds for a skill
16
+ *
17
+ * Value: JSON { id, skillId, a, b, lastSeenAt, timesSeen }
18
+ */
19
+ export declare function createRedisItemRepo<TSkillId = string>(redis: Redis): ItemRepo<TSkillId>;
20
+ /**
21
+ * Create a Redis client from REDIS_URL env var or default to localhost.
22
+ * Note: For Upstash, use the Redis protocol endpoint (rediss://...), not the REST API (https://...)
23
+ */
24
+ export declare function createRedisClient(): Redis;
@@ -0,0 +1,171 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createRedisUserSkillStateRepo = createRedisUserSkillStateRepo;
7
+ exports.createRedisItemRepo = createRedisItemRepo;
8
+ exports.createRedisClient = createRedisClient;
9
+ const ioredis_1 = __importDefault(require("ioredis"));
10
+ const PREFIX = 'irt';
11
+ /**
12
+ * Redis-backed implementation of UserSkillStateRepo.
13
+ *
14
+ * Keys: `irt:user:{userId}:skill:{skillId}`
15
+ * Value: JSON { theta, infoSum, se }
16
+ */
17
+ function createRedisUserSkillStateRepo(redis) {
18
+ return {
19
+ async getUserSkillState(userId, skillId) {
20
+ const key = `${PREFIX}:user:${userId}:skill:${String(skillId)}`;
21
+ const data = await redis.get(key);
22
+ if (!data)
23
+ return null;
24
+ const parsed = JSON.parse(data);
25
+ const state = {
26
+ userId,
27
+ skillId,
28
+ theta: parsed.theta ?? 0,
29
+ infoSum: parsed.infoSum ?? 0,
30
+ };
31
+ return state;
32
+ },
33
+ async saveUserSkillState(state) {
34
+ const key = `${PREFIX}:user:${state.userId}:skill:${String(state.skillId)}`;
35
+ const value = JSON.stringify({
36
+ theta: state.theta,
37
+ infoSum: state.infoSum,
38
+ se: state.se,
39
+ updatedAt: Date.now(),
40
+ });
41
+ await redis.set(key, value);
42
+ },
43
+ };
44
+ }
45
+ /**
46
+ * Redis-backed implementation of ItemRepo.
47
+ *
48
+ * Keys:
49
+ * - `irt:item:{itemId}:skill:{skillId}` - individual item
50
+ * - `irt:skill:{skillId}:items` - set of itemIds for a skill
51
+ *
52
+ * Value: JSON { id, skillId, a, b, lastSeenAt, timesSeen }
53
+ */
54
+ function createRedisItemRepo(redis) {
55
+ return {
56
+ async getItem(itemId, skillId) {
57
+ const key = `${PREFIX}:item:${itemId}:skill:${String(skillId)}`;
58
+ const data = await redis.get(key);
59
+ if (!data)
60
+ return null;
61
+ const parsed = JSON.parse(data);
62
+ const item = {
63
+ id: parsed.id ?? itemId,
64
+ skillId: parsed.skillId ?? skillId,
65
+ a: parsed.a ?? 1,
66
+ b: parsed.b ?? 0,
67
+ lastSeenAt: parsed.lastSeenAt,
68
+ timesSeen: parsed.timesSeen ?? 0,
69
+ };
70
+ return item;
71
+ },
72
+ async saveItem(item) {
73
+ const key = `${PREFIX}:item:${item.id}:skill:${String(item.skillId)}`;
74
+ const value = JSON.stringify({
75
+ id: item.id,
76
+ skillId: item.skillId,
77
+ a: item.a,
78
+ b: item.b,
79
+ lastSeenAt: item.lastSeenAt ?? null,
80
+ timesSeen: item.timesSeen ?? 0,
81
+ updatedAt: Date.now(),
82
+ });
83
+ // Save item data
84
+ await redis.set(key, value);
85
+ // Add to skill's item set for listCandidateItems
86
+ const setKey = `${PREFIX}:skill:${String(item.skillId)}:items`;
87
+ await redis.sadd(setKey, item.id);
88
+ },
89
+ async listCandidateItems(userId, skillId) {
90
+ // Get all item IDs for this skill
91
+ const setKey = `${PREFIX}:skill:${String(skillId)}:items`;
92
+ const itemIds = await redis.smembers(setKey);
93
+ if (itemIds.length === 0)
94
+ return [];
95
+ // Fetch all items in parallel
96
+ const items = await Promise.all(itemIds.map(async (itemId) => {
97
+ const key = `${PREFIX}:item:${itemId}:skill:${String(skillId)}`;
98
+ const data = await redis.get(key);
99
+ if (!data)
100
+ return null;
101
+ const parsed = JSON.parse(data);
102
+ const item = {
103
+ id: parsed.id ?? itemId,
104
+ skillId: parsed.skillId ?? skillId,
105
+ a: parsed.a ?? 1,
106
+ b: parsed.b ?? 0,
107
+ lastSeenAt: parsed.lastSeenAt,
108
+ timesSeen: parsed.timesSeen ?? 0,
109
+ };
110
+ return item;
111
+ }));
112
+ // Filter out any nulls (items that were deleted)
113
+ return items.filter((item) => item !== null);
114
+ },
115
+ };
116
+ }
117
+ /**
118
+ * Create a Redis client from REDIS_URL env var or default to localhost.
119
+ * Note: For Upstash, use the Redis protocol endpoint (rediss://...), not the REST API (https://...)
120
+ */
121
+ function createRedisClient() {
122
+ const redisUrl = process.env.REDIS_URL || 'redis://localhost:6379';
123
+ console.log('Creating Redis client with URL:', redisUrl.replace(/:[^:@]+@/, ':***@'));
124
+ const redis = new ioredis_1.default(redisUrl, {
125
+ maxRetriesPerRequest: 3,
126
+ connectTimeout: 10000, // 10s connection timeout
127
+ commandTimeout: 30000, // 30s command timeout (increase for VPC)
128
+ enableOfflineQueue: true, // Queue commands until ready
129
+ lazyConnect: false, // Connect immediately
130
+ retryStrategy: (times) => {
131
+ console.log(`Redis retry attempt ${times}`);
132
+ if (times > 3) {
133
+ console.error('Redis max retries reached');
134
+ return null; // Stop retrying
135
+ }
136
+ const delay = Math.min(times * 50, 2000);
137
+ return delay;
138
+ },
139
+ reconnectOnError: (err) => {
140
+ console.error('Redis reconnect on error:', err.message);
141
+ const targetErrors = ['READONLY', 'ECONNREFUSED'];
142
+ if (targetErrors.some((targetError) => err.message.includes(targetError))) {
143
+ return true;
144
+ }
145
+ return false;
146
+ },
147
+ });
148
+ redis.on('error', (err) => {
149
+ console.error('[Redis] Error:', err.message);
150
+ });
151
+ redis.on('connect', () => {
152
+ console.log('[Redis] TCP connection established');
153
+ });
154
+ redis.on('ready', () => {
155
+ console.log('[Redis] Ready to accept commands');
156
+ });
157
+ redis.on('reconnecting', (delay) => {
158
+ console.log(`[Redis] Reconnecting in ${delay}ms`);
159
+ });
160
+ redis.on('close', () => {
161
+ console.log('[Redis] Connection closed');
162
+ });
163
+ redis.on('end', () => {
164
+ console.log('[Redis] Connection ended');
165
+ });
166
+ redis.on('wait', () => {
167
+ console.log('[Redis] Waiting for reconnection...');
168
+ });
169
+ return redis;
170
+ }
171
+ //# sourceMappingURL=redis.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redis.js","sourceRoot":"","sources":["../../src/storage/redis.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@more-ink/irt-edge",
3
+ "version": "1.0.0",
4
+ "description": "Edge-function wrapper around the node-irt core library.",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist"
9
+ ],
10
+ "dependencies": {
11
+ "@hono/node-server": "^1.19.6",
12
+ "hono": "^4.10.7",
13
+ "ioredis": "^5.8.2",
14
+ "@more-ink/irt-core": "1.0.0"
15
+ },
16
+ "devDependencies": {
17
+ "@larksuiteoapi/node-sdk": "^1.55.0",
18
+ "@upstash/redis": "^1.35.7",
19
+ "dotenv": "^16.6.1",
20
+ "fc-deploy": "^1.2.3",
21
+ "tslib": "^2.8.1"
22
+ },
23
+ "scripts": {
24
+ "build": "tsc -p tsconfig.json",
25
+ "start": "node dist/index.js",
26
+ "test": "vitest run",
27
+ "test:watch": "vitest watch",
28
+ "lint": "echo \"Add ESLint config\" && exit 0",
29
+ "format": "echo \"Add Prettier config\" && exit 0",
30
+ "dev": "tsx watch src/index.ts",
31
+ "seed": "tsx src/seed.ts",
32
+ "predeploy": "pnpm run build && rm -rf deploy-output && pnpm deploy --prod --legacy --filter=@more-ink/irt-edge deploy-output && tsx scripts/flattenNodeModules.ts",
33
+ "deploy": "tsx scripts/deploy.ts"
34
+ }
35
+ }