v_chat_core 0.1.0-rc.3 copy "v_chat_core: ^0.1.0-rc.3" to clipboard
v_chat_core: ^0.1.0-rc.3 copied to clipboard

Pure Dart runtime foundation for the V Chat end-user SDK.

v_chat_core #

Release candidate 0.1.0-rc.3 is the pure-Dart runtime package in the coordinated V Chat Flutter SDK. Add it with dart pub add v_chat_core. The package supports Dart >=3.10.0-0 <4.0.0.

See the repository support, security, and privacy policies before production adoption.

Pure Dart foundations for V Chat:

  • app-user token lifecycle
  • Dio REST transport
  • WebSocket transport
  • reconnect supervision
  • scoped offline ports
  • stable exceptions and redacted logging
  • lifecycle-safe client shell
  • typed current-user device registration list/upsert/revoke
  • typed current-user push preferences and content-light notification routing
  • typed capability-gated privacy export/delete jobs and ephemeral download metadata
  • typed app-user channel create/get/update
  • scoped exact channel cache, bounded local pages, and cache-then-network snapshots
  • typed active current-user membership projection and scoped offline reconciliation
  • typed top-level message lifecycle and confirmed exact-scope history
  • typed attachment lifecycle, isolated signed-target transfers, and scoped metadata-only cache
  • typed conversation/read/unread projection and confirmed exact-scope offline load
  • authenticated realtime ticket, ready, close mapping, and bounded reconnect lifecycle
  • acknowledged authorized channel subscriptions and connection-local handles
  • typed durable channel/private events with cache-before-callback processing
  • strict realtime frame/command/capacity limits and redacted pressure snapshots
  • immutable client lifecycle snapshots and stable channel/conversation/thread resource facades

Push integration consumes only the released FTR-011 device operations and FTR-027 preference and content-light payload contracts. Provider configuration, native permission prompts, token discovery, presentation, and delivery execution remain host/server concerns. The SDK validates an exact six-field notification route and durably deduplicates it by connected app-user scope before publication.

Privacy lifecycle integration consumes only the four released FTR-028 current-app-user operations. It requires the opt-in self_data_lifecycle token capability, exposes no dashboard retention administration, persists no job or signed URL, and invalidates the SDK session after self-delete.

The additive stable facade binds immutable resource identities while retaining the flat VChatClient methods:

final VChatChannelHandle channel = client.channel(identity);
final VChatConversationHandle conversation = channel.conversation;
final VChatThreadHandle thread = channel.thread(rootMessageId);

final lifecycle = client.snapshots.listen(renderClientState);
final cachedConversations = client.conversations.snapshots(limit: 25).listen(
  renderCachedConversations,
);
try {
  await client.conversations.refresh();
  final current = await channel.fetch();
  final replies = await thread.fetch(limit: 25);
} finally {
  await cachedConversations.cancel();
  await lifecycle.cancel();
}

Resource interfaces delegate to the same released validation, authorization, cancellation, cache, and realtime code as the flat methods. Creating a handle performs no I/O. Cached conversation streams use one validated app-user scope, start no network request, and close on disconnect, disposal, or user switching.

Channel lifecycle consumes only the released FTR-012 app-user operations:

final identity = VChatChannelIdentity(
  channelTypeKey: 'messaging',
  channelId: 'support-123',
);
final created = await client.createChannel(
  identity: identity,
  request: VChatChannelCreateRequest(
    displayName: const VChatOptionalValue<String>.value('Support'),
  ),
);
final current = await client.getChannel(identity: identity);
final updated = await client.updateChannel(
  identity: identity,
  request: VChatChannelUpdateRequest(
    expectedVersion: current.channel.version,
    displayName: const VChatOptionalValue<String>.value('Priority support'),
  ),
);
final cachedPage = await client.queryCachedChannels(
  channelTypeKey: 'messaging',
  limit: 25,
);
await for (final snapshot in client.watchChannel(identity: identity)) {
  renderChannel(snapshot);
}

watchChannel emits an optional cached snapshot and one post-response reconciliation snapshot, then closes. A lower server version retains the existing cache source/staleness metadata. Cached pages are local presentation state and never imply server completeness or current authorization. The server's remote queryChannels operation requires an app-server credential and is deliberately not exposed by this end-user package. The released channel response also has no capability field, so the SDK does not infer one.

The membership projection uses only the released app-user conversation query:

final page = await client.queryCurrentUserMemberships(limit: 25);
final next = page.nextCursor == null
    ? null
    : await client.queryCurrentUserMemberships(
        limit: page.nextCursor!.limit,
        cursor: page.nextCursor,
      );
final refreshed = await client.refreshCurrentUserMemberships();
final cached = await client.queryCachedCurrentUserMemberships(limit: 25);

refreshCurrentUserMemberships traverses at most 500 rows and deletes missing cached rows only after a terminal fresh traversal. Membership add/update/remove and roster operations remain on the customer backend; opaque cursors are never persisted by the SDK.

Messages use the released FTR-014 top-level operations and FTR-023 direct-thread operations:

final sent = await client.sendMessage(
  channel: identity,
  request: VChatMessageSendRequest(text: 'Hello'),
);
final page = await client.queryMessages(channel: identity, limit: 25);
final updated = await client.updateMessage(
  channel: identity,
  messageId: sent.message.messageId,
  request: VChatMessageUpdateRequest(
    expectedVersion: sent.message.version,
    text: const VChatOptionalValue<String>.value('Updated'),
  ),
);
final cached = await client.queryCachedMessages(channel: identity, limit: 25);
await client.deleteMessage(
  channel: identity,
  messageId: updated.message.messageId,
);

final reply = await client.sendMessageReply(
  channel: identity,
  rootMessageId: sent.message.messageId,
  request: VChatMessageSendRequest(text: 'Direct reply'),
);
final thread = await client.getMessageThread(
  channel: identity,
  rootMessageId: sent.message.messageId,
  limit: 25,
);
final cachedThread = await client.getCachedMessageThread(
  channel: identity,
  rootMessageId: sent.message.messageId,
);
await client.markMessageThreadRead(
  channel: identity,
  rootMessageId: sent.message.messageId,
  replyMessageId: reply.message.messageId,
);

Only confirmed server representations and tombstones are durable. Remote cursors, pending message content, and mutation commands are not persisted or automatically replayed. Ordinary message history contains top-level messages only; direct replies are stored under their root and become visible through a coherent completed thread snapshot. Reactions and attachments remain separate resource slices and are not inferred onto message snapshots.

Attachments use only the five released FTR-026 app-user operations:

final intent = await client.attachments.createUploadIntent(
  request: VChatAttachmentUploadIntentRequest(
    attachmentId: callerGeneratedUuidV4,
    kind: VChatAttachmentKind.file,
    fileName: 'report.pdf',
    mimeType: 'application/pdf',
    sizeBytes: sourceLength,
  ),
);
await client.attachments.upload(
  target: intent.upload!,
  source: source,
  contentLength: sourceLength,
);
final completed = await client.attachments.completeUpload(
  attachmentId: intent.attachment.attachmentId,
);

The source and destination are consumed once. Signed URLs and headers use a separate unauthenticated transport and never enter cache, logs, errors, or retry state. Drift stores only validated metadata and deletion tombstones; attachment bytes and offline mutations are excluded.

Conversation and unread state use only the released FTR-016 app-user operations:

final conversations = await client.queryCurrentUserConversations(limit: 25);
final unread = await client.getCurrentUserUnreadSummary();
final latest = conversations.items.first.latestMessage;
if (latest != null) {
  await client.markChannelRead(
    channel: conversations.items.first.channel.identity,
    readThroughMessageId: latest.messageId,
  );
  await client.queryCurrentUserConversations(limit: 25);
  await client.getCurrentUserUnreadSummary();
}
final cached = await client.queryCachedCurrentUserConversations(limit: 25);
final cachedUnread = await client.getCachedCurrentUserUnreadSummary();

The conversation method is the complete SDK-FL-005 query transport; membership callers are projected from it. Remote cursors are never persisted. Mark-read is online-only, accepts an exact observed latest-message target, and never fabricates aggregate cache deltas or automatically retries an ambiguous mutation. Realtime replacement remains SDK-FL-010.

Realtime connection lifecycle uses only the released FTR-017/FTR-021 contract:

final info = await client.connectRealtime();
print(info.connectionId);

await client.suspendRealtimeIfConnected();
await client.resumeRealtimeIfSuspended();
await client.disconnectRealtime();

connectRealtime first requires connectUser, creates a fresh one-use ticket, offers only vchat.realtime.v1, and returns only after a strict connection.ready envelope. Native Ping/Pong is handled by the WebSocket runtime. Automatic reconnect uses fresh authentication and a fresh ticket with bounded jittered backoff; protocol/policy, connection-limit, and slow-consumer closes are terminal for that automatic cycle. No socket, ticket, connection, heartbeat, or retry state is persisted.

Application backgrounding may suspend ticketing, connecting, recovering, connected, or reconnecting work. If suspension wins before ready metadata can be returned, connectRealtime fails with the stable request_cancelled lifecycle code rather than fabricating a protocol error. Foreground resume still requires a lifecycle that was actually suspended.

Authorized channel subscriptions use only the released FTR-018/FTR-021 envelopes:

final subscription = await client.subscribeChannel(identity);
await for (final state in subscription.states) {
  renderSubscriptionState(state);
}
await subscription.unsubscribe();

The handle becomes active only after a strict matching acknowledgement. Concurrent calls for one channel share the same handle; authority changes produce only revoked; cancellation compensates an uncertain activation; and fresh reconnect restoration rechecks authorization without claiming missed-event continuity. Request IDs, handles, cutover sequences, and restoration intent remain memory-only.

The five released FTR-019 event names are decoded into closed typed models. Accepted channel updates and top-level message create/update/delete events are committed to the scoped cache before global and exact-channel callbacks. Current-user conversation and unread dimensions advance independently before private callbacks. Duplicate IDs/positions are bounded in memory; stale subscription conflicts stop delivery for that handle; compatible future v1 names and invalid payloads enter bounded redacted quarantine. Register listeners before the related mutation and cancel every returned registration:

final stopGlobal = client.onRealtimeEvent(renderEvent);
final stopPrivate = client.onUserMessagingState(renderCurrentUserState);
final stopDiagnostic = client.onRealtimeProcessingDiagnostic(renderSafeIssue);
final subscription = await client.subscribeChannel(identity);
final stopExact = subscription.onEvent(renderExactChannelEvent);

// Dispose registrations before the acknowledged server subscription.
stopExact();
stopGlobal();
stopPrivate();
stopDiagnostic();
await subscription.unsubscribe();

FTR-020 reconnect reconciliation is opt-in and leaves the existing connection path unchanged:

final recovery = VChatRealtimeRecoveryOptions(
  // Persist this non-secret ID per installation/profile to recover after reload.
  clientInstanceId: 'client_abcdefghijklmnopqrstuv',
);
final info = await client.connectRealtime(recovery: recovery);
final stop = client.realtimeRecoveryResults.listen((result) {
  renderRecoveryResult(result);
});

Each accepted durable channel event commits its entity and realtime.v1.<clientInstanceId>.<cid> position atomically before callbacks. A fresh socket sends at most one 100-channel/28-KiB manifest before ordinary commands. Safe replay completes before the public state becomes connected; unsafe positions use one acknowledged channel cutover, the authoritative channel plus first 25 messages, and a 64-event/192-KiB live buffer. The separate current-user repair refreshes unread state and the first 25 conversations, then drains buffered private events. channel.subscription_revoked is terminal and neutral. Recovery never queues or replays REST mutations, never stores tokens or event payloads in checkpoints, and can be reset explicitly with resetRealtimeRecovery() while the configured app-user session is active.

clientInstanceId must be client_ plus 22 base64url characters. Omitting it generates an in-memory isolated ID; supply and persist one only when cross-reload continuity is intended. realtimeRecoveryStarts, realtimeRecoveryResults, and the recovering connection state expose bounded lifecycle information without leaking credentials or message content.

Realtime pressure is bounded by the released FTR-021 contract. Inspect only redacted counts and encoded byte totals; never use them as fleet-capacity or billing evidence:

final pressure = client.realtimePressureSnapshot;
if (pressure.isProcessing || pressure.retainedFrames > 0) {
  renderRealtimePressure(
    queuedFrames: pressure.queuedEventFrames,
    queuedBytes: pressure.queuedEventBytes,
  );
}

The production transport rejects server frames above 32 KiB and invalid ready limits. Ordinary subscription commands stay within 4 KiB, one connection owns at most 100 active/pending channel subscriptions and eight in-flight commands, and durable processing yields after eight frames or 32 KiB. A local processing overflow closes only that socket as terminal 4411 slow_consumer; correct the consumer, refresh state, then reconnect explicitly.

Reply create/update/delete events update the scoped reply entity and root thread summary before listeners run. onUserThreadState exposes only the authenticated user's private thread state after its independent version fences and offline commit. Unknown future reply events remain forward-safe and trigger the existing refresh diagnostic path.

addMessageReaction and removeMessageReaction mutate only the connected user's identity and are safe for one bounded in-memory retry; neither is written to the offline command queue. listMessageReactionUsers returns a bounded exact-type cursor page and never persists reactor membership. Full messages expose a VChatReactionSummary whose independent version is merged separately from the message edit version. Ordered reaction events update that summary before listeners run; a forward summary-version gap marks the channel unsafe for authoritative refresh.

An active VChatChannelSubscription exposes connection-local presence and typing controllers. Presence watches atomically replace 1..50 exact user IDs after acknowledgement and expire to the client-only unknown state when their maximum 120-second freshness cannot be proved. Typing supports the main channel and exact one-depth thread contexts, with at most four watches and two emitted contexts per connection. keystroke sends at most once per four seconds while a two-second local idle timer sends stop. Disconnect clears authoritative activity state and typing emissions; recovery restores watches after durable repair but never replays typing. No presence, typing, timer, or watch state is stored in Drift.

See the compile-checked Flutter examples/v_chat_example/lib/channel_lifecycle_example.dart, examples/v_chat_example/lib/realtime_lifecycle_example.dart, and examples/v_chat_example/lib/realtime_subscription_example.dart, and examples/v_chat_example/lib/realtime_events_example.dart, and examples/v_chat_example/lib/realtime_recovery_example.dart, and examples/v_chat_example/lib/thread_lifecycle_example.dart, and examples/v_chat_example/lib/reaction_lifecycle_example.dart, and examples/v_chat_example/lib/presence_typing_example.dart for Drift composition, customer-backend token loading, typed lifecycle, app background/foreground binding, neutral not-found, version-conflict, offline, cancellation, and disposal handling.

0
likes
0
points
268
downloads

Publisher

unverified uploader

Weekly Downloads

Pure Dart runtime foundation for the V Chat end-user SDK.

Homepage
Repository (GitHub)
View/report issues

Topics

#chat #dart #realtime #offline-first #sdk

License

unknown (license)

Dependencies

dio, meta, web_socket

More

Packages that depend on v_chat_core