12 KiB
Pecans Architecture
Project Overview
Pecans is a TypeScript API wrapper for Two Cans & String (API version 1.68). It is compiled with jsii to produce multi-language bindings (TypeScript/Node.js, Python, .NET). All source is in TypeScript under src/, and jsii generates the obj/ (intermediate JS+declarations) and dist/ (language-specific packages) directories.
- License: GPL-3.0-only
- Stability: experimental
- Repository: https://github.com/humanoidsandvichdispenser/pew
Directory Structure
src/
├── index.ts # Barrel exports; re-exports all modules and type namespaces
├── client.ts # Client class — central entry point
├── module.ts # Module base class
├── cache.ts # Generic TTL cache (unused in modules currently)
├── queue.ts # Empty — planned but not implemented
├── types/
│ ├── index.ts # Core types: TCResponse, TCResponseRaw, TCProfile, etc.
│ └── index.test.ts # Tests for core types
├── messages/
│ ├── index.ts # MessagesModule — private messaging API
│ ├── types.ts # FolderViewResponse, MessageViewResponse, etc.
│ └── (commented-out legacy type classes)
├── ask/
│ ├── index.ts # AskModule — question listing/fetching API
│ ├── types.ts # ListDataResponse, QuestionMetadata, QuestionData, etc.
│ └── index.test.ts # Tests for ask types
├── forum/
│ ├── index.ts # ForumModule — forum posts, threads, search
│ └── types.ts # ViewPostsResponse, Post, Thread, SearchResult
├── drawing/
│ ├── index.ts # DrawingModule — save/view/send drawing API
│ └── types.ts # SaveDrawingResponse, ViewDrawingDataResponse
├── answer/
│ ├── index.ts # AnswerModule — answer questions, dismiss, poll vote
│ ├── types.ts # AnswerReplyResponse, QuestionFetchResponse, DismissAction, Poll
│ └── types.test.ts # Tests for answer types
├── notify/
│ ├── index.ts # NotifyModule — sync unread counts, online users
│ ├── types.ts # NotifySyncResponse, WhosOnlineResponse, Feature enum
│ └── types.test.ts # Tests for notify types
├── account/
│ └── index.ts # AccountModule — register, activate
├── auth/
│ ├── index.ts # AuthModule — login
│ └── types.ts # LoginResponse
└── legacy/
└── index.ts # Empty — not yet implemented
Build output:
obj/— jsii intermediate output (JS +.d.ts)dist/— jsii-pacmak language-specific packages.jsii— jsii assembly manifest (auto-generated, gitignored)
Core Architecture
Client/Module Pattern
The Client class is the single entry point. It holds:
authToken— thetwocansandstring_com_auth2cookie valueagent— User-Agent string (default"pecans")profileCache— map of user ID toTCProfile, populated from API responsesshouldTrimErrors— whether to strip non-JSON prefix from error responses (defaulttrue)_cache— generic key-value cache (not yet wired into modules)
Each API domain is exposed as a property on Client:
| Property | Module Class | Source |
|---|---|---|
messages |
MessagesModule |
src/messages/ |
ask |
AskModule |
src/ask/ |
forum |
ForumModule |
src/forum/ |
drawing |
DrawingModule |
src/drawing/ |
answer |
AnswerModule |
src/answer/ |
notify |
NotifyModule |
src/notify/ |
account |
AccountModule |
src/account/ |
auth |
AuthModule |
src/auth/ |
Request Lifecycle
- A module method (e.g.,
client.messages.folderView()) callsthis.client._call(ResponseClass, methodName, args). _callconstructs aMethodCallobject{ fn: methodName, payload: args }.- If batching (
client.isBatching === true): the call is pushed onto#requestQueueand returns aPromise. Whenclient.endBatch()orclient.processBatch()is called, all queued calls are sent in a single HTTP request. - If not batching: a single-request body
{ auth, requests: [methodCall] }is sent immediately. - The HTTP POST targets
https://twocansandstring.com/apiwithContent-Type: application/jsonandCookie: twocansandstring_com_auth2=<token>if authenticated. - The response is parsed as JSON (with optional error-prefix trimming via
shouldTrimErrors). - The raw response data (
TCResponseRaw) is unpacked and passed to theResponseClassconstructor, which extracts the relevant fields. - Any
profilesin the response are cached intoclient.profileCache.
Response Type Pattern
All response classes extend TCResponse (defined in src/types/index.ts):
export class TCResponse {
public ok: boolean;
public error?: string;
public profiles?: TCProfile[];
public constructor(response: TCResponseRaw) { ... }
public toObject(): {[key: string]: any} { ... }
}
Each module's types.ts defines subclasses that parse the raw response. For example:
export class ListDataResponse extends TCResponse {
public questions: QuestionMetadata[];
public constructor(res: TCResponseRaw) {
super(res);
this.questions = (res["questions"] ?? []).map(m => new QuestionMetadata(m));
}
}
The module's index.ts passes the subclass constructor to _call:
return await this.client._call(ListDataResponse, "legacy.askapi", { ... });
Batching
The Client supports request batching to reduce HTTP overhead:
client.beginBatch();
const p1 = client.messages.folderView();
const p2 = client.notify.sync(Feature.ASK);
client.endBatch(); // sends both requests in one HTTP call
const [r1, r2] = await Promise.all([p1, p2]);
Barrel Exports
src/index.ts uses a dual-export pattern for each domain:
- The module class is re-exported directly:
export * from "./messages" - The types are re-exported as a namespace:
export * as messages from "./messages/types"
This gives consumers both MessagesModule and messages.FolderViewResponse access patterns.
Module Convention
Each API domain follows this structure:
src/<domain>/
├── index.ts # <Domain>Module extends Module — API methods
└── types.ts # Response classes (extend TCResponse), request/response interfaces, enums
Adding a New Module
- Create
src/<domain>/types.tswith response classes extendingTCResponse. - Create
src/<domain>/index.tswith a<Domain>Module extends Moduleclass. Usethis.client._call()to make API calls. - Add the module as a private field and getter in
Client(src/client.ts), instantiate it in the constructor. - Add exports to
src/index.ts:export * from "./<domain>"; export * as <domain> from "./<domain>/types"; - Run
npm run buildto verify jsii compilation.
jsii Constraints & Gotchas
Pecans uses jsii for cross-language compilation. This imposes significant constraints on the TypeScript source:
Allowed Features
- Classes, interfaces, enums (non-const)
- Public properties and methods
@internalJSDoc annotation (excludes from generated docs/bindings)- Optional properties (
?) readonlyproperties- Constructor overloads (limited)
Forbidden / Restricted Features
- Private identifier syntax (
#field): jsii does not support ECMAScript private fields. Useprivatekeyword instead for jsii-compatible code. Note: the current codebase already uses#fieldinClientandModule— this works because jsii only checks the public API surface, but these fields are inaccessible to language bindings. - Arrow functions as exported members: jsii requires all exported methods to be regular methods.
- Generic methods on exported classes: jsii does not support generics on classes.
as constassertions on enums: Use regular enums instead.- Dynamic imports /
require(): Not supported. - Mixed default/named exports: Use only named exports.
- Namespace merging or declaration merging: Not supported.
staticblocks in classes: Not supported.typeimports for values: jsii needs to see the value form.
Test File Handling
- Test files (
*.test.ts) are excluded from jsii viapackage.json:"jsii": { "excludeTypescript": ["**/*.test.ts"] } - The
tsconfig.jsonalso excludes**/*.test.ts.
jsii Build Targets
- Python:
pecans(module name), distributed via dist - .NET:
Pecansnamespace and package ID - Java target is not currently configured but jsii supports it
Work-in-Progress / Incomplete Areas
Empty Files
src/queue.ts— Empty. Appears to be planned for request queuing/rate-limiting but not implemented.src/legacy/index.ts— Empty. Planned for legacy API endpoints.
Unused Code
src/cache.ts— Defines a genericCache<T>class with TTL support, but no module currently uses it. TheClient._cacheproperty exists but is never read.src/client.tsreferences to#requestQueue— The batching system is implemented but uses a simpleQueuedCall[]array rather than theCacheclass.
Commented-Out Code
src/messages/types.tscontains large blocks of commented-out alternative implementations forMessageandMessagePreviewclasses (lines 78–229). These appear to be older iterations with rich client-aware objects (this.context = context).
TODOs in Source
src/forum/index.ts:8—fetchType: stringshould be an enum (// TODO: make this an enum)src/forum/types.ts:43—Thread.categoryis commented out (// TODO: add category)src/types/index.ts:41—// TODO: move this to another filereferring toTCUserandTCProfile
API Method Names Using Legacy Prefix
AskModulecalls"legacy.askapi"— this is the actual API method name, not a code smell, but worth noting since other modules use the standarddomain.methodpattern (e.g.,"messages.folderview","forum.viewposts").
Testing
Tests use Vitest and are colocated next to the source they test:
| Test File | Tests |
|---|---|
src/types/index.test.ts |
TCResponse serialization, poll option parsing |
src/ask/index.test.ts |
QuestionMetadata parsing, ListDataResponse, fetchQuestions |
src/notify/types.test.ts |
NotifySyncResponse parsing |
src/answer/types.test.ts |
AnswerReplyResponse and AnswerQueueQuestionResponse parsing |
Test files are excluded from jsii builds via excludeTypescript and tsconfig.json.
Test pattern: Construct response classes directly with a TCResponseRaw object, then assert properties:
const raw: TCResponseRaw = { ok: true, count: 6 };
const response = new NotifySyncResponse(raw);
expect(response.count).toBe(6);
Note: There are currently no integration/E2E tests — all tests are unit tests for type parsing. The npm test script still echoes "Error: no test specified" and needs to be updated to run vitest.
Build & Development Commands
| Command | Description |
|---|---|
npm run build |
Compile with jsii (jsii --strict) |
npm run dist |
Build + generate language packages (jsii-pacmak) |
npm run clean |
Remove dist/ and obj/ |
npx vitest run |
Run unit tests |
npx vitest |
Run tests in watch mode |
Build outputs:
obj/— Intermediate JavaScript + TypeScript declarationsdist/— Language-specific packages (Python wheel, .NET package, etc.).jsii— jsii assembly manifest (auto-generated)