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¶
Request the revisions after the cursor with a bounded page size.
Process revisions in returned order.
Load each revision’s events in position order.
Re-query objects needed by the integration.
Advance the local cursor only after the whole revision is processed.
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_trecord; fields unused by an event kind are zero orNULL, and strings borrow the event set.C++ converts the record into a
std::variantof event structs.Python returns frozen typed values such as
AssetImportedEvent, suitable forisinstancedispatch.Rust returns
RevisionEventKindenum 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.