@propertysimple/client 0.1.0
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/API.md +79 -0
- package/README.md +348 -0
- package/client/src/directory-sync.d.ts +123 -0
- package/client/src/directory-sync.js +404 -0
- package/client/src/index.d.ts +5123 -0
- package/client/src/index.js +285 -0
- package/client/src/managed-sync.d.ts +129 -0
- package/client/src/managed-sync.js +410 -0
- package/contracts/src/catalog-derived.d.ts +7 -0
- package/contracts/src/catalog-derived.js +78 -0
- package/contracts/src/catalog-fields.d.ts +231 -0
- package/contracts/src/catalog-fields.js +194 -0
- package/contracts/src/catalog-inventory.d.ts +14 -0
- package/contracts/src/catalog-inventory.js +10 -0
- package/contracts/src/directory-execution.d.ts +320 -0
- package/contracts/src/directory-execution.js +122 -0
- package/contracts/src/directory-identity.d.ts +1255 -0
- package/contracts/src/directory-identity.js +129 -0
- package/contracts/src/directory-sales.d.ts +333 -0
- package/contracts/src/directory-sales.js +102 -0
- package/contracts/src/directory.d.ts +1088 -0
- package/contracts/src/directory.js +179 -0
- package/contracts/src/index.d.ts +18 -0
- package/contracts/src/index.js +18 -0
- package/contracts/src/intelligence.d.ts +366 -0
- package/contracts/src/intelligence.js +157 -0
- package/contracts/src/location-serving.d.ts +13 -0
- package/contracts/src/location-serving.js +89 -0
- package/contracts/src/location.d.ts +73 -0
- package/contracts/src/location.js +75 -0
- package/contracts/src/openapi.d.ts +22 -0
- package/contracts/src/openapi.js +109 -0
- package/contracts/src/operations.d.ts +8316 -0
- package/contracts/src/operations.js +56 -0
- package/contracts/src/opportunity-events.d.ts +38 -0
- package/contracts/src/opportunity-events.js +69 -0
- package/contracts/src/platform-api.d.ts +12 -0
- package/contracts/src/platform-api.js +1 -0
- package/contracts/src/professional-intelligence.d.ts +256 -0
- package/contracts/src/professional-intelligence.js +90 -0
- package/contracts/src/property-features.d.ts +94 -0
- package/contracts/src/property-features.js +398 -0
- package/contracts/src/publication.d.ts +44 -0
- package/contracts/src/publication.js +42 -0
- package/contracts/src/routes.d.ts +437 -0
- package/contracts/src/routes.js +496 -0
- package/contracts/src/schemas.d.ts +3198 -0
- package/contracts/src/schemas.js +674 -0
- package/contracts/src/search.d.ts +2630 -0
- package/contracts/src/search.js +223 -0
- package/contracts/src/smart-feed.d.ts +3731 -0
- package/contracts/src/smart-feed.js +109 -0
- package/openapi.json +43542 -0
- package/package.json +60 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { operations, opportunityObservation, ProblemSchema, routes, } from '../../contracts/src/index.js';
|
|
2
|
+
export const DEFAULT_BASE_URL = 'https://api.propertysimple.ai';
|
|
3
|
+
export class ApiError extends Error {
|
|
4
|
+
status;
|
|
5
|
+
code;
|
|
6
|
+
problem;
|
|
7
|
+
constructor(status, code, message, problem) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.status = status;
|
|
10
|
+
this.code = code;
|
|
11
|
+
this.problem = problem;
|
|
12
|
+
this.name = 'ApiError';
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export class ContractError extends Error {
|
|
16
|
+
operation;
|
|
17
|
+
constructor(message, operation) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.operation = operation;
|
|
20
|
+
this.name = 'ContractError';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function sleep(ms, signal) {
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
if (signal?.aborted) {
|
|
26
|
+
reject(signal.reason);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const abort = () => {
|
|
30
|
+
clearTimeout(timer);
|
|
31
|
+
reject(signal.reason);
|
|
32
|
+
};
|
|
33
|
+
const timer = setTimeout(() => {
|
|
34
|
+
signal?.removeEventListener('abort', abort);
|
|
35
|
+
resolve();
|
|
36
|
+
}, ms);
|
|
37
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
/** Each instance owns its credential; there is no mutable process-global tenant configuration. */
|
|
41
|
+
export class ListingsClient {
|
|
42
|
+
options;
|
|
43
|
+
constructor(options) {
|
|
44
|
+
if (typeof window !== 'undefined')
|
|
45
|
+
throw new Error('Service credentials belong on the product backend, not in browser bundles.');
|
|
46
|
+
const base = new URL(options.baseUrl ?? DEFAULT_BASE_URL);
|
|
47
|
+
if (base.username || base.password || base.search || base.hash)
|
|
48
|
+
throw new Error('baseUrl must not contain credentials, query parameters or fragments.');
|
|
49
|
+
if (base.protocol !== 'https:' &&
|
|
50
|
+
!(base.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(base.hostname)))
|
|
51
|
+
throw new Error('Use HTTPS except for a loopback development server.');
|
|
52
|
+
if (!options.apiKey || /[\r\n]/.test(options.apiKey))
|
|
53
|
+
throw new Error('A valid service API key is required.');
|
|
54
|
+
this.options = {
|
|
55
|
+
...options,
|
|
56
|
+
baseUrl: base.toString(),
|
|
57
|
+
fetch: options.fetch ?? fetch,
|
|
58
|
+
timeoutMs: options.timeoutMs ?? 15000,
|
|
59
|
+
maxRetries: options.maxRetries ?? 2,
|
|
60
|
+
};
|
|
61
|
+
if (!Number.isSafeInteger(this.options.timeoutMs) ||
|
|
62
|
+
this.options.timeoutMs < 1 ||
|
|
63
|
+
!Number.isInteger(this.options.maxRetries) ||
|
|
64
|
+
this.options.maxRetries < 0 ||
|
|
65
|
+
this.options.maxRetries > 5)
|
|
66
|
+
throw new Error('Invalid timeout or retry limit.');
|
|
67
|
+
}
|
|
68
|
+
async call(operation, input, options = {}) {
|
|
69
|
+
const route = routes.find((candidate) => candidate.operation === operation);
|
|
70
|
+
const parsed = operations[operation].input.safeParse(input);
|
|
71
|
+
if (!parsed.success)
|
|
72
|
+
throw new ContractError(`Invalid input for ${operation}: ${parsed.error.issues.map((issue) => issue.path.join('.')).join(', ')}`, operation);
|
|
73
|
+
const values = { ...parsed.data };
|
|
74
|
+
let path = route.path;
|
|
75
|
+
path = path.replace(/\{([^}]+)\}/g, (_, name) => {
|
|
76
|
+
const value = values[name];
|
|
77
|
+
delete values[name];
|
|
78
|
+
return encodeURIComponent(String(value));
|
|
79
|
+
});
|
|
80
|
+
const url = new URL(path, this.options.baseUrl);
|
|
81
|
+
if (route.method === 'GET')
|
|
82
|
+
for (const [key, value] of Object.entries(values))
|
|
83
|
+
if (value !== undefined)
|
|
84
|
+
url.searchParams.set(key, Array.isArray(value) ? value.join(',') : String(value));
|
|
85
|
+
if (route.method === 'POST' &&
|
|
86
|
+
!('readOnly' in route && route.readOnly) &&
|
|
87
|
+
!options.idempotencyKey)
|
|
88
|
+
throw new ContractError(`An idempotencyKey is required for ${operation}.`, operation);
|
|
89
|
+
const headers = {
|
|
90
|
+
authorization: `Bearer ${this.options.apiKey}`,
|
|
91
|
+
accept: 'application/json',
|
|
92
|
+
};
|
|
93
|
+
if (route.method === 'POST') {
|
|
94
|
+
headers['content-type'] = 'application/json';
|
|
95
|
+
if (options.idempotencyKey)
|
|
96
|
+
headers['idempotency-key'] = options.idempotencyKey;
|
|
97
|
+
}
|
|
98
|
+
const body = route.method === 'POST' ? JSON.stringify(values) : undefined;
|
|
99
|
+
for (let attempt = 0;; attempt++) {
|
|
100
|
+
const timeout = AbortSignal.timeout(this.options.timeoutMs);
|
|
101
|
+
const signal = options.signal ? AbortSignal.any([timeout, options.signal]) : timeout;
|
|
102
|
+
let response;
|
|
103
|
+
try {
|
|
104
|
+
response = await this.options.fetch(url, {
|
|
105
|
+
method: route.method,
|
|
106
|
+
headers,
|
|
107
|
+
body,
|
|
108
|
+
signal,
|
|
109
|
+
redirect: 'error',
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
if (options.signal?.aborted || attempt >= this.options.maxRetries)
|
|
114
|
+
throw error;
|
|
115
|
+
// Every mutation above has the same idempotency identity on every attempt.
|
|
116
|
+
await sleep(Math.min(250 * 2 ** attempt, 2000), options.signal);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if ([429, 503].includes(response.status) && attempt < this.options.maxRetries) {
|
|
120
|
+
const retryHeader = response.headers.get('retry-after');
|
|
121
|
+
const seconds = retryHeader ? Number(retryHeader) : NaN;
|
|
122
|
+
const delay = Number.isFinite(seconds)
|
|
123
|
+
? seconds * 1000
|
|
124
|
+
: retryHeader
|
|
125
|
+
? Date.parse(retryHeader) - Date.now()
|
|
126
|
+
: 250 * 2 ** attempt;
|
|
127
|
+
await response.body?.cancel();
|
|
128
|
+
await sleep(Math.min(Math.max(Number.isFinite(delay) ? delay : 250, 0), 30000), options.signal);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
let value;
|
|
132
|
+
try {
|
|
133
|
+
value = await response.json();
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
// A gateway can deny access before the API emits a problem document. Preserve that
|
|
137
|
+
// status so a replica still hides revoked inventory instead of treating it as bad JSON.
|
|
138
|
+
if (!response.ok)
|
|
139
|
+
throw new ApiError(response.status, 'INVALID_ERROR_RESPONSE', `API request failed (HTTP ${response.status}).`);
|
|
140
|
+
throw new ContractError(`Non-JSON response from ${operation} (HTTP ${response.status}).`, operation);
|
|
141
|
+
}
|
|
142
|
+
if (!response.ok) {
|
|
143
|
+
const problem = ProblemSchema.safeParse(value);
|
|
144
|
+
if (problem.success)
|
|
145
|
+
throw new ApiError(response.status, problem.data.code, problem.data.detail, problem.data);
|
|
146
|
+
throw new ApiError(response.status, 'INVALID_ERROR_RESPONSE', `API request failed (HTTP ${response.status}).`);
|
|
147
|
+
}
|
|
148
|
+
const validated = operations[operation].output.safeParse(value);
|
|
149
|
+
if (!validated.success)
|
|
150
|
+
throw new ContractError(`Response from ${operation} does not match the public contract.`, operation);
|
|
151
|
+
return validated.data;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/** Product backends share one search contract and never handle Typesense keys. */
|
|
155
|
+
search = {
|
|
156
|
+
features: (options) => this.call('searchFeatures', {}, options),
|
|
157
|
+
listings: (input = {}, options) => this.call('searchListings', input, options),
|
|
158
|
+
facets: (input, options) => this.call('searchFacets', input, options),
|
|
159
|
+
map: (input, options) => this.call('searchMap', input, options),
|
|
160
|
+
suggest: (input, options) => this.call('searchSuggest', input, options),
|
|
161
|
+
interpret: (input, options) => this.call('searchInterpret', input, options),
|
|
162
|
+
};
|
|
163
|
+
listings = {
|
|
164
|
+
list: (input = {}, options) => this.call('listListings', input, options),
|
|
165
|
+
get: (id, options) => this.call('getListing', {
|
|
166
|
+
id,
|
|
167
|
+
...(options?.profile ? { profile: options.profile } : {}),
|
|
168
|
+
...(options?.dataset_id ? { dataset_id: options.dataset_id } : {}),
|
|
169
|
+
}, options),
|
|
170
|
+
map: (input, options) => this.call('listListingMap', input, options),
|
|
171
|
+
similar: (input, options) => this.call('listSimilarListings', input, options),
|
|
172
|
+
contacts: (id, options) => this.call('getListingContacts', { id, ...(options?.dataset_id ? { dataset_id: options.dataset_id } : {}) }, options),
|
|
173
|
+
resolveIdentity: (input, options) => this.call('resolveListingIdentity', input, options),
|
|
174
|
+
};
|
|
175
|
+
agents = {
|
|
176
|
+
resolveIdentity: (input, options) => this.call('resolveAgentIdentity', input, options),
|
|
177
|
+
get: (id, options) => this.call('getAgent', { id }, options),
|
|
178
|
+
listings: (id, input = {}, options) => this.call('listAgentListings', { ...input, id }, options),
|
|
179
|
+
};
|
|
180
|
+
publications = {
|
|
181
|
+
report: (input, options) => this.call('reportPublication', input, options),
|
|
182
|
+
destination: (input, options) => this.call('resolveDestination', input, options),
|
|
183
|
+
};
|
|
184
|
+
judgments = {
|
|
185
|
+
evaluate: (input, options) => this.call('evaluateJudgment', input, options),
|
|
186
|
+
get: (id, options) => this.call('getJudgment', { id }, options),
|
|
187
|
+
capabilities: (options) => this.call('intelligenceCapabilities', {}, options),
|
|
188
|
+
};
|
|
189
|
+
catalog = {
|
|
190
|
+
health: (datasetId, options) => this.call('getCatalogHealth', { dataset_id: datasetId }, options),
|
|
191
|
+
createSnapshot: (input, options) => this.call('createSnapshot', input, options),
|
|
192
|
+
snapshot: (id, options) => this.call('getSnapshot', { id }, options),
|
|
193
|
+
snapshotItems: (id, input = {}, options) => this.call('listSnapshotItems', { ...input, id }, options),
|
|
194
|
+
changes: (input = {}, options) => this.call('listCatalogChanges', input, options),
|
|
195
|
+
events: (input = {}, options) => this.call('listEvents', input, options),
|
|
196
|
+
opportunityObservations: async (input = {}, options) => {
|
|
197
|
+
const page = await this.call('listEvents', input, options);
|
|
198
|
+
try {
|
|
199
|
+
return {
|
|
200
|
+
...page,
|
|
201
|
+
data: page.data
|
|
202
|
+
.map(opportunityObservation)
|
|
203
|
+
.filter((event) => event !== null),
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
throw new ContractError('Invalid opportunity observation evidence.', 'listEvents');
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
directory = {
|
|
212
|
+
canonicalIdentity: (input, options) => this.call('directoryGetCanonicalIdentity', input, options),
|
|
213
|
+
mergeIdentities: (input, options) => this.call('directoryMergeIdentities', input, options),
|
|
214
|
+
splitIdentity: (input, options) => this.call('directorySplitIdentity', input, options),
|
|
215
|
+
inventory: (input, options) => this.call('directoryListInventory', input, options),
|
|
216
|
+
salesCandidates: (input, options) => this.call('discoverDirectorySalesCandidates', input, options),
|
|
217
|
+
recordProfile: (input, options) => this.call('directoryRecordProfile', input, options),
|
|
218
|
+
recordRelationship: (input, options) => this.call('directoryRecordRelationship', input, options),
|
|
219
|
+
professionals: (input = {}, options) => this.call('directoryListProfessionals', input, options),
|
|
220
|
+
offices: (input = {}, options) => this.call('directoryListOffices', input, options),
|
|
221
|
+
organizations: (input = {}, options) => this.call('directoryListOrganizations', input, options),
|
|
222
|
+
teams: (input = {}, options) => this.call('directoryListTeams', input, options),
|
|
223
|
+
get: (input, options) => this.call('directoryGetEntity', input, options),
|
|
224
|
+
agent: (input, options) => this.call('directoryResolveAgent', input, options),
|
|
225
|
+
identity: (input, options) => this.call('directoryResolveIdentity', input, options),
|
|
226
|
+
relationships: (input, options) => this.call('directoryListRelationships', input, options),
|
|
227
|
+
contacts: (input, options) => this.call('directoryGetContacts', input, options),
|
|
228
|
+
metrics: (input, options) => this.call('directoryGetMetrics', input, options),
|
|
229
|
+
assess: (input, options) => this.call('requestDirectoryAssessment', input, options),
|
|
230
|
+
assessment: (id, options) => this.call('getDirectoryAssessment', { id }, options),
|
|
231
|
+
latestAssessment: (input, options) => this.call('latestDirectoryAssessment', input, options),
|
|
232
|
+
createSalesPlan: (input, options) => this.call('createDirectorySalesPlan', input, options),
|
|
233
|
+
salesPlan: (id, options) => this.call('getDirectorySalesPlan', { id }, options),
|
|
234
|
+
createSnapshot: (input, options) => this.call('createDirectorySnapshot', input, options),
|
|
235
|
+
snapshot: (id, options) => this.call('getDirectorySnapshot', { id }, options),
|
|
236
|
+
snapshotItems: (id, input = {}, options) => this.call('listDirectorySnapshotItems', { ...input, id }, options),
|
|
237
|
+
changes: (input, options) => this.call('listDirectoryChanges', input, options),
|
|
238
|
+
};
|
|
239
|
+
directoryActions = {
|
|
240
|
+
claim: (input, options) => this.call('claimDirectoryActions', input, options),
|
|
241
|
+
startAttempt: (input, options) => this.call('startDirectoryAttempt', input, options),
|
|
242
|
+
get: (id, options) => this.call('getDirectoryAction', { id }, options),
|
|
243
|
+
outcomes: (input, options) => this.call('recordDirectoryActionOutcomes', input, options),
|
|
244
|
+
};
|
|
245
|
+
opportunityPlans = {
|
|
246
|
+
create: (input, options) => this.call('createPlan', input, options),
|
|
247
|
+
get: (id, options) => this.call('getPlan', { id }, options),
|
|
248
|
+
items: (id, input = {}, options) => this.call('listPlanItems', { ...input, id }, options),
|
|
249
|
+
claimDispatchBatch: (input, options) => this.call('claimDispatchBatch', input, options),
|
|
250
|
+
waitUntilReady: async (id, options = {}) => {
|
|
251
|
+
const deadline = Date.now() + (options.maxWaitMs ?? 60000);
|
|
252
|
+
for (;;) {
|
|
253
|
+
const plan = await this.call('getPlan', { id }, options);
|
|
254
|
+
if (!['queued', 'building'].includes(plan.status))
|
|
255
|
+
return plan;
|
|
256
|
+
if (Date.now() >= deadline)
|
|
257
|
+
throw new ApiError(408, 'PLAN_WAIT_TIMEOUT', 'Plan is still building; its ID remains usable.');
|
|
258
|
+
await sleep(Math.min(1000, deadline - Date.now()), options.signal);
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
actions = {
|
|
263
|
+
startAttempt: (id, input, options) => this.call('startAttempt', { ...input, id }, options),
|
|
264
|
+
outcomes: (input, options) => this.call('recordActionOutcomes', input, options),
|
|
265
|
+
};
|
|
266
|
+
/** Live browsing only. Use catalog snapshots for a complete, consistent inventory replica. */
|
|
267
|
+
async *iterateListings(input = {}, options) {
|
|
268
|
+
let cursor = input.cursor;
|
|
269
|
+
const seen = new Set();
|
|
270
|
+
for (;;) {
|
|
271
|
+
const result = await this.call('listListings', { ...input, ...(cursor ? { cursor } : {}) }, options);
|
|
272
|
+
for (const listing of result.data)
|
|
273
|
+
yield listing;
|
|
274
|
+
if (!result.has_more)
|
|
275
|
+
return;
|
|
276
|
+
if (!result.next_cursor || seen.has(result.next_cursor))
|
|
277
|
+
throw new ContractError('Listing pagination did not advance.', 'listListings');
|
|
278
|
+
seen.add(result.next_cursor);
|
|
279
|
+
cursor = result.next_cursor;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
// Both names refer to the same instance-based client; branding adds no global
|
|
284
|
+
// configuration or second transport implementation for existing callers.
|
|
285
|
+
export { ListingsClient as PropertySimple, ListingsClient as default };
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { CatalogSourceFreshnessSchema, type Listing } from '../../contracts/src/index.js';
|
|
2
|
+
import { type ListingsClient } from './index.js';
|
|
3
|
+
/** Structurally compatible with the platform Database and a consumer's transaction wrapper. */
|
|
4
|
+
export interface TargetSql {
|
|
5
|
+
query<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<{
|
|
6
|
+
rows: T[];
|
|
7
|
+
rowCount: number;
|
|
8
|
+
}>;
|
|
9
|
+
}
|
|
10
|
+
export interface TargetDatabase extends TargetSql {
|
|
11
|
+
transaction<T>(fn: (tx: TargetSql) => Promise<T>): Promise<T>;
|
|
12
|
+
}
|
|
13
|
+
export interface CatalogProjectionProfile {
|
|
14
|
+
/** Change this version whenever eligibility semantics change; an existing stream rejects drift. */
|
|
15
|
+
id: string;
|
|
16
|
+
eligible(listing: Listing): boolean;
|
|
17
|
+
}
|
|
18
|
+
export declare const portalActiveProfile: CatalogProjectionProfile;
|
|
19
|
+
export declare const allVisibleProfile: CatalogProjectionProfile;
|
|
20
|
+
export interface SyncConfiguration {
|
|
21
|
+
streamId: string;
|
|
22
|
+
datasetId: string;
|
|
23
|
+
/** Stable identity for the API environment + tenant/product/entitlement scope, never a secret. */
|
|
24
|
+
sourceNamespace: string;
|
|
25
|
+
profile: CatalogProjectionProfile;
|
|
26
|
+
}
|
|
27
|
+
interface Checkpoint {
|
|
28
|
+
phase: 'create' | 'waiting' | 'snapshot' | 'replay' | 'blocked';
|
|
29
|
+
generation: string;
|
|
30
|
+
snapshotId?: string;
|
|
31
|
+
snapshotCursor?: string;
|
|
32
|
+
replayCursor?: string;
|
|
33
|
+
lastSequence: string;
|
|
34
|
+
stagedCount: number;
|
|
35
|
+
expectedCount?: number;
|
|
36
|
+
sourceFreshness?: Array<ReturnType<typeof CatalogSourceFreshnessSchema.parse>>;
|
|
37
|
+
}
|
|
38
|
+
export interface ProjectionReadOptions {
|
|
39
|
+
/** Time since the consumer last drained its replay to the published boundary. */
|
|
40
|
+
maxStalenessMs: number;
|
|
41
|
+
/** Optional independent source-contact bound; missing coverage information fails closed. */
|
|
42
|
+
maxSourceStalenessMs?: number;
|
|
43
|
+
/** Full source enumeration is needed to discover records that disappeared. */
|
|
44
|
+
maxFullEnumerationStalenessMs?: number;
|
|
45
|
+
}
|
|
46
|
+
export interface SyncState {
|
|
47
|
+
version: string;
|
|
48
|
+
checkpoint: Checkpoint;
|
|
49
|
+
active_generation: string | null;
|
|
50
|
+
synchronized_at: string | null;
|
|
51
|
+
}
|
|
52
|
+
export interface CatalogProjection {
|
|
53
|
+
listing_id: string;
|
|
54
|
+
local_id: string;
|
|
55
|
+
listing: Listing;
|
|
56
|
+
}
|
|
57
|
+
export interface CatalogRecord {
|
|
58
|
+
listing_id: string;
|
|
59
|
+
local_id: string;
|
|
60
|
+
eligible: boolean;
|
|
61
|
+
listing: Listing | null;
|
|
62
|
+
}
|
|
63
|
+
export interface CatalogInvalidation {
|
|
64
|
+
id: string;
|
|
65
|
+
stream_id: string;
|
|
66
|
+
generation: string;
|
|
67
|
+
kind: 'reset' | 'upsert' | 'remove';
|
|
68
|
+
listing_id: string | null;
|
|
69
|
+
local_id: string | null;
|
|
70
|
+
lease_token: string;
|
|
71
|
+
}
|
|
72
|
+
export declare class ProjectionUnavailableError extends Error {
|
|
73
|
+
constructor(message?: string);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* An isolated consumer-owned projection. Product users, saved homes and canonical URLs stay in
|
|
77
|
+
* product tables, joined using local_id. Never point this adapter at a product's listings table.
|
|
78
|
+
*/
|
|
79
|
+
export declare class PostgresCatalogTarget {
|
|
80
|
+
private readonly db;
|
|
81
|
+
readonly configuration: SyncConfiguration;
|
|
82
|
+
constructor(db: TargetDatabase, configuration: SyncConfiguration);
|
|
83
|
+
/** Explicit schema installation. Run only against an approved consumer database. */
|
|
84
|
+
initialize(): Promise<void>;
|
|
85
|
+
state(): Promise<SyncState>;
|
|
86
|
+
/** Adopt an existing product UUID before importing it; never rewrite an established mapping. */
|
|
87
|
+
mapExistingIdentity(listingId: string, localId: string): Promise<void>;
|
|
88
|
+
/** Readers never see staging generations. A caller must choose an explicit freshness budget. */
|
|
89
|
+
list(options: ProjectionReadOptions & {
|
|
90
|
+
limit?: number;
|
|
91
|
+
after?: string;
|
|
92
|
+
}): Promise<CatalogProjection[]>;
|
|
93
|
+
/** Status consumers may inspect a noneligible upsert; a source tombstone has a null payload. */
|
|
94
|
+
get(listingId: string, options: ProjectionReadOptions): Promise<CatalogRecord | null>;
|
|
95
|
+
private assertReadable;
|
|
96
|
+
commit(expected: SyncState, next: Checkpoint, mutate: (tx: TargetSql) => Promise<void>, synchronized?: boolean): Promise<boolean>;
|
|
97
|
+
write(tx: TargetSql, generation: string, listing: Listing | null, listingId: string, invalidate: boolean, snapshot?: boolean): Promise<void>;
|
|
98
|
+
activate(tx: TargetSql, generation: string): Promise<void>;
|
|
99
|
+
private invalidate;
|
|
100
|
+
reset(expected: SyncState, blocked?: boolean): Promise<boolean>;
|
|
101
|
+
/** Explicit operator recovery after correcting denied credentials/scope. Does not change scope. */
|
|
102
|
+
resume(): Promise<void>;
|
|
103
|
+
claimInvalidations(limit?: number, leaseSeconds?: number): Promise<CatalogInvalidation[]>;
|
|
104
|
+
acknowledgeInvalidation(id: string, leaseToken: string): Promise<boolean>;
|
|
105
|
+
/** Bounded retention removes obsolete listing payloads, preserving identities and product data. */
|
|
106
|
+
pruneObsolete(limit?: number): Promise<number>;
|
|
107
|
+
}
|
|
108
|
+
type CatalogSource = Pick<ListingsClient, 'catalog'>;
|
|
109
|
+
export type SyncStep = {
|
|
110
|
+
status: 'progress' | 'idle' | 'waiting' | 'conflict' | 'reset' | 'blocked';
|
|
111
|
+
phase: Checkpoint['phase'];
|
|
112
|
+
applied: number;
|
|
113
|
+
};
|
|
114
|
+
/** One step makes at most one network request and commits at most one bounded page. */
|
|
115
|
+
export declare class CatalogSyncWorker {
|
|
116
|
+
private readonly source;
|
|
117
|
+
private readonly target;
|
|
118
|
+
private readonly pageSize;
|
|
119
|
+
constructor(source: CatalogSource, target: PostgresCatalogTarget, pageSize?: number);
|
|
120
|
+
step(options?: {
|
|
121
|
+
signal?: AbortSignal;
|
|
122
|
+
}): Promise<SyncStep>;
|
|
123
|
+
private validateChanges;
|
|
124
|
+
run(options: {
|
|
125
|
+
maxSteps: number;
|
|
126
|
+
signal?: AbortSignal;
|
|
127
|
+
}): Promise<SyncStep[]>;
|
|
128
|
+
}
|
|
129
|
+
export {};
|