PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.10.18
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.10.18
1.10.20 1.10.19 1.10.18 1.10.17 1.10.16 1.10.15 1.10.13 1.10.14 1.10.12 1.10.11 1.10.10 1.10.9 1.10.8 untagged-3d9b7ccddc54df87c672 1.10.7 1.10.6 1.10.5 1.10.3 1.10.4 1.10.2 1.10.1 1.10.0 1.9.17 1.9.15 1.9.16 All 164 releases
← All changes | includes/Sync/Sync_Journal.php +113 -5 1.10.3 → 1.10.18 View file →
@@ -27,8 +27,27 @@
27 27 /** Per-request dedup of identical customer lifecycle events. */
28 28 private array $recorded_this_request = array();
29 29
30 30 /**
31 + * The ONE order whose `hook:update` row is owed but not yet written.
32 + *
33 + * WooCommerce saves an order many times while building it: one Store API
34 + * checkout fires `woocommerce_update_order` eleven times, and even a plain
35 + * `$order->save()` on a CPT store fires it three times. Every firing used to
36 + * append a row (an fsync, ~3 ms) after a three-query `wc_get_order()` — 12
37 + * rows and ~66 ms for ONE online order, measured 2026-09-03 on dev-next.
38 + * A journal row is a change POINTER (ADR 0033), so one row per order per
39 + * request carries the same information.
40 + *
41 + * Capacity one preserves ordering before a different order or other-origin
42 + * row. Static so that guarantee holds across journal instances. The first
43 + * instance needing the queue binds its writer, including after shutdown;
44 + * all instances write the same table. Retaining the hook's order object
45 + * avoids a refetch. See Request_Write_Queue for the queue mechanics.
46 + */
47 + private static ?Request_Write_Queue $pending_updates = null;
48 +
49 + /**
31 50 * Option-name prefix for the per-object-type lossy-prune watermarks.
32 51 *
33 52 * The watermark is scoped per object type for the same reason heads are
34 53 * stream-scoped: the streams share one AUTO_INCREMENT space, so a single
@@ -243,9 +262,16 @@
243 262 add_action( 'remove_user_role', array( $this, 'record_customer_role_removed' ), 10, 2 );
244 263 add_action( 'woocommerce_update_customer', array( $this, 'record_customer_updated' ), 10, 1 );
245 264 add_action( 'delete_user', array( $this, 'record_customer_deleted' ), 10, 1 );
246 265 add_action( 'woocommerce_new_order', array( $this, 'record_order_created' ), 10, 1 );
247 - add_action( 'woocommerce_update_order', array( $this, 'record_order_updated' ), 10, 1 );
266 + // Two args: the data store passes ($order_id, $order). Keeping the object
267 + // lets the coalesced flush read modified_gmt without a refetch.
268 + add_action( 'woocommerce_update_order', array( $this, 'record_order_updated' ), 10, 2 );
269 + // Request boundary for the coalesced order update row. LAST on shutdown:
270 + // WooCommerce saves the customer at 10 and the session at 20, and any
271 + // save those trigger must still find the slot open. Zero accepted args:
272 + // do_action( 'shutdown' ) passes an empty string otherwise.
273 + add_action( 'shutdown', array( $this, 'flush_pending_order_updates_at_shutdown' ), PHP_INT_MAX, 0 );
248 274 add_action( 'woocommerce_before_trash_order', array( $this, 'record_order_deleted' ), 10, 1 );
249 275 add_action( 'woocommerce_before_delete_order', array( $this, 'record_order_deleted' ), 10, 1 );
250 276 add_action( 'woocommerce_untrash_order', array( $this, 'record_cot_order_untrashed' ), 10, 1 );
251 277 add_action( 'woocommerce_pos_invalidate', array( $this, 'record_invalidation' ), 10, 2 );
@@ -494,12 +520,69 @@
494 520 public function record_order_created( int $order_id ): void {
495 521 $this->record_order_change( $order_id, 'hook:create', false );
496 522 }
497 523
498 - public function record_order_updated( int $order_id ): void {
499 - $this->record_order_change( $order_id, 'hook:update', false );
524 + /**
525 + * Mark an order's `hook:update` row as owed; the row lands on flush.
526 + *
527 + * See {@see $pending_updates} for why this is deferred. Direct callers
528 + * that need an immediate row use {@see record_order_change()}.
529 + *
530 + * @param int $order_id Order id from the hook.
531 + * @param \WC_Abstract_Order|mixed $order Order object from the hook (second
532 + * argument of `woocommerce_update_order`),
533 + * or anything else to fall back to a
534 + * refetch at flush time.
535 + */
536 + public function record_order_updated( int $order_id, $order = null ): void {
537 + $order = $order instanceof \WC_Abstract_Order ? $order : null;
538 + $this->queue()->owe( 'order', $order_id, $order );
500 539 }
501 540
541 + /**
542 + * Write the owed `hook:update` row, if any.
543 + *
544 + * Called from {@see record_order_change()} before any other-origin row and
545 + * from the shutdown flush. Safe to call repeatedly: a flushed order is no
546 + * longer pending.
547 + */
548 + public function flush_pending_order_updates(): void {
549 + if ( null !== self::$pending_updates ) {
550 + self::$pending_updates->flush();
551 + }
552 + }
553 +
554 + /**
555 + * The `shutdown` callback: flush, then write every later update immediately.
556 + */
557 + public function flush_pending_order_updates_at_shutdown(): void {
558 + $this->queue()->flush_at_shutdown();
559 + }
560 +
561 + /**
562 + * Discard per-request coalescing state. Tests only: the PHPUnit process
563 + * never reaches `shutdown`, so the static queue would leak between
564 + * test cases otherwise.
565 + *
566 + * @internal
567 + */
568 + public static function reset_request_state(): void {
569 + self::$pending_updates = null;
570 + }
571 +
572 + /** Bind the first journal instance to the request's shared queue. */
573 + private function queue(): Request_Write_Queue {
574 + if ( null === self::$pending_updates ) {
575 + self::$pending_updates = new Request_Write_Queue(
576 + 1,
577 + function ( $type, $id, $order ): void {
578 + $this->record_order_change( $id, 'hook:update', false, $order );
579 + }
580 + );
581 + }
582 + return self::$pending_updates;
583 + }
584 +
502 585 public function record_order_deleted( int $order_id ): void {
503 586 $this->record_order_change( $order_id, 'hook:delete', true );
504 587 }
505 588
@@ -539,11 +622,36 @@
539 622 };
540 623 add_action( 'woocommerce_order_status_changed', $handler, 10, 2 );
541 624 }
542 625
543 - public function record_order_change( int $order_id, string $origin, bool $deleted ): bool {
626 + /**
627 + * Append one order row immediately.
628 + *
629 + * @param int $order_id Order id.
630 + * @param string $origin Row origin (`hook:create`, `hook:update`, …).
631 + * @param bool $deleted Whether the row is a tombstone.
632 + * @param \WC_Abstract_Order|mixed $order The order object when the caller already holds it;
633 + * anything else triggers a refetch.
634 + *
635 + * @return bool Whether the insert succeeded.
636 + */
637 + public function record_order_change( int $order_id, string $origin, bool $deleted, $order = null ): bool {
544 638 global $wpdb;
545 - $order = wc_get_order( $order_id );
639 + if ( 'hook:update' !== $origin ) {
640 + if ( 'hook:create' === $origin && $this->queue()->owes( 'order', $order_id ) ) {
641 + // The Store API saves a checkout-draft several times BEFORE
642 + // `woocommerce_new_order` fires. Both rows would point at the same
643 + // live record, so the create row makes the owed update row redundant.
644 + $this->queue()->drop( 'order', $order_id );
645 + } else {
646 + // Land the owed update row FIRST so the stream never reads as
647 + // delete-then-update (a replay would resurrect a trashed order).
648 + $this->flush_pending_order_updates();
649 + }
650 + }
651 + if ( ! $order instanceof \WC_Abstract_Order ) {
652 + $order = wc_get_order( $order_id );
653 + }
546 654 $modified_date = $order ? $order->get_date_modified() : null;
547 655 $modified = $modified_date ? gmdate( 'Y-m-d H:i:s', $modified_date->getTimestamp() ) : gmdate( 'Y-m-d H:i:s' );
548 656 // Order revisions are computed at pull time from the served payload (ADR 0033,
549 657 // #1746) — an order journal row is a change pointer, not a content stamp.