Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions templates/web/rollup.config.mjs.twig
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ export default [
file: pkg.jsdelivr,
name: 'Appwrite',
extend: true,
// A script tag has no module loader, so the lazy mqtt.js import is inlined here.
inlineDynamicImports: true,
},
],
},
Expand Down
69 changes: 54 additions & 15 deletions templates/web/src/services/push.ts.twig
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { Buffer } from 'buffer';
import mqtt, {
type IClientOptions,
type IConnackPacket,
type IPublishPacket,
type MqttClient,
import type {
IClientOptions,
IConnackPacket,
IPublishPacket,
MqttClient,
} from 'mqtt';

import { Client } from '../client';
Expand Down Expand Up @@ -52,6 +51,31 @@ export interface PushSubscription {

type AuthMethod = 'appwrite-jwt' | 'appwrite-session' | '';

interface Transport {
connect: typeof import('mqtt').connect;
Buffer: typeof import('buffer').Buffer;
}

// Loaded on first connect, so an app that never uses Push does not bundle mqtt.js.
let transport: Promise<Transport> | null = null;

function loadTransport(): Promise<Transport> {
if (!transport) {
transport = Promise.all([import('mqtt'), import('buffer')]).then(
// The browser build of mqtt.js (dist/mqtt.esm.js) exports only a default.
([mqtt, buffer]) => ({
connect: (mqtt.default ?? mqtt).connect,
Buffer: buffer.Buffer,
}),
);
// A failed load must not poison later connects.
transport.catch(() => {
transport = null;
});
}
return transport;
}

// Fixed connection tuning — not exposed as options.
const KEEP_ALIVE_SECONDS = 60;
const RECONNECT_PERIOD_MS = 2000;
Expand Down Expand Up @@ -80,6 +104,9 @@ const RECONNECT_PERIOD_MS = 2000;
export class Push extends Service {
private mqtt: MqttClient | null = null;
private connecting: Promise<void> | null = null;
private Buffer: Transport['Buffer'] | null = null;
/** Bumped by close(), so a connect still loading mqtt.js knows to stop. */
private generation = 0;
/** Set once the first CONNACK lands, so 'connect' events after are reconnects. */
private everConnected = false;

Expand Down Expand Up @@ -417,6 +444,7 @@ export class Push extends Service {
this.mqtt = null;
}
this.connecting = null;
this.generation++;
this.everConnected = false;
this.subscriptions.clear();
}
Expand Down Expand Up @@ -512,6 +540,13 @@ export class Push extends Service {
: userIdFromSession(credential)) ||
this.fallbackClientId;

const generation = this.generation;
const { connect, Buffer } = await loadTransport();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Lazy-load cancellation lacks coverage

The new cancellation check runs after MQTT loads, but no behavioral test exercises closing Push during that load. The browser Push test uses the IIFE bundle, which inlines the import, and the Node test never connects Push. An ESM or CJS consumer test should verify that closing during the pending load does not open a connection; build and lint checks cannot catch a regression here.

Prompt To Fix With AI
This is a comment left during a code review.
Path: templates/web/src/services/push.ts.twig
Line: 543

Comment:
**Lazy-load cancellation lacks coverage**

The new cancellation check runs after MQTT loads, but no behavioral test exercises closing Push during that load. The browser Push test uses the IIFE bundle, which inlines the import, and the Node test never connects Push. An ESM or CJS consumer test should verify that closing during the pending load does not open a connection; build and lint checks cannot catch a regression here.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code Fix in Codex

if (generation !== this.generation) {
throw new Error('Push was closed while connecting.');
}
this.Buffer = Buffer;

const options: IClientOptions = {
clientId,
protocolVersion: 5,
Expand All @@ -527,7 +562,7 @@ export class Push extends Service {
},
};

const client = mqtt.connect(this.endpointUrl(), options);
const client = connect(this.endpointUrl(), options);
this.mqtt = client;

client.on('message', (_topic, _payload, packet) => {
Expand Down Expand Up @@ -627,6 +662,7 @@ export class Push extends Service {

/** Fan a delivered packet out to every subscription whose filter matches. */
private async dispatch(packet: IPublishPacket): Promise<void> {
const Buffer = this.Buffer!;
const payload = Buffer.isBuffer(packet.payload)
? packet.payload
: Buffer.from(packet.payload);
Expand Down Expand Up @@ -696,11 +732,9 @@ function userIdFromJwt(jwt?: string): string {
return '';
}
try {
const json = Buffer.from(
parts[1].replace(/-/g, '+').replace(/_/g, '/'),
'base64',
).toString('utf-8');
const payload = JSON.parse(json);
const payload = JSON.parse(
decodeBase64(parts[1].replace(/-/g, '+').replace(/_/g, '/')),
);
return typeof payload.userId === 'string' ? payload.userId : '';
} catch {
return '';
Expand All @@ -716,15 +750,20 @@ function userIdFromSession(session?: string): string {
return '';
}
try {
const payload = JSON.parse(
Buffer.from(session, 'base64').toString('utf-8'),
);
const payload = JSON.parse(decodeBase64(session));
return typeof payload.id === 'string' ? payload.id : '';
} catch {
return '';
}
}

/** Base64 to UTF-8 text without `buffer`, which only loads once Push connects. */
function decodeBase64(value: string): string {
return new TextDecoder().decode(
Uint8Array.from(atob(value), (char) => char.charCodeAt(0)),
);
}

/** A hex id, uuid4-like enough for a client-generated client id suffix. */
function randomId(): string {
let out = '';
Expand Down
1 change: 1 addition & 0 deletions templates/web/tsconfig.json.twig
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"lib": ["ESNext", "DOM"],
"listEmittedFiles": false,
"listFiles": false,
"module": "ES2020",
"moduleResolution": "node",
"noFallthroughCasesInSwitch": true,
"pretty": true,
Expand Down
Loading