@voyantjs/distribution 0.20.0 → 0.21.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/channel-push/admin-routes.d.ts +31 -0
- package/dist/channel-push/admin-routes.d.ts.map +1 -0
- package/dist/channel-push/admin-routes.js +165 -0
- package/dist/channel-push/availability-push.d.ts +76 -0
- package/dist/channel-push/availability-push.d.ts.map +1 -0
- package/dist/channel-push/availability-push.js +238 -0
- package/dist/channel-push/booking-push.d.ts +114 -0
- package/dist/channel-push/booking-push.d.ts.map +1 -0
- package/dist/channel-push/booking-push.js +503 -0
- package/dist/channel-push/content-push.d.ts +60 -0
- package/dist/channel-push/content-push.d.ts.map +1 -0
- package/dist/channel-push/content-push.js +256 -0
- package/dist/channel-push/index.d.ts +15 -0
- package/dist/channel-push/index.d.ts.map +1 -0
- package/dist/channel-push/index.js +18 -0
- package/dist/channel-push/plugin.d.ts +18 -0
- package/dist/channel-push/plugin.d.ts.map +1 -0
- package/dist/channel-push/plugin.js +21 -0
- package/dist/channel-push/reconciler.d.ts +85 -0
- package/dist/channel-push/reconciler.d.ts.map +1 -0
- package/dist/channel-push/reconciler.js +175 -0
- package/dist/channel-push/subscriber.d.ts +40 -0
- package/dist/channel-push/subscriber.d.ts.map +1 -0
- package/dist/channel-push/subscriber.js +174 -0
- package/dist/channel-push/types.d.ts +43 -0
- package/dist/channel-push/types.d.ts.map +1 -0
- package/dist/channel-push/types.js +32 -0
- package/dist/channel-push/workflows.d.ts +56 -0
- package/dist/channel-push/workflows.d.ts.map +1 -0
- package/dist/channel-push/workflows.js +100 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/rate-limit.d.ts +69 -0
- package/dist/rate-limit.d.ts.map +1 -0
- package/dist/rate-limit.js +135 -0
- package/dist/routes.d.ts +170 -10
- package/dist/routes.d.ts.map +1 -1
- package/dist/schema-core.d.ts +417 -1
- package/dist/schema-core.d.ts.map +1 -1
- package/dist/schema-core.js +98 -1
- package/dist/schema-push-intents.d.ts +387 -0
- package/dist/schema-push-intents.d.ts.map +1 -0
- package/dist/schema-push-intents.js +77 -0
- package/dist/schema.d.ts +1 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +1 -0
- package/dist/service.d.ts +103 -7
- package/dist/service.d.ts.map +1 -1
- package/dist/validation.d.ts +5 -5
- package/dist/webhook-deliveries.d.ts +86 -0
- package/dist/webhook-deliveries.d.ts.map +1 -0
- package/dist/webhook-deliveries.js +293 -0
- package/package.json +16 -8
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content-push pipeline.
|
|
3
|
+
*
|
|
4
|
+
* Triggered by `product.content.changed`. The subscriber upserts a
|
|
5
|
+
* `channel_content_push_intents` row per (channel, product); concurrent
|
|
6
|
+
* edits collapse to one row. The processor drains intents, hashes the
|
|
7
|
+
* current content, and skips when the hash equals
|
|
8
|
+
* `channel_product_mappings.last_pushed_content_hash` — channel-side
|
|
9
|
+
* idempotency per §6.1.
|
|
10
|
+
*
|
|
11
|
+
* Per docs/architecture/channel-push-architecture.md §6 + §12.3.
|
|
12
|
+
*/
|
|
13
|
+
import { AdapterRateLimitedError, } from "@voyantjs/catalog";
|
|
14
|
+
import { newId } from "@voyantjs/db/lib/typeid";
|
|
15
|
+
import { products } from "@voyantjs/products/schema";
|
|
16
|
+
import { and, asc, eq, sql } from "drizzle-orm";
|
|
17
|
+
import { acquireToken, channelScopeKey, drainBucket } from "../rate-limit.js";
|
|
18
|
+
import { channelContentPushIntents, channelProductMappings, channels } from "../schema.js";
|
|
19
|
+
import { prepareOutboundEnvelope } from "../webhook-deliveries.js";
|
|
20
|
+
import { defaultLogger, getChannelPushDepsOrThrow } from "./types.js";
|
|
21
|
+
/** Stable string identifier for the content-push workflow. */
|
|
22
|
+
export const CHANNEL_CONTENT_PUSH_WORKFLOW_ID = "channel.content.push";
|
|
23
|
+
/**
|
|
24
|
+
* Resolve the channels that want a content push for this product.
|
|
25
|
+
* Per §7.4 — content push uses `channel_product_mappings` (content is
|
|
26
|
+
* product-shaped, not slot-shaped).
|
|
27
|
+
*/
|
|
28
|
+
export async function resolveContentPushTargets(db, productId) {
|
|
29
|
+
const rows = (await db
|
|
30
|
+
.select({
|
|
31
|
+
mapping: channelProductMappings,
|
|
32
|
+
channel: channels,
|
|
33
|
+
})
|
|
34
|
+
.from(channelProductMappings)
|
|
35
|
+
.innerJoin(channels, eq(channelProductMappings.channelId, channels.id))
|
|
36
|
+
.where(and(eq(channelProductMappings.productId, productId), eq(channelProductMappings.active, true), eq(channelProductMappings.pushContent, true), eq(channels.status, "active"))));
|
|
37
|
+
return rows
|
|
38
|
+
.filter((row) => row.mapping.sourceConnectionId)
|
|
39
|
+
.map((row) => ({
|
|
40
|
+
channelId: row.channel.id,
|
|
41
|
+
sourceConnectionId: row.mapping.sourceConnectionId,
|
|
42
|
+
mapping: row.mapping,
|
|
43
|
+
channel: row.channel,
|
|
44
|
+
}));
|
|
45
|
+
}
|
|
46
|
+
export async function upsertContentIntent(db, input) {
|
|
47
|
+
await db
|
|
48
|
+
.insert(channelContentPushIntents)
|
|
49
|
+
.values({
|
|
50
|
+
id: newId("channel_content_push_intents"),
|
|
51
|
+
channelId: input.channelId,
|
|
52
|
+
sourceConnectionId: input.sourceConnectionId,
|
|
53
|
+
productId: input.productId,
|
|
54
|
+
})
|
|
55
|
+
.onConflictDoUpdate({
|
|
56
|
+
target: [channelContentPushIntents.channelId, channelContentPushIntents.productId],
|
|
57
|
+
set: {
|
|
58
|
+
requestedAt: new Date(),
|
|
59
|
+
updatedAt: new Date(),
|
|
60
|
+
attempts: 0,
|
|
61
|
+
lastError: null,
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Drain pending content intents. Hashes current product content and
|
|
67
|
+
* skips when the hash matches the upstream's last-known hash.
|
|
68
|
+
*
|
|
69
|
+
* v1 ships a minimal `content` payload (product row fields). Real
|
|
70
|
+
* verticals supply richer payloads via a future content provider hook.
|
|
71
|
+
*
|
|
72
|
+
* Per §6 + §12.3.
|
|
73
|
+
*/
|
|
74
|
+
export async function processContentPushIntents(input = {}, deps) {
|
|
75
|
+
const { db, registry, logger = defaultLogger } = deps ?? getChannelPushDepsOrThrow();
|
|
76
|
+
const limit = input.limit ?? 100;
|
|
77
|
+
const intents = (await db
|
|
78
|
+
.select({
|
|
79
|
+
intent: channelContentPushIntents,
|
|
80
|
+
channel: channels,
|
|
81
|
+
})
|
|
82
|
+
.from(channelContentPushIntents)
|
|
83
|
+
.innerJoin(channels, eq(channelContentPushIntents.channelId, channels.id))
|
|
84
|
+
.where(and(input.channelId ? eq(channelContentPushIntents.channelId, input.channelId) : sql `true`, eq(channels.status, "active")))
|
|
85
|
+
.orderBy(asc(channelContentPushIntents.requestedAt))
|
|
86
|
+
.limit(limit));
|
|
87
|
+
let succeeded = 0;
|
|
88
|
+
let failed = 0;
|
|
89
|
+
let skipped = 0;
|
|
90
|
+
for (const { intent, channel } of intents) {
|
|
91
|
+
const [product] = (await db
|
|
92
|
+
.select()
|
|
93
|
+
.from(products)
|
|
94
|
+
.where(eq(products.id, intent.productId))
|
|
95
|
+
.limit(1));
|
|
96
|
+
if (!product) {
|
|
97
|
+
await db.delete(channelContentPushIntents).where(eq(channelContentPushIntents.id, intent.id));
|
|
98
|
+
skipped += 1;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const adapter = registry.resolveByConnection(intent.sourceConnectionId);
|
|
102
|
+
if (!adapter?.capabilities.supportsContentPush || !adapter.pushContent) {
|
|
103
|
+
await stampIntentError(db, intent.id, intent.attempts + 1, adapter ? "adapter_unsupported" : "no_adapter_registered");
|
|
104
|
+
failed += 1;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const [mapping] = (await db
|
|
108
|
+
.select()
|
|
109
|
+
.from(channelProductMappings)
|
|
110
|
+
.where(and(eq(channelProductMappings.channelId, channel.id), eq(channelProductMappings.productId, intent.productId)))
|
|
111
|
+
.limit(1));
|
|
112
|
+
if (!mapping) {
|
|
113
|
+
await stampIntentError(db, intent.id, intent.attempts + 1, "no_mapping");
|
|
114
|
+
failed += 1;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
// Build the content payload + hash. v1 = product row fields. Future
|
|
118
|
+
// iterations call a per-vertical content provider so the payload
|
|
119
|
+
// mirrors `GetContentResult` (itinerary, media, options, …).
|
|
120
|
+
const content = buildMinimalContent(product);
|
|
121
|
+
const contentHash = canonicalHash(content);
|
|
122
|
+
// Idempotency: skip when the upstream's last-known hash equals
|
|
123
|
+
// the current hash. Per §6.1.
|
|
124
|
+
if (mapping.lastPushedContentHash === contentHash) {
|
|
125
|
+
await db.delete(channelContentPushIntents).where(eq(channelContentPushIntents.id, intent.id));
|
|
126
|
+
skipped += 1;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
const rlConfig = rateLimitConfigForChannel(channel);
|
|
130
|
+
if (rlConfig) {
|
|
131
|
+
const acq = await acquireToken(db, channelScopeKey(channel.id, intent.sourceConnectionId), rlConfig, "content");
|
|
132
|
+
if (!acq.acquired) {
|
|
133
|
+
await stampIntentError(db, intent.id, intent.attempts + 1, "rate_limited");
|
|
134
|
+
failed += 1;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const request = {
|
|
139
|
+
channelId: channel.id,
|
|
140
|
+
externalProductId: mapping.externalProductId ?? "",
|
|
141
|
+
productId: intent.productId,
|
|
142
|
+
contentHash,
|
|
143
|
+
content,
|
|
144
|
+
contentSchemaVersion: "products/v1",
|
|
145
|
+
};
|
|
146
|
+
const adapterCtx = {
|
|
147
|
+
connection_id: intent.sourceConnectionId,
|
|
148
|
+
};
|
|
149
|
+
const envelope = await prepareOutboundEnvelope(db, {
|
|
150
|
+
sourceModule: "distribution",
|
|
151
|
+
sourceEvent: "channel.content.push",
|
|
152
|
+
sourceEntityModule: "products",
|
|
153
|
+
sourceEntityId: intent.productId,
|
|
154
|
+
targetUrl: `adapter:${adapter.kind}`,
|
|
155
|
+
targetKind: `channel:${adapter.kind}`,
|
|
156
|
+
targetRef: channel.id,
|
|
157
|
+
requestMethod: "POST",
|
|
158
|
+
requestBody: request,
|
|
159
|
+
attemptNumber: intent.attempts + 1,
|
|
160
|
+
});
|
|
161
|
+
try {
|
|
162
|
+
const result = await adapter.pushContent(adapterCtx, request);
|
|
163
|
+
await envelope.complete({ responseStatus: 200, responseBody: result });
|
|
164
|
+
// Persist the acknowledged hash so subsequent pushes skip if
|
|
165
|
+
// content hasn't changed. Per §6.1.
|
|
166
|
+
await db
|
|
167
|
+
.update(channelProductMappings)
|
|
168
|
+
.set({
|
|
169
|
+
lastPushedContentHash: result.acknowledgedHash ?? contentHash,
|
|
170
|
+
lastPushedContentAt: new Date(),
|
|
171
|
+
updatedAt: new Date(),
|
|
172
|
+
})
|
|
173
|
+
.where(eq(channelProductMappings.id, mapping.id));
|
|
174
|
+
await db.delete(channelContentPushIntents).where(eq(channelContentPushIntents.id, intent.id));
|
|
175
|
+
succeeded += 1;
|
|
176
|
+
}
|
|
177
|
+
catch (err) {
|
|
178
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
179
|
+
const isRateLimited = err instanceof AdapterRateLimitedError;
|
|
180
|
+
if (isRateLimited) {
|
|
181
|
+
await drainBucket(db, channelScopeKey(channel.id, intent.sourceConnectionId), err.retryAfterMs);
|
|
182
|
+
}
|
|
183
|
+
await envelope.complete({
|
|
184
|
+
errorClass: isRateLimited ? "rate_limited" : "adapter_error",
|
|
185
|
+
errorMessage: message,
|
|
186
|
+
});
|
|
187
|
+
await stampIntentError(db, intent.id, intent.attempts + 1, message);
|
|
188
|
+
failed += 1;
|
|
189
|
+
logger.error?.(`pushContent failed for product ${intent.productId} channel ${channel.id}`, {
|
|
190
|
+
error: message,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
attempted: intents.length,
|
|
196
|
+
succeeded,
|
|
197
|
+
failed,
|
|
198
|
+
skipped,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
async function stampIntentError(db, id, attempts, message) {
|
|
202
|
+
await db
|
|
203
|
+
.update(channelContentPushIntents)
|
|
204
|
+
.set({ attempts, lastError: message, updatedAt: new Date() })
|
|
205
|
+
.where(eq(channelContentPushIntents.id, id));
|
|
206
|
+
}
|
|
207
|
+
function rateLimitConfigForChannel(channel) {
|
|
208
|
+
if (!channel.rateLimitRps || !channel.rateLimitBurst)
|
|
209
|
+
return null;
|
|
210
|
+
return {
|
|
211
|
+
rps: channel.rateLimitRps,
|
|
212
|
+
burst: channel.rateLimitBurst,
|
|
213
|
+
priorityGates: channel.rateLimitPriorityGates ?? undefined,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
function buildMinimalContent(product) {
|
|
217
|
+
// v1 minimal shape — enough for the demo upstream to acknowledge.
|
|
218
|
+
// Production wiring composes per-vertical content (itinerary, media,
|
|
219
|
+
// options, …) per the doc's `GetContentResult`.
|
|
220
|
+
return {
|
|
221
|
+
id: product.id,
|
|
222
|
+
name: product.name,
|
|
223
|
+
description: product.description ?? null,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Stable canonical-JSON hash. Mirrors the body-fingerprint behavior
|
|
228
|
+
* of `webhook-deliveries.ts` — purely a "is this the same content as
|
|
229
|
+
* before?" fingerprint, not a cryptographic hash.
|
|
230
|
+
*/
|
|
231
|
+
export function canonicalHash(value) {
|
|
232
|
+
let text;
|
|
233
|
+
try {
|
|
234
|
+
text = canonicalJson(value);
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
text = String(value);
|
|
238
|
+
}
|
|
239
|
+
let h = 0xcbf29ce484222325n;
|
|
240
|
+
const prime = 0x100000001b3n;
|
|
241
|
+
const mask = 0xffffffffffffffffn;
|
|
242
|
+
for (let i = 0; i < text.length; i++) {
|
|
243
|
+
h ^= BigInt(text.charCodeAt(i));
|
|
244
|
+
h = (h * prime) & mask;
|
|
245
|
+
}
|
|
246
|
+
return h.toString(16).padStart(16, "0");
|
|
247
|
+
}
|
|
248
|
+
function canonicalJson(value) {
|
|
249
|
+
if (value === null || typeof value !== "object")
|
|
250
|
+
return JSON.stringify(value);
|
|
251
|
+
if (Array.isArray(value))
|
|
252
|
+
return `[${value.map(canonicalJson).join(",")}]`;
|
|
253
|
+
const keys = Object.keys(value).sort();
|
|
254
|
+
const parts = keys.map((k) => `${JSON.stringify(k)}:${canonicalJson(value[k])}`);
|
|
255
|
+
return `{${parts.join(",")}}`;
|
|
256
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@voyantjs/distribution/channel-push` — outbound channel-push pipeline.
|
|
3
|
+
*
|
|
4
|
+
* Per docs/architecture/channel-push-architecture.md.
|
|
5
|
+
*/
|
|
6
|
+
export { type ChannelPushAdminRoutes, createChannelPushAdminRoutes, } from "./admin-routes.js";
|
|
7
|
+
export { CHANNEL_AVAILABILITY_PUSH_WORKFLOW_ID, type ProcessAvailabilityPushInput, type ProcessAvailabilityPushResult, processAvailabilityPushIntents, type ResolveAllotmentTargetsForSlotInput, resolveAllotmentTargetsForSlot, upsertAvailabilityIntent, } from "./availability-push.js";
|
|
8
|
+
export { bookingPushIdempotencyKey, CHANNEL_BOOKING_PUSH_WORKFLOW_ID, type ProcessBookingPushInput, type ProcessBookingPushResult, processBookingPush, resolveBookingPushTargets, upsertPendingBookingLinks, } from "./booking-push.js";
|
|
9
|
+
export { CHANNEL_CONTENT_PUSH_WORKFLOW_ID, canonicalHash, type ProcessContentPushInput, type ProcessContentPushResult, processContentPushIntents, resolveContentPushTargets, upsertContentIntent, } from "./content-push.js";
|
|
10
|
+
export { type ChannelPushPluginOptions, channelPushPlugin, } from "./plugin.js";
|
|
11
|
+
export { type AvailabilityReconcilerOptions, type BookingLinkReconcilerOptions, type ContentReconcilerOptions, type ReconcilerResult, reconcileAvailability, reconcileBookingLinks, reconcileContent, runAllReconcilers, } from "./reconciler.js";
|
|
12
|
+
export { type ChannelPushSubscribersOptions, createChannelPushSubscribers, triggerBookingPushForBooking, } from "./subscriber.js";
|
|
13
|
+
export { type ChannelPushDeps, type ChannelPushLogger, clearChannelPushDeps, defaultLogger, getChannelPushDeps, getChannelPushDepsOrThrow, setChannelPushDeps, } from "./types.js";
|
|
14
|
+
export { channelAvailabilityPushWorkflow, channelBookingPushWorkflow, channelContentPushWorkflow, } from "./workflows.js";
|
|
15
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/channel-push/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EACL,KAAK,sBAAsB,EAC3B,4BAA4B,GAC7B,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EACL,qCAAqC,EACrC,KAAK,4BAA4B,EACjC,KAAK,6BAA6B,EAClC,8BAA8B,EAC9B,KAAK,mCAAmC,EACxC,8BAA8B,EAC9B,wBAAwB,GACzB,MAAM,wBAAwB,CAAA;AAC/B,OAAO,EACL,yBAAyB,EACzB,gCAAgC,EAChC,KAAK,uBAAuB,EAC5B,KAAK,wBAAwB,EAC7B,kBAAkB,EAClB,yBAAyB,EACzB,yBAAyB,GAC1B,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EACL,gCAAgC,EAChC,aAAa,EACb,KAAK,uBAAuB,EAC5B,KAAK,wBAAwB,EAC7B,yBAAyB,EACzB,yBAAyB,EACzB,mBAAmB,GACpB,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EACL,KAAK,wBAAwB,EAC7B,iBAAiB,GAClB,MAAM,aAAa,CAAA;AACpB,OAAO,EACL,KAAK,6BAA6B,EAClC,KAAK,4BAA4B,EACjC,KAAK,wBAAwB,EAC7B,KAAK,gBAAgB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,gBAAgB,EAChB,iBAAiB,GAClB,MAAM,iBAAiB,CAAA;AACxB,OAAO,EACL,KAAK,6BAA6B,EAClC,4BAA4B,EAC5B,4BAA4B,GAC7B,MAAM,iBAAiB,CAAA;AACxB,OAAO,EACL,KAAK,eAAe,EACpB,KAAK,iBAAiB,EACtB,oBAAoB,EACpB,aAAa,EACb,kBAAkB,EAClB,yBAAyB,EACzB,kBAAkB,GACnB,MAAM,YAAY,CAAA;AAKnB,OAAO,EACL,+BAA+B,EAC/B,0BAA0B,EAC1B,0BAA0B,GAC3B,MAAM,gBAAgB,CAAA"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@voyantjs/distribution/channel-push` — outbound channel-push pipeline.
|
|
3
|
+
*
|
|
4
|
+
* Per docs/architecture/channel-push-architecture.md.
|
|
5
|
+
*/
|
|
6
|
+
export { createChannelPushAdminRoutes, } from "./admin-routes.js";
|
|
7
|
+
export { CHANNEL_AVAILABILITY_PUSH_WORKFLOW_ID, processAvailabilityPushIntents, resolveAllotmentTargetsForSlot, upsertAvailabilityIntent, } from "./availability-push.js";
|
|
8
|
+
export { bookingPushIdempotencyKey, CHANNEL_BOOKING_PUSH_WORKFLOW_ID, processBookingPush, resolveBookingPushTargets, upsertPendingBookingLinks, } from "./booking-push.js";
|
|
9
|
+
export { CHANNEL_CONTENT_PUSH_WORKFLOW_ID, canonicalHash, processContentPushIntents, resolveContentPushTargets, upsertContentIntent, } from "./content-push.js";
|
|
10
|
+
export { channelPushPlugin, } from "./plugin.js";
|
|
11
|
+
export { reconcileAvailability, reconcileBookingLinks, reconcileContent, runAllReconcilers, } from "./reconciler.js";
|
|
12
|
+
export { createChannelPushSubscribers, triggerBookingPushForBooking, } from "./subscriber.js";
|
|
13
|
+
export { clearChannelPushDeps, defaultLogger, getChannelPushDeps, getChannelPushDepsOrThrow, setChannelPushDeps, } from "./types.js";
|
|
14
|
+
// Importing this module registers all three durable workflows in the
|
|
15
|
+
// process-local @voyantjs/workflows registry. Hosts that don't run an
|
|
16
|
+
// orchestrator (e.g. the operator template's inline drain) can still
|
|
17
|
+
// import safely — registration is a no-op without a runtime to dispatch.
|
|
18
|
+
export { channelAvailabilityPushWorkflow, channelBookingPushWorkflow, channelContentPushWorkflow, } from "./workflows.js";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Channel-push core plugin.
|
|
3
|
+
*
|
|
4
|
+
* Bundles the EventBus subscribers that listen to `booking.confirmed`
|
|
5
|
+
* (and later `availability.slot.changed`, `product.content.changed`)
|
|
6
|
+
* and writes durable intent rows.
|
|
7
|
+
*
|
|
8
|
+
* Templates wire this via `registerPlugins([channelPushPlugin({ ... })],
|
|
9
|
+
* { eventBus })` AFTER calling `setChannelPushDeps({ db, registry })`.
|
|
10
|
+
*
|
|
11
|
+
* Per docs/architecture/channel-push-architecture.md §4 + §10 (Phase D).
|
|
12
|
+
*/
|
|
13
|
+
import { type Plugin } from "@voyantjs/core";
|
|
14
|
+
import { type ChannelPushSubscribersOptions } from "./subscriber.js";
|
|
15
|
+
export interface ChannelPushPluginOptions extends ChannelPushSubscribersOptions {
|
|
16
|
+
}
|
|
17
|
+
export declare function channelPushPlugin(options?: ChannelPushPluginOptions): Plugin;
|
|
18
|
+
//# sourceMappingURL=plugin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../src/channel-push/plugin.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAgB,KAAK,MAAM,EAAE,MAAM,gBAAgB,CAAA;AAE1D,OAAO,EAAE,KAAK,6BAA6B,EAAgC,MAAM,iBAAiB,CAAA;AAElG,MAAM,WAAW,wBAAyB,SAAQ,6BAA6B;CAAG;AAElF,wBAAgB,iBAAiB,CAAC,OAAO,GAAE,wBAA6B,GAAG,MAAM,CAMhF"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Channel-push core plugin.
|
|
3
|
+
*
|
|
4
|
+
* Bundles the EventBus subscribers that listen to `booking.confirmed`
|
|
5
|
+
* (and later `availability.slot.changed`, `product.content.changed`)
|
|
6
|
+
* and writes durable intent rows.
|
|
7
|
+
*
|
|
8
|
+
* Templates wire this via `registerPlugins([channelPushPlugin({ ... })],
|
|
9
|
+
* { eventBus })` AFTER calling `setChannelPushDeps({ db, registry })`.
|
|
10
|
+
*
|
|
11
|
+
* Per docs/architecture/channel-push-architecture.md §4 + §10 (Phase D).
|
|
12
|
+
*/
|
|
13
|
+
import { definePlugin } from "@voyantjs/core";
|
|
14
|
+
import { createChannelPushSubscribers } from "./subscriber.js";
|
|
15
|
+
export function channelPushPlugin(options = {}) {
|
|
16
|
+
return definePlugin({
|
|
17
|
+
name: "channel-push",
|
|
18
|
+
version: "0.1.0",
|
|
19
|
+
subscribers: createChannelPushSubscribers(options),
|
|
20
|
+
});
|
|
21
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Channel-push reconciler — self-healing catch-up for divergence.
|
|
3
|
+
*
|
|
4
|
+
* Eventually-consistent push works while the channel is reachable most
|
|
5
|
+
* of the time. After a long outage (or when an integration is first
|
|
6
|
+
* turned on for a channel that already has local state), the channel's
|
|
7
|
+
* view of our inventory diverges from ours. The reconciler closes that
|
|
8
|
+
* gap by re-reading current state from owned tables and recreating
|
|
9
|
+
* intent rows for divergent ones — same intent + worker pipeline, not
|
|
10
|
+
* a parallel push path.
|
|
11
|
+
*
|
|
12
|
+
* v1 cadences (per §13.2, tunable per channel):
|
|
13
|
+
* - Booking-link reconciler: every 15 min
|
|
14
|
+
* - Availability reconciler: hourly
|
|
15
|
+
* - Content reconciler: nightly
|
|
16
|
+
*
|
|
17
|
+
* Templates schedule these via cron / the workflows runtime; the
|
|
18
|
+
* functions themselves are plain async so they're testable.
|
|
19
|
+
*
|
|
20
|
+
* Per docs/architecture/channel-push-architecture.md §13.
|
|
21
|
+
*/
|
|
22
|
+
import { type ChannelPushDeps } from "./types.js";
|
|
23
|
+
export interface BookingLinkReconcilerOptions {
|
|
24
|
+
/**
|
|
25
|
+
* Re-process links whose `lastPushAt` is older than this many ms (or
|
|
26
|
+
* has never been pushed). Default 15 min, matching the v1 cadence.
|
|
27
|
+
*/
|
|
28
|
+
staleAfterMs?: number;
|
|
29
|
+
/** Max links to re-process per call. Default 200. */
|
|
30
|
+
limit?: number;
|
|
31
|
+
/** When set, scope the pass to one channel. */
|
|
32
|
+
channelId?: string;
|
|
33
|
+
}
|
|
34
|
+
export interface ReconcilerResult {
|
|
35
|
+
scanned: number;
|
|
36
|
+
triggered: number;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Walk `channel_booking_links` where push_status != 'ok' and reissue
|
|
40
|
+
* via `processBookingPush`. The processor is idempotent on
|
|
41
|
+
* `idempotency_key`, so reissuing succeeded-then-edited links never
|
|
42
|
+
* doubles upstream.
|
|
43
|
+
*
|
|
44
|
+
* Per §13.1.
|
|
45
|
+
*/
|
|
46
|
+
export declare function reconcileBookingLinks(options?: BookingLinkReconcilerOptions, deps?: ChannelPushDeps): Promise<ReconcilerResult>;
|
|
47
|
+
export interface AvailabilityReconcilerOptions {
|
|
48
|
+
/** Look-back window for recent slot changes. Default 1 hour. */
|
|
49
|
+
lookbackMs?: number;
|
|
50
|
+
/** Max slots to enqueue per pass. Default 500. */
|
|
51
|
+
limit?: number;
|
|
52
|
+
channelId?: string;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Walk recently-updated slots and ensure an intent row exists per
|
|
56
|
+
* (channel, slot) where the channel holds an active allotment. The
|
|
57
|
+
* worker's existing UNIQUE constraint collapses duplicates, so this is
|
|
58
|
+
* safe to re-run.
|
|
59
|
+
*
|
|
60
|
+
* Per §13.1 (availability reconciler).
|
|
61
|
+
*/
|
|
62
|
+
export declare function reconcileAvailability(options?: AvailabilityReconcilerOptions, deps?: ChannelPushDeps): Promise<ReconcilerResult>;
|
|
63
|
+
export interface ContentReconcilerOptions {
|
|
64
|
+
/** Max products to scan per pass. Default 200. */
|
|
65
|
+
limit?: number;
|
|
66
|
+
channelId?: string;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Walk syndicated products, hash current content, and recreate an
|
|
70
|
+
* intent row for every (channel, product) where the upstream's
|
|
71
|
+
* `last_pushed_content_hash` doesn't match. Per §13.1 (content
|
|
72
|
+
* reconciler) — content drift converges nightly.
|
|
73
|
+
*/
|
|
74
|
+
export declare function reconcileContent(options?: ContentReconcilerOptions, deps?: ChannelPushDeps): Promise<ReconcilerResult>;
|
|
75
|
+
/**
|
|
76
|
+
* Convenience: run all three reconcilers with default cadences.
|
|
77
|
+
* Templates can call this from a single nightly cron, or schedule
|
|
78
|
+
* each independently for finer control.
|
|
79
|
+
*/
|
|
80
|
+
export declare function runAllReconcilers(deps?: ChannelPushDeps): Promise<{
|
|
81
|
+
bookings: ReconcilerResult;
|
|
82
|
+
availability: ReconcilerResult;
|
|
83
|
+
content: ReconcilerResult;
|
|
84
|
+
}>;
|
|
85
|
+
//# sourceMappingURL=reconciler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reconciler.d.ts","sourceRoot":"","sources":["../../src/channel-push/reconciler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAeH,OAAO,EAAE,KAAK,eAAe,EAA4C,MAAM,YAAY,CAAA;AAE3F,MAAM,WAAW,4BAA4B;IAC3C;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,qDAAqD;IACrD,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,+CAA+C;IAC/C,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;CAClB;AAED;;;;;;;GAOG;AACH,wBAAsB,qBAAqB,CACzC,OAAO,GAAE,4BAAiC,EAC1C,IAAI,CAAC,EAAE,eAAe,GACrB,OAAO,CAAC,gBAAgB,CAAC,CAmC3B;AAED,MAAM,WAAW,6BAA6B;IAC5C,gEAAgE;IAChE,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,kDAAkD;IAClD,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED;;;;;;;GAOG;AACH,wBAAsB,qBAAqB,CACzC,OAAO,GAAE,6BAAkC,EAC3C,IAAI,CAAC,EAAE,eAAe,GACrB,OAAO,CAAC,gBAAgB,CAAC,CAkE3B;AAED,MAAM,WAAW,wBAAwB;IACvC,kDAAkD;IAClD,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED;;;;;GAKG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,GAAE,wBAA6B,EACtC,IAAI,CAAC,EAAE,eAAe,GACrB,OAAO,CAAC,gBAAgB,CAAC,CA4C3B;AAED;;;;GAIG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC;IACvE,QAAQ,EAAE,gBAAgB,CAAA;IAC1B,YAAY,EAAE,gBAAgB,CAAA;IAC9B,OAAO,EAAE,gBAAgB,CAAA;CAC1B,CAAC,CAOD"}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Channel-push reconciler — self-healing catch-up for divergence.
|
|
3
|
+
*
|
|
4
|
+
* Eventually-consistent push works while the channel is reachable most
|
|
5
|
+
* of the time. After a long outage (or when an integration is first
|
|
6
|
+
* turned on for a channel that already has local state), the channel's
|
|
7
|
+
* view of our inventory diverges from ours. The reconciler closes that
|
|
8
|
+
* gap by re-reading current state from owned tables and recreating
|
|
9
|
+
* intent rows for divergent ones — same intent + worker pipeline, not
|
|
10
|
+
* a parallel push path.
|
|
11
|
+
*
|
|
12
|
+
* v1 cadences (per §13.2, tunable per channel):
|
|
13
|
+
* - Booking-link reconciler: every 15 min
|
|
14
|
+
* - Availability reconciler: hourly
|
|
15
|
+
* - Content reconciler: nightly
|
|
16
|
+
*
|
|
17
|
+
* Templates schedule these via cron / the workflows runtime; the
|
|
18
|
+
* functions themselves are plain async so they're testable.
|
|
19
|
+
*
|
|
20
|
+
* Per docs/architecture/channel-push-architecture.md §13.
|
|
21
|
+
*/
|
|
22
|
+
import { availabilitySlots } from "@voyantjs/availability/schema";
|
|
23
|
+
import { products } from "@voyantjs/products/schema";
|
|
24
|
+
import { and, asc, eq, inArray, sql } from "drizzle-orm";
|
|
25
|
+
import { channelBookingLinks, channelInventoryAllotments, channelProductMappings, channels, } from "../schema.js";
|
|
26
|
+
import { upsertAvailabilityIntent } from "./availability-push.js";
|
|
27
|
+
import { processBookingPush } from "./booking-push.js";
|
|
28
|
+
import { canonicalHash, upsertContentIntent } from "./content-push.js";
|
|
29
|
+
import { defaultLogger, getChannelPushDepsOrThrow } from "./types.js";
|
|
30
|
+
/**
|
|
31
|
+
* Walk `channel_booking_links` where push_status != 'ok' and reissue
|
|
32
|
+
* via `processBookingPush`. The processor is idempotent on
|
|
33
|
+
* `idempotency_key`, so reissuing succeeded-then-edited links never
|
|
34
|
+
* doubles upstream.
|
|
35
|
+
*
|
|
36
|
+
* Per §13.1.
|
|
37
|
+
*/
|
|
38
|
+
export async function reconcileBookingLinks(options = {}, deps) {
|
|
39
|
+
const { db, logger = defaultLogger } = deps ?? getChannelPushDepsOrThrow();
|
|
40
|
+
const staleAfter = new Date(Date.now() - (options.staleAfterMs ?? 15 * 60 * 1000));
|
|
41
|
+
const limit = options.limit ?? 200;
|
|
42
|
+
const stale = (await db
|
|
43
|
+
.select({
|
|
44
|
+
bookingId: channelBookingLinks.bookingId,
|
|
45
|
+
})
|
|
46
|
+
.from(channelBookingLinks)
|
|
47
|
+
.where(and(sql `${channelBookingLinks.pushStatus} <> 'ok'`, sql `(${channelBookingLinks.lastPushAt} IS NULL OR ${channelBookingLinks.lastPushAt} < ${staleAfter})`, options.channelId ? eq(channelBookingLinks.channelId, options.channelId) : sql `true`))
|
|
48
|
+
.orderBy(asc(channelBookingLinks.lastPushAt))
|
|
49
|
+
.limit(limit));
|
|
50
|
+
if (stale.length === 0)
|
|
51
|
+
return { scanned: 0, triggered: 0 };
|
|
52
|
+
const uniqueBookings = Array.from(new Set(stale.map((r) => r.bookingId)));
|
|
53
|
+
let triggered = 0;
|
|
54
|
+
for (const bookingId of uniqueBookings) {
|
|
55
|
+
try {
|
|
56
|
+
const result = await processBookingPush({ bookingId }, deps);
|
|
57
|
+
if (result.attempted > 0)
|
|
58
|
+
triggered += 1;
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
logger.error?.(`reconcileBookingLinks: processBookingPush failed for ${bookingId}`, {
|
|
62
|
+
error: err instanceof Error ? err.message : String(err),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return { scanned: stale.length, triggered };
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Walk recently-updated slots and ensure an intent row exists per
|
|
70
|
+
* (channel, slot) where the channel holds an active allotment. The
|
|
71
|
+
* worker's existing UNIQUE constraint collapses duplicates, so this is
|
|
72
|
+
* safe to re-run.
|
|
73
|
+
*
|
|
74
|
+
* Per §13.1 (availability reconciler).
|
|
75
|
+
*/
|
|
76
|
+
export async function reconcileAvailability(options = {}, deps) {
|
|
77
|
+
const { db } = deps ?? getChannelPushDepsOrThrow();
|
|
78
|
+
const lookback = new Date(Date.now() - (options.lookbackMs ?? 60 * 60 * 1000));
|
|
79
|
+
const limit = options.limit ?? 500;
|
|
80
|
+
const slots = (await db
|
|
81
|
+
.select()
|
|
82
|
+
.from(availabilitySlots)
|
|
83
|
+
.where(sql `${availabilitySlots.updatedAt} > ${lookback}`)
|
|
84
|
+
.orderBy(asc(availabilitySlots.updatedAt))
|
|
85
|
+
.limit(limit));
|
|
86
|
+
if (slots.length === 0)
|
|
87
|
+
return { scanned: 0, triggered: 0 };
|
|
88
|
+
// For each slot, find channels with active allotments + matching
|
|
89
|
+
// product mappings, and upsert an intent row.
|
|
90
|
+
let triggered = 0;
|
|
91
|
+
const productIds = Array.from(new Set(slots.map((s) => s.productId)));
|
|
92
|
+
const allotments = (await db
|
|
93
|
+
.select({
|
|
94
|
+
allotment: channelInventoryAllotments,
|
|
95
|
+
mapping: channelProductMappings,
|
|
96
|
+
})
|
|
97
|
+
.from(channelInventoryAllotments)
|
|
98
|
+
.innerJoin(channelProductMappings, and(eq(channelProductMappings.channelId, channelInventoryAllotments.channelId), eq(channelProductMappings.productId, channelInventoryAllotments.productId)))
|
|
99
|
+
.innerJoin(channels, eq(channelInventoryAllotments.channelId, channels.id))
|
|
100
|
+
.where(and(inArray(channelInventoryAllotments.productId, productIds), eq(channelInventoryAllotments.active, true), eq(channelProductMappings.active, true), eq(channelProductMappings.pushAvailability, true), eq(channels.status, "active"), options.channelId ? eq(channelInventoryAllotments.channelId, options.channelId) : sql `true`)));
|
|
101
|
+
for (const slot of slots) {
|
|
102
|
+
for (const row of allotments) {
|
|
103
|
+
if (row.allotment.productId !== slot.productId)
|
|
104
|
+
continue;
|
|
105
|
+
// Allotment may be option-scoped; null option_id matches all.
|
|
106
|
+
if (row.allotment.optionId && row.allotment.optionId !== slot.optionId)
|
|
107
|
+
continue;
|
|
108
|
+
if (!row.mapping.sourceConnectionId)
|
|
109
|
+
continue;
|
|
110
|
+
await upsertAvailabilityIntent(db, {
|
|
111
|
+
channelId: row.mapping.channelId,
|
|
112
|
+
sourceConnectionId: row.mapping.sourceConnectionId,
|
|
113
|
+
slotId: slot.id,
|
|
114
|
+
productId: slot.productId,
|
|
115
|
+
optionId: slot.optionId ?? null,
|
|
116
|
+
startsAt: slot.startsAt,
|
|
117
|
+
});
|
|
118
|
+
triggered += 1;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return { scanned: slots.length, triggered };
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Walk syndicated products, hash current content, and recreate an
|
|
125
|
+
* intent row for every (channel, product) where the upstream's
|
|
126
|
+
* `last_pushed_content_hash` doesn't match. Per §13.1 (content
|
|
127
|
+
* reconciler) — content drift converges nightly.
|
|
128
|
+
*/
|
|
129
|
+
export async function reconcileContent(options = {}, deps) {
|
|
130
|
+
const { db } = deps ?? getChannelPushDepsOrThrow();
|
|
131
|
+
const limit = options.limit ?? 200;
|
|
132
|
+
const mappings = (await db
|
|
133
|
+
.select({
|
|
134
|
+
mapping: channelProductMappings,
|
|
135
|
+
product: products,
|
|
136
|
+
})
|
|
137
|
+
.from(channelProductMappings)
|
|
138
|
+
.innerJoin(products, eq(channelProductMappings.productId, products.id))
|
|
139
|
+
.innerJoin(channels, eq(channelProductMappings.channelId, channels.id))
|
|
140
|
+
.where(and(eq(channelProductMappings.active, true), eq(channelProductMappings.pushContent, true), eq(channels.status, "active"), options.channelId ? eq(channelProductMappings.channelId, options.channelId) : sql `true`))
|
|
141
|
+
.limit(limit));
|
|
142
|
+
let triggered = 0;
|
|
143
|
+
for (const { mapping, product } of mappings) {
|
|
144
|
+
if (!mapping.sourceConnectionId)
|
|
145
|
+
continue;
|
|
146
|
+
const minimalContent = {
|
|
147
|
+
id: product.id,
|
|
148
|
+
name: product.name,
|
|
149
|
+
description: product.description ?? null,
|
|
150
|
+
};
|
|
151
|
+
const currentHash = canonicalHash(minimalContent);
|
|
152
|
+
if (mapping.lastPushedContentHash === currentHash)
|
|
153
|
+
continue;
|
|
154
|
+
await upsertContentIntent(db, {
|
|
155
|
+
channelId: mapping.channelId,
|
|
156
|
+
sourceConnectionId: mapping.sourceConnectionId,
|
|
157
|
+
productId: mapping.productId,
|
|
158
|
+
});
|
|
159
|
+
triggered += 1;
|
|
160
|
+
}
|
|
161
|
+
return { scanned: mappings.length, triggered };
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Convenience: run all three reconcilers with default cadences.
|
|
165
|
+
* Templates can call this from a single nightly cron, or schedule
|
|
166
|
+
* each independently for finer control.
|
|
167
|
+
*/
|
|
168
|
+
export async function runAllReconcilers(deps) {
|
|
169
|
+
const [bookingsResult, availability, content] = await Promise.all([
|
|
170
|
+
reconcileBookingLinks({}, deps),
|
|
171
|
+
reconcileAvailability({}, deps),
|
|
172
|
+
reconcileContent({}, deps),
|
|
173
|
+
]);
|
|
174
|
+
return { bookings: bookingsResult, availability, content };
|
|
175
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Channel-push EventBus subscribers.
|
|
3
|
+
*
|
|
4
|
+
* The `booking.confirmed` subscriber writes durable intent rows to
|
|
5
|
+
* `channel_booking_links` and returns immediately — per §4.5 the
|
|
6
|
+
* subscriber MUST NOT do HTTP work because the EventBus is in-process
|
|
7
|
+
* and sequential. The intent rows are drained by `processBookingPush`
|
|
8
|
+
* (called inline in v1, or by the durable workflow in production).
|
|
9
|
+
*
|
|
10
|
+
* Per docs/architecture/channel-push-architecture.md §4.1.
|
|
11
|
+
*/
|
|
12
|
+
import type { Subscriber } from "@voyantjs/core";
|
|
13
|
+
import { type ChannelPushDeps } from "./types.js";
|
|
14
|
+
export interface ChannelPushSubscribersOptions {
|
|
15
|
+
/**
|
|
16
|
+
* When `true` (default), the subscriber drains intent rows inline
|
|
17
|
+
* after writing them. Useful for dev / single-process templates.
|
|
18
|
+
* Production deployments with the workflow runtime wired set this
|
|
19
|
+
* to `false` and rely on `channel.booking.push` to drain.
|
|
20
|
+
*/
|
|
21
|
+
drainInline?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Optional explicit deps. Falls back to `getChannelPushDeps()`.
|
|
24
|
+
* Tests pass deps directly so they don't have to wire the global.
|
|
25
|
+
*/
|
|
26
|
+
deps?: ChannelPushDeps;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Construct the channel-push subscriber bundle. Templates pass these to
|
|
30
|
+
* `registerPlugins({ subscribers })` or attach them to an EventBus
|
|
31
|
+
* directly via `eventBus.subscribe`.
|
|
32
|
+
*/
|
|
33
|
+
export declare function createChannelPushSubscribers(options?: ChannelPushSubscribersOptions): Subscriber[];
|
|
34
|
+
/**
|
|
35
|
+
* Trigger the booking-push pipeline for an arbitrary booking id —
|
|
36
|
+
* useful for the operator dashboard's "retry sync" button and for the
|
|
37
|
+
* reconciler (Phase G).
|
|
38
|
+
*/
|
|
39
|
+
export declare function triggerBookingPushForBooking(bookingId: string): Promise<void>;
|
|
40
|
+
//# sourceMappingURL=subscriber.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"subscriber.d.ts","sourceRoot":"","sources":["../../src/channel-push/subscriber.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAiB,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAiB/D,OAAO,EACL,KAAK,eAAe,EAIrB,MAAM,YAAY,CAAA;AAmDnB,MAAM,WAAW,6BAA6B;IAC5C;;;;;OAKG;IACH,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB;;;OAGG;IACH,IAAI,CAAC,EAAE,eAAe,CAAA;CACvB;AAED;;;;GAIG;AACH,wBAAgB,4BAA4B,CAC1C,OAAO,GAAE,6BAAkC,GAC1C,UAAU,EAAE,CAwHd;AAED;;;;GAIG;AACH,wBAAsB,4BAA4B,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAMnF"}
|