@neetru/sdk 3.3.0 → 3.4.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/CHANGELOG.md +43 -0
- package/dist/ai.d.cts +1 -1
- package/dist/ai.d.ts +1 -1
- package/dist/auth.cjs +365 -25
- package/dist/auth.cjs.map +1 -1
- package/dist/auth.d.cts +1 -1
- package/dist/auth.d.ts +1 -1
- package/dist/auth.mjs +365 -25
- package/dist/auth.mjs.map +1 -1
- package/dist/catalog.d.cts +1 -1
- package/dist/catalog.d.ts +1 -1
- package/dist/checkout.d.cts +1 -1
- package/dist/checkout.d.ts +1 -1
- package/dist/db-react.d.cts +1 -1
- package/dist/db-react.d.ts +1 -1
- package/dist/db.cjs +202 -22
- package/dist/db.cjs.map +1 -1
- package/dist/db.d.cts +1 -1
- package/dist/db.d.ts +1 -1
- package/dist/db.mjs +202 -23
- package/dist/db.mjs.map +1 -1
- package/dist/entitlements.d.cts +1 -1
- package/dist/entitlements.d.ts +1 -1
- package/dist/firestore-compat.d.cts +1 -1
- package/dist/firestore-compat.d.ts +1 -1
- package/dist/index.cjs +366 -26
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.mjs +366 -26
- package/dist/index.mjs.map +1 -1
- package/dist/mocks.cjs +68 -1
- package/dist/mocks.cjs.map +1 -1
- package/dist/mocks.d.cts +17 -1
- package/dist/mocks.d.ts +17 -1
- package/dist/mocks.mjs +68 -1
- package/dist/mocks.mjs.map +1 -1
- package/dist/notifications.d.cts +1 -1
- package/dist/notifications.d.ts +1 -1
- package/dist/react.d.cts +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/support.cjs +98 -1
- package/dist/support.cjs.map +1 -1
- package/dist/support.d.cts +1 -1
- package/dist/support.d.ts +1 -1
- package/dist/support.mjs +98 -1
- package/dist/support.mjs.map +1 -1
- package/dist/telemetry.d.cts +1 -1
- package/dist/telemetry.d.ts +1 -1
- package/dist/{types-DslphKIM.d.ts → types-BlOLDMR0.d.ts} +192 -4
- package/dist/{types-DQplLXIk.d.cts → types-D-QdG502.d.cts} +192 -4
- package/dist/usage.d.cts +1 -1
- package/dist/usage.d.ts +1 -1
- package/dist/webhooks.d.cts +1 -1
- package/dist/webhooks.d.ts +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { createNeetruClient } from './auth.cjs';
|
|
2
2
|
export { NeetruError, NeetruErrorCode } from './errors.cjs';
|
|
3
|
-
export { A as AiChatChoice, a as AiChatMessage, b as AiChatOptions, c as AiChatResult, d as AiChatUsage, e as AiContentPart, f as AiImageUrlPart, g as AiNamespace, h as AiResponseFormat, i as AiStreamHandle, j as AiStreamMeta, k as AiTextPart, l as AuthNamespace, m as AuthStateListener, C as CatalogListOptions, n as CatalogListResponse, p as CheckoutIntentInfo, q as CheckoutIntentStatus, r as CheckoutNamespace, s as CheckoutStartInput, t as CheckoutStartResult, u as CheckoutTenantType, v as CompleteSignInResult, y as CreateTicketInput, D as DEFAULT_BASE_URL, z as DbBatchOp, B as DbChangeType, E as DbCollectionRef, F as DbDoc, G as DbDocRef, H as DbGetResult, I as DbListResult, J as DbNamespace, K as DbQuery, L as DbSqlLease, M as DbWhereFilter, O as EntitlementCheck, P as FieldSentinel, Q as IncrementSentinel, R as ListNotificationsOptions, S as MockCheckout, T as MockNotifications, U as MockWebhooks, V as NEETRU_SENTINEL_KEY, W as NeetruClient, X as NeetruClientConfig, Y as NeetruDb, _ as NeetruDbEngine, $ as NeetruDbError, a0 as NeetruDbErrorCode, a1 as NeetruDbOptions, a2 as NeetruDbTransport, a3 as NeetruEnv, a4 as NeetruSqlClient, a5 as NeetruUser, a6 as NotificationScopeOptions, a7 as NotificationsNamespace, a8 as Product, a9 as ProductNotification, aa as ProductNotificationSeverity, ab as ProductPlan, ac as ProductStatus, ae as RedirectCallbackResult, af as RegisterWebhookInput, ag as ResolvedConfig, ah as SendNotificationInput, ai as ServerTimestampSentinel, aj as SignInOptions, al as SupportNamespace, am as SupportSeverity, an as SupportStatus, ao as SupportTicket,
|
|
3
|
+
export { A as AiChatChoice, a as AiChatMessage, b as AiChatOptions, c as AiChatResult, d as AiChatUsage, e as AiContentPart, f as AiImageUrlPart, g as AiNamespace, h as AiResponseFormat, i as AiStreamHandle, j as AiStreamMeta, k as AiTextPart, l as AuthNamespace, m as AuthStateListener, C as CatalogListOptions, n as CatalogListResponse, p as CheckoutIntentInfo, q as CheckoutIntentStatus, r as CheckoutNamespace, s as CheckoutStartInput, t as CheckoutStartResult, u as CheckoutTenantType, v as CompleteSignInResult, y as CreateTicketInput, D as DEFAULT_BASE_URL, z as DbBatchOp, B as DbChangeType, E as DbCollectionRef, F as DbDoc, G as DbDocRef, H as DbGetResult, I as DbListResult, J as DbNamespace, K as DbQuery, L as DbSqlLease, M as DbWhereFilter, O as EntitlementCheck, P as FieldSentinel, Q as IncrementSentinel, R as ListNotificationsOptions, S as MockCheckout, T as MockNotifications, U as MockWebhooks, V as NEETRU_SENTINEL_KEY, W as NeetruClient, X as NeetruClientConfig, Y as NeetruDb, _ as NeetruDbEngine, $ as NeetruDbError, a0 as NeetruDbErrorCode, a1 as NeetruDbOptions, a2 as NeetruDbTransport, a3 as NeetruEnv, a4 as NeetruSqlClient, a5 as NeetruUser, a6 as NotificationScopeOptions, a7 as NotificationsNamespace, a8 as Product, a9 as ProductNotification, aa as ProductNotificationSeverity, ab as ProductPlan, ac as ProductStatus, ae as RedirectCallbackResult, af as RegisterWebhookInput, ag as ResolvedConfig, ah as SendNotificationInput, ai as ServerTimestampSentinel, aj as SignInOptions, al as SupportNamespace, am as SupportSeverity, an as SupportStatus, ao as SupportTicket, ar as TelemetryEventAck, as as TelemetryEventInput, aw as UsageEventInput, ax as UsageNamespace, ay as UsageQuota, az as VerifyTokenOptions, aA as VerifyWebhookRequestOptions, aB as VerifyWebhookRequestResult, aC as WebhookEndpoint, aD as WebhookEvent, aE as WebhookScopeOptions, aF as WebhookTestResult, aG as WebhooksNamespace, aH as WithFieldSentinels, aJ as createAiNamespace, aK as createCheckoutNamespace, aL as createNeetruDb, aM as createNotificationsNamespace, aO as createWebhooksNamespace, aQ as increment, aR as isFieldSentinel, aS as isIncrementSentinel, aT as isServerTimestampSentinel, aU as serverTimestamp, aV as verifyWebhookRequest, aW as verifyWebhookSignature } from './types-D-QdG502.cjs';
|
|
4
4
|
export { DEV_FIXTURE_USER, MockAi, MockAuth, MockAuthOptions, MockDb, MockEntitlements, MockSupport, MockUsage } from './mocks.cjs';
|
|
5
5
|
import 'drizzle-orm/node-postgres';
|
|
6
6
|
|
|
@@ -42,6 +42,6 @@ declare function checkCoreVersionCompat(coreVersion: string): void;
|
|
|
42
42
|
*/
|
|
43
43
|
|
|
44
44
|
/** Versão do SDK — exportada explicitamente pra detecção em runtime. */
|
|
45
|
-
declare const VERSION: "3.
|
|
45
|
+
declare const VERSION: "3.4.1";
|
|
46
46
|
|
|
47
47
|
export { VERSION, checkCoreVersionCompat };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { createNeetruClient } from './auth.js';
|
|
2
2
|
export { NeetruError, NeetruErrorCode } from './errors.js';
|
|
3
|
-
export { A as AiChatChoice, a as AiChatMessage, b as AiChatOptions, c as AiChatResult, d as AiChatUsage, e as AiContentPart, f as AiImageUrlPart, g as AiNamespace, h as AiResponseFormat, i as AiStreamHandle, j as AiStreamMeta, k as AiTextPart, l as AuthNamespace, m as AuthStateListener, C as CatalogListOptions, n as CatalogListResponse, p as CheckoutIntentInfo, q as CheckoutIntentStatus, r as CheckoutNamespace, s as CheckoutStartInput, t as CheckoutStartResult, u as CheckoutTenantType, v as CompleteSignInResult, y as CreateTicketInput, D as DEFAULT_BASE_URL, z as DbBatchOp, B as DbChangeType, E as DbCollectionRef, F as DbDoc, G as DbDocRef, H as DbGetResult, I as DbListResult, J as DbNamespace, K as DbQuery, L as DbSqlLease, M as DbWhereFilter, O as EntitlementCheck, P as FieldSentinel, Q as IncrementSentinel, R as ListNotificationsOptions, S as MockCheckout, T as MockNotifications, U as MockWebhooks, V as NEETRU_SENTINEL_KEY, W as NeetruClient, X as NeetruClientConfig, Y as NeetruDb, _ as NeetruDbEngine, $ as NeetruDbError, a0 as NeetruDbErrorCode, a1 as NeetruDbOptions, a2 as NeetruDbTransport, a3 as NeetruEnv, a4 as NeetruSqlClient, a5 as NeetruUser, a6 as NotificationScopeOptions, a7 as NotificationsNamespace, a8 as Product, a9 as ProductNotification, aa as ProductNotificationSeverity, ab as ProductPlan, ac as ProductStatus, ae as RedirectCallbackResult, af as RegisterWebhookInput, ag as ResolvedConfig, ah as SendNotificationInput, ai as ServerTimestampSentinel, aj as SignInOptions, al as SupportNamespace, am as SupportSeverity, an as SupportStatus, ao as SupportTicket,
|
|
3
|
+
export { A as AiChatChoice, a as AiChatMessage, b as AiChatOptions, c as AiChatResult, d as AiChatUsage, e as AiContentPart, f as AiImageUrlPart, g as AiNamespace, h as AiResponseFormat, i as AiStreamHandle, j as AiStreamMeta, k as AiTextPart, l as AuthNamespace, m as AuthStateListener, C as CatalogListOptions, n as CatalogListResponse, p as CheckoutIntentInfo, q as CheckoutIntentStatus, r as CheckoutNamespace, s as CheckoutStartInput, t as CheckoutStartResult, u as CheckoutTenantType, v as CompleteSignInResult, y as CreateTicketInput, D as DEFAULT_BASE_URL, z as DbBatchOp, B as DbChangeType, E as DbCollectionRef, F as DbDoc, G as DbDocRef, H as DbGetResult, I as DbListResult, J as DbNamespace, K as DbQuery, L as DbSqlLease, M as DbWhereFilter, O as EntitlementCheck, P as FieldSentinel, Q as IncrementSentinel, R as ListNotificationsOptions, S as MockCheckout, T as MockNotifications, U as MockWebhooks, V as NEETRU_SENTINEL_KEY, W as NeetruClient, X as NeetruClientConfig, Y as NeetruDb, _ as NeetruDbEngine, $ as NeetruDbError, a0 as NeetruDbErrorCode, a1 as NeetruDbOptions, a2 as NeetruDbTransport, a3 as NeetruEnv, a4 as NeetruSqlClient, a5 as NeetruUser, a6 as NotificationScopeOptions, a7 as NotificationsNamespace, a8 as Product, a9 as ProductNotification, aa as ProductNotificationSeverity, ab as ProductPlan, ac as ProductStatus, ae as RedirectCallbackResult, af as RegisterWebhookInput, ag as ResolvedConfig, ah as SendNotificationInput, ai as ServerTimestampSentinel, aj as SignInOptions, al as SupportNamespace, am as SupportSeverity, an as SupportStatus, ao as SupportTicket, ar as TelemetryEventAck, as as TelemetryEventInput, aw as UsageEventInput, ax as UsageNamespace, ay as UsageQuota, az as VerifyTokenOptions, aA as VerifyWebhookRequestOptions, aB as VerifyWebhookRequestResult, aC as WebhookEndpoint, aD as WebhookEvent, aE as WebhookScopeOptions, aF as WebhookTestResult, aG as WebhooksNamespace, aH as WithFieldSentinels, aJ as createAiNamespace, aK as createCheckoutNamespace, aL as createNeetruDb, aM as createNotificationsNamespace, aO as createWebhooksNamespace, aQ as increment, aR as isFieldSentinel, aS as isIncrementSentinel, aT as isServerTimestampSentinel, aU as serverTimestamp, aV as verifyWebhookRequest, aW as verifyWebhookSignature } from './types-BlOLDMR0.js';
|
|
4
4
|
export { DEV_FIXTURE_USER, MockAi, MockAuth, MockAuthOptions, MockDb, MockEntitlements, MockSupport, MockUsage } from './mocks.js';
|
|
5
5
|
import 'drizzle-orm/node-postgres';
|
|
6
6
|
|
|
@@ -42,6 +42,6 @@ declare function checkCoreVersionCompat(coreVersion: string): void;
|
|
|
42
42
|
*/
|
|
43
43
|
|
|
44
44
|
/** Versão do SDK — exportada explicitamente pra detecção em runtime. */
|
|
45
|
-
declare const VERSION: "3.
|
|
45
|
+
declare const VERSION: "3.4.1";
|
|
46
46
|
|
|
47
47
|
export { VERSION, checkCoreVersionCompat };
|
package/dist/index.mjs
CHANGED
|
@@ -1582,6 +1582,7 @@ function toTicket(raw) {
|
|
|
1582
1582
|
if (typeof r.id !== "string") {
|
|
1583
1583
|
throw new NeetruError("invalid_response", "Ticket missing id");
|
|
1584
1584
|
}
|
|
1585
|
+
const firstAttachment = Array.isArray(r.attachments) && typeof r.attachments[0] === "string" ? r.attachments[0] : void 0;
|
|
1585
1586
|
return {
|
|
1586
1587
|
id: r.id,
|
|
1587
1588
|
subject: typeof r.subject === "string" ? r.subject : "",
|
|
@@ -1590,9 +1591,28 @@ function toTicket(raw) {
|
|
|
1590
1591
|
status: VALID_STATUSES.includes(r.status) ? r.status : "open",
|
|
1591
1592
|
createdAt: typeof r.createdAt === "string" ? r.createdAt : (/* @__PURE__ */ new Date()).toISOString(),
|
|
1592
1593
|
updatedAt: typeof r.updatedAt === "string" ? r.updatedAt : void 0,
|
|
1593
|
-
productSlug: typeof r.productId === "string" ? r.productId : typeof r.productSlug === "string" ? r.productSlug : void 0
|
|
1594
|
+
productSlug: typeof r.productId === "string" ? r.productId : typeof r.productSlug === "string" ? r.productSlug : void 0,
|
|
1595
|
+
pageUrl: typeof r.pageUrl === "string" ? r.pageUrl : null,
|
|
1596
|
+
screenshotUrl: typeof r.screenshotUrl === "string" ? r.screenshotUrl : firstAttachment ?? null,
|
|
1597
|
+
resolvedAt: typeof r.resolvedAt === "string" ? r.resolvedAt : null,
|
|
1598
|
+
resolvedNotifiedAt: typeof r.resolvedNotifiedAt === "string" ? r.resolvedNotifiedAt : null,
|
|
1599
|
+
satisfactionAttempts: typeof r.satisfactionAttempts === "number" && Number.isFinite(r.satisfactionAttempts) ? r.satisfactionAttempts : 0,
|
|
1600
|
+
lastSatisfactionAttemptAt: typeof r.lastSatisfactionAttemptAt === "string" ? r.lastSatisfactionAttemptAt : null,
|
|
1601
|
+
satisfiedAt: typeof r.satisfiedAt === "string" ? r.satisfiedAt : null,
|
|
1602
|
+
rating: typeof r.rating === "number" && Number.isFinite(r.rating) ? r.rating : null,
|
|
1603
|
+
satisfactionComment: typeof r.satisfactionComment === "string" ? r.satisfactionComment : null
|
|
1594
1604
|
};
|
|
1595
1605
|
}
|
|
1606
|
+
function assertTicketId(ticketId) {
|
|
1607
|
+
if (!ticketId || typeof ticketId !== "string") {
|
|
1608
|
+
throw new NeetruError("validation_failed", "ticketId is required");
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
function assertValidComment(comment) {
|
|
1612
|
+
if (typeof comment === "string" && comment.length > 2e3) {
|
|
1613
|
+
throw new NeetruError("validation_failed", "comment max 2000 chars");
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1596
1616
|
function resolveProductId(config, ...candidates) {
|
|
1597
1617
|
for (const c of candidates) {
|
|
1598
1618
|
if (typeof c === "string" && c.length > 0) {
|
|
@@ -1649,6 +1669,15 @@ function createSupportNamespace(config) {
|
|
|
1649
1669
|
if (input.metadata && typeof input.metadata === "object") {
|
|
1650
1670
|
body.metadata = input.metadata;
|
|
1651
1671
|
}
|
|
1672
|
+
if (typeof input.pageUrl === "string") {
|
|
1673
|
+
body.pageUrl = input.pageUrl;
|
|
1674
|
+
}
|
|
1675
|
+
if (typeof input.screenshotBase64 === "string") {
|
|
1676
|
+
body.screenshotBase64 = input.screenshotBase64;
|
|
1677
|
+
}
|
|
1678
|
+
if (typeof input.consoleLogs === "string") {
|
|
1679
|
+
body.consoleLogs = input.consoleLogs;
|
|
1680
|
+
}
|
|
1652
1681
|
const raw = await httpRequest(config, {
|
|
1653
1682
|
method: "POST",
|
|
1654
1683
|
path: "/api/sdk/v1/support/tickets",
|
|
@@ -1674,6 +1703,74 @@ function createSupportNamespace(config) {
|
|
|
1674
1703
|
});
|
|
1675
1704
|
const list = Array.isArray(raw) ? raw : raw && typeof raw === "object" && "tickets" in raw ? raw.tickets ?? [] : [];
|
|
1676
1705
|
return list.map(toTicket);
|
|
1706
|
+
},
|
|
1707
|
+
/**
|
|
1708
|
+
* 3.4: marca a notificação de resolução como vista (`resolvedNotifiedAt`,
|
|
1709
|
+
* idempotente no Core). Endpoint canonical:
|
|
1710
|
+
* `POST /api/sdk/v1/support/tickets/{id}/resolved-seen`.
|
|
1711
|
+
*/
|
|
1712
|
+
async markResolvedSeen(ticketId) {
|
|
1713
|
+
assertTicketId(ticketId);
|
|
1714
|
+
await httpRequest(config, {
|
|
1715
|
+
method: "POST",
|
|
1716
|
+
path: `/api/sdk/v1/support/tickets/${encodeURIComponent(ticketId)}/resolved-seen`,
|
|
1717
|
+
requireAuth: true
|
|
1718
|
+
});
|
|
1719
|
+
},
|
|
1720
|
+
/**
|
|
1721
|
+
* 3.4: registra uma tentativa de exibição do modal de satisfação
|
|
1722
|
+
* (`satisfactionAttempts += 1`). Chamar no SHOW do modal, não na
|
|
1723
|
+
* resposta. Endpoint canonical:
|
|
1724
|
+
* `POST /api/sdk/v1/support/tickets/{id}/satisfaction-attempt`.
|
|
1725
|
+
*/
|
|
1726
|
+
async recordSatisfactionAttempt(ticketId) {
|
|
1727
|
+
assertTicketId(ticketId);
|
|
1728
|
+
await httpRequest(config, {
|
|
1729
|
+
method: "POST",
|
|
1730
|
+
path: `/api/sdk/v1/support/tickets/${encodeURIComponent(ticketId)}/satisfaction-attempt`,
|
|
1731
|
+
requireAuth: true
|
|
1732
|
+
});
|
|
1733
|
+
},
|
|
1734
|
+
/**
|
|
1735
|
+
* 3.4: envia (ou pula) a avaliação de satisfação (CSAT 1-5). Endpoint
|
|
1736
|
+
* canonical: `POST /api/sdk/v1/support/tickets/{id}/satisfaction`.
|
|
1737
|
+
*/
|
|
1738
|
+
async submitSatisfaction(input) {
|
|
1739
|
+
if (!input || typeof input !== "object") {
|
|
1740
|
+
throw new NeetruError("validation_failed", "input is required");
|
|
1741
|
+
}
|
|
1742
|
+
assertTicketId(input.ticketId);
|
|
1743
|
+
if (input.rating !== null && input.rating !== void 0) {
|
|
1744
|
+
if (!Number.isInteger(input.rating) || input.rating < 1 || input.rating > 5) {
|
|
1745
|
+
throw new NeetruError("validation_failed", "rating must be an integer 1-5 or null");
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
assertValidComment(input.comment);
|
|
1749
|
+
await httpRequest(config, {
|
|
1750
|
+
method: "POST",
|
|
1751
|
+
path: `/api/sdk/v1/support/tickets/${encodeURIComponent(input.ticketId)}/satisfaction`,
|
|
1752
|
+
body: {
|
|
1753
|
+
rating: input.rating ?? null,
|
|
1754
|
+
comment: input.comment ?? null,
|
|
1755
|
+
skipped: input.skipped
|
|
1756
|
+
},
|
|
1757
|
+
requireAuth: true
|
|
1758
|
+
});
|
|
1759
|
+
},
|
|
1760
|
+
/**
|
|
1761
|
+
* 3.4: contesta um ticket resolvido — reabre e zera o ciclo de
|
|
1762
|
+
* satisfação/notificação. Endpoint canonical:
|
|
1763
|
+
* `POST /api/sdk/v1/support/tickets/{id}/contest`.
|
|
1764
|
+
*/
|
|
1765
|
+
async contestTicket(ticketId, opts) {
|
|
1766
|
+
assertTicketId(ticketId);
|
|
1767
|
+
assertValidComment(opts?.comment);
|
|
1768
|
+
await httpRequest(config, {
|
|
1769
|
+
method: "POST",
|
|
1770
|
+
path: `/api/sdk/v1/support/tickets/${encodeURIComponent(ticketId)}/contest`,
|
|
1771
|
+
body: { comment: opts?.comment ?? void 0 },
|
|
1772
|
+
requireAuth: true
|
|
1773
|
+
});
|
|
1677
1774
|
}
|
|
1678
1775
|
};
|
|
1679
1776
|
}
|
|
@@ -2444,8 +2541,10 @@ var MutationQueue = class {
|
|
|
2444
2541
|
await this.store.deleteMutation(mutationId);
|
|
2445
2542
|
}
|
|
2446
2543
|
/**
|
|
2447
|
-
* Marca uma mutação como `failed` e registra o erro.
|
|
2448
|
-
* Incrementa `attempts`. Chamado pelo SyncEngine
|
|
2544
|
+
* Marca uma mutação como `failed` (dead-letter) e registra o erro.
|
|
2545
|
+
* Incrementa `attempts` e carimba `lastAttemptAt`. Chamado pelo SyncEngine
|
|
2546
|
+
* quando a mutação esgota o teto de tentativas (mutação-veneno): sai do
|
|
2547
|
+
* conjunto `queued`, então `drain()` para de devolvê-la e a fila DESTRAVA.
|
|
2449
2548
|
*/
|
|
2450
2549
|
async markFailed(mutationId, error) {
|
|
2451
2550
|
const mut = await this.store.getMutation(mutationId);
|
|
@@ -2454,12 +2553,14 @@ var MutationQueue = class {
|
|
|
2454
2553
|
...mut,
|
|
2455
2554
|
status: "failed",
|
|
2456
2555
|
lastError: error,
|
|
2457
|
-
attempts: mut.attempts + 1
|
|
2556
|
+
attempts: mut.attempts + 1,
|
|
2557
|
+
lastAttemptAt: Date.now()
|
|
2458
2558
|
});
|
|
2459
2559
|
}
|
|
2460
2560
|
/**
|
|
2461
|
-
* Incrementa `attempts` e volta para `queued`
|
|
2462
|
-
* Chamado pelo SyncEngine em falha
|
|
2561
|
+
* Incrementa `attempts`, carimba `lastAttemptAt` e volta para `queued`
|
|
2562
|
+
* (falha transiente com backoff). Chamado pelo SyncEngine em falha
|
|
2563
|
+
* transiente (5xx, timeout) da mutação que REALMENTE foi tentada.
|
|
2463
2564
|
*/
|
|
2464
2565
|
async markRetry(mutationId, error) {
|
|
2465
2566
|
const mut = await this.store.getMutation(mutationId);
|
|
@@ -2468,9 +2569,24 @@ var MutationQueue = class {
|
|
|
2468
2569
|
...mut,
|
|
2469
2570
|
status: "queued",
|
|
2470
2571
|
lastError: error,
|
|
2471
|
-
attempts: mut.attempts + 1
|
|
2572
|
+
attempts: mut.attempts + 1,
|
|
2573
|
+
lastAttemptAt: Date.now()
|
|
2472
2574
|
});
|
|
2473
2575
|
}
|
|
2576
|
+
/**
|
|
2577
|
+
* Reverte uma mutação `inflight` de volta a `queued` SEM tocar em `attempts`
|
|
2578
|
+
* nem `lastAttemptAt`. Usado para mutações que foram marcadas inflight mas
|
|
2579
|
+
* NUNCA chegaram a ser enviadas (estavam atrás da head no FIFO e o push
|
|
2580
|
+
* parou antes delas): contá-las como tentativa as empurraria pro dead-letter
|
|
2581
|
+
* por engano, e adiaria seu envio via backoff sem que tenham falhado.
|
|
2582
|
+
* No-op se a mutação não está mais `inflight`.
|
|
2583
|
+
*/
|
|
2584
|
+
async requeueInflight(mutationId) {
|
|
2585
|
+
const mut = await this.store.getMutation(mutationId);
|
|
2586
|
+
if (!mut) return;
|
|
2587
|
+
if (mut.status !== "inflight") return;
|
|
2588
|
+
await this.store.putMutation({ ...mut, status: "queued" });
|
|
2589
|
+
}
|
|
2474
2590
|
};
|
|
2475
2591
|
|
|
2476
2592
|
// src/db/field-value.ts
|
|
@@ -2526,6 +2642,10 @@ function hasFieldSentinel(data) {
|
|
|
2526
2642
|
}
|
|
2527
2643
|
|
|
2528
2644
|
// src/db/offline/sync-engine.ts
|
|
2645
|
+
var MAX_MUTATION_ATTEMPTS = 8;
|
|
2646
|
+
var BACKOFF_BASE_MS = 1e3;
|
|
2647
|
+
var BACKOFF_CAP_MS = 5 * 60 * 1e3;
|
|
2648
|
+
var TRUNCATED_COLLECTIONS_META_KEY = "truncatedCollections";
|
|
2529
2649
|
function hasSentinels(payload) {
|
|
2530
2650
|
return payload !== null && hasFieldSentinel(payload);
|
|
2531
2651
|
}
|
|
@@ -2560,6 +2680,8 @@ var SyncEngine = class {
|
|
|
2560
2680
|
_state;
|
|
2561
2681
|
/** Listeners de mudança de SyncState. */
|
|
2562
2682
|
_stateListeners = /* @__PURE__ */ new Set();
|
|
2683
|
+
/** Listeners de erro de ciclo de sync (P2-C — não mais engolido em silêncio). */
|
|
2684
|
+
_errorListeners = /* @__PURE__ */ new Set();
|
|
2563
2685
|
/** Cleanup functions dos listeners externos. */
|
|
2564
2686
|
_cleanups = [];
|
|
2565
2687
|
/** Timer de resync periódico. */
|
|
@@ -2618,6 +2740,19 @@ var SyncEngine = class {
|
|
|
2618
2740
|
this._stateListeners.delete(cb);
|
|
2619
2741
|
};
|
|
2620
2742
|
}
|
|
2743
|
+
/**
|
|
2744
|
+
* Registra um hook de erro de sync (P2-C). Disparado quando um ciclo falha
|
|
2745
|
+
* (exceção inesperada nas fases, falha de pull/resync ou dead-letter de uma
|
|
2746
|
+
* mutação-veneno) — o que antes era engolido com "log silencioso".
|
|
2747
|
+
* O mesmo diagnóstico também aparece em `SyncState.lastError`.
|
|
2748
|
+
* Retorna uma função de unsubscribe.
|
|
2749
|
+
*/
|
|
2750
|
+
onError(cb) {
|
|
2751
|
+
this._errorListeners.add(cb);
|
|
2752
|
+
return () => {
|
|
2753
|
+
this._errorListeners.delete(cb);
|
|
2754
|
+
};
|
|
2755
|
+
}
|
|
2621
2756
|
/**
|
|
2622
2757
|
* Força um sync imediato (I3 §10.3 `flush()`).
|
|
2623
2758
|
* Resolve quando a fila esvazia OU rejeita se o engine estiver destruído.
|
|
@@ -2650,8 +2785,10 @@ var SyncEngine = class {
|
|
|
2650
2785
|
}
|
|
2651
2786
|
await this._phase2Pull();
|
|
2652
2787
|
this._phase3Realtime();
|
|
2653
|
-
this._updateState({ status: "idle", lastSyncedAt: Date.now() });
|
|
2788
|
+
this._updateState({ status: "idle", lastSyncedAt: Date.now(), lastError: null });
|
|
2654
2789
|
} catch (err) {
|
|
2790
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2791
|
+
this._recordSyncError("cycle", `ciclo de sync falhou: ${msg}`);
|
|
2655
2792
|
this._updateState({ status: "idle" });
|
|
2656
2793
|
} finally {
|
|
2657
2794
|
this._syncing = false;
|
|
@@ -2686,6 +2823,9 @@ var SyncEngine = class {
|
|
|
2686
2823
|
async _phase1Push() {
|
|
2687
2824
|
const mutations = await this._queue.drain();
|
|
2688
2825
|
if (mutations.length === 0) return true;
|
|
2826
|
+
if (this._isInBackoff(mutations[0])) {
|
|
2827
|
+
return true;
|
|
2828
|
+
}
|
|
2689
2829
|
for (const mut of mutations) {
|
|
2690
2830
|
await this._queue.markInflight(mut.mutationId);
|
|
2691
2831
|
}
|
|
@@ -2693,10 +2833,16 @@ var SyncEngine = class {
|
|
|
2693
2833
|
try {
|
|
2694
2834
|
result = await this._transport.pushMutations(mutations);
|
|
2695
2835
|
} catch (err) {
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2836
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
2837
|
+
const busChangesOnThrow = [];
|
|
2838
|
+
const [head, ...behind] = mutations;
|
|
2839
|
+
await this._retryOrDeadLetter(head, errMsg, busChangesOnThrow);
|
|
2840
|
+
for (const mut of behind) {
|
|
2841
|
+
await this._queue.requeueInflight(mut.mutationId);
|
|
2842
|
+
}
|
|
2843
|
+
if (busChangesOnThrow.length > 0) this._bus.emit(busChangesOnThrow);
|
|
2844
|
+
this._refreshPendingWrites().catch(() => {
|
|
2845
|
+
});
|
|
2700
2846
|
return false;
|
|
2701
2847
|
}
|
|
2702
2848
|
const busChanges = [];
|
|
@@ -2723,8 +2869,16 @@ var SyncEngine = class {
|
|
|
2723
2869
|
}
|
|
2724
2870
|
const processedIds = new Set(result.results.map((r) => r.mutationId));
|
|
2725
2871
|
const unprocessed = mutations.filter((m) => !processedIds.has(m.mutationId));
|
|
2726
|
-
|
|
2727
|
-
|
|
2872
|
+
if (unprocessed.length > 0) {
|
|
2873
|
+
const [boundary, ...behind] = unprocessed;
|
|
2874
|
+
await this._retryOrDeadLetter(
|
|
2875
|
+
boundary,
|
|
2876
|
+
"partial_push: muta\xE7\xE3o n\xE3o processada no lote",
|
|
2877
|
+
busChanges
|
|
2878
|
+
);
|
|
2879
|
+
for (const mut of behind) {
|
|
2880
|
+
await this._queue.requeueInflight(mut.mutationId);
|
|
2881
|
+
}
|
|
2728
2882
|
}
|
|
2729
2883
|
if (busChanges.length > 0) {
|
|
2730
2884
|
this._bus.emit(busChanges);
|
|
@@ -2916,6 +3070,67 @@ var SyncEngine = class {
|
|
|
2916
3070
|
});
|
|
2917
3071
|
}
|
|
2918
3072
|
}
|
|
3073
|
+
// ─── Dead-letter + backoff (P1-C) ────────────────────────────────────────────
|
|
3074
|
+
/**
|
|
3075
|
+
* Trata a falha transiente de UMA mutação que foi realmente tentada.
|
|
3076
|
+
*
|
|
3077
|
+
* - Se ainda há orçamento de tentativas → `markRetry` (volta pra fila com
|
|
3078
|
+
* backoff, `attempts++`).
|
|
3079
|
+
* - Se esgotou (`attempts+1 >= MAX_MUTATION_ATTEMPTS`) → DEAD-LETTER:
|
|
3080
|
+
* `markFailed` (sai do conjunto `queued`, então `drain()` para de devolvê-la
|
|
3081
|
+
* e a fila FIFO DESTRAVA para as demais coleções) + grava um ConflictRecord
|
|
3082
|
+
* `failed_max_attempts` (o produto vê via `getConflicts()`) + limpa o estado
|
|
3083
|
+
* pending do doc no cache + registra `lastError`/dispara `onError`.
|
|
3084
|
+
*/
|
|
3085
|
+
async _retryOrDeadLetter(mut, errMsg, busChanges) {
|
|
3086
|
+
if (mut.attempts + 1 < MAX_MUTATION_ATTEMPTS) {
|
|
3087
|
+
await this._queue.markRetry(mut.mutationId, errMsg);
|
|
3088
|
+
return;
|
|
3089
|
+
}
|
|
3090
|
+
await this._queue.markFailed(mut.mutationId, errMsg);
|
|
3091
|
+
const conflictRecord = {
|
|
3092
|
+
collection: mut.collection,
|
|
3093
|
+
docId: mut.docId,
|
|
3094
|
+
mutationId: mut.mutationId,
|
|
3095
|
+
losingData: mut.payload ?? {},
|
|
3096
|
+
winningData: {},
|
|
3097
|
+
reason: "failed_max_attempts",
|
|
3098
|
+
detectedAt: Date.now(),
|
|
3099
|
+
delivered: false
|
|
3100
|
+
};
|
|
3101
|
+
await this._store.appendConflict(conflictRecord);
|
|
3102
|
+
const existing = await this._store.getDoc(mut.collection, mut.docId);
|
|
3103
|
+
if (existing) {
|
|
3104
|
+
await this._store.putDoc({
|
|
3105
|
+
...existing,
|
|
3106
|
+
meta: { ...existing.meta, state: "synced", pendingMutationIds: [] }
|
|
3107
|
+
});
|
|
3108
|
+
busChanges.push({
|
|
3109
|
+
type: "modified",
|
|
3110
|
+
collection: mut.collection,
|
|
3111
|
+
doc: { id: mut.docId, data: existing.data }
|
|
3112
|
+
});
|
|
3113
|
+
}
|
|
3114
|
+
this._recordSyncError(
|
|
3115
|
+
"push",
|
|
3116
|
+
`muta\xE7\xE3o ${mut.mutationId} (${mut.collection}/${mut.docId}) descartada ap\xF3s ${mut.attempts + 1} tentativas: ${errMsg}`
|
|
3117
|
+
);
|
|
3118
|
+
}
|
|
3119
|
+
/**
|
|
3120
|
+
* `true` se a mutação (head do FIFO) ainda está dentro da janela de backoff
|
|
3121
|
+
* desde a última tentativa falha. Só se aplica a mutações já tentadas
|
|
3122
|
+
* (`attempts > 0` e `lastAttemptAt` carimbado).
|
|
3123
|
+
*/
|
|
3124
|
+
_isInBackoff(mut) {
|
|
3125
|
+
if (mut.attempts <= 0) return false;
|
|
3126
|
+
const last = mut.lastAttemptAt;
|
|
3127
|
+
if (typeof last !== "number") return false;
|
|
3128
|
+
return Date.now() < last + this._backoffDelayMs(mut.attempts);
|
|
3129
|
+
}
|
|
3130
|
+
/** Backoff exponencial keyed em `attempts`, com teto `BACKOFF_CAP_MS`. */
|
|
3131
|
+
_backoffDelayMs(attempts) {
|
|
3132
|
+
return Math.min(BACKOFF_BASE_MS * 2 ** (attempts - 1), BACKOFF_CAP_MS);
|
|
3133
|
+
}
|
|
2919
3134
|
// ─── FASE 2 — pull (reconciliar o cache) ─────────────────────────────────────
|
|
2920
3135
|
/**
|
|
2921
3136
|
* Busca mudanças do servidor e aplica ao cache via ConflictResolver.
|
|
@@ -2955,10 +3170,14 @@ var SyncEngine = class {
|
|
|
2955
3170
|
}
|
|
2956
3171
|
const busChanges = [];
|
|
2957
3172
|
await this._applyServerDocs(resyncResult.docs, busChanges, true);
|
|
3173
|
+
const truncatedCollections = resyncResult.truncatedCollections ?? [];
|
|
3174
|
+
const truncatedSet = new Set(truncatedCollections);
|
|
3175
|
+
await this._store.setMeta(TRUNCATED_COLLECTIONS_META_KEY, truncatedCollections);
|
|
2958
3176
|
const returnedKeys = new Set(
|
|
2959
3177
|
resyncResult.docs.map((d) => `${d.collection}::${d.id}`)
|
|
2960
3178
|
);
|
|
2961
3179
|
for (const col of collections) {
|
|
3180
|
+
if (truncatedSet.has(col)) continue;
|
|
2962
3181
|
const queryResult = await this._store.listDocs(col, {});
|
|
2963
3182
|
for (const { id } of queryResult.docs) {
|
|
2964
3183
|
const key = `${col}::${id}`;
|
|
@@ -3118,7 +3337,8 @@ var SyncEngine = class {
|
|
|
3118
3337
|
status: isOnline ? "idle" : "offline",
|
|
3119
3338
|
pendingWrites: 0,
|
|
3120
3339
|
lastSyncedAt: null,
|
|
3121
|
-
isLeaderTab: this._tabCoordinator.isLeader()
|
|
3340
|
+
isLeaderTab: this._tabCoordinator.isLeader(),
|
|
3341
|
+
lastError: null
|
|
3122
3342
|
};
|
|
3123
3343
|
}
|
|
3124
3344
|
/**
|
|
@@ -3128,7 +3348,7 @@ var SyncEngine = class {
|
|
|
3128
3348
|
_updateState(partial) {
|
|
3129
3349
|
const prev = this._state;
|
|
3130
3350
|
const next = { ...prev, ...partial };
|
|
3131
|
-
const changed = prev.status !== next.status || prev.pendingWrites !== next.pendingWrites || prev.lastSyncedAt !== next.lastSyncedAt || prev.isLeaderTab !== next.isLeaderTab;
|
|
3351
|
+
const changed = prev.status !== next.status || prev.pendingWrites !== next.pendingWrites || prev.lastSyncedAt !== next.lastSyncedAt || prev.isLeaderTab !== next.isLeaderTab || (prev.lastError ?? null) !== (next.lastError ?? null);
|
|
3132
3352
|
this._state = next;
|
|
3133
3353
|
if (changed) {
|
|
3134
3354
|
this._emitState(next);
|
|
@@ -3142,6 +3362,21 @@ var SyncEngine = class {
|
|
|
3142
3362
|
}
|
|
3143
3363
|
}
|
|
3144
3364
|
}
|
|
3365
|
+
/**
|
|
3366
|
+
* P2-C: registra a falha de um ciclo de sync — antes engolida em silêncio.
|
|
3367
|
+
* Publica em `SyncState.lastError` (visível via `onSyncStateChanged`) e
|
|
3368
|
+
* dispara o hook `onError`. Nunca lança (diagnóstico é best-effort).
|
|
3369
|
+
*/
|
|
3370
|
+
_recordSyncError(phase, message) {
|
|
3371
|
+
this._updateState({ lastError: message });
|
|
3372
|
+
const event = { phase, message, at: Date.now() };
|
|
3373
|
+
for (const listener of this._errorListeners) {
|
|
3374
|
+
try {
|
|
3375
|
+
listener({ ...event });
|
|
3376
|
+
} catch {
|
|
3377
|
+
}
|
|
3378
|
+
}
|
|
3379
|
+
}
|
|
3145
3380
|
/**
|
|
3146
3381
|
* Atualiza `pendingWrites` no estado.
|
|
3147
3382
|
* Chamado de forma assíncrona quando necessário.
|
|
@@ -3951,7 +4186,9 @@ var DbCollectionRefImpl = class {
|
|
|
3951
4186
|
const isOnline = this._isOnline();
|
|
3952
4187
|
const syncState = this._engine.getSyncState();
|
|
3953
4188
|
const nextCursor = buildNextCursor(result.docs, effectiveLimit);
|
|
3954
|
-
const
|
|
4189
|
+
const truncatedRaw = await this._store.getMeta(TRUNCATED_COLLECTIONS_META_KEY);
|
|
4190
|
+
const truncated = Array.isArray(truncatedRaw) && truncatedRaw.includes(this._collection);
|
|
4191
|
+
const stale = !isOnline || syncState.status === "offline" || truncated;
|
|
3955
4192
|
const fromCache = hasPending || syncState.lastSyncedAt === null || stale;
|
|
3956
4193
|
return {
|
|
3957
4194
|
docs: result.docs.map((d) => ({ id: d.id, data: d.data })),
|
|
@@ -3959,7 +4196,8 @@ var DbCollectionRefImpl = class {
|
|
|
3959
4196
|
fromCache,
|
|
3960
4197
|
stale,
|
|
3961
4198
|
hasPendingWrites: hasPending,
|
|
3962
|
-
changes: []
|
|
4199
|
+
changes: [],
|
|
4200
|
+
truncated
|
|
3963
4201
|
};
|
|
3964
4202
|
}
|
|
3965
4203
|
// ── CRUD ────────────────────────────────────────────────────────────────────
|
|
@@ -4405,6 +4643,9 @@ var NeetruDbDocumentsImpl = class {
|
|
|
4405
4643
|
async getConflicts() {
|
|
4406
4644
|
return this._store.listConflicts();
|
|
4407
4645
|
}
|
|
4646
|
+
onError(cb) {
|
|
4647
|
+
return this._engine.onError(cb);
|
|
4648
|
+
}
|
|
4408
4649
|
};
|
|
4409
4650
|
async function createOfflineDocumentsNamespace(opts) {
|
|
4410
4651
|
const store = isIndexedDBUnavailable() ? new MemoryStore() : new LocalStore(opts.dbName);
|
|
@@ -4880,6 +5121,7 @@ var defaultWebSocketFactory = (url) => {
|
|
|
4880
5121
|
// src/db/client-db.ts
|
|
4881
5122
|
var DATASTORE_COLLECTION_ENDPOINT = (collection) => `/api/sdk/v1/datastore/${encodeURIComponent(collection)}`;
|
|
4882
5123
|
var DATASTORE_DOC_ENDPOINT = (collection, docId) => `/api/sdk/v1/datastore/${encodeURIComponent(collection)}/${encodeURIComponent(docId)}`;
|
|
5124
|
+
var DOCUMENTS_RESYNC_PAGE_CAP = 200;
|
|
4883
5125
|
function resolveTransport(transport, opts, config) {
|
|
4884
5126
|
if (opts._transport) {
|
|
4885
5127
|
return opts._transport;
|
|
@@ -4992,6 +5234,7 @@ function createRestSyncTransport(config) {
|
|
|
4992
5234
|
const tenantHeaders = tenantScopeId ? { "x-neetru-tenant": tenantScopeId } : void 0;
|
|
4993
5235
|
const now = Date.now();
|
|
4994
5236
|
const docs = [];
|
|
5237
|
+
const truncatedCollections = [];
|
|
4995
5238
|
for (const collection of collections) {
|
|
4996
5239
|
let raw;
|
|
4997
5240
|
try {
|
|
@@ -5001,11 +5244,11 @@ function createRestSyncTransport(config) {
|
|
|
5001
5244
|
requireAuth: true,
|
|
5002
5245
|
retries: 2,
|
|
5003
5246
|
headers: tenantHeaders,
|
|
5004
|
-
// Teto do Core
|
|
5005
|
-
//
|
|
5006
|
-
// deleção-por-ausência
|
|
5007
|
-
//
|
|
5008
|
-
query: { limit:
|
|
5247
|
+
// Teto do Core (DOCUMENTS_RESYNC_PAGE_CAP). Coleções acima dele vêm
|
|
5248
|
+
// TRUNCADAS — reportadas em `truncatedCollections` p/ o SyncEngine
|
|
5249
|
+
// (a) NÃO rodar deleção-por-ausência e (b) sinalizar
|
|
5250
|
+
// `DbListResult.truncated`. Paginação por cursor é tarefa Core (gated).
|
|
5251
|
+
query: { limit: DOCUMENTS_RESYNC_PAGE_CAP }
|
|
5009
5252
|
});
|
|
5010
5253
|
} catch (err) {
|
|
5011
5254
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -5016,6 +5259,9 @@ function createRestSyncTransport(config) {
|
|
|
5016
5259
|
}
|
|
5017
5260
|
const resp = raw;
|
|
5018
5261
|
const items = Array.isArray(resp?.items) ? resp.items : [];
|
|
5262
|
+
if (items.length >= DOCUMENTS_RESYNC_PAGE_CAP) {
|
|
5263
|
+
truncatedCollections.push(collection);
|
|
5264
|
+
}
|
|
5019
5265
|
for (const item of items) {
|
|
5020
5266
|
if (!item || typeof item !== "object") continue;
|
|
5021
5267
|
const record = item;
|
|
@@ -5043,7 +5289,7 @@ function createRestSyncTransport(config) {
|
|
|
5043
5289
|
});
|
|
5044
5290
|
}
|
|
5045
5291
|
}
|
|
5046
|
-
return { docs, newWatermark: now };
|
|
5292
|
+
return { docs, newWatermark: now, truncatedCollections };
|
|
5047
5293
|
}
|
|
5048
5294
|
};
|
|
5049
5295
|
}
|
|
@@ -5105,7 +5351,20 @@ function buildTicketProvider(config, opts) {
|
|
|
5105
5351
|
return ticket;
|
|
5106
5352
|
};
|
|
5107
5353
|
}
|
|
5354
|
+
var _hasWarnedDbIdIgnored = false;
|
|
5355
|
+
function warnDbIdIgnoredOnce(config, dbOpts) {
|
|
5356
|
+
if (_hasWarnedDbIdIgnored) return;
|
|
5357
|
+
if (!dbOpts.dbId) return;
|
|
5358
|
+
if (config.env === "prod") return;
|
|
5359
|
+
if (typeof window === "undefined") return;
|
|
5360
|
+
if (typeof console === "undefined" || typeof console.warn !== "function") return;
|
|
5361
|
+
_hasWarnedDbIdIgnored = true;
|
|
5362
|
+
console.warn(
|
|
5363
|
+
'[neetru/sdk] client.db recebeu `dbId` ("' + dbOpts.dbId + '") mas ele N\xC3O roteia nem escopa nada no servidor: as escritas do mundo Documentos (add/set/update/remove/list) SEMPRE caem no datastore gen\xE9rico do Core, escopado s\xF3 por tenant/produto (header x-neetru-tenant). `dbId` apenas nomeia o cache offline LOCAL (IndexedDB). ATEN\xC7\xC3O: usar dbIds distintos no MESMO tenant/produto N\xC3O isola os dados no servidor \u2014 eles se misturam (co-mingling). Veja o JSDoc de NeetruDbOptions.dbId.'
|
|
5364
|
+
);
|
|
5365
|
+
}
|
|
5108
5366
|
function createNeetruDb(config, dbOpts = {}) {
|
|
5367
|
+
warnDbIdIgnoredOnce(config, dbOpts);
|
|
5109
5368
|
let resolvedTransport;
|
|
5110
5369
|
if (dbOpts.transport !== void 0) {
|
|
5111
5370
|
resolvedTransport = dbOpts.transport;
|
|
@@ -5183,6 +5442,20 @@ function createNeetruDb(config, dbOpts = {}) {
|
|
|
5183
5442
|
async getConflicts() {
|
|
5184
5443
|
const docs = await getDocsPromise();
|
|
5185
5444
|
return docs.getConflicts();
|
|
5445
|
+
},
|
|
5446
|
+
onError(cb) {
|
|
5447
|
+
if (_docsResolved) return _docsResolved.onError(cb);
|
|
5448
|
+
let unsub = null;
|
|
5449
|
+
let cancelled = false;
|
|
5450
|
+
getDocsPromise().then((docs) => {
|
|
5451
|
+
if (!cancelled) {
|
|
5452
|
+
unsub = docs.onError(cb);
|
|
5453
|
+
}
|
|
5454
|
+
});
|
|
5455
|
+
return () => {
|
|
5456
|
+
cancelled = true;
|
|
5457
|
+
unsub?.();
|
|
5458
|
+
};
|
|
5186
5459
|
}
|
|
5187
5460
|
};
|
|
5188
5461
|
}
|
|
@@ -6615,7 +6888,18 @@ var MockSupport = class {
|
|
|
6615
6888
|
severity: input.severity ?? "normal",
|
|
6616
6889
|
status: "open",
|
|
6617
6890
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6618
|
-
productSlug: input.productSlug
|
|
6891
|
+
productSlug: input.productSlug,
|
|
6892
|
+
// 3.4 (product-feedback): espelha o contrato — campos novos nullable,
|
|
6893
|
+
// populados a partir do input quando fornecidos.
|
|
6894
|
+
pageUrl: typeof input.pageUrl === "string" ? input.pageUrl : null,
|
|
6895
|
+
screenshotUrl: typeof input.screenshotBase64 === "string" ? input.screenshotBase64 : null,
|
|
6896
|
+
resolvedAt: null,
|
|
6897
|
+
resolvedNotifiedAt: null,
|
|
6898
|
+
satisfactionAttempts: 0,
|
|
6899
|
+
lastSatisfactionAttemptAt: null,
|
|
6900
|
+
satisfiedAt: null,
|
|
6901
|
+
rating: null,
|
|
6902
|
+
satisfactionComment: null
|
|
6619
6903
|
};
|
|
6620
6904
|
this._tickets.push(ticket);
|
|
6621
6905
|
return ticket;
|
|
@@ -6626,6 +6910,62 @@ var MockSupport = class {
|
|
|
6626
6910
|
}
|
|
6627
6911
|
return [...this._tickets];
|
|
6628
6912
|
}
|
|
6913
|
+
_find(ticketId) {
|
|
6914
|
+
if (!ticketId || typeof ticketId !== "string") {
|
|
6915
|
+
throw new NeetruError("validation_failed", "ticketId is required");
|
|
6916
|
+
}
|
|
6917
|
+
const ticket = this._tickets.find((t) => t.id === ticketId);
|
|
6918
|
+
if (!ticket) {
|
|
6919
|
+
throw new NeetruError("not_found", `ticket ${ticketId} not found`);
|
|
6920
|
+
}
|
|
6921
|
+
return ticket;
|
|
6922
|
+
}
|
|
6923
|
+
/** 3.4: espelha `markResolvedSeen` — idempotente (só seta se ainda null). */
|
|
6924
|
+
async markResolvedSeen(ticketId) {
|
|
6925
|
+
const ticket = this._find(ticketId);
|
|
6926
|
+
if (ticket.resolvedNotifiedAt === null) {
|
|
6927
|
+
ticket.resolvedNotifiedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6928
|
+
}
|
|
6929
|
+
}
|
|
6930
|
+
/** 3.4: espelha `recordSatisfactionAttempt` — incrementa contador. */
|
|
6931
|
+
async recordSatisfactionAttempt(ticketId) {
|
|
6932
|
+
const ticket = this._find(ticketId);
|
|
6933
|
+
ticket.satisfactionAttempts += 1;
|
|
6934
|
+
ticket.lastSatisfactionAttemptAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6935
|
+
}
|
|
6936
|
+
/** 3.4: espelha `submitSatisfaction` — terminal, seta `satisfiedAt`. */
|
|
6937
|
+
async submitSatisfaction(input) {
|
|
6938
|
+
if (!input || typeof input !== "object") {
|
|
6939
|
+
throw new NeetruError("validation_failed", "input is required");
|
|
6940
|
+
}
|
|
6941
|
+
if (input.rating !== null && input.rating !== void 0) {
|
|
6942
|
+
if (!Number.isInteger(input.rating) || input.rating < 1 || input.rating > 5) {
|
|
6943
|
+
throw new NeetruError("validation_failed", "rating must be an integer 1-5 or null");
|
|
6944
|
+
}
|
|
6945
|
+
}
|
|
6946
|
+
if (typeof input.comment === "string" && input.comment.length > 2e3) {
|
|
6947
|
+
throw new NeetruError("validation_failed", "comment max 2000 chars");
|
|
6948
|
+
}
|
|
6949
|
+
const ticket = this._find(input.ticketId);
|
|
6950
|
+
ticket.satisfiedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6951
|
+
ticket.rating = input.rating ?? null;
|
|
6952
|
+
ticket.satisfactionComment = input.comment ?? null;
|
|
6953
|
+
}
|
|
6954
|
+
/** 3.4: espelha `contestTicket` — reabre e zera o ciclo de satisfação. */
|
|
6955
|
+
async contestTicket(ticketId, opts) {
|
|
6956
|
+
if (typeof opts?.comment === "string" && opts.comment.length > 2e3) {
|
|
6957
|
+
throw new NeetruError("validation_failed", "comment max 2000 chars");
|
|
6958
|
+
}
|
|
6959
|
+
const ticket = this._find(ticketId);
|
|
6960
|
+
ticket.status = "open";
|
|
6961
|
+
ticket.resolvedAt = null;
|
|
6962
|
+
ticket.resolvedNotifiedAt = null;
|
|
6963
|
+
ticket.satisfactionAttempts = 0;
|
|
6964
|
+
ticket.lastSatisfactionAttemptAt = null;
|
|
6965
|
+
ticket.satisfiedAt = null;
|
|
6966
|
+
ticket.rating = null;
|
|
6967
|
+
ticket.satisfactionComment = null;
|
|
6968
|
+
}
|
|
6629
6969
|
/** Test helper. */
|
|
6630
6970
|
__reset() {
|
|
6631
6971
|
this._tickets = [];
|
|
@@ -7662,7 +8002,7 @@ function createNeetruClient(config = {}) {
|
|
|
7662
8002
|
init_errors();
|
|
7663
8003
|
init_db_errors();
|
|
7664
8004
|
init_http();
|
|
7665
|
-
var VERSION = "3.
|
|
8005
|
+
var VERSION = "3.4.1";
|
|
7666
8006
|
|
|
7667
8007
|
export { DEFAULT_BASE_URL, DEV_FIXTURE_USER, MockAi, MockAuth, MockCheckout, MockDb, MockEntitlements, MockNotifications, MockSupport, MockUsage, MockWebhooks, NEETRU_SENTINEL_KEY, NeetruDbError, NeetruError, VERSION, checkCoreVersionCompat, createAiNamespace, createCheckoutNamespace, createNeetruClient, createNeetruDb, createNotificationsNamespace, createWebhooksNamespace, increment, isFieldSentinel, isIncrementSentinel, isServerTimestampSentinel, serverTimestamp, verifyWebhookRequest, verifyWebhookSignature };
|
|
7668
8008
|
//# sourceMappingURL=index.mjs.map
|