newOS for Developers

Post Actions

Complete API for creating, reading, uploading, and rating posts — the core content units in newOS.

packages/newgraph-signals/src/actions/post.ts (613 lines)

Quick Reference

CRUD Operations

  • readPost — Fetch post by ID
  • createPostSingle — Create with file
  • createPostMultiple — Batch create
  • deletePost — Delete single
  • deletePosts — Delete multiple

Rating / Voting

  • rate — Vote on a post
  • massRate — Vote on multiple

Caching

  • cachePosts — Store to IndexedDB
  • cachePostsBatchAsync — Batched caching
  • postsQuery — Cache query

Utils

  • getRemoteMeta — Fetch URL metadata

readPost

CRUD

Fetch a post by ID. Returns cached data first, then fetches from API.

readPost({
  id: string;  // Required - Post ID
}): ProgressiveHandlerResponse<EnrichedPost>
ParameterTypeRequiredDescription
idstringYesPost ID

API Endpoint

api.post.postList({ id })

Nuances

  • Auto-starts on call
  • Skips API call if cached contentUrl is "processing"
  • Caches result to IndexedDB
  • Returns early if progress.done is already true

Usage

const [post, progress] = readPost({ id: "abc123" });
await progress.value.promise;
console.log(post.value); // EnrichedPost

createPostSingle

CRUD Upload

Create a post with optional file upload. Handles the full S3 upload flow.

createPostSingle(): ProgressiveHandlerResponse<PostReadResponse>

// Execute with:
progress.exec({
  id?: string;               // Optional - Pre-set post ID
  content?: string;          // Text content
  contentType?: string;      // MIME type
  moodId?: string;           // Primary folder ID
  file?: {                   // File to upload
    name: string;
    type: string;
    originFileObj: File;
    preview?: string;        // Base64 thumbnail
  };
  foldersToAttach?: string[]; // Folder IDs to attach to
})

Upload Flow

  1. preparing — Create provisional post with UUID, generate thumbnail
  2. created — Call api.post.postCreate()
  3. attached — Attach to folders via attachToFolders()
  4. upload-requested — Request presigned S3 URL via api.post.uploadCreate()
  5. uploaded — PUT file to S3 presigned URL

API Endpoints

api.post.postCreate(postForm)
api.post.uploadCreate({ filename, targetId, contentType })
PUT {presignedUrl} — Direct S3 upload

Nuances

  • Generates base64 thumbnail via resizeImage()
  • Uses retry() wrapper for resilience on all API calls
  • Manages upload queue via uploadQueueSignal
  • Post must have content OR file (not both optional)
  • Generates UUID via uuidv4() for provisional post
  • Sets contentUrl: "preparing" during upload
  • Uses wait(3) delays between steps for API stability

Provisional Post Structure

{
  id: "uuid-generated",
  thumbUrl: file.preview,
  contentUrl: base64thumb,
  label: "post",
  author: currentUser,
  updated: now,
  created: now,
  contentType: file.type || "text/plain",
  uploadState: {
    blob: file.originFileObj,
    filename: file.name,
    thumb: base64thumb,
    done: false,
    status: "preparing"
  }
}

createPostMultiple

CRUD Upload

Create multiple posts at once with content distribution options.

createPostMultiple(): ProgressiveHandlerResponse<UploadProgressEntry[]>

// Execute with:
progress.exec({
  files: File[];             // Files to upload
  content?: string;          // Text content
  contentMode: "last" | "first" | "each";  // Where to add content
  foldersToAttach?: string[];
})

Content Modes

  • last — Add content to last file only
  • first — Add content to first file only
  • each — Add content to every file

Nuances

  • Queues uploads via uploadQueueSignal
  • Each file creates an UploadProgressEntry
  • Uses createPostSingle uploader internally
  • Returns done: false immediately while queue processes

deletePost

CRUD

Delete a post. Handles both prepared and committed posts.

deletePost(): ProgressiveHandlerResponse<null>

// Execute with:
progress.exec({ id: string })

API Endpoint

api.post.postDelete({ id })

Nuances

  • If post contentUrl is "preparing", just deletes from cache (not committed yet)
  • Otherwise calls API then marks as deleted: true in cache

deletePosts

CRUD

Delete multiple posts at once.

deletePosts(): ProgressiveHandlerResponse<null>

// Execute with:
progress.exec({ ids: string[] })

Nuances

  • Uses forEach with async (not properly awaited — potential race condition)
  • Same logic as deletePost for each ID

rate

Voting

Vote/rate a post. Uses a queue to prevent concurrent rating calls.

rate({
  targetId: string;  // Post ID
}): ProgressiveHandlerResponse<RatingUpdateResponse>

// Execute with:
progress.exec({
  targetId: string;
  value: number;       // Rating value (e.g., 0-100)
  contextType?: string;
  contextValue?: string;
})
ParameterTypeRequiredDescription
targetIdstringYesPost ID to rate
valuenumberYesRating value
contextTypestringNoContext identifier type
contextValuestringNoContext identifier value

API Endpoint

api.post.rateCreate({ targetId, value })

Queue Mechanism

Uses rateQueue array and rateConcurrencyLock to ensure only one rating call executes at a time. Calls are queued and processed sequentially via processRateQueue().

Nuances

  • Updates cache with vote field after successful API call
  • Returns immediately, actual rating happens async in queue

massRate

Voting

Vote/rate multiple posts with the same value.

massRate({
  targetIds: string[];  // Post IDs
}): ProgressiveHandlerResponse<RatingUpdateResponse>

// Execute with:
progress.exec({
  targetIds: string[];
  value: number;
})

Nuances

  • Adds each post to the rating queue
  • All posts rated with same value
  • Uses same queue mechanism as rate

cachePosts

Caching

Store posts to IndexedDB with optional folder attachment.

cachePosts(
  posts: PostReadResponse | EnrichedPost | PostReadResponse[] | EnrichedPost[],
  folders?: MoodReadResponse[],
  relName?: string | string[],  // Edge label (default: "attachment")
  opts?: {
    appendProps?: (keyof EnrichedPost)[];    // Append to existing values
    ignoreShorter?: (keyof EnrichedPost)[];  // Keep longer cached values
  }
): Promise<void>
ParameterTypeDescription
postsPost[]Post(s) to cache
foldersMood[]Additional folders to create edges for
relNamestringEdge label (default: "attachment")
opts.appendPropsstring[]Concatenate string values instead of replacing
opts.ignoreShorterstring[]Keep cached value if incoming is shorter

Edge Creation

Creates edges for all folders in: folders param + post.moods + post.moodId

Nuances

  • Skips caching if incoming updated is older than cached
  • Parses aiMeta from JSON string if it's a string
  • Clears uploadState once contentUrl is set and not base64
  • Sets contentUrl to empty string if it was "processing"
  • Uses bulkPutDelayed for batch efficiency
  • Skips "boring" posts (only id + label)

cachePostsBatchAsync

Caching

Batched version of cachePosts for high-throughput scenarios.

cachePostsBatchAsync: BatchedFunction
// Batches: 100 items, 10s max retention

postsQuery

Cache Query

Query posts from IndexedDB cache.

postsQuery(id: string | string[]): Promise<EnrichedPost[]>

Implementation

cache.post.where("id").anyOf(ids).toArray()

getRemoteMeta

Utility

Fetch metadata from a remote URL (Open Graph, title, etc).

getRemoteMeta({ url: string }): Promise<PostRemoteMetaProxyResponse>

API Endpoint

api.post.utilsRemoteMetaProxyList({ url })

Use Case

Used for link previews when pasting URLs into posts. Extracts Open Graph metadata, page titles, and thumbnails.

Key Types

EnrichedPost

type EnrichedPost = PostReadResponse & {
  uploadState?: PostUploadState;
  thumbUrl?: string;
  aiMeta?: AIMeta;
  reasoning?: string[];
  relativeRating?: number;  // Computed relative to folder
}

PostUploadState

type PostUploadState = {
  blob?: Blob;
  filename?: string;
  thumb?: string;        // Base64 thumbnail
  done: boolean;
  status: "preparing" | "created" | "attached" | 
          "upload-requested" | "uploaded";
}

PostMaybeWithThumb

type PostMaybeWithThumb = PostReadResponse & {
  thumbUrl?: string;
  label?: string;
  uploadState: PostUploadState;
}

UploadProgressEntry

type UploadProgressEntry = {
  localId: string;
  file: File;
  contentUrl: string;
  content: string;
  progress: string;
  foldersToAttach: string[];
}

URL Construction

Read post:GET /api/post/{ id }
Create post:POST /api/post
Request upload URL:POST /api/post/upload
Rate post:POST /api/post/rate
Delete:DELETE /api/post/{ id }
Remote meta:GET /api/post/utils/remote-meta-proxy?url={url}

Upload State Sequence

preparingcreatedattachedupload-requesteduploaded

State Details

preparingProvisional post created in cache with UUID and base64 thumbnail
createdAPI call completed, post exists on server
attachedPost attached to specified folders
upload-requestedPresigned S3 URL obtained
uploadedFile successfully PUT to S3, processing begins server-side

Upload Queue Management

The upload system uses uploadQueueSignal to manage concurrent uploads:

  • New uploads are added to the queue
  • Queue processes one upload at a time
  • After each upload, doNextJob() shifts to next item
  • Semaphore prevents duplicate execution

Edge Cases & Gotchas

Processing state — When contentUrl === "processing", the post is waiting for server-side media processing. readPost skips API calls for these.
Preparing deletion — Posts with contentUrl === "preparing" are local-only and can be deleted from cache without API call.
aiMeta parsing — The aiMeta field may come as a JSON string from the API. cachePosts automatically parses it.
Rating queue — Ratings are processed sequentially to avoid race conditions. The response signal updates after actual API call, not immediately.
File type detection — If file has no type, createPostSingle throws an error. Always ensure file.type is set.