feat!: @rudder/sdk 0.1.0: createClient, idempotent modules.call retries, storage save(type, data), drop resources and projectStorage

This commit is contained in:
edmand46
2026-09-17 15:26:42 +03:00
parent c25b1c2c61
commit 1e4e0d928d
35 changed files with 474 additions and 467 deletions
+48
View File
@@ -0,0 +1,48 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { createTestClient } from '../helpers/createClient.js';
import { createFakeGateway, type FakeGateway } from '../helpers/fakeGateway.js';
// Checked-in output of `rudder client generate --lang ts` for
// test/fixtures/leaderboards.manifest.json; `npm run typecheck` proves the
// generated factory accepts RudderClient.
import { modules, type LeaderboardsSubmitError } from '../fixtures/rudder.modules.js';
import { RudderModuleError } from '../../src/client/RudderError.js';
describe('E2E: generated module client', () => {
let gateway: FakeGateway;
beforeEach(() => {
gateway = createFakeGateway({
moduleCalls: new Map([
['leaderboards/top', { result: { entries: [{ playerId: 'p1', score: 120 }] } }],
]),
});
gateway.install();
});
afterEach(() => vi.unstubAllGlobals());
it('calls through the generated factory with typed results', async () => {
const client = createTestClient();
await client.auth.loginWithDevice();
const m = modules(client);
const top = await m.leaderboards.top({ slug: 'weekly', limit: 10 });
const score: number = top.entries[0].score;
expect(score).toBe(120);
// @ts-expect-error score is required by the generated args type
await m.leaderboards.submit({ slug: 'weekly' });
const call = gateway.recorded.find((r) => r.path === '/sdk/v1/modules/leaderboards/top');
expect(call?.body).toEqual({ args: { slug: 'weekly', limit: 10 } });
expect(call?.headers.get('X-Rudder-Module-Version')).toBe('leaderboards@5.0.0');
});
it('narrows module errors to the generated code union', () => {
const error = new RudderModuleError<LeaderboardsSubmitError>('', '', {
code: 'leaderboard_not_found',
});
const code: LeaderboardsSubmitError = error.code;
expect(code).toBe('leaderboard_not_found');
});
});
+20 -34
View File
@@ -1,16 +1,8 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { RudderClient } from '../../src/client/RudderClient.js';
import { createFakeTokenStore } from '../helpers/FakeTokenStore.js';
import { createTestClient } from '../helpers/createClient.js';
import { createFakeGateway, type FakeGateway } from '../helpers/fakeGateway.js';
function makeClient(): RudderClient {
return new RudderClient({
baseUrl: 'https://api.test.rudder.build',
projectKey: 'test-project',
tokenStore: createFakeTokenStore(),
});
}
describe('E2E: player storage', () => {
let gateway: FakeGateway;
let client: RudderClient;
@@ -18,7 +10,7 @@ describe('E2E: player storage', () => {
beforeEach(() => {
gateway = createFakeGateway();
gateway.install();
client = makeClient();
client = createTestClient();
});
afterEach(() => vi.unstubAllGlobals());
@@ -26,50 +18,44 @@ describe('E2E: player storage', () => {
it('saves items then reads them back (round-trip through the gateway)', async () => {
await client.auth.loginWithDevice();
await client.storage.save([
{ id: 'save_1', type: 'progress', data: JSON.stringify({ level: 7, gold: 1200 }) },
{ id: 'settings', type: 'prefs', data: JSON.stringify({ sound: false }) },
]);
await client.storage.save('progress', JSON.stringify({ level: 7, gold: 1200 }));
await client.storage.save('prefs', JSON.stringify({ sound: false }));
const all = await client.storage.reload();
expect(all.items).toHaveLength(2);
const progress = all.items!.find((i) => i.id === 'save_1');
const progress = all.items!.find((i) => i.type === 'progress');
expect(JSON.parse(progress!.data!)).toEqual({ level: 7, gold: 1200 });
});
it('filters by type', async () => {
await client.auth.loginWithDevice();
await client.storage.save([
{ id: 'a', type: 'progress', data: '{}' },
{ id: 'b', type: 'prefs', data: '{}' },
]);
const all = await client.storage.reload();
const prefs = (all.items ?? []).filter((i) => i.type === 'prefs');
expect(prefs).toHaveLength(1);
expect(prefs[0].id).toBe('b');
const put = gateway.recorded.find((r) => r.method === 'PUT' && r.path === '/sdk/v1/storage');
expect(put?.body).toEqual({ items: [{ type: 'progress', data: '{"level":7,"gold":1200}' }] });
const getReq = gateway.recorded.findLast((r) => r.method === 'GET' && r.path === '/sdk/v1/storage');
expect(getReq?.query.get('limit')).toBe('100');
});
it('deletes a type and the items are gone', async () => {
it('save overwrites the item of the same type', async () => {
await client.auth.loginWithDevice();
await client.storage.save([
{ id: 'a', type: 'progress', data: '{}' },
{ id: 'b', type: 'prefs', data: '{}' },
]);
await client.storage.save('progress', '1');
await client.storage.save('progress', '2');
const all = await client.storage.reload();
expect(all.items?.map((i) => i.data)).toEqual(['2']);
});
it('deletes a type and the item is gone', async () => {
await client.auth.loginWithDevice();
await client.storage.save('progress', '{}');
await client.storage.save('prefs', '{}');
await client.storage.delete('progress');
const remaining = await client.storage.reload();
expect(remaining.items?.map((i) => i.id)).toEqual(['b']);
expect(remaining.items?.map((i) => i.type)).toEqual(['prefs']);
});
it('storage writes carry the auth token', async () => {
const token = (await client.auth.loginWithDevice()).accessToken;
await client.storage.save([{ id: 'a', type: 't', data: '{}' }]);
await client.storage.save('t', '{}');
const put = gateway.recorded.find((r) => r.method === 'PUT' && r.path === '/sdk/v1/storage');
expect(put?.authToken).toBe(token);