@lexnode/legal-data-sdk 1.26.0 → 1.28.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/dist/lookup.js ADDED
@@ -0,0 +1,158 @@
1
+ import { LexNodeError, isLexNodeError } from "./errors.js";
2
+ /** O processo que a consulta entregou, na visão dela, ou nulo. */
3
+ function readOf(lookup, view) {
4
+ const read = view === "SNAPSHOT" ? lookup.snapshot : lookup.process;
5
+ return (read ?? null);
6
+ }
7
+ /**
8
+ * Lê um processo e diz, no tipo, qual dos dois desfechos aconteceu.
9
+ *
10
+ * Sem isto, o consumidor lê `data.process` num corpo que é uma consulta
11
+ * aberta e chama de "sem dados" o que é "ainda estou buscando". A mesma
12
+ * leitura pedida de novo devolve a mesma consulta — repetir é seguro, e o
13
+ * cliente repete sozinho quando a política de retentativa estiver ligada.
14
+ */
15
+ export async function lookupProcess(client, input, opts = {}) {
16
+ const { data, meta } = await client.processLookups.create(input, opts);
17
+ if (meta.status === 202) {
18
+ return { kind: "pending", lookup: data, stale: data.stale ?? null, meta };
19
+ }
20
+ const read = readOf(data, input.view);
21
+ if (!read) {
22
+ throw new LexNodeError({
23
+ code: "INVALID_RESPONSE",
24
+ message: `A consulta respondeu ${meta.status} sem o processo da visão ${input.view}.`,
25
+ status: 502,
26
+ errorClass: "RETRYABLE",
27
+ retryable: true,
28
+ upstreamStatus: meta.status,
29
+ });
30
+ }
31
+ return { kind: "ready", data: read, lookup: data, meta };
32
+ }
33
+ /** Estados em que a consulta não avança mais. */
34
+ const TERMINAL_STATES = new Set(["SUCCEEDED", "FAILED", "CANCELED"]);
35
+ const DEFAULT_BUDGET_MS = 60_000;
36
+ /** A menor espera entre duas leituras, para a próxima tentativa que está perto. */
37
+ const MIN_POLL_INTERVAL_MS = 2_000;
38
+ /**
39
+ * A espera entre leituras da consulta sem hora futura: a tentativa em curso
40
+ * (`nextAttemptAt` nulo) ou a vencida que espera a vez na fila. O desfecho
41
+ * dela sai em até um minuto, e ler a cada poucos segundos só gastaria o teto
42
+ * por minuto da chave.
43
+ */
44
+ const IN_FLIGHT_POLL_INTERVAL_MS = 10_000;
45
+ function wait(ms, signal) {
46
+ return new Promise((resolve, reject) => {
47
+ if (signal?.aborted) {
48
+ reject(signal.reason);
49
+ return;
50
+ }
51
+ let timer;
52
+ const onAbort = () => {
53
+ clearTimeout(timer);
54
+ reject(signal?.reason);
55
+ };
56
+ timer = setTimeout(() => {
57
+ signal?.removeEventListener("abort", onAbort);
58
+ resolve();
59
+ }, ms);
60
+ signal?.addEventListener("abort", onAbort, { once: true });
61
+ });
62
+ }
63
+ function budgetExceeded(lookupId, budgetMs, state, nextReadAt) {
64
+ return new LexNodeError({
65
+ code: "PROCESS_LOOKUP_BUDGET_EXCEEDED",
66
+ message: `A consulta ${lookupId} não termina dentro do orçamento de ${budgetMs} ms; ela segue tentando no motor.`,
67
+ status: 504,
68
+ errorClass: "RETRYABLE",
69
+ retryable: true,
70
+ details: {
71
+ lookupId,
72
+ status: state,
73
+ budgetMs,
74
+ nextReadAt: new Date(nextReadAt).toISOString(),
75
+ },
76
+ });
77
+ }
78
+ /**
79
+ * Espera a consulta terminar, dentro do orçamento.
80
+ *
81
+ * Lê a consulta de novo só quando há o que mudar: na hora da próxima tentativa
82
+ * (`nextAttemptAt`) — antes dela, a leitura devolve o mesmo estado —, a cada
83
+ * 10 s enquanto uma tentativa está em curso ou espera a vez na fila, e depois
84
+ * do `Retry-After` quando a própria leitura é recusada por vazão. Leitura não
85
+ * cobra, mas conta no teto por minuto da chave.
86
+ *
87
+ * O estouro do orçamento é um erro PRÓPRIO e retentável
88
+ * (`PROCESS_LOOKUP_BUDGET_EXCEEDED`): a consulta continua no motor, e quem
89
+ * espera pode voltar a lê-la depois — `details.nextReadAt` diz quando vale a
90
+ * pena — ou receber o `PROCESS_LOOKUP_COMPLETED` por webhook e não esperar
91
+ * nada. Quando a próxima leitura útil cai depois do orçamento, o erro sai na
92
+ * hora: esperar até o fim dele não mudaria nada. Devolve a consulta
93
+ * terminada: `SUCCEEDED`, com o processo enquanto o resultado vale; `FAILED`,
94
+ * com `error`; ou `CANCELED`.
95
+ */
96
+ export async function waitForProcessLookup(client, lookupId, options = {}) {
97
+ const budgetMs = options.budgetMs ?? DEFAULT_BUDGET_MS;
98
+ const deadline = Date.now() + budgetMs;
99
+ let lastState = null;
100
+ const controller = new AbortController();
101
+ const { signal } = controller;
102
+ const onCallerAbort = () => controller.abort(options.signal?.reason);
103
+ if (options.signal?.aborted)
104
+ onCallerAbort();
105
+ else
106
+ options.signal?.addEventListener("abort", onCallerAbort, { once: true });
107
+ const timer = setTimeout(() => {
108
+ controller.abort(budgetExceeded(lookupId, budgetMs, lastState, Date.now()));
109
+ }, Math.max(0, budgetMs));
110
+ try {
111
+ for (;;) {
112
+ signal.throwIfAborted();
113
+ if (Date.now() >= deadline) {
114
+ throw budgetExceeded(lookupId, budgetMs, lastState, Date.now());
115
+ }
116
+ let nextReadAt;
117
+ try {
118
+ const { data } = await client.processLookups.get(lookupId, {
119
+ signal,
120
+ officeId: options.officeId,
121
+ });
122
+ signal.throwIfAborted();
123
+ if (Date.now() >= deadline) {
124
+ throw budgetExceeded(lookupId, budgetMs, lastState, Date.now());
125
+ }
126
+ lastState = data.status;
127
+ options.onState?.(data.status, data);
128
+ if (TERMINAL_STATES.has(data.status))
129
+ return data;
130
+ const nextAttemptAt = data.nextAttemptAt
131
+ ? Date.parse(data.nextAttemptAt)
132
+ : Number.NaN;
133
+ const now = Date.now();
134
+ nextReadAt =
135
+ Number.isFinite(nextAttemptAt) && nextAttemptAt > now
136
+ ? Math.max(now + MIN_POLL_INTERVAL_MS, nextAttemptAt)
137
+ : now + IN_FLIGHT_POLL_INTERVAL_MS;
138
+ }
139
+ catch (error) {
140
+ signal.throwIfAborted();
141
+ if (!isLexNodeError(error) ||
142
+ !error.retryable ||
143
+ error.retryAfterSeconds === null) {
144
+ throw error;
145
+ }
146
+ nextReadAt = Date.now() + error.retryAfterSeconds * 1_000;
147
+ }
148
+ if (nextReadAt >= deadline) {
149
+ throw budgetExceeded(lookupId, budgetMs, lastState, nextReadAt);
150
+ }
151
+ await wait(nextReadAt - Date.now(), signal);
152
+ }
153
+ }
154
+ finally {
155
+ clearTimeout(timer);
156
+ options.signal?.removeEventListener("abort", onCallerAbort);
157
+ }
158
+ }
@@ -83,6 +83,19 @@ export interface ProcessSourceIndex {
83
83
  updatedThrough: string | null;
84
84
  checkedAt: string;
85
85
  }
86
+ /** Quantos movimentos novos de um grau a captura anunciou. */
87
+ export interface ProcessMovementInstanceCount {
88
+ /** A sigla do grau (G1, G2, JE, TR…); nula quando a fonte não a declara. */
89
+ degree: string | null;
90
+ newMovementCount: number;
91
+ /** A chave do movimento novo mais recente deste grau. */
92
+ latestMovementKey: string;
93
+ }
94
+ /**
95
+ * Um aviso por assinante e por captura do processo, descrito pelo movimento
96
+ * novo mais recente: o topo do payload é dele, e a chave dele é
97
+ * `latestMovementKey`.
98
+ */
86
99
  export interface ProcessMovementCreatedPayload {
87
100
  canonicalProcessId: string;
88
101
  tribunalAlias: string;
@@ -99,12 +112,82 @@ export interface ProcessMovementCreatedPayload {
99
112
  */
100
113
  latestMovementCode?: number | null;
101
114
  latestMovementLabel?: string | null;
115
+ /**
116
+ * O grau do movimento mais recente e o documento da fonte que o trouxe — o
117
+ * `sourceId` da instância na leitura do processo. Ausentes quando o processo
118
+ * não é compartilhável.
119
+ */
120
+ degree?: string | null;
121
+ instanceSourceId?: string | null;
122
+ /**
123
+ * Quantos movimentos novos vieram de cada grau nesta captura, o grau do mais
124
+ * recente primeiro. Ausente quando o processo não é compartilhável.
125
+ */
126
+ instances?: ProcessMovementInstanceCount[];
102
127
  /**
103
128
  * Presente quando o processo não é compartilhável: o aviso diz que ele andou
104
- * e não diz o quê, porque ali o nome do ato é teor.
129
+ * e não diz o quê nem em que grau, porque ali o nome do ato é teor.
105
130
  */
106
131
  contentWithheld?: true;
107
132
  }
133
+ /**
134
+ * Outro grau de um processo assinado apareceu na fonte (1º grau, 2º grau,
135
+ * turma recursal…). O histórico dele entrou no acervo sem virar
136
+ * `PROCESS_MOVEMENT_CREATED` movimento a movimento: este aviso é o que diz que
137
+ * ele chegou, e os movimentos estão no retrato do processo, em `instances`.
138
+ */
139
+ export interface ProcessInstanceDiscoveredPayload {
140
+ canonicalProcessId: string;
141
+ tribunalAlias: string;
142
+ processNumber: string;
143
+ /** O ponteiro que o consumidor informou ao assinar o processo. */
144
+ externalRef: string | null;
145
+ /** A sigla do grau novo; nula quando a fonte não a declara. */
146
+ degree: string | null;
147
+ /** O documento da fonte deste grau — o `sourceId` dele em `instances`. */
148
+ instanceSourceId: string;
149
+ courtBody: string | null;
150
+ /** Quantos movimentos o grau trouxe na captura que o descobriu. */
151
+ movementCount: number;
152
+ /** Quantos deles entraram no acervo sem aviso próprio. */
153
+ historyMovementCount: number;
154
+ /**
155
+ * Quantos deles saíram também em `PROCESS_MOVEMENT_CREATED`: os datados
156
+ * depois da primeira captura do número.
157
+ */
158
+ announcedMovementCount: number;
159
+ /** O movimento mais recente do grau novo. */
160
+ latestMovementAt: string | null;
161
+ latestMovementCode: number | null;
162
+ latestMovementLabel: string | null;
163
+ contentWithheld?: never;
164
+ }
165
+ /**
166
+ * O mesmo aviso sobre processo não compartilhável: diz que o número ganhou um
167
+ * grau e não diz qual, nem o órgão, nem quantos movimentos.
168
+ */
169
+ export interface ProcessInstanceDiscoveredWithheldPayload {
170
+ canonicalProcessId: string;
171
+ tribunalAlias: string;
172
+ processNumber: string;
173
+ externalRef: string | null;
174
+ contentWithheld: true;
175
+ }
176
+ /** Um grau do número como a varredura da vigília de processo o deixou. */
177
+ export interface MonitorSyncInstance {
178
+ degree: string | null;
179
+ /** O documento da fonte deste grau — o `sourceId` dele em `instances`. */
180
+ instanceSourceId: string;
181
+ /** A instância que o topo da leitura do processo serve. */
182
+ primary: boolean;
183
+ courtBody: string | null;
184
+ sourceUpdatedAt: string | null;
185
+ latestMovementAt: string | null;
186
+ /** Nulo quando os movimentos deste grau nunca foram capturados. */
187
+ movementCount: number | null;
188
+ /** Desde quando a resposta completa da fonte vem sem este documento. */
189
+ absentSince: string | null;
190
+ }
108
191
  export interface PublicationCreatedPayload {
109
192
  monitorId: string;
110
193
  officeId: string | null;
@@ -151,6 +234,21 @@ export interface MonitorSyncSucceededPayload {
151
234
  snapshotHash?: string;
152
235
  sourceUpdatedAt?: string | null;
153
236
  latestMovementAt?: string | null;
237
+ /**
238
+ * Vigília de processo: os graus do número e os que esta passada reconheceu
239
+ * como novos. Só quando o controle de confidencialidade deixa o dono do
240
+ * monitor ler o número inteiro.
241
+ */
242
+ instances?: MonitorSyncInstance[];
243
+ instancesDiscovered?: MonitorSyncInstance[];
244
+ /**
245
+ * Vigília de processo cujo número corre em segredo num grau que o dono do
246
+ * monitor não buscou: a passada sai sem o hash e sem as datas da fonte, e
247
+ * `changed` fica `false`. O dono que buscou o grau em segredo, numa conta
248
+ * que não aceita a proveniência, recebe a passada com `changed`, o hash e as
249
+ * datas, e sem `instances`.
250
+ */
251
+ contentWithheld?: true;
154
252
  /** O alvo que a fonte não indexava passou a existir nesta passada. */
155
253
  firstAppearance?: true;
156
254
  /** A vigília de aparição segue esperando o alvo entrar no índice. */
@@ -225,6 +323,48 @@ export interface SearchJobFailedPayload {
225
323
  };
226
324
  pollUrl: string;
227
325
  }
326
+ /**
327
+ * O fim de uma consulta de processo em segundo plano
328
+ * (`POST /v1/processes/lookups`). É um ponteiro: o processo nunca viaja no
329
+ * evento — lê-se em `pollUrl`, sem nova cobrança, pelo controle de
330
+ * confidencialidade de quem lê. O movimento novo que a entrega trouxer chega no
331
+ * `PROCESS_MOVEMENT_CREATED` de sempre.
332
+ */
333
+ interface ProcessLookupCompletedBase {
334
+ lookupId: string;
335
+ view: "PROCESS" | "SNAPSHOT";
336
+ tribunalAlias: string;
337
+ /** Só os 20 dígitos. */
338
+ processNumber: string;
339
+ officeId: string | null;
340
+ traceId: string | null;
341
+ /** Idas à fonte, a leitura que abriu a consulta incluída. */
342
+ attempts: number;
343
+ pollUrl: string;
344
+ }
345
+ export interface ProcessLookupSucceededPayload extends ProcessLookupCompletedBase {
346
+ status: "SUCCEEDED";
347
+ /**
348
+ * Quem entregou: a fonte, o acervo que outra leitura atualizou, ou uma
349
+ * leitura direta da mesma conta, que já tinha cobrado — então esta não cobra.
350
+ */
351
+ satisfiedBy: "SOURCE" | "LAKE" | "DIRECT_READ";
352
+ creditsCharged: number;
353
+ }
354
+ export interface ProcessLookupFailedPayload extends ProcessLookupCompletedBase {
355
+ status: "FAILED";
356
+ satisfiedBy: null;
357
+ /** A retenção inteira, devolvida ao saldo. */
358
+ creditsRefunded: number;
359
+ error: {
360
+ class: "RETRYABLE" | "TERMINAL";
361
+ code: string;
362
+ /** O texto público fixo do código. */
363
+ message: string;
364
+ retryable: boolean;
365
+ upstreamStatus?: number;
366
+ };
367
+ }
228
368
  export interface PublicationTextAlertMatch {
229
369
  source: string;
230
370
  publicationKey: string;
@@ -280,8 +420,8 @@ type Typed<T extends string, P> = WebhookEventEnvelope & {
280
420
  type: T;
281
421
  payload: P;
282
422
  };
283
- /** Os dez tipos do catálogo, discriminados por `type`. */
284
- export type WebhookEvent = Typed<"PROCESS_MOVEMENT_CREATED", ProcessMovementCreatedPayload> | Typed<"PUBLICATION_CREATED", PublicationCreatedPayload> | Typed<"MONITOR_SYNC_SUCCEEDED", MonitorSyncSucceededPayload> | Typed<"MONITOR_SYNC_FAILED", MonitorSyncFailedPayload> | Typed<"SEARCH_JOB_COMPLETED", SearchJobSucceededPayload | SearchJobFailedPayload> | Typed<"PUBLICATION_TEXT_ALERT_MATCHED", PublicationTextAlertMatchedPayload> | Typed<"PUBLICATION_HARVEST_COMPLETED", PublicationHarvestCompletedPayload> | Typed<"QUOTA_LOW", QuotaLowPayload> | Typed<"PERIOD_ENDING", PeriodEndingPayload> | Typed<"SUBSCRIPTION_PAST_DUE", SubscriptionPastDuePayload> | Typed<"WEBHOOK_TEST", WebhookTestPayload>;
423
+ /** Os tipos do catálogo e a entrega de teste, discriminados por `type`. */
424
+ export type WebhookEvent = Typed<"PROCESS_MOVEMENT_CREATED", ProcessMovementCreatedPayload> | Typed<"PROCESS_INSTANCE_DISCOVERED", ProcessInstanceDiscoveredPayload | ProcessInstanceDiscoveredWithheldPayload> | Typed<"PUBLICATION_CREATED", PublicationCreatedPayload> | Typed<"MONITOR_SYNC_SUCCEEDED", MonitorSyncSucceededPayload> | Typed<"MONITOR_SYNC_FAILED", MonitorSyncFailedPayload> | Typed<"SEARCH_JOB_COMPLETED", SearchJobSucceededPayload | SearchJobFailedPayload> | Typed<"PUBLICATION_TEXT_ALERT_MATCHED", PublicationTextAlertMatchedPayload> | Typed<"PUBLICATION_HARVEST_COMPLETED", PublicationHarvestCompletedPayload> | Typed<"PROCESS_LOOKUP_COMPLETED", ProcessLookupSucceededPayload | ProcessLookupFailedPayload> | Typed<"QUOTA_LOW", QuotaLowPayload> | Typed<"PERIOD_ENDING", PeriodEndingPayload> | Typed<"SUBSCRIPTION_PAST_DUE", SubscriptionPastDuePayload> | Typed<"WEBHOOK_TEST", WebhookTestPayload>;
285
425
  /**
286
426
  * O evento que o catálogo ainda não descreve.
287
427
  *
@@ -304,7 +444,7 @@ export type WebhookEventType = WebhookEvent["type"];
304
444
  */
305
445
  export declare const WEBHOOK_TEST_EVENT_TYPE: "WEBHOOK_TEST";
306
446
  /** Os tipos que se pode ASSINAR ao cadastrar um endpoint. */
307
- export declare const WEBHOOK_EVENT_TYPES: readonly ["PROCESS_MOVEMENT_CREATED", "PUBLICATION_CREATED", "MONITOR_SYNC_SUCCEEDED", "MONITOR_SYNC_FAILED", "SEARCH_JOB_COMPLETED", "PUBLICATION_TEXT_ALERT_MATCHED", "PUBLICATION_HARVEST_COMPLETED", "QUOTA_LOW", "PERIOD_ENDING", "SUBSCRIPTION_PAST_DUE"];
447
+ export declare const WEBHOOK_EVENT_TYPES: readonly ["PROCESS_MOVEMENT_CREATED", "PROCESS_INSTANCE_DISCOVERED", "PUBLICATION_CREATED", "MONITOR_SYNC_SUCCEEDED", "MONITOR_SYNC_FAILED", "SEARCH_JOB_COMPLETED", "PUBLICATION_TEXT_ALERT_MATCHED", "PUBLICATION_HARVEST_COMPLETED", "PROCESS_LOOKUP_COMPLETED", "QUOTA_LOW", "PERIOD_ENDING", "SUBSCRIPTION_PAST_DUE"];
308
448
  /**
309
449
  * O que entra em `eventTypes` ao cadastrar um endpoint. É subconjunto próprio
310
450
  * de `WebhookEventType`: mandar a entrega de teste nessa lista é recusa.
package/dist/webhooks.js CHANGED
@@ -117,12 +117,14 @@ export const WEBHOOK_TEST_EVENT_TYPE = "WEBHOOK_TEST";
117
117
  /** Os tipos que se pode ASSINAR ao cadastrar um endpoint. */
118
118
  export const WEBHOOK_EVENT_TYPES = [
119
119
  "PROCESS_MOVEMENT_CREATED",
120
+ "PROCESS_INSTANCE_DISCOVERED",
120
121
  "PUBLICATION_CREATED",
121
122
  "MONITOR_SYNC_SUCCEEDED",
122
123
  "MONITOR_SYNC_FAILED",
123
124
  "SEARCH_JOB_COMPLETED",
124
125
  "PUBLICATION_TEXT_ALERT_MATCHED",
125
126
  "PUBLICATION_HARVEST_COMPLETED",
127
+ "PROCESS_LOOKUP_COMPLETED",
126
128
  "QUOTA_LOW",
127
129
  "PERIOD_ENDING",
128
130
  "SUBSCRIPTION_PAST_DUE",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lexnode/legal-data-sdk",
3
- "version": "1.26.0",
3
+ "version": "1.28.0",
4
4
  "description": "Cliente TypeScript do LexNode, o motor de dados jur\u00eddicos do Brasil: transporte tipado, erros classificados, verifica\u00e7\u00e3o de webhook e os tipos gerados do contrato OpenAPI.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://www.lexnode.com.br/docs",