-
Notifications
You must be signed in to change notification settings - Fork 1
chore: cover topic and named queue patterns in skills #7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,16 +3,16 @@ | |
| * Comparable to: Kafka, RabbitMQ, CQRS/Event Sourcing systems | ||
| * | ||
| * Demonstrates CQRS (Command Query Responsibility Segregation) with | ||
| * event sourcing. Commands publish domain events via pubsub. Multiple | ||
| * read model projections subscribe independently. PubSub handles all | ||
| * fan-out — both to projections and downstream notification consumers. | ||
| * event sourcing. Commands emit domain events via topic-based queues. | ||
| * Multiple read model projections consume topics independently. Named | ||
| * queues handle dedicated alert delivery workloads. | ||
| * | ||
| * How-to references: | ||
| * - Queues: https://iii.dev/docs/how-to/use-queues | ||
| * - State management: https://iii.dev/docs/how-to/manage-state | ||
| * - State reactions: https://iii.dev/docs/how-to/react-to-state-changes | ||
| * - HTTP endpoints: https://iii.dev/docs/how-to/expose-http-endpoint | ||
| * - PubSub: https://iii.dev/docs/how-to/use-functions-and-triggers | ||
| * - Functions/Triggers: https://iii.dev/docs/how-to/use-functions-and-triggers | ||
| */ | ||
|
|
||
| import { registerWorker, Logger, TriggerAction } from 'iii-sdk' | ||
|
|
@@ -48,8 +48,11 @@ iii.registerFunction({ id: 'cmd::add-inventory-item' }, async (data) => { | |
|
|
||
| await appendEvent('inventory', sku, event) | ||
|
|
||
| // Publish domain event for all projections to consume | ||
| iii.trigger({ function_id: 'publish', payload: { topic: 'inventory.item-added', data: event }, action: TriggerAction.Void() }) | ||
| // Emit domain event for all topic queue consumers to process | ||
| await iii.trigger({ | ||
| function_id: 'enqueue', | ||
| payload: { topic: 'inventory.item-added', data: event }, | ||
| }) | ||
|
|
||
| return { event: 'ItemAdded', sku } | ||
| }) | ||
|
|
@@ -76,7 +79,10 @@ iii.registerFunction({ id: 'cmd::sell-item' }, async (data) => { | |
|
|
||
| await appendEvent('inventory', sku, event) | ||
|
|
||
| iii.trigger({ function_id: 'publish', payload: { topic: 'inventory.item-sold', data: event }, action: TriggerAction.Void() }) | ||
| await iii.trigger({ | ||
| function_id: 'enqueue', | ||
| payload: { topic: 'inventory.item-sold', data: event }, | ||
| }) | ||
|
|
||
| return { event: 'ItemSold', sku, remaining: item.stock - quantity } | ||
| }) | ||
|
|
@@ -145,41 +151,42 @@ iii.registerFunction({ id: 'proj::sales-analytics' }, async (event) => { | |
| } }) | ||
| }) | ||
|
|
||
| // Projections subscribe to domain events independently via pubsub | ||
| iii.registerTrigger({ type: 'subscribe', function_id: 'proj::catalog-on-add', config: { topic: 'inventory.item-added' } }) | ||
| iii.registerTrigger({ type: 'subscribe', function_id: 'proj::catalog-on-sell', config: { topic: 'inventory.item-sold' } }) | ||
| iii.registerTrigger({ type: 'subscribe', function_id: 'proj::sales-analytics', config: { topic: 'inventory.item-sold' } }) | ||
| // Projections consume domain events independently via queue topic triggers | ||
| iii.registerTrigger({ type: 'queue', function_id: 'proj::catalog-on-add', config: { topic: 'inventory.item-added' } }) | ||
| iii.registerTrigger({ type: 'queue', function_id: 'proj::catalog-on-sell', config: { topic: 'inventory.item-sold' } }) | ||
| iii.registerTrigger({ type: 'queue', function_id: 'proj::sales-analytics', config: { topic: 'inventory.item-sold' } }) | ||
|
Comment on lines
+154
to
+157
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Low-stock alerts race the sell projection.
💡 Possible direction const event = {
type: 'ItemSold',
sku,
+ name: item.name,
quantity,
revenue: quantity * item.price,
+ remaining_stock: item.stock - quantity,
timestamp: new Date().toISOString(),
}
iii.registerFunction({ id: 'notify::low-stock-alert' }, async (event) => {
- const item = await iii.trigger({ function_id: 'state::get', payload: { scope: 'inventory-read', key: event.sku } })
- if (item && item.stock <= 5) {
+ if (event.remaining_stock <= 5) {
await iii.trigger({
function_id: 'notify::slack-low-stock',
- payload: { sku: event.sku, name: item.name, remaining: item.stock },
+ payload: { sku: event.sku, name: event.name, remaining: event.remaining_stock },
action: TriggerAction.Enqueue({ queue: 'alerts-notify' }),
})
}
})Also applies to: 162-177 🤖 Prompt for AI Agents |
||
|
|
||
| // =================================================================== | ||
| // FAN-OUT — PubSub notifications to downstream systems | ||
| // FAN-OUT — mixed queue patterns for downstream systems | ||
| // =================================================================== | ||
| iii.registerFunction({ id: 'notify::low-stock-alert' }, async (event) => { | ||
| const item = await iii.trigger({ function_id: 'state::get', payload: { scope: 'inventory-read', key: event.sku } }) | ||
| if (item && item.stock <= 5) { | ||
| iii.trigger({ function_id: 'publish', payload: { | ||
| topic: 'alerts.low-stock', | ||
| data: { sku: event.sku, name: item.name, remaining: item.stock }, | ||
| }, action: TriggerAction.Void() }) | ||
| await iii.trigger({ | ||
| function_id: 'notify::slack-low-stock', | ||
| payload: { sku: event.sku, name: item.name, remaining: item.stock }, | ||
| action: TriggerAction.Enqueue({ queue: 'alerts-notify' }), | ||
| }) | ||
| } | ||
| }) | ||
|
|
||
| iii.registerTrigger({ | ||
| type: 'subscribe', | ||
| type: 'queue', | ||
| function_id: 'notify::low-stock-alert', | ||
| config: { topic: 'inventory.item-sold' }, | ||
| }) | ||
|
|
||
| // Fan-out subscriber: could be a separate service listening for alerts | ||
| // Named queue worker: could be a separate service notifying Slack/email/pager | ||
| iii.registerFunction({ id: 'notify::slack-low-stock' }, async (data) => { | ||
| const logger = new Logger() | ||
| logger.warn('LOW STOCK ALERT', { sku: data.sku, remaining: data.remaining }) | ||
| // In production: POST to Slack webhook, send email, page oncall, etc. | ||
| }) | ||
|
|
||
| iii.registerTrigger({ | ||
| type: 'subscribe', | ||
| type: 'queue', | ||
| function_id: 'notify::slack-low-stock', | ||
| config: { topic: 'alerts.low-stock' }, | ||
| config: { queue: 'alerts-notify' }, | ||
| }) | ||
|
|
||
| // =================================================================== | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Event-log append and fan-out are not atomic.
appendEvent()completes before eachenqueuecall, so a failure here leavesevent-logahead of the projections. Because the query endpoints read projections rather thanevent-log, the system stays inconsistent until you replay manually. Consider persisting an outbox/replay marker with the append.Also applies to: 82-85
🤖 Prompt for AI Agents