Consuming the revision feed

Use the revision feed when an integration needs to refresh caches, update a UI, or observe changes made by another tool sharing the same production file. Every committed transaction becomes one revision with a production-local sequence number and an ordered list of semantic events. Store the last fully processed sequence as the local cursor.

Polling safely

  1. Request the revisions after the cursor with a bounded page size.

  2. Process revisions in returned order.

  3. Load each revision’s events in position order.

  4. Re-query objects needed by the integration.

  5. Advance the local cursor only after the whole revision is processed.

  6. Repeat until a page is shorter than the requested limit.

static pp_error_code_t process_changes(const pp_production_t *production,
                                       uint64_t *cursor, pp_error_t **error) {
  const uint32_t limit = 100;
  pp_error_code_t status = PP_OK;
  uint64_t page_size = limit;

  while (status == PP_OK && page_size == limit) {
    pp_revision_set_t *page = NULL;
    status = pp_production_changes_since(production, *cursor, limit, &page,
                                         error);
    page_size = status == PP_OK ? pp_revision_set_count(page) : 0;
    for (uint64_t i = 0; status == PP_OK && i < page_size; ++i) {
      pp_uuid_t revision_id;
      pp_uuid_t transaction_id;
      uint64_t sequence = 0;
      int64_t committed_at = 0;
      const char *origin_name, *origin_version, *origin_uri, *message;
      pp_revision_event_set_t *events = NULL;
      status = pp_revision_set_get(page, i, &revision_id, &sequence,
                                   &transaction_id, &committed_at,
                                   &origin_name, &origin_version, &origin_uri,
                                   &message, error);
      if (status == PP_OK) {
        status = pp_production_revision_events(production, &revision_id,
                                               &events, error);
      }
      for (uint64_t j = 0;
           status == PP_OK && j < pp_revision_event_set_count(events); ++j) {
        pp_revision_event_t event;
        status = pp_revision_event_set_get(events, j, &event, error);
        if (status == PP_OK) {
          /* Switch on event.kind; unused fields are zero or NULL. */
          handle_event(&event);
        }
      }
      pp_revision_event_set_release(events);
      if (status == PP_OK) {
        /* Persist the cursor only after the whole revision is processed. */
        *cursor = sequence;
      }
    }
    pp_revision_set_release(page);
  }
  return status;
}
std::uint64_t process_changes(const postproject::Production &production,
                              std::uint64_t cursor) {
  constexpr std::uint32_t limit = 100;
  for (;;) {
    const auto page = production.changesSince(cursor, limit);
    for (const auto &revision : page) {
      for (const auto &event : production.revisionEvents(revision.id)) {
        // Dispatch with std::visit on event.payload.
        handle_event(event);
      }
      // Persist the cursor only after the whole revision is processed.
      cursor = revision.sequence;
    }
    if (page.size() < limit) {
      return cursor;
    }
  }
}
def process_changes(production: Production, cursor: int) -> int:
    limit = 100
    while True:
        page = production.changes_since(cursor, limit)
        for revision in page:
            for event in production.revision_events[revision.id]:
                # Dispatch with isinstance on event.payload.
                handle_event(event)
            # Persist the cursor only after the whole revision is processed.
            cursor = revision.sequence
        if len(page) < limit:
            return cursor
fn process_changes(production: &SqliteProduction, mut cursor: u64) -> Result<u64> {
    loop {
        let page = production.changes_since(cursor, 100)?;
        for revision in &page {
            for event in production.events_for_revision(revision.id())? {
                handle_event(&event);
            }
            // Persist the cursor only after the whole revision is processed.
            cursor = revision.sequence();
        }
        if page.len() < 100 {
            return Ok(cursor);
        }
    }
}
CURSOR=0
while :; do
  PAGE=$(postproject --json revisions since production.pproj \
    --after "$CURSOR" --limit 100)
  for REVISION_ID in $(jq -r '.[].id' <<<"$PAGE"); do
    postproject --json revisions events production.pproj "$REVISION_ID"
    # Persist the cursor only after the whole revision is processed.
    CURSOR=$(jq -r --arg id "$REVISION_ID" '.[] | select(.id == $id) | .sequence' <<<"$PAGE")
  done
  [[ $(jq length <<<"$PAGE") -lt 100 ]] && break
done

Persisting the cursor after each complete revision gives at-least-once processing after a consumer crash. Handlers should therefore tolerate seeing a revision again. A cursor is meaningful only for the production that produced it.

Events

Each event has a position within its revision and a typed payload, such as an imported asset, an added representation or locator, an added or removed external identifier or metadata property, or a created activity with its edges. The surfaces expose the same payloads idiomatically:

  • C returns a tagged pp_revision_event_t record; fields unused by an event kind are zero or NULL, and strings borrow the event set.

  • C++ converts the record into a std::variant of event structs.

  • Python returns frozen typed values such as AssetImportedEvent, suitable for isinstance dispatch.

  • Rust returns RevisionEventKind enum values.

  • The CLI prints events as JSON with --json.

Where the surface offers typed payloads, dispatch on the payload type rather than on the numeric C event kinds.

Attributing changes

A transaction may carry an optional revision context: an origin identity (name, version, and URI of the integrating tool) and a short message. Set it before committing, as shown in create a production and import media, so other consumers of the feed can tell which tool made a change. CLI mutations use the postproject-cli origin with the package version and a short operation message.